Skip to main content

Crate guillotine

Crate guillotine 

Source
Expand description

§Guillotine

§Guillotine

CI Crates.io Docs.rs Ask DeepWiki

A no-std graphical user interface framework for embedded devices prioritizing resource efficiency and ergonomics. The UI declaration API is heavily inspired by GPUI.

Works everywhere embedded-graphics works.

§Demo

A demo Guillotine UI on a Waveshare ESP32-C6 1.47“ LCD board:

use embedded_graphics::{
    prelude::*, 
    mock_display::MockDisplay, 
    pixelcolor::Rgb565,
    mono_font::ascii::FONT_9X18_BOLD,
};

use guillotine::*; 

struct BasicView {
    greeting: &'static str,
}

impl Render for BasicView {
    // Render lets you declaratively build your UI tree.
    fn render<'a>(&'a self, _cx: &mut Context) -> impl IntoElement<Element = Element<'a>> {
        column()
            .padding(10)
            .margin(10)
            .border(2)
            .border_color(Rgb565::BLUE)
            .child(text(self.greeting).background(Rgb565::RED).margin(5))
            .child(text("GUILLOTINE").margin(5).font(Font::mono(&FONT_9X18_BOLD)))
    }
}

fn main() {
    // Display should implement embedded_graphics DrawTarget
    let display = MockDisplay::new();

    let view = BasicView {
        greeting: "Hello world!"
    };

    let mut ui = Ui::new(display);

    // Render the view
    ui.render(&view);
}

§Core Concepts

§Declarative Definition

  • Explain the idea of declaratively building your UI

§TODO: Hybrid Immediate & Retained Mode

§TODO: Similar tree-based layout to X

  • GPUI

§State Management

§Layout Engine

  • Conceptually similar to Flutter (i.e. constraints go down, sizes go up)
    • constraints flow downward, sizes flow upward, positions flow downward
  • Requirement: single pass.

§API

extern crate alloc;

use embedded_graphics::{prelude::*, mock_display::MockDisplay, pixelcolor::Rgb565};

use guillotine::*;

struct Home {
    show_button: bool,
    header: &'static str,
}

impl Render for Home {
    fn render<'a>(&'a self, _cx: &mut Context) -> impl IntoElement<Element = Element<'a>> {
        row()
            .bg(Rgb565::RED)
            .child(text(self.header))
            .when(self.show_button, |row| row.child(text("Click me")))
            .children([text("Copyright"), text("ACME Corp")])
    }
}

struct Page {
    power: f32,
    current: f32,
    voltage: f32,
}

impl Render for Page {
    fn render<'a>(&'a self, _cx: &mut Context) -> impl IntoElement<Element = Element<'a>> {
        column()
            .child(text(alloc::format!("Power: {}", self.power)))
            .child(text(alloc::format!("Current: {}", self.current)))
            .child(text(alloc::format!("Voltage: {}", self.voltage)))
    }
}

fn main() {
    let display = MockDisplay::new();

    let mut ui = Ui::new(display);

    let mut home = Home {
        show_button: false,
        header: "Some Title",
    };

    ui.render(&home).unwrap();

    home.show_button = true;

    ui.render(&home).unwrap();

    let mut page = Page { 
        power: 50.0,
        voltage: 230.0,
        current: 0.2173
    };

    ui.render(&page);
}

Element trees use the display’s PixelColor type throughout. Rgb565 views keep the API shown above; other targets select their color once at the Render<Color> boundary, after which element constructors and style methods infer it. See the binary-color example.

§Insets and the box model

Margin, padding, and border widths accept CSS-like physical-edge shorthands:

column()
    .margin(10)                 // all edges
    .padding((4, 8))            // vertical, horizontal
    .border((1, 2, 3))          // top, horizontal, bottom
    .margin((4, 8, 12, 16));    // top, right, bottom, left

Use Insets::new(top, right, bottom, left) when a named value is clearer. Insets are non-negative pixel lengths. Guillotine doesn’t currently support percentages, auto, logical edges, negative margins, margin collapsing, per-edge border colors, or border styles. Adjacent margins in rows and columns add together.

Style::size is the border-box size: padding and border are placed inside it, and margin is added outside it. The box grows to contain its padding and border when parent constraints allow.

§Examples

To run the examples, you need to enable the simulator feature. This pulls in a bundled sdl2 for opening windows. You will need cmake to compile it.

cargo run --example power_monitor --features simulator

§Roadmap

§v0.0.1

  • Try to mimic GPUI declaration style: https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/hello_world.rs
  • Low-level Element / ParentElement trait for custom elements and widgets
  • Support generic PixelColor
  • Full immediate mode redrawing
  • Make repo ready for publishing:
    • README documentation (a la Dioxus)
    • Rustdoc documentation
    • Examples
      • Sizing (insets)
      • Fonts
      • Cool
      • ESP32
    • Fix exports
    • Dual Apache / MIT license
    • Fix sdl2 vendoring for embedded-graphics-simulator
  • Benchmarks for Frame building
  • TextStyle fonts
  • Support for non-interactive elements:
    • Row
    • (formatted) Text
    • Column
    • Spinner
  • Container gaps

§v0.1.0

  • Custom render modes: Incremental (only repaint changed regions, requires more memory), or Redraw (full redraw on every render, lowest memory footprint). Either as a feature or runtime flag.
  • Define inremental redrawing triggers / states:
enum DrawState {
    Clean,
    Paint,     // same geometry; repaint old/current bounds
    Layout,    // size or position changed
    Structure, // child added, removed, moved, or keyed differently
    Full,      // theme, rotation, display reset, etc.
}
  • New elements
    • Dialogs / Modals (floating containers)
    • Charts
  • Overflow behaviour:
    • Visible
    • Clip
  • profile feature with defmt logs
  • No alloc
  • Custom elements
  • Alignment
  • Support for interaction
  • Support interactive elements:
    • Button
    • Slider

§Why?

I was trying to build a clean-looking dashboard on a small LCD screen powered by an ESP32-C6, that’s supposed to monitor and display the power consumption of my home lab (project here). I wanted to do this in Rust, with the esp-rs ecosystem. The ecosystem is quite mature, but I couldn’t really find a UI framework that was:

  1. Performant
  2. Very low memory footprint (no Slint / LVGL)
  3. Beautiful

Additionally, I wanted to learn what it would take to build something like this.

§Prior Work & Inspiration

Re-exports§

pub use style::Insets;
pub use style::Style;
pub use style::StyledElement;

Modules§

style
Styling utilities.

Structs§

Column
A vertical container declaration.
ColumnStyle
Style for this column.
Context
For now, unused. In the future, will be used for context management, such as:
Row
A horizontal container declaration.
RowStyle
Style for this row.
Text
An ephemeral text declaration.
TextStyle
Text style.
Theme
Colors used by the UI when an element doesn’t specify a color explicitly.
Ui
The Ui struct is the main entrypoint for the Guillotine UI framework. It manages the display and takes care of rendering the UI from a tree of Elements, with Self::render.

Enums§

Element
An ephemeral element declaration produced while rendering a frame.
Font
Font for text rendering.

Traits§

FluentBuilder
A helper trait for building complex objects with imperative conditionals in a fluent style.
IntoElement
A value that can be converted into Guillotine’s closed element enum.
ParentElement
This is a helper trait to provide a uniform interface for constructing elements that can accept any number of any kind of child elements
Render
The Render trait is implemented by types that can be rendered into an Element. Use this trait to define UI elements.
TextStyledElement
A trait for elements that can be styled with text properties.

Functions§

column
Creates an empty vertical container.
row
Creates an empty horizontal container.
text
Creates a text declaration borrowing content for this render.