ControlTree - Grisgram/gml-raptor GitHub Wiki
Base Controls - Containers - Tooltips - Clickables - Checkables - ListBox - InputBox - Mouse Cursor - ✔ControlTree
Important
Before reading to deep into the theory of the control tree, I strongly recommend, that you watch this 2-minute video from the release preview, which will show you the effects of anchoring, docking, spreading, alignment and UI design. Those two minutes will answer many questions, that might arise, when reading through this page!
This class has been added with Release 3.0
and is the new recommended way to create your UI since then.
It will be created by Containers in the variable definitions
and contains a hierarchical tree of components (controls), that are aligned, placed, docked or anchored within their parent (the container). The strategy to build your UI is similar to other environments, you might already know, like WinForms, WPF, MAUI or mobile apps, like the Android xml-based ui-layouts.
Before we go deeper in the details, let's take a look at a simple code sample, that is used to create the layout for the MessageBox Functions in raptor
(I adapted the code a bit to fit better on the documentation page here).
control_tree
.add_control(Panel, { startup_height: /*...some calculation */ })
.set_margin(MESSAGEBOX_INNER_MARGIN, 0, MESSAGEBOX_INNER_MARGIN, MESSAGEBOX_INNER_MARGIN)
.set_dock(dock.bottom)
.add_control(Panel, { startup_height: ACTIVE_MESSAGE_BOX.__get_max_button_height() })
.set_name("panButtons")
.set_align(fa_middle, fa_center)
.step_out()
.step_out()
.add_control(Panel)
.set_dock(dock.fill)
.set_name("panContent")
.set_padding_all(MESSAGEBOX_INNER_MARGIN)
.add_control(MESSAGEBOX_TEXT_LABEL, {
text: ACTIVE_MESSAGE_BOX.text,
font_to_use: MESSAGEBOX_FONT,
scribble_text_align: "[fa_top][fa_center]"
})
.set_dock(dock.fill)
.set_name("lblText")
.step_out()
.step_out()
;
Let us analyze, what you see here:
- All functions are chain-able,
- So you simply build your UI as a series of
.add_control
calls, - And each of them is followed by a series of
.set_*
setter functions for the details of the control.
- So you simply build your UI as a series of
- You can also see, that initializer structs (
{ startup_height: ... }
) are supported - There are Panels added, and in the Panels there is a Label and other things, so this structure is recursive and can be nested freely.
- To tell the control_tree, at which level something is added, you need also a
.step_out()
function. This works like closing brackets}
in code, telling the tree, that the next command is one level outdent. - A closer look at the first added panel lets you see, that there are no Buttons added at the moment, but the inner panel has a
name
("panButtons")- You can refer a control at any time at runtime in the tree by getting it through its name.
- For the MessageBox, the Buttons get added, when the
.show()
method is invoked, as this is the moment, where the MessageBox knows, that there won't be any more buttons added, so it can do its final calculations and add the buttons.
- The readability of the code you create for these trees, depends on your discipline in doing exact intendation. Without proper formatting I can imagine, that you might lose control, of what's going on. Keep that in mind. Safety first ;-)
Those two properties always affect the Container
, never a Control
.
The white rectangle represents the Container .The Margin is the outer space, the distance to neighbor controls this container shall keep,and the Padding is the inner space, how much distance to the border of the container a control shall keep |
Set the margin for each side, or all at once with the _all
function.
/// @function set_margin(_margin_left, _margin_top, _margin_right, _margin_bottom)
/// @function set_margin_all(_margin)
Set the padding for each side, or all at once with the _all
function.
/// @function set_padding(_padding_left, _padding_top, _padding_right, _padding_bottom)
/// @function set_padding_all(_padding)
/// @function add_control(_objtype, _init_struct = undefined)
You simply tell the tree the object type and an optional initialization struct, and the control will be added.
/// @function add_sprite(_sprite_asset, _init_struct = undefined)
/// @description Adds a sprite to the tree. Internally this is wrappend
/// in a ControlTreeSprite object, which is a _baseControl,
/// so you can use the _init_struct freely to assign all
/// variables, you'd like to change, from image_angle, scale,
/// blend_color, plus everything a _baseControl has in stock!
/// In addition, you can align, anchor, dock it as you would
/// with any other control.
Note
These functions also immediately invoke create_instance
and the object will exist from the moment, you call add_control
or add_sprite
!
After adding a control, you tell the tree, what it shall do with it, how it shall be placed in the client area of the container, by invoking the set_*
functions.
Sometimes you will need to remove controls from the tree, if the layout shall change dynamically.
/// @function remove_control(_control_or_name)
You will need to supply the object instance to this function, or its name set through set_name
, to remove it. So make sure, all dynamic controls, that are subject to be removed, have a name
set, so you can access them later on.
These functions apply to the Controls
and will define, where on the layout they will appear. Your options are docking, aligning, anchoring, positioning and spreading (and almost any combination of them).
/// @function set_align(_valign = fa_top, _halign = fa_left, _xoffset = 0, _yoffset = 0)
/// @function remove_align()
You align a control vertically and horizontally, by using the constants, GameMaker offers:
- fa_top, fa_middle, fa_bottom for vertical alignment and
- fa_left, fa_center, fa_right for horizontal alignment
- They work the same as the
halign/valign
functions you know from GameMaker
In addition, you may apply an offset in both dimensions, if the control shall not be exact at the aligned position, but a little off. This can make sense for instance, if you want to negotiate a padding set on the container, when this one control shall be touching the border, shall be a bit outside. There are several scenarios imaginable, where this makes sense.
Note
Aligning means, that the control will keep its distance to the containers border and will move (but not resize!), when the container changes size, to fulfill this rule.
/// @function set_anchor(_anchor)
/// The _anchor is an enum:
enum anchor {
none = 0,
right = 1,
top = 2,
left = 4,
bottom = 8,
all_sides = 15
}
As you can see, the anchor
enum is a bit-field, so you may apply multiple anchors on a control.
As an example, if you want to anchor a control to the left and to the right, you would use a logical or
(|
) operator:
.set_anchor(anchor.left | anchor.right);
Note
Anchoring means, the distance of the control's border to the container's border stays constant and the control will be resized, when the container changes size, to fulfill this rule.
/// @function set_position(_xpos, _ypos, _relative = false)
/// @description Sets an absolute position in the client area of the parent control,
/// unless you set _relative to true, then the values are just added to the
/// currently set xpos and ypos
/// @function set_position_from_align(_valign, _halign, _xoffset = 0, _yoffset = 0)
You have two functions available to do a coordinate based positioning.
-
set_position
will simply render the control at the specified coordinates. It will not move or resize or react in any way, when the container changes size. The_relative
argument allows you to just "increase/decrease" the current position by the specified values. This is handy, if you combine positioning with other functions, like alignment or anchoring and can be seen as another way to supply an offset. -
set_position_from_align
is kind of a cheat-function. It will simulate the supplied alignment, but will not activate it on the control, instead it just measures the final position and sets it as xpos/ypos on the control. This is useful, when you have a static (non-sizeable) window and, for instance, want a control, like the minimap or a quest tracker, to be "in the top right corner of the screen", no matter, what resolution the game currently has.
/// @function set_spread(_spreadx = -1, _spready = -1)
Spreading is something in-between of anchoring and docking. You define the spread of a control as a normalized percentage (ranging from 0..1), which tells the tree "how many percent of the width or height of the container" this control shall occupy.
Leave an argument at its default of -1
to tell the tree "no spreading in this dimension, please".
A value of 0.1
means 10%
, 0.5
means 50%
and 1.0
means 100%
of the width or height.
/// @function set_dock(_dock)
/// dock is also an enum value, but not a bit field
enum dock {
none, right, top, left, bottom, fill
}
This is the most powerful option you have. Docking means, that you attach a control to one edge of the container, where it will stay but it will always be the maximum size (height or width) of the other dimension. So, a dock.top
will be like a menu bar, at the top edge of the container, and at maximum width.
However, there's a bit of additional information necessary:
If you have multiple docked controls in a container, the order matters. The first dock.top
control will be on the top edge, the second dock.top
will also be attached up there, but below the first dock. Same is true for all other sides. So you can create multiple dockings and you control the ordering through the order in which you add the controls to the container.
The right and bottom docks have a bit a different default behavior due to the way, we humans think. Take a look at the function below and its description. Keep this in mind, so you know, how you can change the stacking strategy of right and bottom docks, if you wish.
/// @function set_reorder_docks(_reorder)
/// @description True by default. Reorder dock means a more "natural" feeling of
/// adding right- and bottom docked elements.
/// When you design a form, you think "left-to-right" and "top-to-bottom",
/// so you likely want to have the second bottom added to appear BENEATH the first bottom!
/// If you do not want that, just turn reorder off.
/// @function bind_to(_control)
/// @description Binds this tree to its Container's object instance.
/// Normally, you do never invoke this function,
/// it is done by the container container control, when it creates its own tree.
/// @function get_root_control()
/// @description Gets the container control of the root tree of this hierarchy
/// @function get_root_tree()
/// @description Gets the root ControlTree of this hierarchy
/// @function is_root_tree()
/// @description True, if this instance of ControlTree is the root tree, otherwise false
/// @function set_name(_name)
/// @description Give a child control a name to retrieve it later through get_element(_name)
/// @function select_element(_control_or_name)
/// @description Searches through the tree for the specified control
/// and sets it as the active element, if found.
/// "Active element" means, all ".set_*" function will apply to it
/// NOTE: This function throws an exception if the control is not in the tree!
/// @function get_element(_name)
/// @description Retrieve a child control by its name. Returns the instance or undefined
/// @function step_out()
/// @description Think of this like a closing bracket in code. Outdents current flow by 1 level
/// @function on_window_opened(_callback)
/// @description Callback to invoke when the root container gets rendered the first time
/// @function on_window_closed(_callback)
/// @description Callback to invoke when the root container gets destroyed
/// @function on_shown(_callback)
/// @description Invoked once after the first draw of the tree
There is also one "root" tree available in each room, created by the RoomController. You can access it through the UI_ROOT
macro. It is recommended to setup your ingame-UI through this tree, for instance in the Room Start
event of the RoomController.
Base Controls - Containers - Tooltips - Clickables - Checkables - ListBox - InputBox - Mouse Cursor - ✔ControlTree