Immediate mode UI library for Retro Gadgets
- Drop
ui.luafile intoImportfolder inside your Gadget folder - Import it to your gadget
- Require it as a module in your
CPU#.luafile:
local ui = require("ui.lua")local ui = require("ui.lua") -- Import the library
-- Define some shortcuts
local video = gdt.VideoChip0
local cpu = gdt.CPU0
local font = gdt.ROM.System.SpriteSheets["StandardFont"]
local theme = ui.whiteOnBlack(font)
-- Get text and button styles
local normalText = ui.label(theme)
local normalButton = ui.button(theme)
-- Create state for a list
local listState = ui.listState(
ui.VERTICAL,
ui.START,
1
)
-- Generate a bunch of buttons
local buttons = {}
for i = 1, 40 do
table.insert(buttons, ui.buttonState(function()
log("pressed "..i)
end))
end
-- update function is repeated every time tick
function update()
video:Clear(theme.BG) -- Clear the screen
-- Draw some UI
ui.drawToScreen(
ui.flex(
ui.HORIZONTAL,
ui.START,
true,
ui.rigid(normalText:layout("offsetted")),
ui.flexed(1, ui.flex(
ui.VERTICAL,
ui.START,
true,
ui.rigid(normalText:layout("offset")),
ui.flexed(1, ui.list(
theme,
listState,
#buttons,
function(index)
return normalButton:layout(buttons[index], "Test "..index)
end
))
))
)
,
video,
cpu
)
endEvery widget method of the library uses following types to describe themselves:
export type LayoutContext = {
min: vec2,
max: vec2,
contain: (self: LayoutContext, pos: vec2) -> vec2,
copy: (self: LayoutContext) -> LayoutContext
}
export type Buffer = {
buffer: RenderBuffer,
push: (self: Buffer, pos: vec2) -> Buffer,
dirtyPush: (self: Buffer, pos: vec2) -> Buffer,
pop: (self: Buffer) -> Buffer,
draw: (self: Buffer) -> Buffer,
release: () -> ()
}
export type DrawContext = {
video: VideoChip,
cpu: CPU,
claimBuffer: (width: number, height: number) -> Buffer,
posToScreen: (pos: vec2) -> vec2,
screenToPos: (screen: vec2) -> vec2
}
export type Result = {
size: vec2,
draw: (pos: vec2, dctx: DrawContext) -> ()
}
export type LayoutFunc = (ctx: LayoutContext) -> Result- LayoutFunc and Result - the thing that this whole library is based around, this represents a layouting function that will calculate how much size it needs to render, it then should return its size and a drawing function, drawing function will be called when the UI hierarchy is being rendered to the screen
- LayoutContext - layouting context, tells you the minimum and maximum size expected from your widget
:contain(pos: vec2): vec2- clamps provided position to fit insideminandmax:copy()- makes a copy of the context, so you can modify the copy rather than the original context
- DrawContext - drawing context, provides you things you would need to draw your widget on the screen
.claimBuffer(width: number, height: number): Buffer- lets you claim a buffer that you can use to "clip" whatever you're trying to render, make sure to release the buffer once you're done! Do not keep the buffer outside of the drawing function..posToScreen(pos: vec2): vec2- transforms your relative position of the widget to absolute position on the screen, this is needed because being drawn inside of a buffer, you'll be getting position relative to the buffer. Useful withVideoChip.TouchPosition.screenToPos(screen: vec2): vec2- transforms absolute position on the screen to relative position of the widget, reverses previously mentioned transform.
- Buffer - wrapper around
RenderBufferto include functions that you'll likely going to be using to clip your widget contents:push(pos: vec2): Buffer- clears theRenderBufferand makes everything after this render to theRenderBufferinstead,postells where the buffer will be drawn later:dirtyPush(pos: vec2): Buffer- same as above, but doesn't clear the buffer:pop(): Buffer- returns rendering to whatever was there previously, call this after you're done rendering to the buffer.:draw(): Buffer- draws contents of the buffer to the screen.release()- releases the buffer, so it can be used by next things that have to render
So you can see concepts above in action
- Following widget takes in
PixelDataand returns a layouting function that returns size of the image and function to draw it.PixelDatacannot be resized, so there's no point in trying to adhere to min and max ofctx
function ui.image(data: PixelData): LayoutFunc
return function(ctx)
return {
size = vec2(data.Width, data.Height),
draw = function(pos, dctx)
dctx.video:BlitPixelData(pos, data)
end
}
end
end- Following widget creates a line that takes up the entire width of the context. This can then be used anytime you want a horizontal separator in your stacks/lists
function ui.horSeparator(clr: color): LayoutFunc
return function(ctx)
local size = vec2(
ctx.max.X,
3
)
return {
size = size,
draw = function(pos, dctx)
dctx.video:DrawLine(
pos + vec2(0, 1),
pos + vec2(size.X - 1, 1),
clr
)
end
}
end
endThis library does not support overlaying elements and clickable layers.
I got too far into design of the library before I thought about that, and I don't have time to redesign the library to support those.
Functions you probably want to use for a simpler time working with this library
ui.layout(layoutFunc: LayoutFunc, video: VideoChip): Result- takes in whatever widget hierarchy you came up with and lays it out into aResultthat can be then drawnui.drawToScreen(layout: LayoutFunc | Result, video: VideoChip, cpu: CPU)- renders your widget hierarchy or already calculated results to the screenui.context(min: vec2, max: vec2): LayoutContext- creates a new layouting context that can be passed into widgets with whatever minimums and maximums you want from themui.contextFromVideo(video: VideoChip): LayoutContext- same thing as above, but takes width and height of video chip's screen for context min and maxui.drawContext(video: VideoChip, cpu: CPU): DrawContext- creates a new drawing context if you for some reason want it
This UI library also supports themes, themes contain all the colors included widgets will use and default settings. Themes have following properties:
export type Theme = {
BG: color,
SecondBG: color,
inactiveFG: color,
activeBG: color,
activeFG: color,
heldBG: color,
heldFG: color,
buttonInset: Inset,
defaultFont: SpriteSheet,
scrollThickness: number,
useTouch: boolean
}BG- background color that should be used for the screenSecondBG- second background color that would appear on second level of the UIinactiveFG- aka normal text coloractiveBG- background color that "active" elements would use, think scroller in lists and buttonsactiveFG- text color that active elements would useheldBG- background color that elements would use while they're being pressed downheldFG- text color for pressed down elementsbuttonInset- inset that buttons will use by defaultdefaultFont- font spritesheet that would be used by default for labels and buttonsscrollThickness- default thickness of scrollers foroverflowandlistuseTouch- if elements should respond at all to screen touches
The library comes with 2 default themes:
ui.whiteOnBlack(font: SpriteSheet): Theme- black background and white textui.blackOnWhite(font: SpriteSheet): Theme- white background and black text
A lot of times widgets would ask you for axis or alignment, they specifically mean the following constants:
- Axes:
ui.HORIZONTAL = 0
ui.VERTICAL = 1
ui.BOTH = 2- Alignments:
ui.START = 0
ui.MIDDLE = 1
ui.END = 2Widgets that define the layout of your screen, all of them layout child widgets in one way or another
ui.background(color: color, child: LayoutFunc): LayoutFunc- adds a background color around the childui.frame(color: color, child: LayoutFunc): LayoutFunc- draws a frame on the edges of the childui.center(child: LayoutFunc): LayoutFunc- places child into the center of the container, takes as much space as it can!ui.inset(top: number, bottom: number, left: number, right: number): Inset- this widget will inset a child widget with provided margins.top,.bottom,.left,.right- margin propertiesInset:layout(child: LayoutFunc): LayoutFunc- takes in a child widget to inset and returns its layouting function
ui.uniformInset(uni: number): Inset- same as above, but all sides will have the same provided valueui.stack(axis: number?, alignment: number?, gap: number?, ...: LayoutFunc): LayoutFunc- layouts its children one after the other on the provided axis, aligns them with provided alignment, and adds provided gap between each child- default
axisis horizontal - default
alignmentis start alignment - default
gapis 0
- default
ui.flex(axis: number?, alignment: number?, expand: boolean?, ...: FlexChild): LayoutFunc- flexbox, container that lays out children one after the other, but also supports children that expand to cover a ratio of the available space,expandshould be true if you want expanding children to work- default
axisis horizontal - default
alignmentis start alignment
- default
ui.rigid(widget: LayoutFunc): FlexChild- wraps a widget into a type that above mentionedflexwould understand as fixed size childui.flexed(flex: number, widget: LayoutFunc): FlexChild- wraps a widget into a type that above mentionedflexwould understand as expanding child
Widgets that require a separately provided state to function, you do want to keep your list scroll position, right?
ui.scrollState(): ScrollState- scrolling state that's used byoverflowandlist, only useful if you're going to be making your own scrollableScrollState.offset: number- current scrolling offset from top of the containerScrollState.scrolling: boolean- if scroll bar is currently being heldScrollState.holdOffset: vec2- relative position of where the scroller was grabbed
ui.overflowState(): OverflowState- creates state foroverflowwidgetOverflowState.hor: ScrollStateandOverflowState.ver: ScrollState- horizontal and vertical scroll states
ui.overflow(theme: Theme, state: OverflowState, axis: number?, child: LayoutFunc): LayoutFunc- this will show scroll bars around whatever child widget you provide, axis determines which scroll bars you'll seeui.listState(axis: number?, alignment: number?, gap: number?, thickness: number?): ListState- creates state forlistwidgetaxis: number?- direction to use for the list, defaults to horizontalalignment: number?- how to align the children, defaults to start alignmentgap: number?- gap in pixels between each widget in the list, defaults to 0thickness: number?- thickness of the scroll bar, defaults to whatever is defined in the theme
ui.list(theme: Theme, state: ListState, amount: number, template: ListElementFunc): LayoutFunc- this will use the template function (that acceptsindex: numberfor element index and returns a layouting function) to populate the list withamount: numberof elementsui.buttonState(callback: () -> any): ButtonState- creates state forbuttonwidget, provided callback function will be called whenever user clicks on the buttonButtonState.callback- setting this can replace what function will be called on button press
ui.button(theme: Theme): ButtonStyle- result of this will render a button.BG: color- background color that the button will normally use.FG: color- text color that the button will normally use.heldBG: color- background color while user is pressing on the button.heldFG: color- text color while pressed down.inset: Inset- margin of the button.font: SpriteSheet- font that button will use for its text:layout(state: ButtonState, text: string) -> LayoutFunc- renders a button using provided state and text
These widgets usually return a style rather than directly returning a layouting function, you should create them at start of your CPU and keep them around. Styles don't have state, so this is done just for less things you'd constantly have to repeat while creating your UI
ui.label(theme: Theme): TextStyle- this widget will render text.font: SpriteSheet- font that will be used for the text.fallbackColor: color- color that will be used when it's not provided in:layoutfunction:layout(text: string, clr: color?): LayoutFunc- takes in text to render and optionally color to use, fallbacks tofallbackColorif no color is provided
ui.customLabel(font: SpriteSheet, fallback: color): TextStyle- same as above, but lets you provide custom font and colorui.sprite(sheet: SpriteSheet, spriteX: number, spriteY: number): SpriteStyle- this will render a sprite from provided spritesheet.sheet: SpriteSheet- provided spritesheet.pos: vec2- grid position on the spritesheet:layout(clr: color): LayoutFunc- takes in color and renders the sprite with the color being used for tint
ui.progress(theme: Theme): ProgressStyle- this will render a progress bar.height: number- height of the progress bar, by default it will try to match height of your font.minWidth: number- minimum width of the progress bar in pixels, by default it's50.color: color- color of the progress bar:layout(expand: boolean, progress: number): LayoutFunc- renders a progress bar. Ifexpandis true, it will take up as much space as it can horizontally.progressis a number between 0 and 1
ui.scrollingLabel(style: TextStyle, stayTime: number, speed: number, asLerpTime: boolean, sync: ScrollingSync?): ScrollingLabelStyle- this will render scrolling text based onlabel's style.stayTimeis how much time the text will stay in place.speedis pixels/second of how fast the text will be scrolled.asLerpTimeorScrollingLabelStyle.asLerpTimewill switch previousspeedto be treated as interpolation time, or how fast the text will scroll to the opposite side.syncis the optional synchronization object, will make your entire list scroll as one.:layout(text: string, clr: color?): LayoutFunc- renders the scrolling text, it will take up as much horizontal space as it can!
Widgets that don't have much complexity
ui.empty(): LayoutFunc- empty widget whenever you want to have nothingui.emptyFlex(flex: number): FlexChild- expandingflexversion ofemptyui.image(data: PixelData): LayoutFunc- simply renders whatever image you provideui.horSeparator(clr: color): LayoutFunc- horizontal separator, line that will visually separate your itemsui.verSeparator(clr: color): LayoutFunc- vertical separator, line that will visually separate your itemsui.flexHorSeparator(clr: color): FlexChild-flexversion ofhorSeparatorui.flexVerSeparator(clr: color): FlexChild-flexversion ofverSeparatorui.wSpacer(space: number): LayoutFunc- simple spacer widget that will take provided amount of horizontal space in pixelsui.hSpacer(space: number): LayoutFunc- simple spacer widget that will take provided amount of vertical space in pixelsui.flexWSpacer(space: number): FlexChild-flexversion ofwSpacerui.flexHSpacer(space: number): FlexChild-flexversion ofhSpacer
Helpers that will do various things to your layout
ui.contain(child: LayoutFunc): LayoutFunc- forces the child to adhere to layouting min and max, usesBufferto clip it to fitui.limitX(limit: number, child: LayoutFunc): LayoutFunc- limits how much horizontal space in pixels the child can takeui.limitY(limit: number, child: LayoutFunc): LayoutFunc- limits how much vertical space in pixels the child can takeui.minX(min: number, child: LayoutFunc): LayoutFunc- forces child to take at least that much horizontal space in pixelsui.minY(min: number, child: LayoutFunc): LayoutFunc- forces child to take at least that much vertical space in pixels
