zgui

Custom elements

Build retained widgets that measure children and paint with zgui render primitives.

A custom element is one retained widget that application code implements. It can measure and place ordinary children, and it can paint quads and vector paths through zgui's render pipelines.

Use a custom element when composition would require many nodes or when built-in layout cannot place the children. Use a canvas when only a mutable shape list is necessary. Use a surface when another renderer already produces a texture.

Implement the trait

zgui::custom::custom returns an ordinary element builder and a typed handle:

use zgui::canvas::zgui_color::Color;
use zgui::geom::{DevicePx, Point, Rect, Size};
use zgui::prelude::*;

struct Meter {
    value: f32,
}

impl CustomElement for Meter {
    fn layout(&mut self, cx: &mut CustomLayoutCx<'_>) -> CustomMeasured {
        CustomMeasured {
            width: cx.known_width.unwrap_or(120.0 * cx.scale),
            height: cx.known_height.unwrap_or(8.0 * cx.scale),
            ..CustomMeasured::default()
        }
    }

    fn paint(&mut self, painter: &mut ScenePainter<'_>) {
        let size = painter.size();
        let scale = painter.scale();
        let color = painter.current_color();
        let track = Rect::new(Point::new(DevicePx(0.0), DevicePx(0.0)), size);
        painter.fill(track, 4.0 * scale, Color::srgb(0.2, 0.2, 0.25, 1.0));

        let fill = Rect::new(
            Point::new(DevicePx(0.0), DevicePx(0.0)),
            Size::new(DevicePx(size.width.0 * self.value), size.height),
        );
        painter.fill(fill, 4.0 * scale, color);
    }
}

let (meter, handle) = zgui::custom::custom(Meter { value: 0.4 });
let view = meter.class("meter");

The returned view supports classes, inline styles, listeners, attributes, and children. The custom implementation lives on the UI thread and keeps its state between frames.

Layout contract

layout returns the content size in device pixels. CSS owns the outer box. zgui applies width, height, min and max constraints, aspect ratio, padding, borders, and box-sizing around the custom answer.

The context supplies:

FieldMeaning
known_width, known_heightA size that layout has already fixed; use it when present
availableDefinite, min-content, or max-content space on each axis
scaleDevice pixels per CSS pixel
styleThe computed style of the custom element
final_passtrue when layout will keep this answer
accessMeasure, lay out, and place the element's children

Layout can call the implementation several times. Do not place children during a probe. On the final pass, use layout_child and then place_child for every child that the element owns. Use measure_child for a non-committing size probe. Repeated equivalent requests can use the normal layout cache.

Child indices follow document order. Coordinates passed to place_child are device pixels from the custom element's border-box corner. CustomMeasured can also report first and last baselines.

Paint contract

paint receives a constrained ScenePainter. Its coordinate origin is the content-box corner, and its units are device pixels. The painter applies the element's clip, transform, and inherited opacity.

Use:

  • fill for a solid rounded rectangle through the quad pipeline;
  • stroke for a solid rounded-rectangle border;
  • fill_path for a solid path;
  • shape for the full vector shape vocabulary;
  • glyphs for a pre-shaped glyph run from the application's font collection;
  • current_color for the element's computed CSS color.

Prefer quads for rectangles. A quad has the same rendering cost as an ordinary background. Paths use the vector rasterizer. The painter does not expose group boundaries or custom clips because those operations must remain balanced with the surrounding scene. Put an inner child in overflow: hidden when it needs an additional clip.

Custom primitives paint after the element's background and border and before its descendants. zgui records them in the fragment's retained paint chunk. It replays an unchanged custom element without calling paint again, including after frames in which the fragment was not visited.

Shape and draw glyph runs

Use the glyph seam when the custom element owns text layout, such as a terminal grid or a code view that positions one line at a time. Resolve Fonts in the component body and keep its Shaper in the retained element:

use zgui::app::LineRequest;
use zgui::custom::{PaintSlot, ShapedRunOwned};
use zgui::view::Ident;

let fonts = expect_context::<Fonts>();
let scale = use_window().scale().get_untracked();
let mut shaper = fonts.shaper();
let families = [Ident::new("JetBrains Mono")];
let request = LineRequest {
    families: &families,
    weight: 400,
    italic: false,
    size_device_px: 14.0 * scale,
    letter_spacing: 0.0,
    ligatures: false,
};
let runs: Vec<ShapedRunOwned> = shaper.shape_line("cargo check", &request);

shape_line never wraps. It returns owned, style-uniform runs in visual order. Font fallback can produce several runs. Each glyph also records the source byte of its cluster, which lets a cell view place a caret without shaping the line again.

Store the runs in the custom element and draw each at the same line-box origin:

for run in &self.runs {
    painter.glyphs(
        &run.as_run(PaintSlot(0)),
        self.origin,
        painter.current_color(),
    );
}

The brush slot is supplied when the owned run is borrowed; glyphs uses the explicit colour argument. A colour glyph keeps its embedded colours and uses only that argument's alpha.

LineRequest uses device pixels. Reshape when the text, requested style, registered fonts, or window scale changes. Keep the shaper and the owned runs between frames. The shaper owns reusable scratch space, and paint must not shape the same unchanged line again.

The FaceId values in a run belong to the Fonts collection that shaped it. The application provides that same collection to every window and to the frame's glyph rasterizer. Do not draw a run after replacing its font collection with another one.

Update and invalidate

Use the typed handle from an event handler or effect:

handle.update(|meter| meter.value = 0.7);
handle.repaint();

update only changes the retained state. It does not invalidate the element.

  • Call repaint when only emitted primitives changed.
  • Call relayout when the content size or child placement can change. relayout also repaints.

Both operations advance the custom element's revision. The revision is part of the paint-record signature, so a repaint cannot replay primitives from before the update.

The handle is cloneable, but it is UI-thread state. layout and paint also run on the UI thread during a frame. Installing a custom layout source keeps layout serial because this trait has no Sync contract. Do not block in either method and do not mutate the document from them.

The standard zgui::app() wiring installs the custom layout and paint sources. Code that drives zgui_runtime::App directly must install zgui_custom::sources() with App::with_custom.

On this page