reclutch icon indicating copy to clipboard operation
reclutch copied to clipboard

Rust UI Core

Reclutch

Build Status

A strong foundation for building predictable and straight-forward Rust UI toolkits. Reclutch is:

  • Bare: Very little UI code is included. In practice it's a utility library which makes very little assumptions about the toolkit or UI.
  • Platform-agnostic: Although a default display object is provided, the type of display object is generic, meaning you can build for platforms other than desktop. For example you can create web applications simply by using DOM nodes as display objects while still being efficient, given the retained-mode design.
  • Reusable: Provided structures such as unbound queue handlers allow for the reuse of common logical components across widgets.

Overview

Reclutch implements the well-known retained-mode widget ownership design within safe Rust, following along the footsteps of popular desktop frameworks. To implement this behavior, three core ideas are implemented:

  • A widget ownership model with no middleman, allowing widgets to mutate children at any time, but also collect children as a whole to make traversing the widget tree a trivial task.
  • A robust event queue system with support for futures, crossbeam and winit event loop integration, plus a multitude of queue utilities and queue variations for support in any environment.
  • An event queue abstraction to facilitate just-in-time event coordination between widgets, filling any pitfalls that may arise when using event queues. Beyond this, it also moves the code to handle queues to the constructor, presenting an opportunity to modularize and reuse logic across widgets.

Note for MacOS

There appears to be a bug with shared OpenGL textures on MacOS. As a result, the opengl example won't work correctly. For applications that require rendering from multiple contexts into a single texture, consider using Vulkan or similar.

Also see:

  • Events and event queues
  • Otway

Example

All rendering details have been excluded for simplicity.

#[derive(WidgetChildren)]
struct Button {
    pub button_press: RcEventQueue<()>,
    graph: VerbGraph<Button, ()>,
}

impl Button {
    pub fn new(global: &mut RcEventQueue<WindowEvent>) -> Self {
        Button {
            button_press: RcEventQueue::new(),
            global_listener: VerbGraph::new().add(
                "global",
                QueueHandler::new(global).on("click", |button, _aux, _event: WindowEvent| {
                    button.button_press.emit_owned(());
                }),
            ),
        }
    }
}

impl Widget for Button {
    type UpdateAux = ();
    type GraphicalAux = ();
    type DisplayObject = DisplayCommand;

    fn bounds(&self) -> Rect { /* --snip-- */ }

    fn update(&mut self, aux: &mut ()) {
        // Note: this helper function requires that `HasVerbGraph` be implemented on `Self`.
        reclutch_verbgraph::update_all(self, aux);
        // The equivalent version which doesn't require `HasVerbGraph` is;
        let mut graph = self.graph.take().unwrap();
        graph.update_all(self, aux);
        self.graph = Some(graph);
    }

    fn draw(&mut self, display: &mut dyn GraphicsDisplay, _aux: &mut ()) { /* --snip-- */ }
}

The classic counter example can be found in examples/overview.


Children

Children are stored manually by the implementing widget type.

#[derive(WidgetChildren)]
struct ExampleWidget {
    #[widget_child]
    child: AnotherWidget,
    #[vec_widget_child]
    children: Vec<AnotherWidget>,
}

Which expands to exactly...

impl reclutch::widget::WidgetChildren for ExampleWidget {
    fn children(
        &self,
    ) -> Vec<
        &dyn reclutch::widget::WidgetChildren<
            UpdateAux = Self::UpdateAux,
            GraphicalAux = Self::GraphicalAux,
            DisplayObject = Self::DisplayObject,
        >,
    > {
        let mut children = Vec::with_capacity(1 + self.children.len());
        children.push(&self.child as _);
        for child in &self.children {
            children.push(child as _);
        }
        children
    }

    fn children_mut(
        &mut self,
    ) -> Vec<
        &mut dyn reclutch::widget::WidgetChildren<
            UpdateAux = Self::UpdateAux,
            GraphicalAux = Self::GraphicalAux,
            DisplayObject = Self::DisplayObject,
        >,
    > {
        let mut children = Vec::with_capacity(1 + self.children.len());
        children.push(&mut self.child as _);
        for child in &mut self.children {
            children.push(child as _);
        }
        children
    }
}

(Note: you can switch out the reclutch::widget::WidgetChildrens above with your own trait using #[widget_children_trait(...)])

Then all the other functions (draw, update, maybe even bounds for parent clipping) are propagated manually (or your API can have a function which automatically and recursively invokes for both parent and child);

fn draw(&mut self, display: &mut dyn GraphicsDisplay) {
    // do our own rendering here...

    // ...then propagate to children
    for child in self.children_mut() {
        child.draw(display);
    }
}

Note: WidgetChildren requires that Widget is implemented.

The derive functionality is a feature, enabled by default.

Rendering

Rendering is done through "command groups". It's designed in a way that both a retained-mode renderer (e.g. WebRender) and an immediate-mode renderer (Direct2D, Skia, Cairo) can be implemented. The API also supports Z-Order.

struct VisualWidget {
    command_group: CommandGroup,
}

impl Widget for VisualWidget {
    // --snip--

    fn update(&mut self, _aux: &mut ()) {
        if self.changed {
            // This simply sets an internal boolean to "true", so don't be afraid to call it multiple times during updating.
            self.command_group.repaint();
        }
    }

    // Draws a nice red rectangle.
    fn draw(&mut self, display: &mut dyn GraphicsDisplay, _aux: &mut ()) {
        let mut builder = DisplayListBuilder::new();
        builder.push_rectangle(
            Rect::new(Point::new(10.0, 10.0), Size::new(30.0, 50.0)),
            GraphicsDisplayPaint::Fill(Color::new(1.0, 0.0, 0.0, 1.0).into()),
            None);

        // Only pushes/modifies the command group if a repaint is needed.
        self.command_group.push(display, &builder.build(), Default::default(), None, true);

        draw_children();
    }

    // --snip--
}

Updating

The update method on widgets is an opportunity for widgets to update layout, animations, etc. and more importantly handle events that have been emitted since the last update.

Widgets have an associated type; UpdateAux which allows for a global object to be passed around during updating. This is useful for things like updating a layout.

Here's a simple example;

type UpdateAux = Globals;

fn update(&mut self, aux: &mut Globals) {
    if aux.layout.node_is_dirty(self.layout_node) {
        self.bounds = aux.layout.get_node(self.layout_node);
        self.command_group.repaint();
    }

    self.update_animations(aux.delta_time());

    // propagation is done manually
    for child in self.children_mut() {
        child.update(aux);
    }

    // If your UI doesn't update constantly, then you must check child events *after* propagation,
    // but if it does update constantly, then it's more of a micro-optimization, since any missed events
    // will come back around next update.
    //
    // This kind of consideration can be avoided by using the more "modern" updating API; `verbgraph`,
    // which is discussed in the "Updating correctly" section.
    for press_event in self.button_press_listener.peek() {
        self.on_button_press(press_event);
    }
}

Updating correctly

The above code is fine, but for more a complex UI then there is the possibility of events being processed out-of-order. To fix this, Reclutch has the verbgraph module; a facility to jump between widgets and into their specific queue handlers. In essence, it breaks the linear execution of update procedures so that dependent events can be handled even if the primary update function has already be executed.

This is best shown through example;

fn new() -> Self {
    let graph = verbgraph! {
        Self as obj,
        Aux as aux,

        // the string "count_up" is the tag used to identify procedures.
        // they can also overlap.
        "count_up" => event in &count_up.event => {
            click => {
                // here we mutate a variable that `obj.template_label` implicitly/indirectly depends on.
                obj.count += 1;
                // Here template_label is assumed to be a label whose text uses a template engine
                // that needs to be explicitly rendered.
                obj.template_label.values[0] = obj.count.to_string();
                // If we don't call this then `obj.dynamic_label` doesn't
                // get a chance to respond to our changes in this update pass.
                // This doesn't invoke the entire update cycle for `template_label`, only the specific part we care about; `"update_template"`.
                reclutch_verbgraph::require_update(&mut obj.template_label, aux, "update_template");
                // "update_template" refers to the tag.
            }
        }
    };
    // ...
}

fn update(&mut self, aux: &mut Aux) {
    for child in self.children_mut() {
        child.update(aux);
    }

    reclutch_verbgraph::update_all(self, aux);
}

In the verbgraph module is also the Event trait, which is required to support the syntax seen in verbgraph!.

#[derive(Event, Clone)]
enum AnEvent {
    #[event_key(pop)]
    Pop,
    #[event_key(squeeze)]
    Squeeze(f32),
    #[event_key(smash)]
    Smash {
        force: f64,
        hulk: bool,
    },
}

Generates exactly;

impl reclutch::verbgraph::Event for AnEvent {
    fn get_key(&self) -> &'static str {
        match self {
            AnEvent::Pop => "pop",
            AnEvent::Squeeze(..) => "squeeze",
            AnEvent::Smash{..} => "smash",
        }
    }
}

impl AnEvent {
    pub fn unwrap_as_pop(self) -> Option<()> {
        if let AnEvent::Pop = self {
            Some(())
        } else {
            None
        }
    }

    pub fn unwrap_as_squeeze(self) -> Option<(f32)> {
        if let AnEvent::Squeeze(x0) = self {
            Some((x0))
        } else {
            None
        }
    }

    pub fn unwrap_as_smash(self) -> Option<(f64, bool)> {
        if let AnEvent::Smash{force, hulk} = self {
            Some((force, hulk))
        } else {
            None
        }
    }
}

get_key is used to find the correct closure to execute given an event and unwrap_as_ is used to extract the inner information from within the given closure (because once get_key is matched then we can be certain it is of a certain variant).

License

Reclutch is licensed under either

at your choosing.

This license also applies to all "sub-projects" (event, derive and verbgraph).