ControlTree - coldrockgames/gml-raptor GitHub Wiki

You are here:

Base Controls - Containers - Tooltips - Clickables - Checkables - ListBox - InputBox - Mouse Cursor - ✔ControlTree


ControlTree

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).

add_control(Panel, {
    min_height: ACTIVE_MESSAGE_BOX.__get_max_button_height() + MESSAGEBOX_INNER_MARGIN
  })
  .set_margin(MESSAGEBOX_INNER_MARGIN, 0, MESSAGEBOX_INNER_MARGIN, MESSAGEBOX_INNER_MARGIN)
  .set_dock(dock.bottom)
  .add_control(Panel, {
      min_height: ACTIVE_MESSAGE_BOX.__get_max_button_height()
    })
    .set_name("panButtons")
    .set_align(fa_middle, fa_center)
  .build()
.build()
.add_control(Panel)
  .set_dock(dock.fill)
  .set_name("panContent")
  .set_margin(16, 0, 16, 0)
  .add_control(MESSAGEBOX_TEXT_LABEL, { 
      skin_flavor: control.skin_flavor,
      text: ACTIVE_MESSAGE_BOX.text,
      font_to_use: MESSAGEBOX_FONT,
      scribble_text_align: "[fa_middle][fa_center]"
    })
    .set_dock(dock.fill)
    .set_name("lblText")
.build();

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.
  • You can also see, that initializer structs ({ startup_height: ... }) are supported
  • (Not in this example) Styled UI style-structs are also 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 .build() function. This works like closing brackets } in code, telling the tree, that the current container is finished.
  • 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 ;-)

Margin and Padding

Those two properties always affect the Container, never a Control.

image 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_margin

Set the margin for each side.

static set_margin = function(_left_or_all, _top = undefined, _right = undefined, _bottom = undefined) {
static set_margin_left		= function(_margin) {
static set_margin_top		= function(_margin) {
static set_margin_right		= function(_margin) {
static set_margin_bottom	= function(_margin) {

set_padding

Set the padding for each side.

static set_padding = function(_left_or_all, _top = undefined, _right = undefined, _bottom = undefined) {
static set_padding_left		= function(_padding) {
static set_padding_top		= function(_padding) {
static set_padding_right	= function(_padding) {
static set_padding_bottom	= function(_padding) {

The add_control function

/// @func	add_control(_objtype, _style_struct = undefined)
static add_control = function(_objtype, _style_struct = undefined) {

You simply tell the tree the object type and an optional style struct, and the control will be added.

Note

These function also immediately invokes create_instance and the object will exist from the moment, you call add_control!

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 (if you did not specify already that in the style struct).

The remove_control function

Sometimes you will need to remove controls from the tree, if the layout shall change dynamically.

/// @function remove_control(_control_or_name)
static remove_control = function(_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.

The set_* functions

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).

Alignment

/// @func	set_align(_valign = fa_top, _halign = fa_left, _xoffset = 0, _yoffset = 0)
static set_align = function(_valign = fa_top, _halign = fa_left, _xoffset = 0, _yoffset = 0) {
/// @func	remove_align()
static remove_align = function() {

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.

Anchoring

/// @function set_anchor(_anchor)
static set_anchor = function(_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.

Positioning

/// @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
static set_position = function(_xpos, _ypos, _relative = false) {
/// @function set_position_from_align(_valign, _halign, _xoffset = 0, _yoffset = 0)
static set_position_from_align = function(_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.

Spreading

/// @function set_spread(_spreadx = -1, _spready = -1)
static set_spread = function(_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.

Docking

/// @function set_dock(_dock)
static set_dock = function(_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.

set_reorder_docks

/// @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.
static set_reorder_docks = function(_reorder) {

Other Functions

bind_to

/// @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.
static bind_to = function(_control) {

get_root_control

/// @function get_root_control()
/// @description Gets the container control of the root tree of this hierarchy
static get_root_control = function() {

get_root_tree

/// @function get_root_tree()
/// @description Gets the root ControlTree of this hierarchy
static get_root_tree = function() {

is_root_tree

/// @function is_root_tree()
/// @description True, if this instance of ControlTree is the root tree, otherwise false
static is_root_tree = function() {

get_parent

/// @func	get_parent() 
/// @desc	Get the parent tree of this tree (or self, if this is the root)
static get_parent = function() {

get_instance

/// @func	get_instance()
/// @desc	Returns the last added instance of the control tree or a named
///		instance, if you specify a name or undefined, if not found.
static get_instance = function(_name = undefined) {

get_control

/// @func	get_control() 
/// @desc	Gets the control that holds this control_tree.
///		In contrast to get_instance(...), which returns any named
///		instance or the last instance ADDED to this tree, this
///		function returns the owner of the tree 
///		(which is always a _baseContainerControl)
static get_control = function() {

set_visible

/// @func	set_visible(_visible)
/// @desc	Sets the visible state for this and all children
static set_visible = function(_visible) {

set_enabled

/// @func	set_enabled(_enabled)
/// @desc	Sets the enabled state for this and all children
static set_enabled = function(_enabled) {

set_name

/// @function set_name(_name)
/// @description Give a child control a name to retrieve it later through get_element(_name)
static set_name = function(_name) {

select_element

/// @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!
static select_element = function(_control_or_name) {

get_element

/// @function	 get_element(_name)
/// @description Retrieve a child control by its name. Returns the instance or undefined
static get_element = function(_name) {

build

/// @func	 build()
/// @description Performs layout building of this container.
///		 Must be the final call for each container.
static build = function() {

on_window_opened

/// @function on_window_opened(_callback)
/// @description Callback to invoke when the root container gets rendered the first time
static on_window_opened = function(_callback) {

on_window_closed

/// @function on_window_closed(_callback)
/// @description Callback to invoke when the root container gets destroyed
static on_window_closed = function(_callback) {

on_shown

/// @function on_shown(_callback)
/// @description Invoked once after the first draw of the tree
static on_shown = function(_callback) {

UI_ROOT

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.


You are here:

Base Controls - Containers - Tooltips - Clickables - Checkables - ListBox - InputBox - Mouse Cursor - ✔ControlTree

⚠️ **GitHub.com Fallback** ⚠️