Creating a gui tree

I read Using Trait Objects That Allow for Values of Different Types - The Rust Programming Language
And I need to write something similar: a nested control tree.
I have some questions

  1. What would be the best way to avoid basic field duplication for all 'derived' controls?
  2. What would be the best way to implement basic methods for ControlTrait for all 'derived' controls? A macro?
  3. Is dynamic dispatch avoidable, looping through the children? Making it magically static? I guess each method (location(), size(), draw() etc. will be a dynamic call).
  4. What would be the best way to implement a parent ControlTrait to Control? Rc or Arc or lifetime reference field? (Locations of controls are relative to it's parent and probably we need the parent for other purposes as well).

Down here a simplified example

pub struct Point {
    x: i32,
    y: i32,
}
pub struct Size {
    width: i32,
    height: i32,
}
pub trait ControlTrait {
    fn location(&self) -> Point;
    fn size(&self) -> Size;
    fn draw(&self);
}
// shared properties struct
pub struct Control {
    location: Point,
    size: Size,
    children: Vec<Box<dyn ControlTrait >>
}
pub struct Button {
    location: Point, // duplicated
    size: Size, // duplicated
    children: Vec<Box<dyn ControlTrait >>, // duplicated
    text: String,
}

It's possible to structure a UI to not necessarily need all of those things.

If you definitely know you need a certain set of fields for all controls, you can put them in a separate struct that you either keep inside the control structs or on the side. Rust favors composition and this is one way of doing it. For example:

struct ControlShared {
    location: Point,
    size: Vector,
    children: Vec<...>,
}

struct Button {
    shared: ControlShared,
    on_click: ...,
}

A macro could be an option, but bundling the shared properties helps a lot with the verbosity. But, also...

Yes, if you know you have a fixed number of control types, you can use an enum for the specific parts.

struct Control {
    location: Point,
    size: Vector,
    children: Vec<Control>,
    ty: ControlType,
}

enim ControlType {
    Button {
       on_click: ...,
    },
    ...
}

By turning the the structure inside-out, you get rid of the trait entirely. This is fine if you don't need third parties to extend it with more types.

I would recommend against this if you can. Having two-way references is generally tricky in Rust and you aren't going to have much fun making it work. Luckily, most UI are fine without them. An alternative approach is to have a feedback loop with message passing. This is essentially how modern frameworks, like React, Elm and a number of Rust frameworks do it.

The basic idea is that the parents own the shared state and passes parts of it down to the children. The user input then goes the other way, either as some sort of callback or as a return value of some update function. That triggers a state update and the feedback loop keeps going. This can all be done without too much spaghetti, but the specifics depend on what you need.

I hope this helps with finding a way forward.


A bit of a side note is that I personally find it more useful to compose behaviors than to have a number of "primitive controls", but that's not necessary to bother with for something small. For example, a button could be a rectangle with some content and an "on click" behavior.

1 Like

Thanks, certainly helpful.

I surely need some shared properties. This is a good way i know. The only thing is you get long accessors like button.shared.location.x but i can live with that.
Also the gui needs to be extendible with unknown controls, but maybe it is possible to have a fixed set of controls in combination with one custom type to avoid everything being dynamic.

I would recommend against this if you can. Having two-way references is generally tricky in Rust
True, passing down stuff by code could be more convenient.

A bit of a side note is that I personally find it more useful to compose behaviors than to have a number of "primitive controls"

I am not sure if I understand this...
In my mind I need a 'base' for basic properties / behaviour and specialization in Button, Label, Image etc. for handling events and drawing.

The upside is that you can operate on the shared data as a unit, but that's something you can experiment with and see what works best. A more complex case would perhaps also have more than one shared part for different purposes.

The "classic" idea with one control inheriting from another sort of requires that everything is classified as something. Another way of looking at things is to see a control as a composition of other controls. You will still need some primitives, but most of them can be "shapes" (text, icon, frame, ...).

My point with the side note is that I like to also separate "shape" from "behavior" so it's not necessary to ask oneself if a clickable icon inherits from a button or an icon. It's an icon shape with attached click behavior. That may be more than what you need, though.

Going back to the general composition model, any additional controls beyond the primitives would be built from the primitives, with some additional state. This can be expressed as a transformation from input parameters and child controls to some output control:

fn special_control(input: SomeInput, children: Vec<Control>) -> Control {
    frame_control(..., vec![
        list_control(..., children),
        button_control(...),
    ])
}

The largest unknown is handling state. You need to store it somewhere, either in the tree or outside it. What I want to point out is the mental model.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.