Skip to content

UI Contributor Guide

A hands-on guide for contributing layout fixes and alternate screen layouts to HelixScreen. Whether you’re fixing clipping at 480x320 or building a portrait layout from scratch, this document covers everything you need. XML changes don’t require a rebuild — just relaunch the app.

For C++ internals, threading, and observer patterns, see the Deep Dive References at the bottom.


Build once, then iterate on XML without rebuilding:

Terminal window
# Build the binary
make -j
# Run at any screen size (mock printer, debug logging)
./build/bin/helix-screen --test -vv -s 800x480

The -s WIDTHxHEIGHT flag sets the window size. The --test flag runs against a mock printer so you don’t need real hardware. -vv gives you debug-level logs (helpful when things don’t look right).

SizeCategoryNotes
480x320TinySmallest supported. Where most bugs live.
800x480Standard / MediumThe “default” target. Most common screen.
1024x600LargeWaveshare 7” and similar.
1280x720XLargeLarger desktop-class displays.
1920x480UltrawideBar-style displays. Very wide, very short.
480x800PortraitRotated standard display.

Take a screenshot of a specific panel at a specific size:

Terminal window
# Saves to /tmp/ui-screenshot-<name>.png
./scripts/screenshot.sh helix-screen tiny-home home --test -s 480x320

This requires ImageMagick. You can also press S while the app is running for a quick screenshot.

Terminal window
./build/bin/helix-screen --test -vv --layout ultrawide -s 1920x480

home, print-select, controls, filament, settings, advanced

motion, print-status, console, bed-mesh, input-shaper, macros, spoolman, ams

XML changes don’t need a rebuild. Edit any .xml file in ui_xml/, relaunch the app, and see your changes immediately. This makes layout iteration very fast.


Breakpoints are based on screen height, because vertical space is always the constraint. Width varies wildly (480 to 1920+), but it’s running out of vertical room that causes clipping, overlapping, and broken layouts.

TierIndexSuffixHeight RangeTarget DevicesFallback
MICRO0_micro<= 272px480x272Falls back to _tiny
TINY1_tiny273 — 390px480x320Falls back to _small
SMALL2_small391 — 460px480x400, 1920x440Required (core tier)
MEDIUM3_medium461 — 550px800x480Required (core tier)
LARGE4_large551 — 700px1024x600Required (core tier)
XLARGE5_xlarge> 700px1280x720+Falls back to _large

Every responsive value needs three core variants: _small, _medium, and _large. The _tiny, _micro, and _xlarge tiers are optional — define them only when values actually need to differ from their fallback tier.

The index column matters for XML reactive bindings. The ui_breakpoint subject holds the current breakpoint as an integer, and you can use bind_flag_if_* or bind_style_if_* with these values. For example, ref_value="0" means Micro, ref_value="2" means Small.

In globals.xml, you define the suffixed variants of each token:

<px name="space_lg_small" value="12"/>
<px name="space_lg_medium" value="16"/>
<px name="space_lg_large" value="20"/>

At startup, theme_manager detects the screen height, picks the matching suffix, and registers the base name (space_lg) pointing to the correct value. So when your XML says style_pad_all="#space_lg", it resolves to 12, 16, or 20 depending on the screen.

CRITICAL: Never define the base name in globals.xml

Section titled “CRITICAL: Never define the base name in globals.xml”

This is the most common mistake. Do NOT do this:

<!-- WRONG -- this will silently break responsive overrides -->
<px name="space_lg" value="16"/>
<px name="space_lg_small" value="12"/>
<px name="space_lg_medium" value="16"/>
<px name="space_lg_large" value="20"/>

LVGL ignores duplicate variable registrations. If the base name space_lg is already registered (from the first line), the responsive override from theme_manager is silently discarded. Only define the suffixed variants.


Design tokens are the shared vocabulary for spacing, sizing, and typography. Use them everywhere instead of hardcoded values. They automatically adapt to the current breakpoint.

TokenSmallMediumLargeUse Case
#space_xxs2px3px4pxKeypad rows, compact icon gaps
#space_xs4px5px6pxButton icon+text gaps, dense info
#space_sm6px7px8pxTight layouts, minor separations
#space_md8px10px12pxStandard flex gaps, compact padding
#space_lg12px16px20pxContainer padding, major sections
#space_xl16px20px24pxEmphasis cards, major separations
#space_2xl24px32px40pxToast/overlay offsets

In XML:

<lv_obj style_pad_all="#space_lg" style_pad_gap="#space_md"/>

In C++:

int padding = theme_manager_get_spacing("space_lg");
TokenComponentSmallMediumLarge
#font_heading<text_heading>noto_sans_20noto_sans_26noto_sans_28
#font_body<text_body>noto_sans_14noto_sans_18noto_sans_20
#font_small<text_small>noto_sans_light_12noto_sans_light_16noto_sans_light_18
#font_xs<text_xs>noto_sans_light_10noto_sans_light_12noto_sans_light_14

You almost never need to reference font tokens directly. Use the semantic <text_*> components instead (see Pre-Themed Widgets).

TokenSmallMediumLargePurpose
#border_radius4px9px12pxCorner radius for cards, buttons
#button_height48px52px72pxStandard button height
#button_height_sm36px40px48pxSmall buttons (back, icon-only)
#button_height_lg64px70px96pxLarge buttons
#header_height48px56px60pxPanel header height
#temp_card_height64px72px80pxTemperature card in print status
#icon_sizemdlgxlResponsive icon size string
#spinner_lg48px56px64pxLarge spinner
#spinner_md24px28px32pxStandard spinner
#spinner_sm16px18px20pxSmall spinner
#spinner_xs12px14px16pxCompact spinner

Follow the triplet pattern in globals.xml:

<!-- In globals.xml — define suffixed variants only -->
<px name="my_widget_height_small" value="48"/>
<px name="my_widget_height_medium" value="56"/>
<px name="my_widget_height_large" value="72"/>

Then use the base name in your layout:

<!-- In your panel XML -->
<lv_obj height="#my_widget_height"/>

From C++:

int h = theme_manager_get_spacing("my_widget_height");

HelixScreen uses 16 semantic color tokens. These work across light and dark modes automatically — you just reference the token name and the system resolves the right value.

TokenPurpose
#screen_bgMain application background
#overlay_bgSidebar/panel backgrounds
#card_bgCard surfaces
#elevated_bgElevated surfaces (dialogs, inputs)
#borderBorders and dividers
#textPrimary text
#text_mutedSecondary/dimmed text
#text_subtleHint/tertiary text
#primaryPrimary accent color
#secondarySecondary accent
#tertiaryTertiary accent
#infoInfo state (purple in Nord)
#successSuccess state (green)
#warningWarning state (amber)
#dangerError/danger (red)
#focusFocus ring outline

Just reference the token. The system handles light/dark mode for you:

<lv_obj style_bg_color="#card_bg"/>
<text_body style_text_color="#warning" text="High temperature"/>

Themes are defined in config/themes/. Here’s a snippet from nord.json:

{
"name": "Nord",
"dark": {
"screen_bg": "#2e3440",
"card_bg": "#434c5e",
"text": "#eceff4",
"text_muted": "#d8dee9",
"primary": "#88c0d0",
"danger": "#bf616a",
"success": "#a3be8c",
"warning": "#ebcb8b"
},
"light": {
"screen_bg": "#eceff4",
"card_bg": "#ffffff",
"text": "#2e3440",
"text_muted": "#3b4252",
"primary": "#5e81ac",
"danger": "#b23a48",
"success": "#3fa47d",
"warning": "#b08900"
},
"border_radius": 12,
"border_width": 1,
"border_opacity": 40
}
// Token lookup -- correct
lv_color_t bg = theme_manager_get_color("card_bg");
// Hex string parsing -- correct
lv_color_t red = theme_manager_parse_hex_color("#FF0000");
// WRONG -- parse_hex_color does NOT look up tokens
// lv_color_t bad = theme_manager_parse_hex_color("#card_bg");

Define _light and _dark variants. The system auto-discovers them by suffix:

<color name="my_custom_light" value="#E0E0E0"/>
<color name="my_custom_dark" value="#3B4252"/>
<!-- Usage in any XML layout -->
<lv_obj style_bg_color="#my_custom"/>

HelixScreen provides semantic widgets that already have the right colors, fonts, spacing, and responsive behavior baked in. Use these instead of raw LVGL widgets whenever possible — it saves you from manually specifying styles and keeps things consistent.

WidgetFont TokenText StyleUse Case
<text_heading>font_headingMutedSection titles
<text_body>font_bodyPrimaryBody paragraphs
<text_muted>font_bodyMutedSecondary metadata
<text_small>font_smallMutedHelper text
<text_xs>font_xsMutedCompact info, badges
<text_button>font_bodyPrimaryButton labels (centered)

All of these support bind_text="subject_name" for dynamic content and text="static text" for fixed content. You can override the color with style_text_color="#token".

<text_heading text="Temperature"/>
<text_body bind_text="nozzle_temp_display"/>
<text_muted text="Last updated 5 min ago"/>
<text_small text="Firmware v1.2.3"/>

A standard card surface with card_bg background, themed border, and border_radius already applied.

<ui_card width="100%" height="content">
<text_heading text="Temperature"/>
<text_body bind_text="nozzle_temp"/>
</ui_card>

Don’t re-specify style_radius, style_bg_color, or style_border_* — they’re already themed.

Themed button with automatic contrast text color and responsive height.

Variants: primary, secondary, danger, success, warning, tertiary, ghost

<ui_button variant="primary" text="Save"/>
<ui_button variant="danger" text="Delete" icon="trash_can"/>
<ui_button variant="ghost" text="Cancel"/>
<ui_button icon="settings"/> <!-- Icon only, no text -->

Material Design Icons with size and color variants.

Size variants: xs (16px), sm (24px), md (32px), lg (48px), xl (64px).

Color variants: text, muted, primary, secondary, tertiary, success, warning, danger, info, disabled.

<icon src="home" size="lg" variant="primary"/>
<icon src="settings" size="#icon_size" variant="muted"/> <!-- Responsive sizing -->

Responsive loading spinner. Sizes adapt per breakpoint.

<spinner size="lg"/> <!-- 48/56/64px depending on breakpoint -->
<spinner size="md"/> <!-- 24/28/32px -->
<spinner size="sm"/> <!-- 16/18/20px -->

Simple horizontal and vertical dividers with themed colors.

<divider_horizontal/>
<divider_vertical/>

A card with a severity-colored left border. Great for status messages.

Severities: info, success, warning, error

<ui_severity_card severity="warning">
<text_body text="Nozzle temperature is high"/>
</ui_severity_card>

Responsive toggle switch. Sizes scale with the current breakpoint.

<ui_switch size="medium" checked="true"/>

Sizes: tiny, small, medium, large

Theme-aware markdown viewer for rich text content.

<ui_markdown bind_text="release_notes" width="100%"/>

These widgets come pre-themed. Adding redundant style attributes clutters the XML and can conflict with theming:

WidgetAlready Themed (skip these)
ui_cardstyle_radius, style_bg_color, style_border_*
ui_buttonstyle_radius, style_bg_color, height, text color
text_*style_text_font, style_text_color
iconFont selection
divider_*style_bg_color, width/height
ui_markdownAll styling

This section covers the patterns you’ll use most in layout work. For a complete reference, see LVGL9_XML_GUIDE.md and LVGL9_XML_ATTRIBUTES_REFERENCE.md.

Almost everything uses flexbox. The three flows you’ll see:

<lv_obj flex_flow="row"/> <!-- Horizontal: children side by side -->
<lv_obj flex_flow="column"/> <!-- Vertical: children stacked -->
<lv_obj flex_flow="row_wrap"/> <!-- Horizontal, wraps to new rows when full -->

Children with flex_grow expand to fill remaining space in their parent. The parent must have an explicit size (not content).

<lv_obj flex_flow="row" width="100%" height="100%">
<lv_obj flex_grow="3" height="100%"><!-- Left column, 30% --></lv_obj>
<lv_obj flex_grow="7" height="100%"><!-- Right column, 70% --></lv_obj>
</lv_obj>

Unlike CSS flexbox, LVGL needs three properties to fully center items — not two. This trips up almost everyone:

<!-- Fully centered column -->
<lv_obj flex_flow="column"
style_flex_main_place="center"
style_flex_cross_place="center"
style_flex_track_place="center">
<text_body text="I am actually centered"/>
</lv_obj>

Without style_flex_track_place, children with explicit widths stay left-aligned even though the other two properties suggest centering. If something isn’t centering the way you expect, add style_flex_track_place="center" first.

<!-- Gap between children -->
<lv_obj flex_flow="row" style_pad_gap="#space_md">
<ui_button text="A"/>
<ui_button text="B"/>
</lv_obj>
<!-- Internal padding around all edges -->
<lv_obj style_pad_all="#space_lg">
<text_body text="Content with breathing room"/>
</lv_obj>

Show or hide elements based on subject values:

<!-- Hide this widget when status == 0 -->
<lv_obj>
<bind_flag_if_eq subject="status" flag="hidden" ref_value="0"/>
</lv_obj>
<!-- Show only when connected (hide when not equal to 1) -->
<lv_obj>
<bind_flag_if_not_eq subject="connected" flag="hidden" ref_value="1"/>
</lv_obj>

Available operators: bind_flag_if_eq, bind_flag_if_not_eq, bind_flag_if_gt, bind_flag_if_ge, bind_flag_if_lt, bind_flag_if_le.

<lv_button name="save_btn">
<event_cb trigger="clicked" callback="on_save_clicked"/>
<text_body text="Save"/>
</lv_button>

Callbacks are registered in C++. For layout-only work, just keep existing <event_cb> elements in place — don’t remove them or rename them.

When you can’t tell why something is overflowing or misaligned, add a temporary background color to see the actual widget bounds:

<lv_obj style_bg_color="#ff0000" style_bg_opa="100%">
<!-- Now you can see exactly where this container starts and ends -->
</lv_obj>

Remove the debug styles before submitting your PR.

Our theme makes lv_obj a pure layout container by default: transparent background, no border, no padding, sized to content. You don’t need to clear any of these — just use lv_obj as a flexbox wrapper and it stays invisible.

WrongRightWhy
width="LV_SIZE_CONTENT"width="content"XML uses string names, not C constants
flex_align="center center"style_flex_main_place="center"flex_align is silently ignored
style_img_recolorstyle_image_recolorFull words, not abbreviations
<lv_dropdown options="A\nB\nC"/>options="A&#10;B&#10;C"Use XML entity for newlines
Hardcoded style_pad_all="12"style_pad_all="#space_lg"Always use design tokens
Hardcoded style_text_font="..."<text_body>Use semantic typography components
style_bg_color="#2e3440"style_bg_color="#screen_bg"Use color tokens, not hex values

HelixScreen supports layout-specific XML overrides so you can rearrange panels for different screen shapes without touching the standard layouts.

LayoutDetectionExample Screens
standardNormal landscape (4:3 to 16:9)800x480, 1024x600, 1280x720
ultrawideAspect ratio > 2.5:11920x480, 1920x400
portraitAspect ratio < 0.8:1480x800, 600x1024
tinyMax dimension <= 480, landscape480x320, 320x240
tiny-portraitMax dimension <= 480, portrait320x480, 240x320

Force a layout with --layout ultrawide on the command line, or set display.layout in settings.json.

ui_xml/
globals.xml <-- Shared by ALL layouts (never override this)
home_panel.xml <-- Standard home panel
controls_panel.xml <-- Standard controls panel
... <-- ~169 XML files total
ultrawide/ <-- Ultrawide overrides
home_panel.xml <-- The only override that exists so far
portrait/ <-- Doesn't exist yet
tiny/ <-- Doesn't exist yet

If ui_xml/<layout>/<panel>.xml exists, it’s used instead of ui_xml/<panel>.xml. Otherwise the standard version is loaded. You only need to override the panels that actually need different layouts — everything else falls through automatically.

  1. Copy the standard layout as a starting point:

    Terminal window
    cp ui_xml/controls_panel.xml ui_xml/ultrawide/controls_panel.xml
  2. Edit the copy for the target screen shape.

  3. Test it:

    Terminal window
    ./build/bin/helix-screen --test -vv --layout ultrawide -s 1920x480
  4. No rebuild needed. XML loads at runtime.

When creating a layout override, you’re rearranging the same content for a different screen shape. The C++ code still expects certain widgets, bindings, and callbacks to exist.

  1. Keep all named widgets that C++ looks up via lv_obj_find_by_name(). Search the panel’s .cpp file to find required names:

    Terminal window
    grep lv_obj_find_by_name src/ui/panels/controls_panel.cpp
  2. Keep all subject bindings (bind_text, bind_value, bind_flag_if_*, etc.). These connect the UI to live data.

  3. Keep all event callbacks (<event_cb> elements). These wire up button presses and interactions.

  4. Use design tokens for all colors, spacing, and fonts. No hardcoded values.

  5. Don’t modify globals.xml. It’s shared across all layouts.

You’re free to rearrange the visual hierarchy, change flex directions, adjust sizes, hide optional decorative elements, or add new layout containers. Just preserve the functional widgets.

Ultrawide (1920x480): Tons of horizontal space, very little vertical. Favor flex_flow="row" to spread content across columns. Aim for everything visible at once with no scrolling. Think “dashboard with columns” — put related info side by side instead of stacking it.

Portrait (480x800): Lots of vertical space, narrow width. Content stacks naturally with flex_flow="column". The navbar probably needs to move to the bottom of the screen. Consider overriding navigation_bar.xml and app_layout.xml to change the overall chrome.

Tiny (480x320): Very limited in both directions. Reduce information density, use bigger touch targets (48px minimum), show fewer labels. Hide optional elements with conditional visibility or just remove decorative content.

Start with the panels that matter most:

PriorityPanelWhy
Highhome_panel.xmlFirst thing users see
Highapp_layout.xmlOverall chrome (navbar + content area)
Highnavigation_bar.xmlNav position/orientation differs per layout
Mediumcontrols_panel.xmlMultiple cards that benefit from rearranging
Mediumprint_status_panel.xmlImportant during active prints
Mediumsettings_panel.xmlCompact 6-row category menu; sub-panels may benefit from multi-column
LowOverlaysUsually modal dialogs that adapt reasonably well

Responsive Setting Rows (no micro/ overrides)

Section titled “Responsive Setting Rows (no micro/ overrides)”

The setting row components (setting_toggle_row, setting_slider_row, setting_dropdown_row, setting_action_row, setting_section_header) handle the Micro breakpoint responsively within a single XML file. There are no micro/ directory overrides for settings — do not create them.

The pattern used by all setting rows:

  1. Two bind_style_if_* for padding — compact padding on Micro (breakpoint 0), standard padding on Tiny and above (breakpoint >= 1):
<styles>
<style name="pad_standard" pad_left="#space_lg" pad_right="#space_lg"
pad_top="#space_lg" pad_bottom="#space_lg" pad_gap="#space_sm"/>
<style name="pad_micro" pad_left="#space_md" pad_right="#space_md"
pad_top="#space_lg" pad_bottom="#space_lg" pad_gap="#space_sm"/>
</styles>
<view ...>
<bind_style_if_eq name="pad_micro" subject="ui_breakpoint" ref_value="0"/>
<bind_style_if_ge name="pad_standard" subject="ui_breakpoint" ref_value="1"/>
  1. Description text hidden on small screens — the description label is hidden on Micro and Tiny (breakpoint < 2) via bind_flag_if_lt:
<text_small name="description" text="$description">
<bind_flag_if_lt subject="ui_breakpoint" flag="hidden" ref_value="2"/>
</text_small>
  1. Info icon for small screens — an info icon appears on Micro/Tiny when a description prop is non-empty. Uses the parse-time hidden_if_empty attribute so it never renders when there is no description, and bind_flag_if_gt to hide it on Medium and above:
<lv_obj name="info_btn" clickable="true" hidden_if_empty="$description">
<bind_flag_if_gt subject="ui_breakpoint" flag="hidden" ref_value="1"/>
<icon src="info_outline" size="sm" variant="muted"/>
<event_cb trigger="clicked" callback="on_setting_info_clicked"/>
</lv_obj>

Tapping the info icon toggles the description label’s visibility inline.

This same responsive pattern should be used for any new setting row components. The convention is: make the component responsive internally rather than creating a micro/ override file.


This is where to start if you want to contribute. Issues are organized by severity and area. All observations below are primarily at 480x320 unless noted otherwise.

These cut across many screens and are high-value fixes:

  • Numeric keypad overlay doesn’t fit vertically at 480x320 — bottom rows are cut off. This affects every panel that uses the keypad for numeric input.
  • Many modals don’t respect viewport height — content clips at top and bottom on small screens.
  • Navbar icons are clipped at 480x320, outlines overlap, and click targets may overlap each other.
  • Temperature labels collide with values on the controls and filament panels.
  • Settings dropdown menus are hardcoded too wide for small screens.

These are the most impactful fixes:

  • Print Select list view — The most broken screen. Padding is wrong, row sizing is off, horizontal overflow everywhere.
  • Print Status overlay — Action buttons, temperature cards, and metadata are all fighting for space. Nothing fits.
  • Filament panel — Multi-filament card is invisible, material buttons crush the operations section.

Usable but clearly broken in places:

  • Controls panel — Position card labels overlap the header, Z-offset value wraps, cooling section overflows, quick actions are clipped.
  • Print Select card view — Metadata area is too tall, squeezing the file thumbnails.
  • PID Calibration — Chips are clipped, text doesn’t wrap, slider padding is off, values wrap awkwardly.

Things that work but could look better:

  • Home panel — Tip text is borderline too large, status icon temperature padding is slightly off.
  • Print File Detail — Pre-print options are cramped when toggles are present.
  • Z-Offset Calibration — Slightly too tall for the viewport, could use scroll.
  • Spoolman — Too much padding, wasted space.
  • Print History list — Filter fields are too wide.

These panels work well and can serve as reference for how to do things right:

  • Motion overlay
  • Advanced settings
  • Settings panel (except dropdown widths)
  • Theme view and edit
  • Print History dashboard
  • Only home_panel.xml has an override (and it needs refinement).
  • Every other panel uses the standard layout and would benefit from ultrawide-specific arrangements.
  • Not started at all. The directories don’t even exist yet.
  • Wide open for contributions. If you have a portrait display, this is a great place to make a big impact.

Terminal window
# Tiny (480x320)
./scripts/screenshot.sh helix-screen tiny-home home --test -s 480x320
# Standard/Medium (800x480)
./scripts/screenshot.sh helix-screen medium-home home --test -s 800x480
# Large (1024x600)
./scripts/screenshot.sh helix-screen large-home home --test -s 1024x600
# XLarge (1280x720)
./scripts/screenshot.sh helix-screen xlarge-home home --test -s 1280x720
# Ultrawide (1920x480)
./scripts/screenshot.sh helix-screen ultrawide-home home --test --layout ultrawide -s 1920x480
# Portrait (480x800)
./scripts/screenshot.sh helix-screen portrait-home home --test --layout portrait -s 480x800

Screenshots save to /tmp/ui-screenshot-<name>.png.

Set HELIX_SCREENSHOT_DISPLAY=0 to prevent the app from opening a visible display window (useful for CI or batch screenshots).

Replace home with any panel or overlay name: controls, filament, settings, print-status, motion, etc.

Two rules, both required — they address different crashes.

Rule 1 — never destroy a container from inside an event callback on its own child (issue #80). The child widget is still on the call stack; deleting its parent synchronously causes use-after-free. Defer the rebuild to the next tick.

Rule 2 — never call synchronous widget deletion inside a deferred callback (issue #776). lv_obj_clean(), lv_obj_del(), and helix::ui::safe_delete() all run synchronously. Multiple sync deletions in the same UpdateQueue::process_pending() batch corrupt LVGL’s event linked list → SIGSEGV in lv_event_mark_deleted. ui_queue_update, lifetime_.defer, tok.defer, and observer callbacks all share that batch — the deferral alone is not enough.

Use the safe replacement — it reparents children to lv_layer_top() and schedules them for lv_obj_delete_async(), which runs outside our UpdateQueue batch:

// ❌ BAD (#80): swatch click handler destroys its own parent synchronously
void handle_color_selected(...) {
lv_obj_clean(container);
rebuild(container);
}
// ❌ STILL BAD (#776): deferred, but lv_obj_clean is still a sync batch deletion
void handle_color_selected(...) {
ui_queue_update([this]() {
lv_obj_clean(container); // Corrupts event list under load
rebuild(container);
});
}
// ✅ CORRECT: defer the rebuild AND use safe_clean_children
void handle_color_selected(...) {
lifetime_.defer([this]() {
helix::ui::safe_clean_children(container);
rebuild(container);
});
}

Replacements table:

❌ Banned in deferred callbacks✅ Use instead
safe_delete(ptr)safe_delete_deferred(ptr)
lv_obj_delete(obj) / lv_obj_del(obj)lv_obj_delete_async(obj)
lv_obj_clean(container)helix::ui::safe_clean_children(container)

See include/ui_utils.h and ARCHITECTURE.md § “No Sync Widget Deletion Inside UpdateQueue Callbacks” for the full rationale.

Before submitting, verify these for every change:

  • No text clipping or overflow at the target size
  • Touch targets are large enough (48px minimum recommended)
  • Design tokens used throughout (no hardcoded pixel values, colors, or fonts)
  • All subject bindings preserved from the standard layout
  • All event callbacks preserved
  • All named widgets still present (check with grep lv_obj_find_by_name in the C++ source)
  • Standard layout still works at 800x480 (no regression)

PRs are welcome. To make review smooth:

  • Include before/after screenshots at the target resolution.
  • Test at the target size and at standard (800x480) to verify no regression.
  • If you created a layout override, mention which named widgets you found in the C++ source so reviewers can verify coverage.
  • Keep changes focused. One panel per PR is easier to review than five.

For the full details on any of these topics, see the dedicated docs:

DocumentWhat It Covers
LVGL9_XML_GUIDE.mdFull XML system guide — subjects, events, component creation, implementation patterns
LVGL9_XML_ATTRIBUTES_REFERENCE.mdQuick-lookup cheatsheet for every XML attribute
DEVELOPER_QUICK_REFERENCE.mdC++ patterns — observer factory, threading, class structures
ARCHITECTURE.mdSystem architecture and high-level design decisions

For most layout and styling work, you shouldn’t need these. But if you’re adding new components, wiring up new subjects, or debugging why a binding isn’t working, that’s where the answers live.