UI in luxe¶
work in progress
The UI has parts that are still being defined, but the basics are there.
UI is a Modifier¶
UI exists within a World
, and is attached to an Entity
like other modifiers.
This means that a UI can easily follow the entity if it is moved.
When you create a UI on an Entity
, you're creating a canvas.
A canvas is like an isolated little space, dedicated to elements within that canvas.
These elements are called controls, a canvas is a container for controls. UI controls don't belong to the world (they are not entities), they belong to the canvas. We'll see how that looks below.
A canvas also has it's own coordinate space, where 0,0 is top left for the origin, and Y values increase downward. This is different from world space, where Y+ is up.
Creating a canvas¶
To create a UI, we need an entity to attach it to.
When we attach the UI, we also give it a camera, which it will use to calculate input. That means if your object is in a 3D world, input should work as expected without extra effort.
var ui = Entity.create(app.world, "ui")
var x = 0
var y = 0
var w = 500 //these are in world units
var h = 500
var depth = 0
UI.create(ui, x, y, w, h, depth, app.camera)
Creating a control¶
Now that we have a canvas, we can create controls inside it.
Controls are positioned relative to the canvas unless they're a child of another control.
Controls have a bounds
, which is a rectangle, and is also top left, y+ going down.
Let's create a panel, as a background, so we can see our canvas.
To create a control, we pass in the canvas that it will be created in. Once created, we can configure it.
The first step is usually setting the size or the bounds, via Control.set_bounds(control, x,y,w,h)
.
//we'll use the same w/h from above
var bg = UIPanel.create(ui)
Control.set_bounds(bg, 0,0,w,h)
UIPanel.set_color(bg, [0,0,0,0.25])
So here we can see that we got a panel control in return from create
.
This value is an instance of a control that belongs to this canvas.
Committing UI Changes¶
Now, this part is important.
A concept that luxe uses is the commit
concept.
UI is a great example of this concept, where you want to make several changes to the UI that could be expensive, or could have dependencies (like layout being relative).
Once you've made a series of changes, you MUST call commit to finalize them.
In fact, in this example, if you don't, nothing will render!
So we must call UI.commit(ui)
to make sure it shows up.
Controls and Specialized Controls¶
All controls are a Control
, but not all controls are a UIPanel
.
Controls like UIPanel
specialize a control, and offer their own API
on top of the Control
API, like UIPanel.set_color
. The Control
API
is valid for all controls, regardless of their type, but specialized controls
only work with the same API that their create function is from.
Let's also create a button that takes up the upper right hand side of our canvas. This would be half the size on width and height, and be positioned at the middle of the canvas on x. We'll use this to hook up an event!
var button = UIButton.create(ui)
Control.set_bounds(button, w/2, 0, w/2, h/2)
UIButton.set_text(button, "click!")
Control events¶
When we have a control, we can listen for events on that control and respond to them as needed.
Like our button will have a UIEvent.release
event when clicked.
Control.set_events(button) {|event|
if(event.type == UIEvent.release) {
Log.print("The button was clicked! x %(event.x) y %(event.y)")
}
}
There are many default events for a control, but it's possible for controls to send custom events too. Some events are dependent on settings like Control.set_allow_input
/Control.set_allow_keys
, which determine if a control will receive mouse and keyboard input.
You can also print the events to see what kind of events are happening and when:
Control.set_events(button) {|event|
Log.print("event from button %(event)")
}
Empty controls as containers¶
If you don't use a specialized create
function,
you get a blank control that has all the default behaviours of a control,
like input and bounds.
A blank control is often used as a container to make groups, so you can easily refer to a single control to move a bunch of controls.
Full example¶
import "luxe: io" for IO
import "luxe: game" for Ready
import "luxe: input" for Input, Key
import "luxe: world" for UI, UIEvent, Entity
import "luxe: ui" for UIButton, Control, UILabel, UISlider, UIPanel
import "outline/app" for App
class Game is Ready {
construct ready() {
super("ready!")
app = App.new()
var ui = Entity.create(app.world, "ui")
var x = 0
var y = 0
var w = 500 //these are in world units
var h = 500
var depth = 0
UI.create(ui, x, y, w, h, depth, app.camera)
var bg = UIPanel.create(ui)
Control.set_bounds(bg, 0,0,w,h)
UIPanel.set_color(bg, [0,0,0,0.25])
//we'll use the ui w/h from above
var button = UIButton.create(ui)
Control.set_bounds(button, w/2, 0, w/2, h/2)
UIButton.set_text(button, "click!")
Control.set_events(button) {|event|
if(event.type == UIEvent.release) {
Log.print("The button was clicked! x %(event.x) y %(event.y)")
}
}
//create a blank container
var container = Control.create(ui)
Control.set_bounds(container, 0, h/2, 128, 32)
//create a background as well
var container_bg = UIPanel.create(ui)
Control.set_bounds(container_bg, 0,0,128,32)
UIPanel.set_color(container_bg, [0,0,0,1])
//create some controls to put inside it
var label = UILabel.create(ui)
Control.set_bounds(label, 4, 0, 64, 30)
UILabel.set_text(label, "progress")
var slider = UISlider.create(ui)
Control.set_bounds(slider, 64, 10, 60, 10)
//now add them to the container
Control.child_add(container, container_bg)
Control.child_add(container, label)
Control.child_add(container, slider)
UI.commit(ui)
} //ready
tick(delta) {
if(Input.key_state_released(Key.escape)) {
IO.shutdown()
}
} //tick
app { _app }
app=(v) { _app=v }
} //Game
Debug Visualization¶
In your settings file (like outline/settings.settings.lx
), we can specify a debug flag
that will draw outlines for each control, and show some info about what controls are hovered.
Inside the settings file, put engine.ui.debug_vis = true
at the root.
Also relevant: A Control
doesn't have a name by default, you have to set one using Control.set_id(control, "some name")
.
Final Note: Specialized Containers¶
Some containers are specialized because they provide additional behaviour. UIList and UIScroll are two examples of such controls, as the both provide scrolling.
In both cases, adding a child (via Control.child_add
) to the control itself does not count
as adding it to the container, but is treated as a regular child of the control.
You would need to use UIScroll.add(scroll, control)
or UIList.add(scroll, control)
instead.
why?
Imagine you had a scroll area (or a list), and you wanted to place a button on the top right.
You wouldn't want this button to scroll as part of the container, but instead be fixed at the top.
If the scroll used Control.child_add
it wouldn't have a way (currently) to distinguish this type
of child, from one you did want to scroll around as part of the container.
potential change
This behaviour may change, as I think it's got its own set of quirks.