Quick-start orientation for AI assistants and new contributors.
Framework: gui/ module, immediate-mode declarative UI in V.
Immediate-mode pattern — every frame rebuilds the UI from a pure function.
Key types:
Window(window.v) — owns the render loop, layout tree, animations, IME, A11y, and command queue. One per app.View(view_*.v) — interface; user-facing config structs that implementgenerate_layout(mut Window) Layout.Layout(layout*.v) — resolved node tree: shape + children. Output ofcompose_layout. Discarded each frame (full rebuild) or amended (render-only path).Shape(shape*.v) — drawing descriptor: geometry, color, optional sub-structstc &TextConfig,fx &ShapeEffects,&EventHandlers.Renderer/renderers_draw— flat list of draw commands emitted byrender_layout. Consumed once per frame byrenderers_draw.
Example program convention: the first container must be gui.fixed_fixed
with width and height matching the window dimensions.
Entry point pattern:
gui.window(gui.WindowCfg{
on_init: fn (mut w gui.Window) { w.update_view(my_view_fn) }
})frame_fn
flush_commands // thread-safe state mutations
init_ime / init_a11y // lazy first-frame init
if refresh_layout // full rebuild (view fn + layout + renderers)
update()
view_generator() // calls user view fn
compose_layout() // View tree → Layout tree
generate_layout // View → Layout nodes
layout_arrange // size/position passes (amend_layout fires here)
rebuild_renderers() // Layout tree → flat []Renderer
else if refresh_render_only // renderer rebuild only (layout reused)
update_render_only()
rebuild_renderers()
process_svg_filters // offscreen passes before swapchain
renderers_draw() // draw flat renderer list
Trigger full rebuild: w.update_window() sets refresh_layout = true
Trigger render-only: w.rerender_window() sets refresh_render_only = true
refresh_layout takes priority over refresh_render_only.
When to use each path:
| Condition | Path |
|---|---|
| View state changed (app data, focus, etc.) | refresh_layout |
Animations with render_only: true |
refresh_render_only |
| Cursor blink, progress bars | refresh_render_only |
| Tween / Spring / Hero transitions | refresh_layout |
Important: layout/hero transitions lerp from a snapshot to the current
position. Repeated rerender corrupts the interpolated values — use full
rebuild.
compose_layout(mut view)
generate_layout(mut view, mut window) // View → Layout (recursive)
layout_arrange(mut layout, mut window) // 3-pass: size, amend, position
layout_sizes() // distribute grow/shrink space (layout_sizing.v)
amend_layout callbacks // user hooks run HERE (not in render_layout)
layout_positions() // x/y assignment, scroll offset clamping
wrap root in transparent Shape
amend_layout runs during layout_arrange, not during render_layout.
Use it to mutate layout geometry or inject child layouts.
render_cursor runs during render_layout, reads input_cursor_on live
(never captured in a closure).
V uses the Boehm conservative GC. Key hazards:
clear() sets len=0 but does not zero the backing memory. The GC scans
the entire allocated block, so stale pointers in cleared arrays cause false
retention (objects never collected).
Rule: use array_clear(mut arr) (defined in gc.v) for any array containing
pointers or pointer-containing types. It calls vmemset before zeroing len.
array_clear(mut window.renderers) // NOT window.renderers.clear()fn [cfg] closures capture the entire @[heap] cfg struct pointer.
Conservative GC scans ALL pointer-sized words in the struct → false retention
proportional to struct size.
Rule: extract only the fields needed into locals, then capture those:
// Bad — captures entire InputCfg (many pointer-sized fields):
on_char: fn [cfg] (l &Layout, mut e Event, mut w Window) { ... }
// Good — extract minimal fields:
id_focus := cfg.id_focus
color_hover := cfg.color_hover
on_char: fn [id_focus, color_hover] (l &Layout, mut e Event, mut w Window) { ... }Bound methods stored as callbacks (cv.method_name) are closures too —
they capture cv pointer. Convert to standalone functions.
Applied to: view_input.v (root cause), view_select.v, view_menubar.v,
view_color_picker.v, view_container.v (tooltip), view_input_date.v,
view_table.v.
See make_input_on_char, make_select_on_keydown, make_menubar_amend_layout
for the extraction pattern.
Old Layout nodes must be freed between frames. layout_clear zeros and
frees each node's Shape. Guard: never free empty_layout.shape (module
constant).
Shape uses optional sub-structs to reduce per-shape memory footprint:
| Field | Type | Contains | Default |
|---|---|---|---|
tc |
&TextConfig |
text, text_style, password flag, … | unsafe{nil} |
fx |
&ShapeEffects |
shadow, gradient, border_gradient, shader, blur | unsafe{nil} |
| (events) | &EventHandlers |
on_click, on_hover, on_key_down, … | unsafe{nil} |
Access: shape.tc.text, shape.fx.shadow. Always nil-check via has_events().
Lazy alloc: range_slider / date_picker_roller allocate &EventHandlers on
first use; color_picker allocates &ShapeEffects for gradient.
scrollbar_orientation ScrollbarOrientation (1-byte enum) identifies scrollbar
shapes — the old name string field was removed.
resource string holds either image_name or svg_name, discriminated by
shape_type.
Containers with clip: true and radius > 0 (or circle()) clip
child images to the rounded boundary via an SDF alpha-mask shader.
window.clip_radiuspropagates duringrender_layoutrecursion and is composed per clip scope. Child clips reduce radius withmin(parent, child); non-rounded child clips inherit parent.DrawImage.clip_radius > 0triggersdraw_image_rounded()in the dispatch — a custom SGL pipeline (image_clip) that samples the texture and applies SDF rounded-rect masking in the fragment shader.- Inline RTF object images use the same
DrawImageclip-radius path, so rounded clipping is consistent. image_clipinit failure is latched and fallback warning is emitted once; fallback draws unclipped.- Non-clipped images (
clip_radius == 0) use the standardctx.draw_imagepath unchanged.
gui.column(clip: true, radius: 40, width: 80, height: 80,
content: [gui.image(src: "avatar.jpg", sizing: gui.fill_fill)])
gui.circle(clip: true, width: 80, height: 80,
content: [gui.image(src: "avatar.jpg", sizing: gui.fill_fill)])import svg // correct — short name
// import gui.svg // wrong — "unknown type" errorsSame pattern: import nativebridge. V cannot resolve submodule types when
imported with the full parent-module path.
map[string][6]f32 has broken in checks and optional access at the call
site. Passing such a map to a helper function while assigning to it silently
produces a zero matrix instead of identity — all geometry collapses to origin.
Workaround: direct assignment m[key] = value works. Do not extract a
helper that takes the map as a parameter. See render_svg.v group_matrices
for the inline comment.
V cannot return &T from an inline if expression when the other branch
returns voidptr. Use a helper method returning &T with unsafe { nil }.
When adding a str_* field to Locale:
locale.v— new field with en-US defaultlocale_bundle.v—str_or(b.strings, …)line into_locale()locale_presets.v— de-DE and ar-SA preset constantsexamples/locales/*.json— all three JSON bundles (de-DE, ar-SA, ja-JP)
Showcase embeds locale JSON via $embed_file; no disk I/O at runtime.
locale_registry.v init() auto-registers 3 built-in presets.
- IME overlay created lazily in
frame_fnviainit_ime()— NSWindow not ready duringinit_fn. vglyph.ime_overlay_set_focused_field()MUST be called to make overlay first responder; without it, no IME interception.update_ime_focusMUST only activate overlay for shapes withon_ime_commit != nil. Activating for menus/other focusables steals events, breaks hover/click.- Timing:
set_id_focusfires duringon_init(layout gen) beforeinit_imeruns. Fix: after overlay creation, re-apply focus if already set. - Tab navigation: use
w.set_id_focus(), not directw.view_state.id_focus = …—set_id_focustriggers IME hooks. keyDown:forwards unhandled keys to MTKView; no double-insertion for ASCII.
Relevant files: ime.v, window.v, view_input.v, view_container.v,
render_layout_tree.v, shape.v, xtra_window.v, view_text.v.
id_focus is a u32 that identifies focusable widgets. Composite
widgets consume multiple consecutive IDs from their base:
| Widget | IDs used |
|---|---|
color_picker |
base .. base+7 (SV area, RGBA, HSV channels) |
radio_button_group |
base .. base+N-1 (one per option) |
numeric_input |
1 |
input |
1 |
select |
1 |
When placing multiple focusable widgets in the same view, space
id_focus values to avoid collisions. Overlapping IDs cause
unintended focus jumps (e.g. selecting a radio button activates a
color picker channel).
gui/ # main module — Window, View, Layout, Shape, Renderer
svg/ # SVG parsing, tessellation, geometry (import svg)
nativebridge/ # platform bridge (import nativebridge)
Files that need Window/View/Renderer stay in gui/: svg_load.v,
render_svg.v, view_svg.v, svg_textpath.v.
SvgColor in the submodule mirrors gui.Color; conversion via
svg_to_color() at the boundary in svg_load.v / render_svg.v.
Test files in module svg must not use svg as a local variable name
(shadows the module name).