After trying to use a few other exist rendering libraries (bugs, bad readability, etc.), I was not satisfied with what was out there and decided to make my own.
Some things that makes my library stand out a bit:
- Unminified source has full Lua language server type annotation support. Works great in VS Code!
- Full OOP (uses https://github.com/lua-rocks/object to provide OOP interface)
- Virtual screens/outputs (Frames)
- Positional anchoring
- Seemly handles both terminal and monitors
This library uses cc-updater, but that is automatically installed as part of installing this so there are no extra steps needed.
Run the following command in your computer:
wget run https://raw.githubusercontent.com/AngellusMortis/cc-render/master/install.lua
You can disable autoupdating on computer boot by removing the startup.lua
that was downloaded and running the following command:
/ghu/core/programs/ghuconf set autoUpdate false
You can find some examples using the rendering library in the /example folder. You can also install these examples on your computer with the following command:
wget run https://raw.githubusercontent.com/AngellusMortis/cc-render/master/installExamples.lua
The main file for the project should be pretty well documented, so for anything missing from this readme, check out that file for more.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
uiLoop = ui.UILoop()
screen = ui.Screen(term, {textColor=colors.white, backgroundColor=colors.black})
screen:add(ui.Text(ui.a.Top(), "Title", {id="titleText"})
exitButton = ui.Button(ui.a.Bottom(), "Exit", {fillColor=colors.red})
exitButton:addActivateHandler(function()
uiLoop:cancel()
end)
screen:add(exitButton)
function runUILoop()
uiLoop:run(screen)
end
function main()
-- do stuff
-- update title
title = screen:get("titleText")
title:update("New Title")
end
parallel.waitForAll(runUILoop, main)
A UI Object is not directly bound to a computer screen. This makes it easier to move the UI object to another screen in the event of a redirect or just wanting to move it. You can bind a UI Object to an output for rendering. The Screen
helper allows you to never have to really worry about binding and it essentially just becomes an "under the hood" thing.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
text = ui.Text(ui.a.Top(), "Title", {id="titleText"}
-- manual binding
boundText = text:bind(term)
boundText:render()
-- using screen
screen = ui.Screen(term)
screen:add(text)
screen:render()
Rather then rendering text, buttons, etc. to specific coords on a computer screen, Anchors are used instead. Usually we do not want to render a button at a specific location, but rather "the bottom of the screen" or the "top right corner". If you do still want a specific coordinate, you can use the base Anchor
which allows for absolute positioning.
Any of the anchors that let you anchor something to middle of a line also lets you offset that anchor by a specific amount to either the left or right. offsetDir
is either the value of .c.Offset.Left
or .c.Offset.Right
with the value of offsetAmount
to indicate how much to offset it by.
Available anchors (all anchors are relative to the require
import):
.a.Anchor(x, y)
- Base anchor. Absolute position.a.Left(y)
- Anchors object to left side of screen at specified y position.a.Right(y)
- Anchors object to right side of screen at specified y position.a.Center(y, offsetDir, offsetAmount)
- Anchors object to center of screen at specified y position.a.Middle()
- Anchors object to center of screen.a.Top(offsetDir, offsetAmount)
- Anchors object to center of the first line of the screen.a.Bottom(offsetDir, offsetAmount)
- Anchors object to center of the last line of the screen.a.TopLeft()
- Anchors object to top left corner of a screen (1, 1).a.TopLeft()
- Anchors object to top right corner of a screen.a.BottomLeft()
- Anchors object to bottom left corner of a screen.a.BottomRight()
- Anchors object to bottom right corner of a screen
All events fired by UI objects are pretty consistent. All of the event names are prefixed with ui.
and they only ever have a single argument: a table with the data encoded into it.
Every UI event has the following event data:
.objId
- the ID of the frame the event if for.outputType
- the type of output the frame was rendered on (term
,monitor
orframe
).outputId
- the ID of the output so you can get a reference to itnil
for term
Whenever you render a UI object, you need to provide the parent output or Frame to render it to. Since this can be very tedious when dealing with multiple monitors and Frames, the .Screen
helper exists. You bind the top level output to the Screen and then add the child UI objects to the Screen to handle rendering.
Any more advanced rendering should always use a top level Screen object to handle rendering. Especially when you start using Frames to section off parts of the physical screen.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
screen = ui.Screen(term, {textColor=colors.white, backgroundColor=colors.black})
screen:add(ui.Text(ui.a.Top(), "Title")
screen:render()
.UILoop
is a helper to handle rendering UI elements and automatically handling all of the events needed to update the UI for you. It should be ran in the background with parallel
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
uiLoop = ui.UILoop()
screen = ui.Screen(term, {textColor=colors.white, backgroundColor=colors.black})
-- other UI init
function runUILoop()
uiLoop:run(screen)
end
function main()
-- non-UI stuff
end
parallel.waitForAll(runUILoop, main)
All of the more complex items in the library uses Frames, which is essentially a rectangle that gets rendered and allows for a "virtual" computer screen to render children items inside of. For example, a button is a Frame with a Text element Anchored inside (defaults to rendering in the middle of the button Frame.
The most generic usage of the a .Frame
is just to use to render plain rectangle:
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
-- makes a blue rectangle with no border at (3, 4) on the terminal with the width of 5 and height or 3
frame = ui.Frame(ui.a.Anchor(3, 4), {width=5, height=3, fillColor=colors.blue, border=0})
frame:render(term)
However, all of the more complex items in the library uses Frames and they allow you create a "virtual" screen to let you render things to.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
width, height = term.getSize()
-- will make a blue rectangle on the right half of the terminal
frame = ui.Frame(ui.a.TopRight(), {width=math.floor(width / 2), height=height, fillColor=colors.blue, textColor=colors.green, border=0})
frame:render(term)
output = frame:makeScreen(term)
width, height = output.getSize()
output.setCursorPos(3, height)
-- writes "Test" to (3, frameHeight) within the Frame
-- which actually becomes (width + 3, height)
output.write("Test")
Frames also support vertical scrolling. To do so though, they must have a fixed height rather then it being calculated on the fly.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
frame = ui.Frame(ui.a.TopRight(), {width=math.floor(width / 2), height=100, scrollBar=true})
Frames also provides click events to mirror the ones for monitors and terminals.
In addition to the above event data, Frame events also have the following data:
.x
- x coord for event (can be 0 or negative if frame has padding or border).y
- y coord for the event (can be 0 or negative if frame has padding or border).clickArea
0
if the click event was on the writable screen for the frame1
if the click event was in the padding for the frame2
if the click event was in the border for the frame
ui.frame_touch
- Fired when a frame is touched (monitor only)ui.frame_click
- Fired when there is a mouse clicked (terminal only)- Has additional data value of
.clickType
for the mouse button
- Has additional data value of
ui.frame_up
- Fired when the a mouse button is released- Has additional data value of
.clickType
for the mouse button
- Has additional data value of
Handling events can be completed if you have multiple outputs/monitors. So it is recommended you use a Screen
object to handle all of the nested outputs and such. Frame
does not handle anything to determine if output for the raw computer event is the correct output.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
screen = ui.Screen(term)
screen:add(ui.Frame(ui.a.TopRight(), {width=math.floor(width / 2), height=height, fillColor=colors.blue, textColor=colors.green, border=0}))
while true do
-- Will fire a Frame click/up event if there is a
-- `mouse_` event on the right half of the screen
screen:handle(os.pullEvent())
end
A tabbed frame or just tabs is multiple frames you can switch between. You can change tabs using the setActive
method or by adding built in tab navigation at the top of the frame using the showTabs
option on initialization.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
tabs = ui.TabbedFrame(ui.a.Anchor(3, 4), {showTabs=true})
tab1 = tabs:getTab("main") -- default tab added
-- set tab label to "Tab 1" in header tabs
-- instead of "main"
tabs:setLabel("main", "Tab 1")
-- add stuff to tab1
tab2 = tabs:createTab("tab2")
--- add stuff to tab2
tabs:render(term) -- tab1 will render
tabs:setActive("tab2")
tabs:render(term) -- tab2 will render
Text objects allow you to anchor text on the screen. Text objects also support some basic color encoding to let you change the color dynamically based on the "alert level" of the message you are displaying. Great for status lines and such. The encoding is based on Bootstrap alerts and work by adding the alert level as a prefix before the text.
error:
will make the textcolors.red
warning:
will make the textcolors.yellow
success:
will make the textcolors.green
info:
will make the textcolors.blue
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
text = ui.Text(ui.a.Middle(), "Test")
text:render(term)
Buttons are frames that can be activated and have a Text object inside of them.
ui.button_activate
- Fires when a button is activated- Has additional data value of
.touch
for if activation was from a touch event
- Has additional data value of
ui.button_deactivate
- Fires when a button is deactivated
If you are using a ui.Screen
or ui.UILoop
there is also a way to automatically handle the events for you. You can call addActivateHandler
with your a function for to handle a button click.
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
screen = ui.Screen(term)
screen:add(ui.Frame(ui.a.TopRight(), {width=math.floor(width / 2), height=height, fillColor=colors.blue, textColor=colors.green, border=0}))
exitButton = ui.Button(ui.a.Bottom(), "Test")
exitButton:addActivateHandler(function(button, output, event)
-- button clicked!
end)
while true do
-- Will fire a Frame click/up event if there is a
-- `mouse_` event on the right half of the screen
screen:handle(os.pullEvent())
end
require(settings.get("ghu.base") .. "core/apis/ghu")
ui = require("am.ui")
bar = ui.ProgressBar(ui.a.TopLeft(), "Test")
bar = bar:bind(term)
-- renders bar at 0%
bar:render()
-- renders bar at 50%
bar:update(50)
bar:render()