Guillotine
A no-std, allocation-fre graphical user interface framework for embedded devices prioritizing efficiency and ergonomics. The UI declaration API is heavily inspired by GPUI. Built with (and inherits compatibility from)
embedded-graphics.
Demo
A demo Guillotine UI on a Waveshare ESP32-C6 1.47" LCD board from the shellyctl project:

Quickstart
This example uses the optional flexbox feature.
use ;
use ;
const CANVAS: Rgb565 = new;
const PANEL: Rgb565 = new;
const ACCENT: Rgb565 = new;
Frame Buffers
The framebuffer feature (on by default) provides a BufferedTarget type that
works with caller-provided frame buffers. It is highly recommended to use this
over DirectTarget, since it can dramatically increase frame rates and reduce
flicker to near unnoticeable if you can cover the whole display.
use ;
use ConstStaticCell;
use ;
/// The size of the frame buffer, should ideally cover your whole display
/// (width * height).
const FRAMEBUFFER_PIXELS: usize = 320 * 172;
/// Allocate the buffer in static memory with your
/// chosen color.
static FRAMEBUFFER: =
new;
Large buffers should generally use static storage (with static_cell).
Small buffers can live on the stack if the stack size permits it.
Note that the memory used by an Rgb565 buffer is pixels x 2 bytes.
Picking Sizes
Ideally, your frame buffer covers the whole display. However, you can provide a
buffer of any size, and render() will execute a greedy top-down traversal to
find the first subtree that fits.
Passing buffers that are smaller than the area of any element on the screen will
fall back to direct drawing.
[!NOTE] If your frame buffer doesn't fit the root element, it is painted directly before its children are considered. An opaque root background may therefore appear as a visible clear before buffered children are presented, so you may still notice a flicker.
Memory Management
Guillotine does not require an allocator, and uses heapless to store fixed-capacity node and text arrays inline.
This library exposes FrameStorage as the frontend for all memory management. The FrameStorage
signature looks like this:
Since heapless stores
data inline, capacity has to be specified upfront through const generics. FrameStorage contains 2 buffers:
nodeswith capacityN(number of nodes): stores the tree of UI elements. 64 by default.textwith capacityT(bytes): stores the UTF-8 bytes for all text elements present in the UI. 1024 by default.
It's recommended to tune N and T to fit the specifics
of your UI. FrameStorage exposes some methods to help you do that:
usage()returns the used length of both buffers.capacity()returns the capacity of both buffers.
[!NOTE] These buffers are only populated after calls to
render(), and will contain the element tree and text bytes for the currently rendered frame.
Layout
By default, Guillotine supports a limited subset of the flexbox layout engine. Rows are start-aligned containers along the horizontal axis with support for gaps, and columns are their vertical counterpart.
Flexbox
More complete flexbox support is gated behind a flexbox feature and is turned off by default,
because these flexbox properties require the layout tree to be traversed
twice, demanding extra compute and slightly render latency.
With flexbox on, you'll get access to CSS-like flexbox functionality, including justify-content and
align-items (for containers), and flex-grow and flex for items.
[!TIP] Some properties, like
AlignItems::Stretch, currently have a complexity ofO(N x D), whereNis the number of nodes andDis the tree depth. For small trees, this shouldn't be a problem, but keep it in mind if you have more complex layouts.
Refer to flexbox.rs for a flexbox layout example:

Styling
Insets and the box model
Margin, padding, and border widths accept CSS-like physical-edge shorthands:
cx.column
.margin // all edges
.padding // vertical, horizontal
.border // top, horizontal, bottom
.margin; // 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.
size, width, and height configure border-box dimensions: padding and border are placed inside
them, and margin is added outside. Width and height are independent; an omitted dimension is sized
automatically from the element's contents. Configured dimensions grow to contain padding and border
when parent constraints allow.
Examples
See the examples README.
Core Concepts
Declarative Definition
- Explain the idea of declaratively building your UI
Status: implemented ✅
Hybrid Immediate & Retained Mode
Status: unimplemented ❌
Similar tree-based layout to X
- GPUI
Status: implemented ✅
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.
Status: implemented ✅
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:
- Performant
- Very low memory footprint (no Slint / LVGL)
- Beautiful
Additionally, I wanted to learn what it would take to build something like this.
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/ParentElementtrait 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
-
TextStylefonts - Support for non-interactive elements:
- Row
- (formatted) Text
- Column
v0.1.0
- No alloc
v0.2.1
-
framebufferfeature with frame buffer support
v0.3.0
- Refactored layout engine
- Support flexbox layout (
flexboxfeature)
v0.3.1
- Support for absolute positioning
(relative by default). Introduces a new explicit
positionproperty toStyle.
v0.3.2
- New elements
- Dialogs / Modals (floating containers)
- Charts
- Spinner (going to be interesting as this is essentially a self-rendering element). Will
probably require a global
frame_rateto be set on theUi. However, this would be a full retained mode approach with an async polling loop. Another approach is to first implement incremental redrawing, and rely on the caller to callrender()at their chosen rate. However, eachrender()would do a bunch of compute, so probably not super efficient?
Backlog
- Mirrored debugger / inspector:
- When plugged into an MCU, this feature launches an interactive inspector on your host machine (think the Chrome inspector). Displays realtime total / per element memory consumption, frame rendering times, boxes, frame buffer utilization etc. Builds on the embedded-graphics simulator.
- Add memory usage for examples
- cargo binutils for examples in CI (cargo size). This will detect regressions.
- Documentation
- Guide for finding the ideal
FrameStoragecapacity.
- Guide for finding the ideal
- Benchmarks for:
- Frame rendering
- Frame drawing
- Stats for frame buffers (to determine optimal sizing)
- Incremental drawing behind an
inrementalfeature - Explicit behaviour:
- Hidden
- Visible
- Scroll
- Custom elements
- Support for interaction behind an
interactionfeature - Support interactive elements:
- Button
- Slider