An immediate-mode GUI library in Mach.
Widgets accumulate an indexed draw list of colored, textured triangles and report interaction. The consumer feeds input each frame, runs the widgets, and uploads and renders the resulting vertices and indices, so blit stays out of the windowing and rendering.
use blit;
# once, at startup (fallible calls return err[allocator.Error]):
val made: err[allocator.Error] = blit.context.init(?ctx, ?a);
if (sel made.err) { ... }
# per frame:
blit.context.begin(?ctx, in, screen_w, screen_h);
val panel: blit.widget.Block = blit.widget.begin_panel(?ctx, 8.0::f32, 8.0::f32, 200.0::f32);
blit.widget.text(?ctx, "controls");
if (blit.widget.button(?ctx, "step")) { ... }
blit.widget.checkbox(?ctx, "running", ?running);
blit.widget.slider_f(?ctx, "rate", ?rate, 0.0::f32, 1.0::f32);
blit.widget.end_panel(?ctx, panel);
blit.context.end(?ctx);
# upload the atlas pages that changed (see Rendering), then the vertices and
# indices, and draw each run with its texture and scissor.
use blit; binds the surface; reach everything through its submodule:
blit.draw, blit.path, blit.icon, blit.glyph, blit.bitmap, blit.font, blit.atlas,
blit.input, blit.layout, blit.hit, blit.band, blit.interact, blit.field, blit.edit, blit.ease, blit.writer, blit.theme, blit.style, blit.context, blit.state, blit.anim, blit.text, blit.widget, blit.menu, blit.controls, blit.value, blit.color, blit.table, blit.textarea, blit.chart, blit.payload, blit.dnd, blit.tabs, blit.dock, blit.driver, blit.editor, blit.inspect. A submodule can also be
imported directly, e.g. use w: blit.widget;.
Text comes from glyph sources, records of functions over each source's own
state (blit.glyph.GlyphSource). blit owns UTF-8 decoding, line breaking, the
glyph cache and the atlas, and asks a source only for what a face knows:
pub rec GlyphSource {
self: ptr;
line: fun(ptr, f32) LineMetrics; # (self, scale)
glyph: fun(ptr, u32, f32, *Glyph) bool; # (self, glyph id, scale, out)
kern: fun(ptr, u32, u32, f32) f32; # (self, left id, right id, scale), or nil
raster: fun(ptr, u32, f32, *u8, usize) bool; # (self, glyph id, scale, coverage, stride)
shape: fun(ptr, *u32, usize, f32, *Shaped, usize) usize; # (self, run, n, scale, out, cap), or nil
}
- Glyph ids, with an optional shaper. Everything past shaping is keyed on
glyph id. A shaping source (ligatures, combining marks, complex scripts)
turns a line's codepoints into
Shapedglyphs: an id, the cluster it came from, an advance with kerning included and an offset. A source withshapenil has glyph ids that are its codepoints, one to one, kerned pair by pair throughkern, as the bitmap font and a simple TrueType face do. Such a source only addsshape: nilto the record it filled before. - Metrics are floats in pixels at the scale asked for:
LineMetricsis ascent, descent and gap, and aGlyphis its advance, the rect its bitmap covers relative to the pen on the baseline (y down), and the bitmap's size in texels. Scale is a text style's size times the interface scale (ctx.scale, 1.0 by default), so text at 200% is rasterised at that size, not stretched. - Glyphs come on demand. A glyph is measured the first time it is laid out and rasterised the first time it is drawn, then cached by source, glyph id and scale. Measuring never rasterises.
- Codepoints, not bytes. Text is UTF-8. A codepoint a non-shaping source
lacks draws as U+FFFD, or
?when it lacks that too. A shaper falls back itself, its glyph id 0 being the missing glyph. Control codepoints take no space and a newline starts the next line. - Bitmap by default.
blit.bitmap.source(), the built-in 8x8 font, is source 0, so a context needs no configuration.blit.context.set_glyph_sourcereplaces source 0 between frames, such as with a TrueType face from the host, andadd_glyph_sourceholds more. blit itself never depends on one. - The atlas evicts. When every page is full, the page least recently drawn from is cleared and refilled. A page drawn from this frame is never evicted, so a glyph drawn this frame is never dropped, and a glyph whose page was evicted is rasterised again when it is next drawn.
A style (blit.font.Style) names a glyph source and a size relative to that
source's own, so one face serves body text, headings and small print. Style 0,
blit.font.STYLE_BODY, is source 0 at size 1.0. Register more with
blit.context.add_style, change one with set_style, and draw in one with
push_style/pop_style. Every text call, text_at, text_span, glyph,
text_width, line_height, and every widget that draws text, uses the
current style, and every frame starts in the body style.
val heading: opt[u32] = blit.context.add_style(?ctx, blit.font.Style{source: 0, size: 2.0::f32});
blit.context.push_style(?ctx, heading.some);
blit.widget.text(?ctx, "Simulation");
blit.context.pop_style(?ctx);
blit.context.text_width_n measures a byte range, and glyph_advance steps
one codepoint at a time for layout built outside blit, without a shaper's
ligatures.
blit.text.fit cuts one line to a width with an ellipsis at the end
(CUT_END), at the start for paths (CUT_START) or in the middle
(CUT_MIDDLE), as byte offsets into the caller's string, and fit_at draws
it. The ellipsis is U+2026 when the style's source has it, else ....
A rich line is a run of blit.text.Spans, each text in its own style and
color, on one shared baseline: spans_at draws it, spans_width and
spans_line measure it, and blit.widget.rich draws it at the cursor.
var sp: [4]blit.text.Span;
sp[0] = blit.text.Span{s: "gen ", style: blit.font.STYLE_BODY, color: t.text_dim};
sp[1] = blit.text.Span{s: "13", style: bold, color: t.text};
sp[2] = blit.text.Span{s: " pop ", style: blit.font.STYLE_BODY, color: t.text_dim};
sp[3] = blit.text.Span{s: "9,252", style: bold, color: t.text};
blit.widget.rich(?ctx, ?sp[0], 4);
Every color and length a widget draws with comes from a theme
(blit.theme.Theme) in two layers. The tokens are what a look is designed
in: a palette (panel, window, text, text_dim, text_faint, accent,
control, track, handle and the rest) and metrics (row, gap, container
padding pad, control inset inset, corner, edge_w, ...). The styles
are one blit.theme.Style per registered widget kind (blit.theme.BUTTON,
SLIDER, WINDOW_TITLE, MENU_ITEM, ...): its corner radii, padding, inset, margins,
least sizes and shadow, and a Paint per state (NORMAL, HOT, ACTIVE,
ON, DISABLED, FOCUSED) with its fill, gradient end, text, secondary mark,
border and shadow colors. Widgets draw from the styles alone, and no widget
holds a color or a size of its own. Charts read the plot fields (series,
line_w, tick, area) as they are, and animation the motion fields
(hover_time, hover_ease, open_time, open_ease, scroll_time,
scroll_ease: seconds, and a blit.ease curve, 0 linear, 1 out, 2 in and
out) through blit.theme.motion (see Animation). The tokens focus and
focus_w derive the focus_ring kind, the ring the control the keyboard
reached wears.
blit.theme.derive(?t) builds every style from the tokens, so after changing
tokens in code, derive. theme.set changes one field, and for a token it
moves every style field that took its value from the old token while keeping
the ones set by hand (rederive).
val look: *blit.theme.Theme = blit.context.theme_of(?ctx);
blit.theme.dark(look);
look.accent = blit.draw.rgba(0.37::f32, 0.83::f32, 0.63::f32, 1.0::f32);
look.corner = 3.0::f32;
blit.theme.derive(look);
blit.theme.style(look, blit.theme.BUTTON).states[blit.theme.HOT].fill_to = blit.draw.hex(0x2A2E37, 1.0::f32);
blit.context.set_scale(?ctx, 2.0::f32);
- Zero configuration. A context sets its theme up with the default look,
and
theme_ofhands back the live one to adjust in place. A theme of your own is set up withblit.theme.init(?t, ?a), released withfree, and copied into a context withblit.context.set_theme(?ctx, ?t).warnis there for consumers drawing their own content in the same palette. No widget uses it. - Built-in themes.
default(t)is blit's own look (dark, square, 4 pixel spacing),dark(t)a rounded dark look with shaded buttons and shadows under floating surfaces,light(t)a light one andcontrast(t)a high-contrast one with outlined controls and wide focus rings. Each gives a theme its look in place, keeping its kinds, andbuiltin(t, name)andBUILTIN_NAMESreach them by name. - Kinds are a registry.
blit.theme.register_kind(t, name, doc, derive)adds a widget kind and returns its handle:derive(t, kind, ?style)builds its style from the tokens, starting fromblit.theme.base(t), and runs again whenever the styles are derived. The built-in kinds are registered the same way when a theme is set up, in the order of their constants. A registered kind is addressable by its name everywhere a built-in one is: paths, wildcards, TOML tables, classes and the editor. A module adds kinds of its own this way, asblit.dockdoes (dock_space,dock_splitter,dock_zone,dock_preview, seedock.register_kinds). - The box painter.
blit.context.box(?ctx, kind, state, x0, y0, x1, y1)paints a kind in a state with the shapes of Shapes & paths: its shadow (a rounded rect faded overshadow_blur), a flat face, or a vertical gradient whenfill_todiffers fromfill, and its outline.box_partrounds only some corners and outlines some sides, andbox_shadowandbox_facepaint the two halves apart. A custom widget paints through it and looks like the built-ins.blit.theme.pick(hot, active, on, focused)is the state they paint in: on, then focused, then active, then hot. - One scale.
set_scalemultiplies every theme length and the size text is laid out and rasterised at (see glyph sources), so 2.0 doubles the whole interface, layout and hit rects included, for a HiDPI display or a user's choice. Lengths you pass (panel and window widths, dock sizes, your own geometry) stay in pixels.blit.context.px(?ctx, v)scales them to match.blit.widget.row_gap(?ctx),text_row_height,control_row_heightandcontrol_height(a control row without its gap) report the spacing at the current theme and scale. - Rows fit their text. A control row is its style's
min_htall, or a line of text plus itsinsetabove and below if that is taller, so a larger glyph source never overflows its rows.
Every field has a path: palette.accent, metrics.gap, plot.series.3,
button.radius for a style field and button.hot.fill for a paint field,
with * for every kind or state. blit.style pushes a value onto every field
a path names until the matching pop, so a scope of widgets draws differently
and nothing else does:
blit.style.push_color(?ctx, "button.*.fill", blit.draw.hex(0xB03030, 1.0::f32));
blit.style.push_length(?ctx, "*.*.border_w", 0.0::f32);
blit.widget.button(?ctx, "delete");
blit.style.pop(?ctx);
blit.style.pop(?ctx);
A paint's fill_to follows its fill: every write of a fill through a path
(a push, a class, theme.set and set_path, a TOML document) writes fill_to
too, so a fill alone gives a flat face. Writing fill_to as well, in the same
push, class or document in any order, or in a later push, gives the gradient,
and a pop restores both. A save writes fill_to after fill whenever the two
differ, so the gradient reads back.
push_color, push_length, push_radii, push_edges and the untyped push
refuse a path naming no field of their unit, and still open an empty push so
every push pairs with its pop. A frame left with pushes open is restored at
the next begin.
A class is a named list of such settings, applied to a widget call with
push_class and pop:
val danger: opt[u32] = blit.style.add_class(?ctx, "danger");
blit.style.set(?ctx, danger.some, "button.*.fill", blit.theme.of_color(red));
blit.style.set(?ctx, danger.some, "button.hot.fill", blit.theme.of_color(bright_red));
# per frame:
blit.style.push_class(?ctx, danger.some);
blit.widget.button(?ctx, "delete");
blit.style.pop(?ctx);
Themes are TOML, read with std.data.toml and written with blit.writer.
blit.style.load_theme(?ctx, text) replaces the context's theme with one a
document describes, and reads its classes, so a running interface reloads its
look whenever the host reads the file again. save_theme(?ctx) writes the
theme and the classes back out. blit.theme.load(t, text) and save read and
write a theme alone. A document styles a registered kind by name, and a kind
not registered when it is read is refused like any unknown key, so a host
registers its kinds before loading.
base = "dark" # a built-in to start from, default when absent
[palette] # tokens: every style built from one follows it
accent = "#5b9dff"
[metrics]
corner = 6
[plot]
series = ["#5b9dff", "#f0a040"]
[button] # a kind's style fields
inset = [8, 4, 8, 4] # one length for every side, or left, top, right, bottom
[button.hot] # a kind's paint fields in a state
fill = "#3a4050"
fill_to = "#2a2e37"
["*".focused] # every kind
border_w = 2.0
[class.danger] # a class: paths and their values
"button.*.fill" = "#b03030"Colors are #rgb, #rrggbb or #rrggbbaa, or an array of three or four
numbers in [0, 1]. A key naming no field, kind or state, a value that does not
fit its field and a base naming no built-in are refused with their byte
offset in the document (blit.theme.LoadError), and a refused theme changes
nothing. A save writes the tokens and plot in full and a style field only
where it differs from what the tokens derive, colors in hex when hex holds
them exactly, so the file stays short and reads back to the same theme
exactly.
blit.editor.theme_editor(?ctx, key, h) edits the context's live theme with
blit's own widgets: built-in themes by button, a group (the palette, the
metrics, the chart fields or a widget kind) and a state by dropdown, and the
group's fields in a scrolling list h pixels tall, each with its meaning from
the field table and a slider per float. It returns blit.editor.EDITED on a
frame an edit changed the theme and SAVE when its save button was pressed,
and the host writes blit.style.save_theme(?ctx) wherever it keeps themes.
The field table (blit.theme.FIELDS) is the one source of the paths, units
and meanings below: TOML, overrides, classes and the editor read it, and a
test holds this README's copy to it (blit.theme.document writes it). The
kind table lists the built-in kinds; a registered kind carries its own
meaning, which document lists for the theme it is given and the editor
shows.
| path | unit | meaning |
|---|---|---|
palette.panel |
color | panel background, opaque in the built-in themes |
palette.window |
color | window, popup, menu, tooltip, toast and modal background, opaque in the built-in themes |
palette.dock |
color | docked container background |
palette.header |
color | window titlebar and section heading |
palette.header_hot |
color | a titlebar or heading under the cursor |
palette.edge |
color | a dock's inner edge and a separator's rule |
palette.text |
color | text |
palette.text_dim |
color | secondary text and glyphs: notes, readings, carets, axis labels |
palette.text_faint |
color | the faintest text: hints, placeholders and disabled text |
palette.accent |
color | a mark that is on (a checked box, a chosen button, a selected row) and a focused field's outline |
palette.accent_text |
color | text and marks drawn on an accent fill |
palette.warn |
color | warnings, for consumers drawing their own content; no widget uses it |
palette.control |
color | a control's face at rest |
palette.control_hot |
color | a control under the cursor |
palette.control_on |
color | a control pressed, a dropdown open or a menu's header open |
palette.track |
color | a slider, toggle, scrollbar, progress or tab bar track, an option row at rest and a text field's face |
palette.handle |
color | a slider handle, toggle knob or scrollbar thumb at rest |
palette.handle_on |
color | a handle, knob or thumb being dragged or hovered |
palette.select |
color | the highlight behind selected text, translucent in the built-in themes |
palette.grid |
color | chart grid lines |
metrics.row |
length | least height of a control row, which grows to fit a line of text and its inset |
metrics.gap |
length | space between layout rows, cells and buttons in a row |
metrics.pad |
length | container padding: below a panel's, window's, popup's or menu's content, around a dock's column, and between a scrollbar and its content |
metrics.inset |
length | control inset: between a control's edge and its text, and between a box and its label |
metrics.handle_w |
length | slider handle width, 0 for none |
metrics.bar_w |
length | scrollbar width |
metrics.thumb_min |
length | shortest scrollbar thumb |
metrics.corner |
length | corner radius of controls and containers, 0 for square |
metrics.edge_w |
length | width of an edge line: a dock's inner side, a chart's rules and a focused field's outline |
metrics.caret_w |
length | width of a text field's caret |
palette.focus |
color | the ring around the control the keyboard reached |
metrics.focus_w |
length | width of the focus ring |
plot.series |
color x 8 | chart series colors, series k drawing in series[k % 8] |
plot.line_w |
length | width of a chart line |
plot.tick |
length | length of a chart axis tick |
plot.area |
factor | alpha factor of the area filled under a chart line, in [0, 1] |
motion.hover_time |
factor | seconds a control's face takes to follow hover and press, 0 for at once |
motion.hover_ease |
factor | the curve a hover or press transition eases by: 0 linear, 1 out, 2 in and out |
motion.open_time |
factor | seconds a section takes to unfold or fold, 0 for at once |
motion.open_ease |
factor | the curve a section folds by: 0 linear, 1 out, 2 in and out |
motion.scroll_time |
factor | seconds a scroll region takes to glide to a new offset, 0 for at once |
motion.scroll_ease |
factor | the curve a scroll glide eases by: 0 linear, 1 out, 2 in and out |
<kind>.radius |
4 lengths | corner radii: top-left, top-right, bottom-right, bottom-left |
<kind>.pad |
4 lengths | container padding between its edges and its content: left, top, right, bottom |
<kind>.inset |
4 lengths | control inset between its edges and its text: left, top, right, bottom |
<kind>.margin |
4 lengths | space around the widget: left, top, right (between cells), bottom (between rows) |
<kind>.min_w |
length | least width |
<kind>.min_h |
length | least height, a control's row height before it grows to fit its text |
<kind>.shadow_x |
length | shadow offset right |
<kind>.shadow_y |
length | shadow offset down |
<kind>.shadow_blur |
length | how far the shadow fades out, 0 for a hard edge |
<kind>.overflow |
factor | how a label too wide for its box fits: 0 clips it at the box, 1 cuts it with an ellipsis at its end |
<kind>.<state>.fill |
color | the face, at its top when it is a gradient |
<kind>.<state>.fill_to |
color | the face at its bottom, a vertical gradient from fill: written with every write of fill, so a fill alone is flat |
<kind>.<state>.text |
color | text drawn on the face |
<kind>.<state>.mark |
color | secondary ink: a glyph, a caret, a reading or a hint |
<kind>.<state>.border |
color | the outline, transparent for none |
<kind>.<state>.border_w |
length | the outline's width |
<kind>.<state>.shadow |
color | the shadow's color, transparent for none |
| kind | what it styles |
|---|---|
label |
a line of text, and a wrapped note in mark |
layout |
layout itself: margin.b between items down a column, margin.r between cells, columns and items across a row |
panel |
a panel's background, pad around its column |
window |
a window's body and its shadow, pad around its column, margin the resize grips outside its edges |
window_title |
a window's titlebar: text the title, mark the collapse glyph, hot under the cursor |
dock |
a docked panel: fill the body, border its inner edge, pad around its column |
popup |
a popup's body, such as a dropdown's options |
button |
a button, on when drawn chosen; margin.r between buttons in a row |
checkbox |
a checkbox's box: pad around the mark, inset.l between the box and the label |
checkbox_mark |
a checkbox's mark, drawn in its on state |
toggle |
a toggle's switch track and label, on while set: pad between the row and the track |
toggle_knob |
a toggle's knob, in the toggle's state: margin between the knob and the track |
slider |
a slider's track: text the label, mark the reading |
slider_handle |
a slider's handle: min_w its width, 0 for none |
dropdown |
a dropdown's header, on while open |
field |
a text field: mark the hint, focused while it holds the keyboard |
field_caret |
a text field's caret and its composition underline: fill, min_w the width |
field_select |
the highlight behind selected text: fill |
section |
a collapsible section heading: mark the caret, min_w its size, 0 to follow the caption's ascent |
scrollbar |
a scrollbar's track: min_w its width, margin.l between it and the content |
scrollbar_thumb |
a scrollbar's thumb: min_h its least length |
list |
a list's body |
list_item |
a list's row, on while selected |
chart |
a chart: border the grid, mark the axes, ticks, labels and hover guide, inset around labels, margin.r between bars |
tab |
a tab, on while chosen, and a tab bar's overflow buttons, disabled when they cannot act: margin.r between tabs |
tooltip |
a tooltip, and a chart's read-out: pad around its content, inset around a read-out's text, margin.t below the widget it describes |
menu |
a menu's body, pad around its rows, min_w its least width |
menu_item |
a row of a menu, on while its submenu is open: mark its shortcut and arrow, inset.l its indent |
overlay |
an overlay, chrome-free unless its style gives it a fill or a border: pad around its content |
modal |
a modal dialog's body |
scrim |
the scrim over everything behind a modal dialog, translucent in the built-in themes |
radio |
a radio button's ring: pad around the dot, inset.l between the ring and the label |
radio_mark |
a radio button's dot, drawn in its on state |
progress |
a progress bar's track and its text |
progress_bar |
a progress bar's done part |
separator |
a separator: border its rule and border_w the rule's width, mark a label, inset.r between label and rule |
tree_item |
a tree's row, on while selected: mark the caret, min_w its size, 0 to follow the caption's ascent |
drop_target |
where a drag would drop: fill and border |
drag_preview |
what a drag carries, drawn at the cursor |
menu_bar |
a menu bar's background |
menu_title |
a menu's header in a menu bar, on while its menu is open |
option |
a row of a dropdown's or a tab list's options, on while chosen |
tab_bar |
a tab bar's strip behind its tabs |
tab_close |
a tab's close box: fill under the cursor, border_w the cross's stroke |
table_header |
a table's header row and cells: mark the sort arrow, inset around labels, min_w a column's least width |
table_cell |
a table's body cell: inset around its widgets, its row height from min_h and inset |
table_rule |
a table's column and frozen-row rules: border and border_w, hot over a resize grip, min_w the grip's width |
value |
a value editor's drag cell: inset around its text and before its row's cells, min_w its least width |
picker |
a color picker: text its label, mark and border a marker's inner and outer rings, inset.l between swatch and label |
checker |
the checkerboard alpha shows through: fill and mark its two cells |
toast |
a toast's card: pad around its text, margin between the stack and the surface's edges |
focus_ring |
the ring around the control the keyboard reached: border and border_w its stroke, radius its corners |
slider_fill |
a slider's fill from the track's start to the value, transparent unless a theme gives it a fill |
| state | when |
|---|---|
normal |
at rest |
hot |
under the cursor |
active |
pressed or dragged |
on |
set: checked, chosen, selected or open |
disabled |
drawn but not taking input |
focused |
holding the keyboard |
Motion is state: blit.anim keeps animated values in the store under ids
(see State store), so a widget animates by asking each frame for its value
with the target it wants, and the value eases from wherever it stands to the
target on the host's clock (in.time).
val id: u64 = blit.context.id_of(?ctx, "drawer");
val open: f32 = blit.anim.value(?ctx, id, target, blit.theme.MOTION_OPEN); # eased toward target
val hot: f32 = blit.anim.fade(?ctx, blit.id.child(id, "#hot"), h.hot, blit.theme.MOTION_HOVER);
val face: blit.draw.Color = blit.anim.mix(rest, hover, hot);
- Built in. A control's face eases between the paints of the states it
passes through (
anim.box(?ctx, id, kind, state, x0, y0, x1, y1), ascontext.boxpaints one state, andanim.paintfor the paint alone), a toggle's knob slides, a section opened withbegin_sectionunfolds and folds (see Widgets & layout), and a scroll region glides to where the wheel, a key or the keyboard focus sent it. - Values.
value(?ctx, id, target, kind)is timed by the theme's motion of a kind andtoward(?ctx, id, target, motion)by atheme.Motionof the caller's. A value seen for the first time starts at its target, and a new target mid-run turns back from where the value stands, so nothing jumps.fade(?ctx, id, on, kind)is a 0 to 1 value from 0 that keeps no state at rest, for hovers.snapputs a value at once (a value dragged under the pointer),runningsays whether one moves, andlerpandmixblend numbers and colors. - Timing. Each kind of motion,
theme.MOTION_HOVER(hover and press),MOTION_OPEN(sections) andMOTION_SCROLL(scroll glides), takes its duration andblit.easecurve (LINEAR,OUT,IN_OUT) from the theme'smotionfields, paths such asmotion.hover_time, so TOML, overrides and the editor reach them like any field. Every widget reads them throughanim.timing(?ctx, kind)alone. - Reduced motion.
blit.context.set_reduce_motion(?ctx, 1)makes every animation jump straight to its end, as a host passes on its platform's setting. A host that never advancesin.timegets the same. - Sleeping. A value asks for frames through
wake_atonly while it moves, so an interface whose animations have all settled reportsnonefromnext_frameand an idle host sleeps.
blit.dock docks windows into a dock space, Dear ImGui style: a space fills
a rect with a tree of splits and tab stacks, windows dragged over it dock
into it, and tabs dragged out of it float again.
# once: the layout saves and loads with the windows
blit.dock.persist(?ctx);
blit.widget.persist_windows(?ctx);
# per frame: a default layout while there is none, such as before a load
val sid: u64 = blit.context.id_of(?ctx, "main");
if (blit.dock.empty(?ctx, sid)) {
val root: u64 = blit.dock.root(?ctx, sid);
val left: u64 = blit.dock.split(?ctx, root, blit.context.Side.left{}, 0.25);
blit.dock.add(?ctx, left, blit.context.id_of(?ctx, "Scene"));
blit.dock.add(?ctx, root, blit.context.id_of(?ctx, "Viewport"));
}
# the space first, then its windows, exactly as they are drawn floating
blit.dock.space(?ctx, "main", blit.context.free_area(?ctx));
val w: blit.widget.WindowArea = blit.widget.begin_window(?ctx, scene);
if (w.body != 0) { ... }
blit.widget.end_window(?ctx, w);
- The tree. A split node divides its rect between two children along an
axis by a ratio, and a leaf is a tab stack of windows.
root,split(a new empty leaf on one side of a node, taking a share of it),add(dock a window as a leaf's last tab),insert(dock it at an index in the leaf's tab stack),select(bring a docked window to its leaf's front),remove(float it again) andnode_ofbuild and read it in code. A window is its title's id. The tree lives in the state store, one small entry per space, node and docked window, so a layout of any size fits, and everything in it is pinned. - Drawing.
space(?ctx, key, area)lays the tree out over the rect, in the docked band, and draws a splitter between the children of every split and a tab bar (blit.tabs) across the top of every leaf. Each docked window learns where its body goes throughblit.widget.Docked, and its ownbegin_windowdraws it there without chrome, only while its tab is selected. Its content code, its ids, its scroll and every widget's state are the same as when it floats. Draw the space before its windows, or they lag it by a frame. - Splitters. Dragging a splitter moves its split, keeping each side at
least a few rows, or a docked window's declared
min_wandmin_h. - Tabs. A leaf's tab bar selects, reorders by drag, closes (the window's
WindowState.closed) and tears off its windows: a tab released over no target floats there. A tab dragged onto another leaf's tab bar joins it. A leaf whose windows are all closed or not drawn gives its room to its sibling, and comes back when they do. - A see-through centre.
central(?ctx, sid)makes the space's root its central node, the leaf kept open for what the app draws beneath the space, such as a 3D scene. Ask for it before splitting the root, and split it to dock panels around it: it keeps its id, so it stays the centre. While it holds no window it keeps its share of the space and never collapses, paints no background, and the space claims no input over it, so a claim the app registered beneath the space (earlier, in a lower band) takes the pointer there.central_area(?ctx, sid)gives its rect once the space has drawn this frame, none while a window is docked in it, which makes it an ordinary leaf until the window leaves. - Docking by drag. A window's titlebar is a drag source of
widget.WINDOW_KINDcarrying its id. While it, or a docked window's tab, is dragged over a space, the space shows drop zones over the leaf under the pointer (the centre docks as a tab, four edges split the leaf) and near its own outer edges (splitting the whole space), with a preview of where the window would land. A window released over a leaf's tab bar docks as a tab. Holding shift, or the modifiersset_suppresspicks, shows no zones and docks nothing. - Persistence.
persistregisters the spaces, nodes and docked windows as[dock.<id>],[docknode.<id>]and[docked.<id>]tables, loaded pinned, so a loaded layout waits for its space however late it is drawn.emptytells a space with no layout yet, to build a default one only then. - Look. The chrome draws in four kinds
blit.dock.register_kinds(t)registers on a theme, which a dock does on the context's theme the first time it draws:dock_space(the background),dock_splitter(a splitter while hot or dragged,min_wits thickness),dock_zone(a drop zone, hot under the drag) anddock_preview(where a window would land). A side panel's edge and padding are the built-indockstyle's. A host loading a theme file that styles them registers them first.
blit.dock.begin_dock(?ctx, key, ?d)/end_dock attach a side panel to a
screen edge (blit.context.Side: left, right, top or bottom) in one call. Each
panel takes a strip from the frame's free area, so panels opened in turn stack
inward, and blit.context.free_area reports what they leave for the rest of
the screen, such as a world view. Call them at the root.
The strip is a dock space whose root holds the panel's own content. Alone it looks like a plain panel, with no tab bar. Windows dragged over it dock beside it or as tabs with it, and then its key labels its tab. The panel itself never floats, and while another tab is selected its widgets lay out without drawing or claiming, so the caller places them every frame either way.
A panel's body is a scroll region. blit.widget.begin_scroll(?ctx, key, ?s, h)/end_scroll
open one as the next item of the layout on its own: its column is clipped and scrolls by the
wheel (Input.wheel, pixels, positive turned away from the user) and by a
draggable scrollbar when its content is taller than it. The wheel goes to the
innermost region holding the topmost claim under the cursor. The content
glides to the new offset rather than jumping (ScrollArea.top is the offset
it is drawn at this frame, which a region drawing only its rows in view
reads), except under a held thumb, and a tab stop inside the region that the
keyboard moved to is scrolled into view.
blit.widget.section(?ctx, title, ?open) is a collapsible heading that returns
whether the rows beneath it should be placed.
This is a breaking change: begin_dock, end_dock, Dock and DockArea
moved from blit.widget to blit.dock, unchanged otherwise.
blit never touches a window. Each frame the consumer fills a
blit.input.Input, plain data written against the hardest host (a desktop
window with an IME, high-resolution wheels and several pointer buttons), and
hands it to begin. A simpler host leaves what it lacks zeroed.
var in: blit.input.Input;
blit.input.init(?in, ?a); # once: the events grow in storage `in` owns
# per frame:
blit.input.clear_keys(?in);
in.time = clock_seconds(); # the host's monotonic clock, as f64
in.present = 1; # 0 while the pointer is off the surface
in.mx = x;
in.my = y;
in.down = blit.input.BUTTON_LEFT; # BUTTON_* bits held after the frame's last event
in.mods = blit.input.MOD_SHIFT; # MOD_* bits held this frame
in.wheel = dy_pixels; # both wheels in pixels
in.wheel_x = dx_pixels;
blit.input.press_button(?in, blit.input.BUTTON_LEFT, bx, by); # then every event, in arrival order
blit.input.type_text(?in, cp);
blit.context.begin(?ctx, in, w, h);
# ... widgets ...
blit.context.end(?ctx);
# blit.input.free(?in) at shutdown
- Time.
in.timeis the host's monotonic clock in seconds, andblit.context.dt(?ctx)is the time since the previous frame (0 on the first). Double clicks, caret blink and every other timed behaviour need it: a host that never advancesin.timegets single clicks only and a caret that does not blink. - Pointer.
downandprev_downare bitmasks ofBUTTON_LEFT,BUTTON_RIGHT,BUTTON_MIDDLE,BUTTON_X1andBUTTON_X2, andblit.input.pressed,releasedandheldtake the button. The context carriesprev_downacross frames. Withpresent0 nothing is hovered andin_rectmisses. Widgets act on the left button throughblit.interact(see Interaction). - Button events. A host that only samples the buttons sets
downand nothing more, and a press and release that both land between two frames are then lost. A host that sees each one also adds it as it arrives,press_button(?in, button, x, y)andrelease_button(?in, button, x, y)with the cursor where it happened, and still setsdownto the buttons held after the last of them.beginhands them to widgets in order, at most one change of a button per frame, holding the rest back and asking for the next frame throughnext_frame, so a quick click is a press in one frame and a release in the next, and a double click is a click, then a press. A frame that applies one sees the cursor where it happened, sopressedandreleasedkeep their per-frame meaning. - Keyboard events.
type_text(?in, cp)for each typed codepoint,press_key(?in, code, mods)for each key press or repeat,release_key(?in, code, mods)for each release andcompose(?in, text, caret)for the IME's composition in progress (an empty one ends it; the committed text arrives as typed text). They live in storage theInputowns, bound to an allocator byinit, so a frame holds any number of them, and passing theInputby value copies only the view. A key that types a printable ASCII character is that character, letters in uppercase ('A','7',' '); every other key is aKEY_*constant (KEY_ENTER,KEY_ESCAPE,KEY_BACKSPACE,KEY_DELETE, the arrows,KEY_HOME,KEY_END, ...). Modifiers areMOD_SHIFT,MOD_CTRL,MOD_ALTandMOD_SUPERbits, both held (in.mods) and carried by each key event, andblit.input.shortcut(mods)is ctrl, or super as darwin's command, without alt. - Back to the host. After
end,blit.context.cursor(?ctx)is the pointer shape to show (CURSOR_ARROW,TEXT,HAND,MOVE,RESIZE_EW,RESIZE_NS,RESIZE_NWSE,RESIZE_NESW,NOT_ALLOWED), which widgets set while hovered, andime_rect(?ctx)is where to put the IME's candidate window, in screen pixels, none while nothing takes text. - Scheduling. Widgets call
blit.context.wake_at(?ctx, t)for a time they need a frame by (a hover delay, an animation, a caret blink). Afterend,next_frame(?ctx)issome(0)to draw again now (also while button events are held back),some(t)to draw by timet, ornoneto draw only on input, so an idle tool can sleep instead of redrawing every frame.
- Focus. One widget at a time holds the keyboard, by id across frames
(
blit.context.focus,focused,unfocus), sofocus(?ctx, blit.context.id_of(?ctx, key))hands it to a widget from code. A press that lands anywhere else takes it back, and so does a frame that does not draw the holder.blit.context.typing(?ctx)is true while a widget holds it for its keys: read it beforebeginto keep the consumer's own key bindings quiet for the keys that frame will type into a field. A control reached by tab, such as a button, holds the focus without taking the keys a consumer binds, so it stays false then. - Keyboard navigation. Every control is a tab stop. Tab and shift tab move the focus through them in the order they are drawn, wrapping, across windows, docked tabs and menu bars alike. Controls drawn as one row (a button row or grid, a segmented choice, radios, a dropdown's options, a tab bar, a menu bar's headers, a window's chrome, a value editor's cells, a table's header) are a group: tab reaches the group once, at its chosen member or the one that last held the keyboard, and the arrows move within it. Enter and space activate the control holding the keyboard as a click does. A slider takes left and right, home and end, a list and a tree their arrows and pages, and a text field all its keys. A press focuses the control it lands on as well. Keys move the focus by the previous frame's stops, as the pointer routes by its claims.
- Focus ring. The control the keyboard moved to wears a ring, the
focus_ringstyle (derived from thefocusandfocus_wtokens), drawn above it on its layer (blit.context.focus_visible), until a press. A window the keyboard moves into comes to the front, and a scroll region brings the control into view. - Owning the keyboard. A focus held by something that is not a tab stop, such as an open menu, keeps every key: tab and the arrows move nothing while a menu is open. A modal keeps the keyboard inside it: tab moves from the modal onto its controls and never past them (see Layers & input routing).
- Clipboard hand-off. blit never reads the system clipboard. After
end,blit.context.copied(?ctx)is text a copy or cut left for the consumer to put on the clipboard (nil when none), andwants_paste(?ctx)asks for the clipboard's text, which the consumer hands to the next frame asin.paste. - Text field.
blit.widget.text_field(?ctx, key, ?f, hint)edits ablit.field.Field: UTF-8 in a buffer the consumer owns, with a caret, a selection, a maximum length in characters and a per-field filter.A press on the field focuses it and puts the caret under the cursor, or extends the selection to it with shift held. A double click selects a word and a triple click the line, and dragging extends the selection by characters, words or lines to match. Runs of clicks are counted byvar buf: [64]u8; var name: blit.field.Field; blit.field.init(?name, ?buf[0], 64, 24, nil); # 24 characters, any printable # per frame, inside a panel: val did: u8 = blit.widget.text_field(?ctx, "name", ?name, "name"); if ((did & blit.field.ENTERED) != 0) { ... }blit.interact(seeblit.context.set_double_click). The field takes typed text, backspace and delete, left, right, home and end (shift extends the selection), word movement and deletion withblit.input.MOD_WORD(option on darwin, ctrl elsewhere) held with the arrows, backspace and delete, shortcut A to select all, shortcut Z to undo and shortcut shift Z or Y to redo, shortcut C, X and V through the clipboard hand-off, and enter or escape, which end the edit and give the keyboard back. It returns this frame'sEDITED,ENTEREDandESCAPEDbits. While focused the caret blinks everyblit.edit.BLINKseconds, asking for the frames it needs throughnext_frame, and the field shows the IME's composition inline at the caret, underlined, and places the candidate window at the caret. - Field flags.
f.flagsshapes a field, 0 afterinit:blit.field.MASKEDdraws*for every character, never copies and takes no composition (a password);READ_ONLYmoves, selects and copies but refuses every edit;COUNTshows the length in characters, againstmaxwhen set;LINESmakes enter type a newline and home and end act on the line. - Undo. Undo history lives in a second buffer the consumer owns, so a field
without one has no undo:
Typing and single-character deletions coalesce into word-sized steps, any other edit or a caret movement ends a step, and the oldest steps are dropped when the buffer fills.
var hist: [4096]u8; blit.field.keep_history(?name, ?hist[0], 4096);blit.field.setclears the history. - Text area.
blit.textarea.text_area(?ctx, key, ?f, ?view, h)edits a field over many lines in a boxhtall across the column. The text wraps at word boundaries, up and down move the caret by wrapped rows toward the column they started in, page up and page down by a box of rows, and the box scrolls by wheel and scrollbar and to keep the caret in view. Clicks, drags, the keyboard, undo, the clipboard, the caret and the composition behave as in the text field, enter types a newline (the area makes its fieldLINES) and escape ends the edit. ACOUNTfield shows its length on a row below. Theblit.textarea.TextAreaview keeps the scroll and the start of every wrapped row, laid out again only when the text, width, style or scale changes, so each frame draws only the rows in view of however long a text:var view: blit.textarea.TextArea; blit.textarea.init(?view, ?a); # once blit.textarea.text_area(?ctx, "notes", ?notes, ?view, 200.0); blit.textarea.free(?view); # at shutdown - The edit model.
blit.fieldneeds no context, so a field can be driven directly:insert,paste,erase,key,undo,redo,select_word,select_line,word_left,word_right,placeandmove.blit.editholds what both text widgets share between the frame's input and a field (event routing, click and drag selection, the caret blink and masked drawing), for a consumer building a text widget of its own.
Push a clip rect with blit.context.push_clip(?ctx, x0, y0, x1, y1),
intersected with the active rect, and restore it with pop_clip. Geometry
wholly outside the rect is dropped on the CPU, and the rest is kept whole: every
run of the draw list carries the clip rect it was drawn under, which the
renderer sets as its scissor (see Rendering). So any shape clips, rotated,
curved or feathered, and a clipped-out widget never becomes hot.
blit.context.begin_surface(?ctx, x, y, w, h, scroll_x, scroll_y) opens a
clipped region with its own scrolled local coordinate space: emit content and
place widgets at local coordinates, and read the returned Surface's
local_mx/local_my/inside to hit-test custom content against blit.input.
Close it with end_surface.
Coordinates compose one way:
- Every rect you pass is local. Geometry, widgets,
region_clicked,push_clipandbegin_surfaceall take coordinates in the current local space, which is screen space shifted by the current origin. At the root the origin is zero, so local and screen coordinates are the same there. - Surfaces nest. A surface's origin is its parent's origin plus
(x - scroll_x, y - scroll_y), so a surface opened inside another is placed relative to the outer surface's content. - A child never draws outside its parent. Clips are kept in screen space and every new clip, including a surface's region, is intersected with the active one, whatever rect you pass.
- A band is a new root. Inside
push_band(and so inside a window, a dock or a popup) the origin is zero and the clip is the whole screen. - The cursor is screen space.
ctx.in.mx/myandinput_visibleare in screen pixels.blit.context.local_mx(?ctx)/local_myare the cursor in the current local space, as are aSurface'slocal_mx/local_myand a hit'smx/my.
blit.interact.hit(?ctx, id, x0, y0, x1, y1, buttons) is how every widget
meets the pointer, built-in or not: it claims the rect in the current local
space (clipped and layered like geometry) and returns a blit.interact.Hit
for the BUTTON_* bits it answers to. Every built-in widget and chart calls
it, so a custom control behaves exactly like one.
val id: u64 = blit.context.id_of(?ctx, "node");
val h: blit.interact.Hit = blit.interact.hit(?ctx, id, x0, y0, x1, y1,
blit.input.BUTTON_LEFT | blit.input.BUTTON_RIGHT);
if (h.double) { open_node(); }
or (h.clicked && h.button == blit.input.BUTTON_RIGHT) { open_menu(h.mx, h.my); }
if (h.held) { drag_by(h.dx, h.dy); }
- Hover.
hotwhile it is the topmost claimant under the cursor. - Owning the pointer. A press over it with one of its buttons makes it
activeuntil that button comes up, andpressedon that frame. A press while another button owns the pointer takes nothing. While active,heldsays the button is still down anddx/dyare the cursor's travel since the press, wherever the cursor goes.releasedis the frame the button comes up, over it or not, andclickedis a release over it.buttonis theBUTTON_*bit it acted on. - Double clicks. The context keeps each button's last press
(
blit.context.last_press): its time, where it landed in screen space, the claimant it landed on and its run of clicks. A press on the same claimant within the double-click time and distance of the one before extends the run:clicksis 1, 2 or 3 for a single, double or triple click, anddoubleis the second.blit.context.set_double_click(?ctx, seconds, pixels)sets the threshold (DOUBLE_TIME, 0.3 s, andDOUBLE_DIST, 6 unscaled pixels, by default), so a host can pass its platform's settings. - Hover only.
buttons0 reports hover and the cursor and never owns the pointer, as a chart's read-out does. - Moving with a drag.
blit.interact.drag(?ctx, id, ?dx, ?dy)reports the same travel without claiming, for a part that moves with its drag (a title bar, a scrollbar thumb) and must place its rect before claiming it. - Focus. A widget that takes the keyboard does it on
pressedwithblit.context.focus(?ctx, id). Its id isid_of(?ctx, key)in the scope it was drawn in, so a caller that refuses a text field's entry (ENTEREDwith text it will not take) callsfocuswith that id to hand the keyboard straight back, andfocused(?ctx, id)says whether it holds it. - Tab stops.
blit.interact.FOCUSamong the buttons makes the widget a tab stop (see Keyboard navigation), registered in call order even while clipped out of sight: a left press focuses it,focusedsays it holds the keyboard, and enter or space while it held the keyboard from the frame's start setactivatedandclicked.ARROWSsays it takes the arrow keys itself while focused andTEXTthat it takes typed text, which also keeps enter and space its own.interact.presses(?ctx, code)counts a key's presses for a widget reading its own keys. A widget whose claim is a container's, as a list's or a tree's is its scroll region, registers its stop withinteract.stop(?ctx, id, x0, y0, x1, y1, flags). - Groups. Stops placed between
g = interact.begin_group(?ctx, id, axis)andend_group(?ctx, g)are one group, and the arrows alongaxis(ROW,COLUMNorBOTH) move between them.blit.context.mark_current(?ctx, id)names the member tab enters the group at, such as the chosen segment. - Disabled.
blit.context.begin_disabled(?ctx, cond)andend_disabled(?ctx)wrap widgets that draw and lay out, keeping their ids and state, but take no input whencondholds. Inside,hitstill claims the rect but reports nothing hot, pressed, clicked or dragged, the focus is neither given nor kept (a holder that becomes disabled loses it), tab and the arrows pass over it, scroll regions and tables take no wheel, menu items and their shortcuts never fire, and every kind paints in itsDISABLEDstate throughboxandpaint_of. Scopes nest, and one withcondfalse inside a disabling one stays disabled, so a caller writes one call, not a branch.blit.context.disabled(?ctx)says whether the call site is disabled, for a custom control that reads input other than throughhit.
blit.dnd lets any widget be a drag source or a drop target. Both ends take
the widget's own blit.interact.Hit, so a row, a tab or a window's title bar
becomes a source or a target by passing its hit along.
# a source: past a small move threshold its drag starts with a typed payload,
# a type tag and bytes the context copies
val h: blit.interact.Hit = blit.interact.hit(?ctx, id, x0, y0, x1, y1, blit.input.BUTTON_LEFT);
val d: blit.dnd.Drag = blit.dnd.source(?ctx, h, "layer", (?index)::ptr, $size_of(u64), x0, y0, x1, y1);
if (d.on) {
# the source draws its own preview, on a layer above the interface
val pv: blit.dnd.Preview = blit.dnd.begin_preview(?ctx);
blit.context.quad(?ctx, pv.x0, pv.y0, pv.x1, pv.y1, ctx.theme.control_on);
blit.dnd.end_preview(?ctx, pv);
}
if (d.missed) { float_off(); }
# a target: after drawing the widget, name the type it accepts over its rect
val t: blit.dnd.Drop = blit.dnd.target(?ctx, h, "layer", x0, y0, x1, y1);
if (t.dropped) { move_layer(@(t.data::*u64), here); }
- Source.
sourceis called every frame with the widget's hit. Once the widget holds the pointer and has movedpayload.THRESHOLD(4 unscaled pixels,set_thresholdchanges it) from the press, the drag starts. The payload is copied into storage the context owns, again every frame the drag is on, so a source can hand over a record on its stack, and the rect given is what the preview follows.Drag.onis true while its drag is in flight, and through the frame after it ends exactly one ofdropped(a target took it),missed(released over no target) orcancelled(Escape) is set, so a dragged tab can become a window when no tab bar took it. - Target.
targetreports a drag of its type over it when its hit is hot, the topmost claimant under the pointer, and draws the accept highlight (thedrop_targetstyle) over the rect.Drop.droppedis the frame the button comes up over it, with the payload indataandnand the starting widget insource. A target accepting several types callstargetonce per type with the same hit. A target with nothing else to do claims its rect withinteract.hitand no buttons. - Preview.
begin_previewopens a layer in the drag band, above every other band (see Layers & input routing), and returns the source's rect in screen pixels, kept under the pointer where the press grabbed it. The source draws anything there, and the layer claims nothing, so targets beneath still see the pointer. - Cancel. Escape ends a drag at once, and the source cannot start another until its button comes up. A release over no target ends it as missed.
- Anywhere. The drag belongs to the context (
blit.payload), not to its source, so it outlives the source's frames and reaches targets in any window, dock, popup or surface: the claim order that routes every hover decides which target is under the pointer.carried(?ctx, kind)peeks at a drag in flight, for a widget that shows where a drag would land before it is over a target, withreleasedset on the frame its button comes up.take(?ctx)then takes it as dropped, for a target that works out where it lands from the pointer, such as a dock whose zones lie under the window being dragged.
Every widget asks the layout for its rect, draws through the painter in it, and moves the layout on, so each one clips and scrolls like any geometry and works inside surfaces, docks and windows alike.
- A stack of frames. Layout is a stack of frames on the context
(
blit.layout.Frame): a content box, a pen, the axis items advance along, the gap between them and how they align across it. Every container (panel, window, popup, dock, scroll region, columns, stack, flow) pushes its frame at its begin and pops it at its end, so a panel opened inside a window leaves the window's layout where it was. Outside any container, widgets lay out down a column over the screen. - Stacks.
begin_stack(?ctx, key, s)/end_stackopen a horizontal or vertical stack as the next item of the current frame, and stacks nest.blit.widget.stack(?ctx, axis)gives the options at the theme's gap: adjustgap,align(start,center,endorstretch, across the axis), the stack's ownwandhin its parent, anditem_w/item_h, the rules its items take unless they set one.var bar: blit.layout.Stack = blit.widget.stack(?ctx, blit.layout.Axis.horizontal{}); bar.align = blit.layout.Align.center{}; bar.item_w = blit.layout.fit(); blit.widget.begin_stack(?ctx, "tools", bar); blit.widget.button(?ctx, "Open"); blit.widget.button(?ctx, "Save"); blit.widget.size_next(?ctx, blit.layout.fill(1.0::f32), blit.layout.auto()); blit.widget.text_field(?ctx, "find", ?find, "find"); blit.widget.end_stack(?ctx); - Flows.
begin_flow(?ctx, key, s)/end_flowopen a flow as the next item of the current frame: its items go left to right, and one that would pass the right edge starts the next row. A row is as tall as its tallest item, and its items sit in that height asalignsays, read from what the row measured last frame. An item wider than a whole row takes the row alone at its width and cuts its label to it, as a control placed by hand does withblit.text.fit_boxorfit_pair.blit.widget.flow(?ctx)gives the options at the layout style's gaps (margin.rbetween items,margin.bbetween rows): adjustgap,row_gap,align, the flow's ownwandhanditem_w/item_h, as for a stack. A flow is sized in its parent as a stack is, and its width fitted to its content is its items in one row, solimit(fit(), 0, max)wraps atmax. Flows and stacks nest in each other, and in windows, docked bodies and overlays.blit.widget.size_next(?ctx, blit.layout.limit(blit.layout.fit(), 0.0::f32, max_w), blit.layout.auto()); blit.widget.begin_flow(?ctx, "readout", blit.widget.flow(?ctx)); # one item per label and value pair, cut to the row when wider than it val r: blit.context.Area = blit.widget.place(?ctx, blit.layout.fit(), blit.layout.fit(), pair_w, line_h); blit.text.fit_pair(?ctx, blit.theme.LABEL, "gen", 3, faint, gen, ink, pair_gap, r.y0, r); blit.widget.end_flow(?ctx); - Sizing. Each side of an item is
blit.layout.fixed(px),fit()(what its content needs),fill(weight)(a share of the space the other items leave) orfrac(f)(a fraction of the space left), andlimit(s, min, max)clamps any of them.size_next(?ctx, w, h)sizes the next item,auto()leaving a side to the item. Widgets that spanned the column (buttons, sliders, toggles, fields, dropdowns, sections, scroll regions) fill the width by default, and text, checkboxes, images and grids fit their content. - A hidden first frame. A single pass cannot know a container's content
before placing it, so a stack fitted to its content, a stack centered or
end-aligned in its parent, and fill shares along a stack read what the
stack measured last frame, kept in the state store under its id. A stack
that would place anything by such a measure before it has one lays out its
first frame only to measure: its geometry and claims, and its children's,
are dropped (
blit.context.push_measure/pop_measure), and it asks for the next frame at once, sonext_frameissome(0). A guess is never seen or clicked, as with Dear ImGui's hidden first frame for auto-fit windows. After that, a change of content settles one frame late. - Same line.
same_line(?ctx)puts the next item beside the last one in a column, for quick inline rows. The line is as tall as its tallest item. - By hand.
place(?ctx, w, h, nat_w, nat_h)places a control built outside blit as the next item, with its own rules and natural size, and returns its rect, andbegin_scroll_atopens a scroll region over such a rect.avail(?ctx)is the space the next item may take, from the pen to the frame's far edges.advance(?ctx, h)moves past a row placed by hand andspace(?ctx, h)leaves room.cell_x0/cell_x1(?ctx, i, n)split the column into n equal cells, gaps between, for widgets that take a rect, such asbutton_at(?ctx, label, x0, y0, x1, y1, on).begin_columns(?ctx, n),next_columnandend_columnslay whole widgets side by side and resume below the tallest column. - Style. The gaps between items come from the
layoutstyle's margins (margin.bdown a column,margin.racross a row), and the padding a container keeps from its own style'spad, at the context's scale.
This is a breaking change from the loose layout fields: Context.ox, oy,
cx, cy and pw are gone (read avail instead), as are the saved-layout
fields of Popup, ScrollArea and DockArea, and Columns holds its row
instead of ox and pw.
- Sections.
section(?ctx, title, ?open)is a heading with a caret, pointing right when closed and down when open, that returns whether to place the rows beneath it.s = begin_section(?ctx, title, ?open)is the same heading over a body that unfolds and folds: place the rows whiles.bodyis true, thenend_section(?ctx, s)whatever it is. Opening or closing, the rows show in a clip that grows or shrinks to the height they last took whole, over the theme'sMOTION_OPEN, and the layout below follows it. The caret is as tall as the caption's ascent in the current text style, whatever the line height, or the section style'smin_wwhen it sets one (section.min_w). - Buttons.
buttonspans the column,buttons(?ctx, ?labels[0], n)is a row of n, andbutton_grid(?ctx, ?labels[0], n, cols, on)wraps them cols to a row with buttonondrawn chosen. Both return the index clicked, or n. - Choices.
segmented(?ctx, ?labels[0], n, ?choice)picks one of n,toggle(?ctx, label, ?state)is an on/off switch across the row, andcheckboxa box beside its label. - Sliders.
slider(?ctx, label, reading, ?v, lo, hi)shows the caller's formattedreadingof the value beside its label, andslider_fis the same without one. A theme fills the track up to the value throughslider_fill, clear in the built-in themes, and aslider_handle.min_wof 0 leaves the handle out for a fill alone. - Text.
textis one line, andnote(?ctx, s)is dim text wrapped at spaces to the column's width. - Lists.
blit.list.show(?ctx, key, ?l, rows, h)is a scrolling listhpixels tall, described each frame by ablit.list.Rows:blit.list.rows(?labels[0], count)fills one with labels alone, and its fields add the rest.detailsholds a secondary label per row, drawn right-aligned in the dim text color, with the label clipped short of it.match(user, index, query)decides which items are shown, defaulting to the items whose label holdsquery, ignoring ASCII case (blit.widget.matches, for a matcher to build on).draw(ctx, user, row)paints a row's content in place of the labels: the list still claims the row, paints its hover and selection face beneath and scrolls it, so a drawn row keeps hit, selection and scrolling, and anything the drawer claims sits above the row. Theblit.list.Rowit receives carries the item, the row's id andHit(where a drag source or drop target for reordering attaches), its rect, whether it is selected and the text colors for that.row_hsets a row height other than the theme's. Clicking a row selects its item (List.selected, the count for none). WithList.markspointing at one byte per item the list is a multiple selection: a click selects an item alone, ctrl (command on darwin) toggles it, and shift selects the shown items from the last one clicked. A row's id is its item index under the list's, so it keeps its hit identity as the query or matcher changes. The list is one tab stop, focused by tab or a press on a row: up and down move the selection over the shown items, home and end to either end and page up and page down by a list's height, scrolling it into view, a plain move selecting alone and shift extending from the anchor (List.cursoris where the keyboard is in a multiple selection). A tree (blit.tree) is one tab stop the same way, its keyboard cursor on a node (Tree.cursor): up, down, page up, page down, home and end move it a row at a time, right opens a node or steps into it, left closes one or steps out to its parent, enter activates the node as a double click does and space selects it. A list lays out and draws only the rows in view, passing over the rest in blocks, so it costs its visible rows and, with a query or a matcher, one test per item. Its natural width, which a container sized to its content takes, is the widest row it has laid out so far (List.width): it grows as wider rows scroll into view and never shrinks while scrolling. It resets when the list has no items, and a caller whose labels change sets it to 0 to measure afresh.
demo/panel/ builds a docked application panel from these widgets alone, in
the shape of an application's side panel (a header, then run, view and files
sections), and drives it headlessly through blit.input.
Beyond the v0 widgets, blit.widget.dropdown is a select whose options open in
a popup over later widgets, its open state kept in the state store under its
id, blit.widget.begin_window/end_window is a full window (see
Windows), blit.widget.begin_popup(?ctx, key, open, x, y, w)/end_popup
opens an overlay column, and blit.widget.region_clicked(?ctx, key, x0, y0, x1, y1) is a left click on an arbitrary rect for consumer-drawn affordances,
the simplest use of blit.interact.hit.
use blit;
# once: windows save and load with the state store
blit.widget.persist_windows(?ctx);
# per frame: the rect is where the window first opens, the store keeps the rest
var tools: blit.widget.Window = blit.widget.window("tools", 40.0::f32, 40.0::f32, 220.0::f32, 300.0::f32);
tools.min_w = 160.0::f32;
val w: blit.widget.WindowArea = blit.widget.begin_window(?ctx, tools);
if (w.body != 0) {
blit.widget.button(?ctx, "go");
}
blit.widget.end_window(?ctx, w);
# reopen it after its close button closed it
blit.widget.window_state(?ctx, "tools").closed = 0;
- Identity and state. A window is its title's id (see Widget ids). Its
place, size, stack order, and open, collapsed, pinned and locked flags are a
WindowStatethe context's store keeps under that id, so reordering the calls never moves or restacks a window, andpersist_windowssaves and loads them with everything else the store persists, as[window.<id>]tables.Windowis only what the call declares: the rect the window first opens at, its size bounds (min_w,min_h,max_w,max_h, 0 for none) and its flags.window_state(?ctx, key)reaches the state to open, close, pin or lock a window from code. - Chrome. The titlebar drags the window and holds a collapse box on the left and a close button on the right. Grips just outside every edge and corner resize it within its bounds and ask for the matching resize cursor. A locked window neither moves nor resizes.
- Body. The body is a layout column (see Widgets & layout) in a scroll region
under the titlebar, which scrolls by wheel and scrollbar when its widgets
are taller than it. With
WINDOW_AUTO_SIZEthe window fits its widgets instead, within its bounds and without grips: as tall as they reach this frame, and as wide as the widest of them and its titlebar measured the frame before. An auto-sized window declared 0 wide lays out its first frame hidden, only to measure. Place widgets only whilebodyis 1: a closed or collapsed window has none. The window's coordinates are screen pixels, wherever it is called. - Stacking. Windows live in the windows band, above docks and beneath popups. A press anywhere on a window brings it to the front, whatever order the windows are called in, and a pinned window stays in front of every unpinned one.
- Docking. The titlebar is a drag source of
WINDOW_KINDcarrying the window's id, which a dock space takes in (see Docking). A docked window draws in the rect its dock lays out, with no chrome and no grips, its body a scroll region as when it floats (WINDOW_AUTO_SIZEdoes not apply there), and has no body while another tab of its stack is selected. - Flags.
WINDOW_NO_TITLE,WINDOW_NO_RESIZE,WINDOW_NO_MOVE,WINDOW_NO_BACKGROUND(the body still stops input),WINDOW_NO_CLOSEandWINDOW_AUTO_SIZE, combined with|.
This is a breaking change: begin_window takes a Window declaration by
value and returns a WindowArea, end_window takes only that, and Window
no longer holds open or the window's live position.
blit.controls holds the small controls a tool expects, each placed at the
layout cursor across the column like blit.widget's:
- Radio buttons.
radio(?ctx, label, ?choice, value)is a circle beside its label that sets@choicetovaluewhen clicked, filled in the accent while chosen, so buttons sharing one choice make a group.radios(?ctx, ?labels[0], n, ?choice)stacks n of them, button i standing for i. - Progress bars.
progress(?ctx, frac, text)fills tofracof the column, andprogress_busy(?ctx, text)sweeps a segment across it everyBUSY_PERIODseconds ofin.timefor work of unknown length. A busy bar asks for the frame its segment next moves a pixel in throughwake_at, so it animates while drawn and an interface without one still reportsnonefromnext_frame. Either takes text to centre over the bar, nil for none. - Separators.
separator(?ctx)is a rule across the column in theseparatorstyle'sbordercolor,border_wthick, andseparator_label(?ctx, label)runs the rule on from a dim label. - Combo.
combo(?ctx, label, ?selected, ?options[0], count)is a select whose popup holds a filter field that takes the keyboard when it opens. Typing narrows the options to those holding the text, ignoring ASCII case, up and down move the highlight among them, and enter or a click picks one. Escape or a press outside closes it without a pick.COMBO_ROWSoptions show at once and the rest scroll. Its open state, filter, highlight and scroll live in the state store under its id, and its parts are reached by path:pick/popup/filteris the filter andpick/popup/optionsthe list, each option an index under it.
blit.table lays rows of any widgets out under columns the user can resize,
reorder, sort and hide, with frozen leading columns and rows, and draws only
the rows in view, so a table of a million rows costs what one of a screenful
does. The caller describes its columns and runs its rows:
var cols: [3]blit.table.Column;
cols[0] = blit.table.Column{label: "Name", width: 160.0::f32};
cols[1] = blit.table.Column{label: "Size", width: 60.0::f32};
cols[2] = blit.table.Column{label: "Done", flags: blit.table.NO_SORT};
# per frame:
var o: blit.table.Options = blit.table.options(count, 300.0::f32);
o.freeze_cols = 1;
var t: blit.table.Table = blit.table.begin(?ctx, "files", ?cols[0], 3, o);
if (t.sorted) { order_rows(t.sort, t.dir); }
for (blit.table.next_row(?ctx, ?t)) {
val f: *File = ?files[order[t.row]];
if (blit.table.cell(?ctx, ?t, 0)) { blit.widget.text(?ctx, f.name); }
if (blit.table.cell(?ctx, ?t, 1)) { blit.widget.text(?ctx, f.size_text); }
if (blit.table.cell(?ctx, ?t, 2)) { blit.widget.checkbox(?ctx, "done", ?f.done); }
}
blit.table.end(?ctx, ?t);
- Rows in view.
next_rowyields the frozen rows, then only the rows the view shows, settingt.row. Rows are one height (Options.row_h, a control row by default), so the first row in view is found by arithmetic and a frame never walks the rows above it. - Cells.
cell(?ctx, ?t, c)opens columnc's cell in the current row and returns false for a hidden column or one scrolled out of view. A cell is a horizontal stack across the column, its widgets centred down the row and clipped to the cell. - Ids. A row is an id scope keyed by its key under the table's id, its
index unless
Options.keymaps it (a caller that sorts keys rows by their data), and a cell a scope keyed by its column's label under the row. A widget in a cell keeps its id and its state however the rows scroll.cell_id(?t, key, c)is a cell's scope, the parent of its widgets' ids. - The header. Dragging the grip at a header cell's right edge resizes the
column. A click, or enter or space on a header the keyboard reached (the
header is a group of tab stops), sorts by the column, ascending and then descending, and the
table reports it as
t.sort(the column's index, the column count for none) andt.dir, witht.sortedset on the frame it changed: the table never sorts, the caller orders its rows. Dragging a header drops the column on another's place throughblit.dnd, and a right click opens a context menu (blit.menu, underMENU_KEY) whose checked items show and hide columns.NO_RESIZE,NO_REORDER,NO_HIDEandNO_SORTturn each off per column andHIDDENstarts a column hidden. - Frozen columns and rows. The header, the first
freeze_colsshown columns and the firstfreeze_rowsrows stay put while the rest scrolls, by the wheels and by scrollbars that appear when the content outgrows the table. Frozen and scrolled parts are clipped to rects that do not overlap. - Column state. Each column's width, place and visibility live in the
state store under the column's id (its label under the table's id), and
the sort and scroll under the table's. A change the user makes pins them,
so a table not drawn for a while keeps its layout.
blit.table.register(?ctx)makes the layout and sort persist throughblit.state.saveandload, astable_columnandtabletables. - Where things went.
beginfills eachColumn'sid,shown,frozen,pos(its place in the display order),xandw, andorder, the index of the column shown at that record's own place.
blit.value edits numbers of any of i8 to i64, u8 to u64, f32 and
f64 without loss: nothing passes a 64-bit integer through a float or an
f64 through anything narrower, a value shows as the shortest text that
reads back as exactly that value, and typed text is read exactly.
- Drag.
drag[T](?ctx, label, ?v, speed, lo, hi)moves@vbyspeedper pixel dragged across it,FINEtimes as far while shift is held. An integer moves by whole steps and keeps the fraction for the next pixel, and a float lands on the decimals of its value at the press or of the speed, so dragging 1.5 by 0.01 a pixel gives 1.6. A double click, or enter or space while it holds the keyboard, turns it into a text field with the value selected: enter or a press elsewhere commits, escape leaves the value.drag_n[T](?ctx, label, ?vec[0], n, speed, lo, hi)edits 2 to 4 values in one row, anddrag_range[T](?ctx, label, ?a, ?b, speed, lo, hi)a low and a high value that never cross. - Typed numbers.
number[T](?ctx, label, ?v, step, lo, hi)is a text field with-and+steppers, committing as a double-clicked drag does. - Bounds.
lo < hiclamps dragged, stepped and typed values, and equal bounds leave a value to its type's range. Typed text that is no number of the type leaves the value as it was.
Each value's cell is a part of its editor keyed #0 to #3, so a driver
reaches the first cell of speed as speed/#0, and a number's steppers as
speed/- and speed/+. drag_scalars, range_scalars and number_scalar
take a kind (I8 to F64) and a pointer for values typed at run time, and
value.entry is the typed text cell under them all.
blit.color.picker(?ctx, label, ?c, flags) edits a colour in hue, saturation
and value: a square of saturation and value beside a hue bar, or inside a hue
ring with RING, an alpha bar over a checkerboard with ALPHA, and beneath
them a swatch, the label and hex text (#RRGGBB, #RRGGBBAA with alpha)
typed in like a value. The picker keeps its hue across greys and black, and
hex text read back shows as the same text.
A widget's id is a 64-bit FNV-1a hash of its key folded into the seed of the current id scope and finished with a 64-bit mixer, never its place in the call order. A widget drawn only some frames, a clipped row or a reordered window therefore moves no other widget's id, and the active drag, the focus holder and the open dropdown stay where they were. 0 is never an id: it means "no widget".
- Labels are keys. A labelled widget (
button,checkbox,toggle,slider,dropdown,section, a window's title) is keyed by its label. Text after##is hashed but not drawn, so"Save##toolbar"showsSaveand is a different widget from another"Save". A label holding###is keyed by the text from###on alone, so what it shows can change without changing its id:"Frames: 12###fps"and"Frames: 13###fps"are one widget. Widgets without a label (text_field,region_clicked, popups, scroll regions, docks, lists and charts) take a key argument the same way. - Scopes.
blit.context.push_id_str(?ctx, s)andpush_id_int(?ctx, n)open a scope andpop_id(?ctx)closes it, and a pop with no scope open is ignored. Windows, open popups and scroll regions (so docks and lists too) open their own scope for what they hold, so two windows can each hold a button calledok. Repeated rows built from one label, such as buttons in a loop, takepush_id_intwith their index. The stack grows from the context's allocator. - Parts. A widget's internal parts (a window's title, collapse box, close button, resize grips and body, a scrollbar thumb, a popup's outside claim, a dropdown's options, a list's items) take ids derived from the widget's id with a fixed suffix or index.
- Looking ids up.
blit.context.id_of(?ctx, key)is the id a widget with that label or key gets in the current scope, forfocus, state lookups and tests.blit.id.of_path(blit.id.ROOT, "Settings/Audio/volume")names a widget from outside its scopes: each segment is a container's label or key (a window's title, a dock's, popup's, scroll region's or list's key, apush_id_strkey) and the last is the widget's. A key holding/cannot be named by a path; use its id. - Collisions. Two claims with one id in a frame are recorded:
blit.context.collisions(?ctx)counts them afterendandcollision_at(?ctx, i)names each repeated id, until the nextbegin.
This is a breaking change from call-order ids: blit.context.next_id and
Context.seq are gone, and begin_popup, begin_scroll, begin_dock,
blit.list.show, text_field, region_clicked, blit.chart.line, bars and
sparkline take a key argument after the context.
State that must outlive a frame can live on the context, keyed by widget id.
blit.state.get[T](?ctx, id) returns the T that id keeps, zeroed the first
time it is asked for. One id can keep several types of state, each its own
entry, so a widget never reads another's bytes.
- Lifetime. The pointer is valid until
end. Eachgetmarks the entry reached, andenddrops every entry no frame reached formax_ageframes (blit.state.MAX_AGE, 60, by default;set_max_age(?ctx, n)changes it and 0 keeps everything), so state for widgets that stopped drawing does not pile up. Agetbetween frames counts toward the next frame.pin[T](?ctx, id, 1)keeps an entry however long it goes untouched, andpin[T](?ctx, id, 0)lets it age out again.find[T](?ctx, id)looks an entry up without making or reaching it, nil when there is none, anddrop[T](?ctx, id)drops it at once. - Failure. Entries are allocator-backed and the store grows. When it cannot,
getsets the context's oom (seecontext.ok) and hands back zeroed scratch state that every refused entry shares, never a dangling pointer. A state type is at most 1 KiB and 16-byte aligned, a compile error otherwise. - Persistence.
register[T](?ctx, name, save, load)makesTa persisted kind.save(?ctx)writes every entry of a registered kind into one TOML document, a[<name>.<id>]table per entry that the kind's save hook fills throughput_int,put_float,put_boolandput_str. The text stays the context's until the next save.load(?ctx, text)reads such a document throughstd.data.toml, zeroes each entry and runs the kind's load hook over its table. Tables of unregistered kinds are skipped. A load hook copies any string it keeps, since the parsed document is freed whenloadreturns.keep[T](?ctx, 1)loads a registered kind's entries pinned, for state that must wait for whatever reaches it.
The store is one owner, not the only one. Widgets that take a caller-owned
record (Scroll, List, Field) keep taking it, so an app can own
its state where it wants to.
blit.menu draws a menu bar, the menus it drops, submenus and context menus.
A bar is the next item of the current layout frame, one row high across it.
A main bar (begin_main_bar, closed by the same end_bar) instead takes its
row off the top of the free area, as a docked container does, so a dock space
placed from blit.context.free_area after it starts below it:
var bar: blit.menu.Menu = blit.menu.begin_main_bar(?ctx, "main");
...
blit.menu.end_bar(?ctx, ?bar);
blit.dock.space(?ctx, "work", blit.context.free_area(?ctx));
Each menu's body runs every frame, open or not, so its items answer their shortcuts while it is closed:
var bar: blit.menu.Menu = blit.menu.begin_bar(?ctx, "main");
var file: blit.menu.Menu = blit.menu.begin_menu(?ctx, ?bar, "File");
var save: blit.menu.Item = blit.menu.of("Save");
save.shortcut = blit.menu.keys('S', blit.menu.MOD_PRIMARY);
if (blit.menu.item(?ctx, ?file, save)) { ... }
var recent: blit.menu.Menu = blit.menu.begin_menu(?ctx, ?file, "Recent");
blit.menu.item(?ctx, ?recent, blit.menu.of("notes.txt"));
blit.menu.end_menu(?ctx, ?recent);
blit.menu.end_menu(?ctx, ?file);
blit.menu.end_bar(?ctx, ?bar);
var cm: blit.menu.Menu = blit.menu.begin_context(?ctx, "canvas", x0, y0, x1, y1);
if (blit.menu.item(?ctx, ?cm, blit.menu.of("Cut"))) { ... }
blit.menu.end_menu(?ctx, ?cm);
- Opening. A bar header opens on a press, and while one is open, moving
onto another opens that one. A submenu row opens its menu on hover. A context
menu opens at the cursor on a right press over its region, or through
open_context. - Items. An
Itemcarries a label, aShortcutshown at its right, a check (*u8, flipped when it fires, nil when it is not checkable), an icon drawn before the label (text, such as ablit.icon.STR_*icon, nil for none) and a disabled flag. - Shortcuts. An item fires when its key is pressed with exactly its
modifiers, open or closed, and stays quiet while another widget holds the
keyboard (
context.typing).MOD_PRIMARYis the platform's command key: ctrl or super (seeinput.shortcut), shown asCmdon darwin andCtrlelsewhere. - Focus. An open tree owns the keyboard: it takes the focus when it opens and gives it back to the previous holder the frame after it closes.
- Keys. The deepest open menu takes up and down (over enabled items), enter, right and left (into and out of submenus, and across a bar's headers) and escape. A bar's headers are a group of tab stops: left and right move between them, and enter, space or down opens a header's menu on its first item, the focus going back to the header when it closes.
- Closing. A click or enter on an item closes the whole tree, and so does a press of any button outside it, which is consumed.
- Ids. A bar's key, then each header and submenu label, then the item's
label:
driver.find(?d, "main/File/Recent/notes.txt").
blit.modal puts up a dialog that blocks everything beneath it until it
closes. Its open state lives in the state store under its title's id, so
open and close take it up and down from anywhere in the same id scope,
and begin_modal and end_modal run every frame, open or not:
if (blit.widget.button(?ctx, "Delete")) { blit.modal.open(?ctx, "Delete file"); }
val m: blit.modal.ModalArea = blit.modal.begin_modal(?ctx, blit.modal.dialog("Delete file"));
if (m.shown != 0) {
blit.widget.text(?ctx, "notes.txt goes for good.");
if (blit.widget.button(?ctx, "Delete")) { ...; blit.modal.close(?ctx, "Delete file"); }
}
blit.modal.end_modal(?ctx, m);
var labels: [2]str = [2]str{"Save", "Discard"};
val c: usize = blit.modal.confirm(?ctx, "Unsaved", "Save the changes first?", ?labels[0], 2);
if (c == 0) { ... } # c is the button chosen, CANCELLED, or NO_CHOICE
- Blocking. A modal paints and claims in the modals band, above docks,
windows, overlays and popups, behind a scrim over the whole screen. The scrim
claims every button, so nothing beneath it is hovered, pressed or scrolled,
while the widgets inside the modal work as usual. Popups, dropdowns and menus
opened inside a modal stack above it.
The scrim paints in the
scrimstyle, the body and its shadow inmodaland the titlebar inwindow_title. - Placement. A
Modaldeclares a title, a width (0 to fit its widgets), a greatest width andMODAL_*flags. It opens centred, or with its top left atx,yunderMODAL_ANCHORED, and stays on the screen. Its size is measured as it is drawn, so the frame it opens lays out hidden behind a scrim that already blocks. - Closing. Escape closes the top modal while it holds the keyboard itself
or a control inside it that takes no text does (escape in a text field
inside it ends the edit first), and so does the close
button in its titlebar (
MODAL_NO_CLOSE,MODAL_NO_TITLE). A press on the scrim closes it only withMODAL_SCRIM_CLOSES.ModalArea.closedandwhyreport the frame it closed. - Stacking. A modal opened inside another's body nests above it, and modals called one after another stack by call order. Only the top one takes the pointer and escape.
- Focus. A modal takes the keyboard the frame it opens, so a field beneath stops seeing keys, and gives it back to the previous holder the frame after it closes. Tab moves from the modal onto its controls, and tab and the arrows stay among the top modal's controls while it is open.
- Confirm.
confirmis a dialog with a message and a row of buttons. It returns the index chosen, closing itself,CANCELLEDon the frame it is closed without a choice (escape, close button or scrim), andNO_CHOICEotherwise. - Ids. A modal is the id scope of its widgets:
driver.find(?d, "Delete file/Delete").
blit.overlay places widgets that float above the interface without being
windows: a HUD readout, a help button, the cursor's coordinates. An overlay is
placed by an anchor, not by the layout, and sizes to its content:
# a readout in the top right corner of the surface
var hud: blit.overlay.Overlay = blit.overlay.on_surface(blit.overlay.TOP_RIGHT);
hud.dx = -8.0::f32;
hud.dy = 8.0::f32;
val s: blit.overlay.Shown = blit.overlay.begin(?ctx, "hud", hud);
blit.widget.text(?ctx, "fps 60");
blit.overlay.end(?ctx, s);
# a note just below a rect, flipped above it and clamped where it would run off
var o: blit.overlay.Overlay = blit.overlay.on_rect(r, blit.overlay.BOTTOM_LEFT, blit.overlay.TOP_LEFT);
o.keep = 1;
- Anchors. An
Anchoris a point of a rect as fractions of its size:TOP_LEFT,TOP,TOP_RIGHT,LEFT,CENTER,RIGHT,BOTTOM_LEFT,BOTTOMandBOTTOM_RIGHT, or any other.atis the point of the target (the surface, ortargetin the caller's local pixels withto_rect1),pivotthe point of the overlay put there, anddx/dyan offset in pixels.on_surface(a)puts the overlay's ownaat the surface's, so a corner anchor sits in that corner.keepkeeps it on the surface: an overlay running off an edge flips to the far side of its anchor on that axis, then is clamped. - Size. Its widgets lay out down a column it fits to them, measured into
the state store under its id. A placement that reads the size (any pivot
but the top left, or
keep) uses last frame's measure, and an overlay never measured spends its first frame hidden, only measuring, and asks for the next at once, as a fitted stack does. - No chrome, no claim. A bare overlay draws no title, frame or background unless its style gives it one, and claims nothing itself: input stops only where its widgets claim, so the world under its empty space and its text stays interactive.
- Order. Overlays paint and take input in the overlays band, above docks
and windows and beneath popups, menus and modals.
orderplaces one among the others, higher above, and call order breaks a tie. - Fade.
fade(aFadeofdist,nearandfar) runs the overlay's opacity fromnear, with the pointer on it, tofar, with the pointerdistpixels away or more or off the surface: near 0.2 and far 1 lets a HUD get out of the way, near 1 and far 0 shows it only as the pointer nears. It fades everything it holds throughblit.context.push_alpha(?ctx, a)/pop_alpha, an opacity scope every color the painter emits passes through, which nests by multiplying. A consumer span is drawn by the consumer and is not faded.
blit.toast stacks timed notifications at an anchor of the surface or of
any rect:
var toasts: blit.toast.Toasts;
blit.toast.init(?toasts, ?a); # once
val posted: err[allo.Error] = blit.toast.post(?toasts, "saved", 0.0); # anywhere: 0 for SECONDS
val st: err[allo.Error] = blit.toast.post_keyed(?toasts, "status", "identifying...", 0.0); # one card per key
val re: err[allo.Error] = blit.toast.post_keyed(?toasts, "status", "a cat", 2.0); # the same card, new text
val held: bool = blit.toast.withdraw(?toasts, "status"); # take it down early
blit.toast.show(?ctx, "toasts", ?toasts, blit.overlay.BOTTOM_RIGHT); # per frame
blit.toast.show_in(?ctx, "toasts", ?toasts, viewport, blit.overlay.TOP_RIGHT); # or inside a rect
- Life. A toast's clock starts the first frame
showdraws it. It fades in over its firstFADEseconds and out over its last, and is dropped once its time is up.postcopies the text into storage theToastsowns, so a transient buffer will do, andfreereleases it. - Keys.
post_keyednames a toast by a key, copied like the text. Posting under a key the stack holds replaces that toast in place: it keeps its place in the stack, takes the new text and time, and its clock starts over at the next show without fading in again if it was on screen.withdrawtakes a keyed toast down before its time is up, fading it out overFADEfrom the next show, or dropping it unseen if no frame showed it yet, and returns whether the stack held the key. - Stack. The cards stack away from the anchor's edge, up from a bottom
anchor and down from any other, the newest nearest the anchor, lined up on
its side and kept off the edges by the
toaststyle's margin.showanchors to the surface,show_into a rect in local pixels, such as a viewport beside a docked panel, the stack inside it asshow's is inside the surface. - Frames.
showasks for frames only while a toast fades, and otherwise for the moment the next one starts to fade out, so once they are gonenext_frameisnoneagain. A post or withdraw between frames asks for the next one withblit.context.redraw.
blit.tooltip shows an overlay over a widget once the pointer has rested on
it, named by id after the widget is drawn:
blit.widget.button(?ctx, "Save");
blit.tooltip.text(?ctx, blit.context.id_of(?ctx, "Save"), "write the file");
var t: blit.tooltip.Tip = blit.tooltip.tip();
t.follow = 0; # below the widget, not the pointer
val tip: blit.tooltip.Shown = blit.tooltip.begin(?ctx, id, t);
if (tip.open != 0) { blit.widget.text(?ctx, "any widgets"); }
blit.tooltip.end(?ctx, tip);
- Delay. It shows once the widget has been the hovered claimant for
delayseconds (DELAY, 0.5 s), asking for that frame throughwake_atwhile it waits and for nothing once shown. A held button hides it and starts the delay over. - Placement. Below and right of the pointer, clear of the cursor by
CURSOR_GAP, or below the widget withfollow0, in the tooltips band above everything but a drag preview, and always kept on the surface. - Pass-through. A tooltip claims nothing, so the widget under it keeps the pointer. Its content is for reading: a widget in it that claims would take the hover from the widget it describes.
Overlays, toasts and tooltips draw in the overlay, toast and tooltip
styles, which blit.overlay.chrome(?ctx, kind) reads at rest and
blit.overlay.paint draws. A bare overlay's style has no fill or outline, so
it stays chrome-free until a theme gives it one.
blit.chart plots columns of values into a rect you give it, in the current
local space, and returns a Hover for the value under the cursor:
var energy: [64]f32; # filled by you, oldest first
var income: [64]f32;
var series: [2]blit.chart.Series;
series[0] = blit.chart.series(blit.chart.f32s(?energy[0]), 64);
series[0].fill = 1;
series[1] = blit.chart.series(blit.chart.f32s(?income[0]), 64);
val hov: blit.chart.Hover = blit.chart.line(?ctx, "energy", x, y, w, h, ?series[0], 2, blit.chart.options());
if (hov.hot != 0) { ... } # hov.index, hov.series, hov.x, hov.value
# profiler samples: u64 timestamps at uneven spacing, a counter that holds between them
var t: [256]u64;
var bytes: [256]u64;
var heap: blit.chart.Series = blit.chart.series(blit.chart.u64s(?bytes[0]), n);
heap.x = blit.chart.u64s(?t[0]);
heap.shape = blit.chart.STEP;
blit.chart.bars(?ctx, "net", x, y, w, h, blit.chart.f32s(?net[0]), count, blit.chart.options());
blit.chart.sparkline(?ctx, "spark", x, y, w, h, series[0]);
- Columns. A
Columnis values ofF32,F64orU64atdata + i * stride, so packed arrays (f32s,f64s,u64s) and fields of an array of records (a stride of the record's size) plot alike, without copying. - Line. Each series has its own
countand, optionally, anxcolumn of ascending positions, so spacing may be uneven and series need not share samples. Withoutx, sampleisits atx = i.shape = STEPholds each value until the next sample. A series withfillset fills the area between its line and zero. - Bars. One bar per value in equal slots, up from zero when positive and
down when negative.
Options.flush = 1draws them with no gap. - Sparkline. A compact series fitted to its values, with no axes, for a row or a cell. It keeps the series' x, shape and fill.
- Exact values.
Numholds one exact value,Num.f{f64}orNum.u{u64}. An axis whose values are all u64 keeps an exact origin and steps its ticks in whole numbers, so timestamps and counters past 2^53 label and read out to the last digit. Hovers reportxandvalueasNum. - Axes.
Options.yandOptions.xareAxisrecords: fixed withblit.chart.fixed(lo, hi), else fitted to the values. A fitted y axis is widened to whole ticks (linear bars always hold zero), a fitted x axis spans the samples edge to edge.log = 1spaces either axis by powers of ten, with a tick per decade or per few; values at or below zero sit at its floor. Bars label their first and last bar with a fixedOptions.x, their index otherwise. Ticks step by 1, 2 or 5 times a power of ten and labels come from the glyph source, with k, M, G or T for large steps.Options.axes = 0gives the whole rect to the plot. - Hover. A chart takes its id from its key and claims its plot, so it reads out only when it is the topmost claimant under the cursor, like any widget. A line reads, in each series, the sample nearest the cursor (for a step series the one whose value holds there) and reports the series nearest the cursor, bars report the slot under the cursor, and both draw a read-out of the value inside the plot. A sparkline reports and marks its sample.
- Caller's cursor. While a line chart is not hovered,
Options.cursorplaces its vertical guide, marker and read-out atcursor.xoncursor.series. Feeding one chart'sHover(xandseries) to the others keeps a cursor in step across charts, and a playhead is the same call. - Crisp at any scale. A line is one quad per screen pixel column, the
path swept by a square brush of
line_w, so no sample is ever skipped when samples outnumber pixels. Columns, rules, bars and labels land on whole screen pixels, and every length is a theme metric at the context's scale. - Plain quads. Charts emit through the painter like every widget, clipped to their rect and to any clip or sub-surface they sit in, and allocate nothing beyond their vertices and indices.
Paint order and input order come from one key, so the widget that receives a click is always the one visibly on top.
- Bands. Layers come in bands, listed bottom to top as data in
blit.band: base content, docked content, floating windows, overlays, popups and menus, modals, tooltips and the drag preview. A layer is a band and a slot within it (band.layer(b, slot),band_of,slot_of).blit.context.push_band(?ctx, b, slot)puts subsequent geometry and claims on a layer of bandb, in screen coordinates with the clip reset to the screen, andpop_bandreturns. Each band picks the slot by its rule: a flat band shares one slot, a nesting band (popups, modals) stacks a child one slot above a parent of the same band or, opened inside a higher band, one slot above its parent in that band, and an ordered band (windows, overlays) takes the slot given, a window's z or an overlay's order.end()composes the draw list by layer, keeping call order within a layer, so a popup opened early in the frame still paints over a window called after it, and sibling popups share a layer.run_atreports each run'slayer.push_layerandpop_layerremain, deprecated, aspush_band(?ctx, band.POPUPS, 0)andpop_band. - Trapping the keyboard. A band whose row sets
trap(modals) keeps keyboard navigation inside its topmost layer holding a tab stop, so tab cycles through the top modal's controls alone. - Window order. The context keeps the windows' z order:
blit.context.raisehands out a z above every window andnote_znotes one a window kept. Once z passesZ_RENUMBER, the windows renumber every z the store keeps, drawn this frame or not, andrestacktells the context where they ended. - Channels. A container that paints its background once its contents are
known (a panel, window or popup) splits the runs drawn after it into
channels:
ch = blit.context.split(?ctx, 2),set_channel(?ctx, ch, 1)for its children, thenset_channel(?ctx, ch, 0)to draw its background andmerge(?ctx, ch). A lower channel paints beneath a higher one whatever the order they were drawn in, so the background can be any shape. Splits nest, and each is merged on the layer it was made on. - Claims. Interactive widgets claim through
blit.interact.hit, which callsblit.context.claim(?ctx, id, x0, y0, x1, y1): it records the rect with the key (layer, then claim order) and returns whether the widget is hovered. Containers callreserve_claimbefore their children andfill_claimat their end, so their empty areas stop input instead of letting it reach what they cover. - Routing. Frame N's input goes only to the topmost of frame N-1's claims
under frame N's cursor, resolved once in
begin. If the topmost claimant disappears, nothing is hovered for one frame rather than the click reaching whatever was beneath it. - A press in the first frame a widget exists does nothing. Which widget is on top can only be known from rects that already exist, so routing uses the previous frame's claims. A widget that appears this frame has no claim there yet, so it becomes hoverable and clickable from the next frame. A popup opened by a click cannot be pressed in the same frame it opens. The next press always comes at least one frame later, since it needs the button to go up and down again. In tests, hover for one frame before the first press. This is chosen: the alternatives (a second pass over the widgets, or letting some widgets jump the queue) would make some widgets special.
- Popups. A press anywhere outside an open popup dismisses it (
Popup.dismissed) and is consumed: it does not activate the widget underneath.
blit draws textures it does not own. A consumer texture is named by an opaque
u64 handle, whatever the renderer understands (a GL texture name, an index
into its own table). blit never creates, owns or binds one, it only passes the
handle through to the draw list's runs. 0 is reserved for the atlas.
- Image.
blit.draw.Imageis a handle, a source rect in uv and a filter.blit.draw.region(tex, tw, th, x, y, w, h, filter)builds one from a rect in texels,whole(tex, filter)covers the texture. - Drawing.
blit.context.image_at(?ctx, img, x0, y0, x1, y1, tint)stretches the region into a rect, clipped like any geometry by its run's scissor.blit.widget.image(?ctx, img, w, h)places it in the layout. - Grids.
blit.widget.Gridis a row-major field of cells, each colored by the consumer (colors) or by mappingvaluesthrough aPaletteof equal steps fromlotohi.grid_atfills a rect with it andgridplaces it at the cursor. Cells are solid quads on the atlas, so a small field needs no texture. A large one is better uploaded as a texture and drawn withFILTER_NEAREST.
The painter draws shapes in the current local space, clipped by the run's scissor like everything else. Every edge but a plain quad's is antialiased by feathered geometry: a strip one pixel wide whose outer vertices carry no color, so antialiasing needs nothing of the renderer beyond the contract below. Curves and arcs stay within a quarter pixel of the true shape, so their segment count follows their size on screen and the interface scale.
- Rects.
quadfills a rect with one quad andframe(?ctx, x0, y0, x1, y1, w, c)outlines it with a bandwwide inside its edges, both crisp on whole pixels.rounded_rectandstroke_rounded_recttake a radius per corner (blit.draw.Radii,blit.draw.radii(r)for all four), scaled down together when they would overlap.fillis a rounded rect with one radius on the cornersCORNERS_*names. - Triangles, lines and arcs.
trifills a triangle.lineandpolyline(?ctx, ?pts[0], n, closed, w, join, c)strokewwide, centred on their points, ending square, withblit.draw.JOIN_MITER,JOIN_BEVELorJOIN_ROUNDat each corner (a miter past 4 half widths bevels).circlefills,stroke_circlerings inside the radius, andarc(?ctx, cx, cy, r, a0, a1, w, c)strokes from anglea0toa1in radians, clockwise on screen. - Paths.
blit.pathbuilds contours ofmove,line,quad(quadratic),cubicandclosecommands into aPaththe consumer owns:A fill takes the nonzero rule, so a hole is a contour wound against its outline and crossing contours fill their union. Contours that overlap still show the faint feathers of the edges they hide, so icons are cleanest drawn as contours that meet without overlapping. A fill sweeps its edges once, sorted, so a large path such as a chart area or a text outline costs e log e in its edge count rather than e squared.var p: blit.path.Path = blit.path.init(?a); blit.path.move(?p, 0.0::f32, 0.0::f32); blit.path.cubic(?p, 4.0::f32, -6.0::f32, 12.0::f32, -6.0::f32, 16.0::f32, 0.0::f32); blit.path.line(?p, 8.0::f32, 12.0::f32); blit.path.close(?p); blit.context.fill_path(?ctx, ?p, look.accent); blit.context.stroke_path(?ctx, ?p, 1.5::f32, blit.draw.JOIN_ROUND, look.text); - Gradients.
blit.draw.gradient(x0, y0, c0, x1, y1, c1)is a linear gradient in local space, clamped beyond its ends, drawn byquad_gradient,rounded_rect_gradientandfill_path_gradient. - Colors.
blit.draw.hex(0xRRGGBB, alpha)sits besideblit.draw.rgba.
blit.icon is a set of icons drawn from path data, with no font or image
behind them, so they scale to any size and take any color: PLAY, PAUSE,
STEP, STOP, CLOSE, CHEVRON_UP, CHEVRON_DOWN, CHEVRON_LEFT,
CHEVRON_RIGHT, PLUS, MINUS, SEARCH, SETTINGS, HELP, PIN and the
dock drop targets DOCK_CENTER, DOCK_LEFT, DOCK_RIGHT, DOCK_TOP and
DOCK_BOTTOM.
blit.context.icon_at(?ctx, blit.icon.SETTINGS, x, y, 24.0::f32, look.text);
blit.widget.button(?ctx, blit.icon.STR_PLAY); # an icon for a label
blit.widget.button(?ctx, "\xF3\xB0\x80\x82 step"); # inline with text
- Path data. An icon (
blit.icon.Icon) is a flatf32stream of path commands and the box it was drawn in. Each command is its verb (blit.path.MOVE,LINE,QUAD,CUBICorCLOSE, as a float) followed by its coordinates: two for a move or line, four for a quadratic (control, then end), six for a cubic (both controls, then end) and none for a close. It is filled by the nonzero rule, scaled from its box to the size drawn. The built-ins sit on ablit.icon.BOX(16) square, their contours meeting without overlapping. - Drawing.
icon_at(?ctx, id, x, y, size, c)draws an iconsizepixels tall with its box's top-left at(x, y), feathered and clipped like any path.icon_width(?ctx, id, size)is the width it takes. - Inline with text. Every icon id is a codepoint,
blit.icon.CODEPOINT_BASE(U+F0000) plus the id, in supplementary private use area A. Text holding one draws the icon in place, a line tall (the style's ascent and descent), tinted with the text, and measures it the same way, so labels, buttons and every other text call take icons with no change.blit.icon.STR_*is each built-in as a UTF-8 string, andblit.icon.encode(id, ?buf[0], 4)writes any id's. A codepoint in that range with no icon goes to the glyph source as before. - Your own icons.
blit.context.add_icon(?ctx, icon)registers an icon in the same form, in a box of any size, copying its data, and returns its id, fromblit.icon.APP_FIRST(0x1000) up, so a later built-in never moves it. Malformed data or an empty box is refused.val TRI: [10]f32 = [10]f32{0.0, 0.0, 0.0, 1.0, 24.0, 12.0, 1.0, 0.0, 24.0, 4.0}; val id: opt[u32] = blit.context.add_icon(?ctx, blit.icon.Icon{data: ?TRI[0], n: 10, w: 24.0::f32, h: 24.0::f32});
blit emits one indexed triangle list that draws solid shapes and text through
a single shader per texture. Solid shapes sample a white block every atlas
page carries, so color * texel is the flat color, and glyphs sample their
cell. Colors and texels are premultiplied.
- Buffers. After
end, upload the vertices (draw_verts/draw_count,blit.draw.Vert) and the u32 indices (draw_indices/index_count), three per triangle, in composed paint order. Vertices stay in the order they were drawn. Only the indices are reordered to paint layers and channels. - Runs.
run_count/run_atdivide the index list into runs, in paint order. ARunis plain numbers:kind(u32),tex(u64),page,layerandfilter(u32), the scissorclip_x0,clip_y0,clip_x1,clip_y1(f32),startandcount(usize, in indices), and for a consumer spanidanddata(u64) and its rectx0,y0,x1,y1(f32). Every index lies in one run. Draw the runs in order:- Kind.
blit.context.RUN_TRIANGLES(0): bind, scissor and draw as below.blit.context.RUN_CUSTOM(1) is a consumer span (see below). Skip a run of any other kind, so later kinds need no change to a renderer that ignores them. - Texture. When
texisblit.draw.ATLAS(0), bind atlas pagepage. Otherwisetexis the consumer's own texture handle, passed through untouched, andfilterasks forFILTER_NEAREST(0) orFILTER_LINEAR(1) sampling. On the atlasfilteris alwaysFILTER_NEARESTand how to sample it stays the renderer's choice. Rebind only whentex,pageorfilterchange. - Scissor. The clip is in screen pixels, y down, the space vertices are
in. Round each edge to the nearest pixel, scale by the framebuffer's pixels
per screen pixel when they differ, and in GL flip y:
glScissor(x0, fb_h - y1, x1 - x0, y1 - y0). Set it whenever it changes, withGL_SCISSOR_TESTenabled. - Draw.
glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, start * 4): indices are absolute vertex numbers, so no base vertex. - Consumer spans. A
RUN_CUSTOMrun holds no indices (count0). Set its scissor, call the app's own drawing foridwithdataand the rect (x0,y0,x1,y1, screen pixels, y down), then restore blit's state (program, buffers, blend, texture binding) before the next run.blit.context.custom(?ctx, x0, y0, x1, y1, id, data)places one: the rect is in local space like every rect, it takes the current clip and layer, and it sorts like any run, so a popup on a higher layer, or anything drawn after it, paints over it.idanddataare passed through untouched.
- Kind.
- State. Blend premultiplied:
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA). Disable face culling (triangles come in either winding) and depth testing. - Atlas.
blit.context.atlas_of(?ctx)is the glyph atlas:page_countpages, each an RGBA8 square ofblit.atlas.PAGE_SIDE(512) texels atpage_pixels(at, i), premultiplied (a glyph's coverage in all four channels, the white block opaque). Pages open as glyphs arrive and never resize, and pastblit.atlas.PAGE_MAX(16) the least recently drawn one is cleared and reused, wholly dirty. Each has apage_versionbumped on every write and apage_dirtyrect: upload the dirty rect of each changed page before drawing, thenpage_cleanit. Every glyph has a transparent gutter, so linear filtering is safe. The built-in font at whole scales looks sharpest with nearest. - Consumer textures are sampled as premultiplied alpha: upload them premultiplied.
- Color. Colors are authored as straight rgba and premultiplied as they are
emitted. When the target encodes sRGB on write, call
blit.context.set_srgb(?ctx, 1): every color is then converted to linear light before premultiplying, so the encoding brings it back to what was authored. Alpha is never converted. Each distinct color is converted once and cached on the context (blit.draw.LinearCache), so sRGB output costs next to nothing. Gradients interpolate the emitted colors. - Vertex.
blit.draw.Vertis 8f32, 32-byte stride:aPos(vec2) at 0,aUV(vec2) at 8,aColor(vec4) at 16. Positions in pixels, uv in [0, 1], premultiplied rgba. - Shader. One
vec2 uScreenuniform:gl_Position = vec4(aPos.x / uScreen.x * 2.0 - 1.0, 1.0 - aPos.y / uScreen.y * 2.0, 0.0, 1.0); FragColor = aColor * texture(atlas, aUV);
- Per frame. Fill an
Input(mx,my,down,wheel, the keyboard and button events and anypaste),begin, widgets,end, handcopiedto the clipboard and answerwants_paste, upload the changed atlas pages, the vertices and the indices, and draw each run as above. - From 0.9. A renderer written for the 0.9 contract changes in these
places, and the atlas upload, the vertex layout and the shader stay as they
were:
- upload the index buffer and draw each run with
glDrawElementsoverstartandcount, which now count indices, where it drew arrays - apply each run's scissor
- blend
ONE/ONE_MINUS_SRC_ALPHAwhere it blendedSRC_ALPHA/ONE_MINUS_SRC_ALPHA, and leave face culling off - call back into the app for
RUN_CUSTOMruns, and skip runs of any other kind that is notRUN_TRIANGLES - upload consumer textures premultiplied
- upload the index buffer and draw each run with
blit.driver runs an interface headless, the way blit's own tests do, so an
app built on blit can test its interface. A Driver owns a context and an
input, a screen size and a clock, and runs the interface through a callback
between begin and end, one frame per step:
fun ui(ctx: *blit.context.Context, user: ptr) {
val app: *App = user::*App;
# ... widgets, as in a frame, without begin and end ...
}
var d: blit.driver.Driver;
val made: err[allo.Error] = blit.driver.init(?d, ?a, 800.0::f32, 600.0::f32, ui, (?app)::ptr);
blit.driver.step(?d); # lay out once
val save: u64 = blit.driver.find(?d, "Settings/Save");
blit.driver.click(?d, save);
blit.driver.click(?d, blit.driver.find(?d, "Settings/name"));
blit.driver.type_text(?d, "untitled");
blit.driver.key(?d, blit.input.KEY_ENTER, 0);
if (!str_equals(blit.driver.text_of(?d, save), "Save")) { ... }
blit.driver.free(?d);
- Finding.
find(?d, path)is the id a label path names from the root (see Widget ids),at(?d, x, y)the widget a pointer there reaches andinside(?d, x0, y0, x1, y1)the topmost widget drawn wholly inside a rect. Any id works,blit.context.id_ofandblit.id.childincluded. - Acting.
hover,click,double_click,right_clickandclick_with(?d, id, button)move the pointer onto the widget's centre in a frame of their own, since input routes by the previous frame's claims, then press and release a frame each.drag(?d, id, x, y)presses on the widget and moves to the point inDRAG_STEPSheld frames before releasing, anddrag_onto(?d, id, target)drops on another widget's centre.scroll(?d, id, dx, dy)turns the wheels over a widget,type_text(?d, s)types text in one frame, andkey(?d, code, mods)presses a key in one frame and releases it in the next.move,leave,pressandreleasedrive the pointer and buttons by hand, a frame each, and a press or release is sent as a button event at the pointer as well as indown. An action on a widget the last frame did not draw runs nothing and returns false.settle(?d)runs frames while the interface asks for the next one at once, so its animations and glides reach their ends. - Frames. Each step carries the clock (
d.time, advanced byd.dt, 1/60 s by default) and clears the frame's key events, wheel and paste, so input set between steps lands in exactly one frame.d.ctxandd.inare the context and input, free to read and set between steps. - Queries read the last frame:
drawn,rect(the screen rect the widget claimed, clipped, none when it was not visible),hot,activeandfocused,focus_of(?d)(whichever widget holds the keyboard), andtext_in(?d, x0, y0, x1, y1)andtext_of(?d, id), the text drawn inside a rect or a widget's rect. Text lines whose box has its centre inside are joined in drawing order: directly when one continues the last on its row, by a space further along the row, by a newline otherwise. - Snapshots.
snapshot(?d)is a stable text dump of the last frame's draw list for golden comparison: the screen size, then each run with its kind, texture, page, layer, filter and scissor. A consumer span adds its callback id, data and rect, and any other run its triangle count, followed by its geometry one shape a line. A flat-coloured, axis-aligned rect is aquad(its corners' position and uv, then its colour), anything else atriof three vertices. Positions print to two decimals, uvs to four, colours as the premultiplied 0 to 255 channels emitted.
The driver's text (text_in, text_of, snapshot) stays valid until the
next of those calls. The context underneath keeps per-widget rects from its
claims (blit.context.rect_of, widget_at, widget_in) and, while
set_trace(?ctx, 1) is on, every drawn line of text (traced_count,
traced_at), off by default so an app's frames pay nothing for it.
blit.inspect is an interface inspector built with blit, for debugging the
interfaces built with it: a window over the frame showing what the context
keeps, as a tree whose sections open to their entries.
var insp: blit.inspect.Inspector;
blit.inspect.init(?insp, ?a); # once, closed
# per frame, after every other widget:
blit.inspect.show(?ctx, ?insp);
# bound to a key or a button:
blit.inspect.toggle(?insp);
# at shutdown: blit.inspect.free(?insp)
- What it shows. The frame: the screen, scale and dt, and the hovered, hot, active and focused ids (whether the focus wears the ring and takes typed keys). The focus order: every tab stop in the order tab moves through them, with its layer, group and the keys it takes. Layers and claims: each band with its rule and its claims and runs, and each claim's id, layer and rect. Collisions: every id claimed twice. The draw list: its vertex, index and run counts and each run's layer, texture, triangles and clip. Windows: each window's rect, z and flags. Dock spaces: each space's tree of splits and leaves and the windows docked in them. The state store: every entry's id, kind (a registered kind's name, or its type), size, age and pin.
- Outlines. Hovering an entry outlines its rect on the surface, in the
tooltips band above everything but a drag, in the
inspect_outlinekind (blit.inspect.register_kindsregisters it, asshowdoes the first time it draws): a claim's rect, a widget's, a window's, a dock node's, a tab stop's, a run's clip. - Costing nothing when off. It records nothing: everything it shows is
what the context already keeps for routing, drawing and its store, read
where it stands. The
Inspectoris the caller's, and while closedshowreturns at once. Called last, it reads the app's whole frame before drawing itself, and while open it copies what it lists into storage it owns, so drawing itself never moves what it reads. Closing its window closes it.
mach dep pull .
mach build .
mach test .
demo/harness/ is its own project with a path dependency on this checkout. It
drives a headless frame end to end through a bare use blit; and prints the
vertex, index and run counts and atlas pages. It takes std from this checkout's dep/std, so
pull the root first:
mach dep pull demo/harness
mach build demo/harness
demo/harness/out/linux-x86_64/debug/bin/harness
demo/panel/ builds and runs the same way, driven by blit.driver. Its
test compares the scripted panel's last frame with the golden snapshot
demo/panel/src/bin/panel.snap, and panel --snapshot prints a new one when
a change to the draw list is meant. mach dep pull demo/panel again after
changing blit, since a demo builds against its pulled copy:
mach test demo/panel
demo/panel/out/linux-x86_64/debug/bin/panel --snapshot > demo/panel/src/bin/panel.snap
demo/gallery/ is the reference people learn blit from, as Dear ImGui's demo
window is: one window covering every widget and module, each section beside
the code that builds it. The screen is a dock space with the gallery docked
in it, a list of sections on the left, the chosen section's widgets in the
middle and its source on the right. Each section is a file of its own under
demo/gallery/src/sections/, embedded, so the code shown is the code that
runs, and a new section is one file and one row of the table in
demo/gallery/src/app.mach. A host with a window and a renderer makes a
Gallery and runs gallery.app.ui each frame. Its test visits every
section headless, checking each frame is whole with no id collisions, and
compares the last frame with demo/gallery/src/bin/gallery.snap:
mach dep pull demo/gallery
mach test demo/gallery
demo/gallery/out/linux-x86_64/debug/bin/gallery --snapshot > demo/gallery/src/bin/gallery.snap
demo/bench/ measures blit's per-frame cost on a few representative
interfaces: a dock of eight open sections of controls, a list of 10,000 rows, a
line chart of 100,000 samples, six overlapping windows each holding a
section and a scroll region, and one path of 10,000 edges filled. Each scene runs with sRGB output off and then on,
and the table reports the median frame time over 21 timed batches with the
spread between the fastest and slowest batch, the vertices and runs the frame
emits, the allocations a frame makes, and what sRGB adds. The off and on
batches are interleaved so drift in the machine's speed lands on both. It is local only,
never a CI job. Build it in the release profile:
mach dep pull demo/bench
mach build demo/bench -p release
demo/bench/out/linux-x86_64/release/bin/bench
Baseline on dev ahead of 0.10.0 (after #152), on an AMD Ryzen 7 5800X3D, to
compare later work against. Run it on an idle machine and pinned to one core
(taskset -c 15): other load shows up as a large spread, and a run whose
spread is large is not worth comparing. Two consecutive runs recorded this way
agreed within 2% on every scene.
scene srgb us/frame spread vertices runs allocs srgb cost
dense panel off 253.4 3.4% 3532 3 0
dense panel on 258.0 0.4% 3532 3 0 +1.8%
10k row list off 60.4 2.3% 1136 3 0
10k row list on 60.7 0.4% 1136 3 0 +0.5%
100k chart off 1697.3 4.4% 10492 3 0
100k chart on 1756.1 0.7% 10492 3 0 +3.4%
windows off 241.5 3.9% 3672 18 0
windows on 246.4 0.6% 3672 18 0 +2.0%
10k-edge fill off 14413.6 1.5% 231520 1 0
10k-edge fill on 14404.5 0.9% 231520 1 0 +0.0%
main/dev long-lived branches; feat/* and fix/* branch off dev and
merge back; dev integrates to main for releases. Conventional commits,
semver tags.