Reactive components in rust, designed to make GTK more manageable

Related tags

GUI gflux
Overview

gflux

gflux is a tiny experimental reactive component system for rust, designed to make GTK more manageable.

gflux:

  • is about 300 lines of code
  • contains no macros
  • is independent of any particular GUI library
  • tracks which components have had their model's mutated
  • is orthogonal to any model diffing code needed to optimize updates

Why GTK is hard in rust

Let's look at a GTK Button. You register a callback to handle a button click with this method

fn connect_clicked<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId

The 'static lifetime bound means that the callback function provided can only capture static references. This makes sense, because a GTK Button is a reference counted object that might live beyond the current stack frame. But I believe this is the single greatest source of difficulty of GTK in rust.

There are usually 3 ways to handle this:

  • Wrap your application state in Rc<RefCell<T>>. This works when your application state is simple. For complex applications, you don't want every widget to be aware of your entire application state. This means you end up putting Rc<RefCell<T>> all over your application state. This gets unmanageable.
  • Send a message to a queue. Many rust gtk component frameworks that currently exist, choose this method. This works. But occasionally, you want to provide a callback that blocks an action based on its return value. For example, a delete-event on a window in GTK can return true or false. Sending a message requires picking a return value without access to your application state.
  • Create custom widgets and put your application state inside of them. This is the GTK way. This works, if you don't mind putting your application state inside of object oriented widgets and not maintaining separation.

gflux components

gflux works by building a component tree. Each component, on creation, is given a "lens" function. A chain of lens functions from each component, works together to always be able to go from the global application state down to the state that an individual component cares about.

When a component is created, a lens function in provided. But first, let's look at an example of a simple component for a task in a todo list.

use crate::{AppState, Task};
use glib::clone;
use gtk::{prelude::*, Align};
use gflux::{Component, ComponentCtx};

pub struct TaskComponent {
    hbox: gtk::Box,
    label: gtk::Label,
}
  
impl Component for TaskComponent {
    type GlobalModel = AppState;
    type Model = Task;
    type Widget = gtk::Box;
    type Params = ();

    // The root widget
    fn widget(&self) -> Self::Widget {
        self.hbox.clone()
    }

    // Called when the component is constructed
    fn build(ctx: ComponentCtx<Self>, params: ()) -> Self {
        let checkbox = gtk::CheckButton::new();
        checkbox.connect_toggled(clone!(@strong ctx => move  |cb| {
            ctx.with_model_mut(|task| task.done = cb.is_active());
        }));
  
        let label = gtk::Label::new(None);

        let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        hbox.append(&checkbox);
        hbox.append(&label);
  
        // rebuild will be called immediately afterwards
        Self { hbox, label }
    }

    // Called after a change in state is detected
    fn rebuild(&mut self, ctx: ComponentCtx<Self>) {
        let name = ctx.with_model(|task| task.name.clone());
        if ctx.with_model(|task| task.done) {
            // If the task is done, make it strikethrough
            let markup = format!("<s>{}</s>", glib::markup_escape_text(&name));
            self.label.set_markup(&markup);
        } else {
            self.label.set_text(&name);
        }
    }
}

When rebuild is called, it's up to the component to make sure the widgets match the Task struct in the component's model.

ComponentCtx<Self> does all of the component bookkeeping for you. And most importantly, it provides access to the component's model, which is a simple Task struct. It provides two methods for this: with_model and with_model_mut.

with_model_mut marks the component a dirty so that rebuild will be called soon afterwards.

Initializing a component tree

// Create the global application state
let global = Rc::new(RefCell::new(AppState { ... }));

// Create the root of the component tree
let mut ctree = ComponentTree::new(global);

Creating a component

Given a "lens" function, and parameters, you can create new component. A similar method exists on a ComponentCtx to create a child component of an existing component.

let task_comp: ComponentHandle<TaskComponent> = ctree.new_component(|app_state| app_state.get_task_mut(), ());

Change tracking

A component tree provides two methods:

  • on_first_change to register a callback that gets called every time the component tree moves from a completely clean state to a dirty state.
  • rebuild_changed to call the rebuild method on all components that have been marked as dirty by with_model_mut, and all of their ancestor components, from the oldest to the newest.

With both of these methods, we can register the GTK main loop to always rebuild any component whose model has been mutated:

// When the tree first moves from clean to dirty, use `idle_add_local_once`
// to make sure that `ctree.rebuild_changed()` later gets called from the gtk
// main loop
ctree.on_first_change(clone!(@strong ctree => move || {
    glib::source::idle_add_local_once(clone!(@strong ctree => move || ctree.rebuild_changed()));
}));

Guidelines to having a good time

  • Creating a component returns a ComponentHandle. Keep these objects alive if your component still exists.
  • Calls to with_model and with_model_mut should be kept short, and no GTK functions should be called from inside of them. Copy out parts of the model before calling GTK functions.
  • Avoid calling GTK functions that recursively call the main loop, such as dialog.run()

Inspirations

You might also like...
GTK 4 front-end to ChatGPT completions written in Rust
GTK 4 front-end to ChatGPT completions written in Rust

ChatGPT GUI Building git clone [email protected]:teunissenstefan/chatgpt-gui.git cd chatgpt-gui cargo build --release Todo Connect insert_text to only al

An example of searching iBeacon using gtk-rs and btleplug.
An example of searching iBeacon using gtk-rs and btleplug.

Beacon Searcher Screenshot Compile & Run Install GTK 3 dev packages: macOS: $ brew install gtk+3 $ brew install gnome-icon-theme Debian / Ubuntu: $ s

A collection of components and widgets that are built for bevy_ui and the ECS pattern

Widgets for Bevy UI A collection of components and widgets that are built for bevy_ui and the ECS pattern. Current State This was started recently and

Test bed for gtk-rs-core experiments

Rust GObject Experiments class macro #[gobject::class(final)] mod obj { #[derive(Default)] pub struct MyObj { #[property(get, set)]

Make HTTP requests and test APIs. Work in progress.
Make HTTP requests and test APIs. Work in progress.

Cartero Make HTTP requests and test APIs. Motivation This project exists because there aren't many native graphical HTTP testing applications / graphi

Build smaller, faster, and more secure desktop applications with a web frontend.
Build smaller, faster, and more secure desktop applications with a web frontend.

TAURI Tauri Apps footprint: minuscule performance: ludicrous flexibility: gymnastic security: hardened Current Releases Component Descrip

A simple, cross-platform GUI automation module for Rust.

AutoPilot AutoPilot is a Rust port of the Python C extension AutoPy, a simple, cross-platform GUI automation library for Python. For more information,

A data-first Rust-native UI design toolkit.
A data-first Rust-native UI design toolkit.

Druid A data-first Rust-native UI toolkit. Druid is an experimental Rust-native UI toolkit. Its main goal is to offer a polished user experience. Ther

The Rust UI-Toolkit.
The Rust UI-Toolkit.

The Orbital Widget Toolkit is a cross-platform (G)UI toolkit for building scalable user interfaces with the programming language Rust. It's based on t

Owner
Brian Vincent
Brian Vincent
Sycamore - A reactive library for creating web apps in Rust and WebAssembly

Sycamore What is Sycamore? Sycamore is a modern VDOM-less web library with fine-grained reactivity. Lightning Speed: Sycamore harnesses the full power

Sycamore 1.8k Jan 5, 2023
Idiomatic, GTK+-based, GUI library, inspired by Elm, written in Rust

Relm Asynchronous, GTK+-based, GUI library, inspired by Elm, written in Rust. This library is in beta stage: it has not been thoroughly tested and its

null 2.2k Dec 31, 2022
Provides Rust bindings for GTK libraries

The gtk-rs organization aims to provide safe Rust binding over GObject-based libraries

null 431 Dec 30, 2022
Highly customizable finder with high performance. Written in Rust and uses GTK

Findex Highly customizable finder with high performance. Written in Rust and uses GTK Installation Automatic Binary Clone from https://aur.archlinux.o

MD Gaziur Rahman Noor 442 Jan 1, 2023
Rust bindings and wrappers for GLib, GDK 3, GTK+ 3 and Cairo.

THIS REPOSITORY IS DEPRECATED SEE: https://github.com/rust-gnome rgtk Rust bindings and wrappers for GLib, GDK 3, GTK+ 3 and Cairo. Building rgtk expe

Jeremy Letang 124 Jul 10, 2022
A GUI for NordVPN on Linux that maintains feature parity with the official clients, written with Rust and GTK.

Viking for NordVPN This project aims to provide a fully usable and feature-complete graphical interface for NordVPN on Linux. While it attempts to clo

Jacob Birkett 2 Oct 23, 2022
Provides Rust bindings for GTK libraries

gtk3-rs The gtk-rs organization aims to provide safe Rust binding over GObject-based libraries. You can find more about it on https://gtk-rs.org. This

null 431 Dec 30, 2022
Graphical font editor (GTK + Rust)

gerb *gerb ʰ-: reconstructed Proto-Indo-European root, meaning to carve gerb: a WIP font editor in gtk3 and rust Introduction gerb is an experimental,

Manos Pitsidianakis 40 Jan 1, 2023
A Rust library to parse Blueprint files and convert them into GTK UI files

?? gtk-ui-builder A Rust library to parse Blueprint files and convert them into GTK UI files Inspired by the Blueprint project Example 1 - blueprints

Observer KRypt0n_ 5 Oct 22, 2022
A powerful color picker and formatter, built with GTK and Rust

Eyedropper A powerful color picker and formatter. More screenshots Features Pick a Color Enter a color in Hex-Format Parse RGBA/ARGB Hex-Colors View c

Jonathan 108 Dec 24, 2022