Skip to content

feat(lvgl): real widgets + modern dialogs, CJK chrome, selection polish & OEM branding slot#40

Merged
MarsDoge merged 17 commits into
mainfrom
experimental/lvgl-spike
Jun 7, 2026
Merged

feat(lvgl): real widgets + modern dialogs, CJK chrome, selection polish & OEM branding slot#40
MarsDoge merged 17 commits into
mainfrom
experimental/lvgl-spike

Conversation

@MarsDoge

@MarsDoge MarsDoge commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Advances the LVGL display backend from "cue glyphs over native text" toward real widgets for every value-bearing IFR opcode, then builds out the in-setup experience: modern dialogs, a localized (CJK-capable) chrome, a clean selection treatment, and an original OEM-branding slot. Everything is display-only — edk2 FormBrowser still owns selection, editing, ConfigAccess, callbacks, and variable writes. MODERN_SETUP_DISPLAY_ENGINE=lvgl|modern selects the LVGL or GOP renderer behind the same ModernDisplayEngineDxe.

1. IFR opcode → real LVGL widgets (display-only)

  • one-of → lv_dropdown, checkbox → lv_checkbox, numeric/string/password → lv_textarea (password masked), ordered-list / date-time mapped too. Built as transient display-only lv_objs → lv_snapshot_take(ARGB8888) → alpha-composited over the row background in the shadow canvas → BLT. Wired into the in-setup DisplayEngine via ModernDisplayDrawValueWidget, stripping FormBrowser glyph-width markers.
  • The text-input caret is renderer-drawn (ModernDisplayDrawTextCaret) instead of the native ConOut cursor, so it is themed on both backends.
  • The GOP backend gains matching ModernUiRenderOneOf/Checkbox/String/Password/Numeric/OrderedList/DateTime entry points (themed boxes) — no non-LVGL regression.

2. Modern dialogs

  • Confirm/error/selectable popups render as clean modern panels: full text (the proportional font no longer truncates to a char-count budget), and no retro box-draw seam (U+2500–257F stripped under the modern renderer; the panel surface supplies the frame).

3. Localized (CJK) chrome

  • The in-setup header product/mode names and tab-hint labels route through ModernUiStringLib, so under the zh-Hans default the setup header reads 现代UEFI设置工具 / 高级模式 with tabs 设置分类 / 设备 / 启动 / 安全 / 退出, matching the front-page app. CJK glyphs come from the existing embedded Noto Sans CJK SC subset. ModernUiStringLib is now DXE_DRIVER-usable and wired into the DisplayEngine overlay on all four targets.

4. Selection & label polish

  • The highlighted row is now a clean single-intent selection: a subtle warm-tinted fill (sourced once in ModernUiGetSelectableRowBackground, so the row fill and per-cell text anti-alias background stay matched) plus one solid left-accent stripe — replacing the muddy multi-layer band. The per-cell print path suppresses its flat fill only on the styled row (scoped so highlighted popup options are unaffected).
  • The "CONTEXT HELP" rail label gets a soft muted-gold accent, consistent with — but subdued below — the CPU/Memory/Voltage telemetry rail.

5. OEM branding slot (original art)

  • A new ModernUiDrawOemWatermark composites an original, theme-tinted brand mark (document glyph + project URL) into the in-setup content whitespace. 100% original geometry + the project URL — no IBV/commercial reuse: an SVG design source rasterized to an A8 coverage map by Scripts/gen-oem-watermark.py. Drawn after the statement rows and confined to the empty band below the last menu row, so it is suppressed on row-filled forms (never tints a statement). LVGL composites it; the GOP backend is a no-op for now.

6. Docs

  • Docs/PRODUCTIZATION_STATUS.md — a living, checklist-style read on how far the engine (GOP/LVGL lines) and the front-page app are from shipping, with effort sizing and the gating decisions (LVGL graduation, CJK coverage strategy, memory budget, hardware validation).

Self-review fixes (pre-merge)

A multi-angle review of this branch caught and fixed two real bugs: an EFIAPI/ABI mismatch on ModernDisplayDrawOemWatermarkOverlay (declared EFIAPI, defined without it), and a stale highlight-row record across popups that could drop a selectable option's highlight fill. Both fixed and re-verified.

Validation

  • OVMF X64 builds clean in both LVGL and GOP modes; python3 Tests/Smoke/smoke_validate.py — PASS.
  • Verified by OVMF X64 lvgl screendumps: widgets on DriverSample, modern confirm/selectable popups, Chinese chrome, clean selection at rest / after highlight move, watermark shown on a short form and suppressed on a dense form, app front page fully in zh-Hans.
  • QEMU LoongArch LVGL-mode FD built for separate hardware verification.

External/edk2 is intentionally untouched (separate baseline work, not part of this branch).

🤖 Generated with Claude Code

MarsDoge and others added 17 commits June 5, 2026 15:45
ReadString's edit caret used gST->ConOut->EnableCursor, which draws straight
to the GraphicsConsole framebuffer and is invisible/misplaced behind an
off-screen canvas (the LVGL backend). Suppress the native cursor and paint a
thin accent caret at the edit cell via a new renderer helper,
ModernDisplayDrawTextCaret (declared in FormDisplay.h, implemented in
ModernUiCustomizedDisplayLib alongside the row-cue helpers), redrawn each
keystroke after the field text so it leaves no trail.

This closes the last interaction surface that bypassed the renderer: the LVGL
backend now composites the full FormBrowser interaction set (rows, one-of and
dialog popups, and input editing) end-to-end, identically on the GOP backend.
App-local; no HII/value/storage semantics touched.

Validated: OVMF X64 lvgl-mode screendump of the DriverSample string editor
showing the caret, GOP + lvgl builds, and smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nly)

First IFR-opcode -> LVGL-widget mapping. A new renderer entry point,
ModernUiRenderOneOf, abstracts a one-of control: the LVGL backend builds a
transient display-only lv_dropdown (rounded box, border, LV_SYMBOL_DOWN arrow),
renders it via lv_snapshot_take (enables LV_USE_SNAPSHOT + adds lv_snapshot.c),
and alpha-composites the ARGB8888 snapshot over the row background in the shadow
canvas; the GOP backend keeps composing the value box from primitives, so there
is no GOP regression.

ModernUiEngineDrawValue routes one-of values to it (front-page App path); the
in-setup DisplayEngine overlays it on the value lane via the new
ModernDisplayDrawOneOfWidget, using the option string FormBrowser just printed
(NARROW_CHAR/WIDE_CHAR glyph markers stripped). The one-of affordance cue is
skipped for these rows since the rendered control carries its own arrow.

Display-only: edk2 FormBrowser still owns the selection popup, ConfigAccess,
and callbacks. This is the reusable widget pipeline (create -> snapshot ->
alpha-composite) for further opcode->widget mappings.

Validated: OVMF X64 lvgl + GOP screendumps of the DriverSample VFR form and the
App preferences page (one-of renders as a real dropdown on LVGL, a value box on
GOP, with clean option text), both builds clean, smoke PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the IFR-opcode -> LVGL-widget mapping beyond one-of to the rest of the
value-bearing controls. On the LVGL backend: checkbox -> a checked lv_checkbox;
numeric/string/password -> a styled lv_obj field with an lv_label (password
masked). All share a new LvglComposeSnapshot helper (lv_snapshot_take +
ARGB8888 alpha-composite) and LvglStyleControl/LvglAsciiLabel helpers.

Renderer layer gains per-control entry points (ModernUiRenderCheckbox/Numeric/
String/Password) plus an arrow-less ModernUiDrawFieldBox; the GOP backend
implements them as themed value/field boxes (no regression). The App path
(ModernUiEngineDrawValue) dispatches per value type, and the in-setup
DisplayEngine generalizes ModernDisplayDrawOneOfWidget into
ModernDisplayDrawValueWidget (opcode-dispatched over the value lane). The
affordance cue is skipped for all mapped rows. Date/time and ordered-list keep
the cue (no clean single-widget mapping yet); action/reference keep the arrow.

Display-only throughout: edk2 FormBrowser still owns selection/editing,
ConfigAccess, and callbacks.

Validated: OVMF X64 lvgl + GOP screendumps of the DriverSample VFR form (every
value control renders as a real widget on LVGL / a themed box on GOP, with clean
text), both builds clean, smoke PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the IFR-opcode -> LVGL-widget mapping for the remaining
value-bearing controls:

- ordered-list -> a real list-style lv_obj field (LV_SYMBOL_LIST prefix +
  the current option order), wired into both the App draw path and the
  in-setup DisplayEngine overlay; its affordance cue is dropped since the
  widget carries its own.
- date/time -> a segmented field (value spaced around / : - delimiters so
  month/day/year or H:M:S read as discrete cells) on the app-facing draw
  path. In the in-setup DisplayEngine date/time deliberately keeps native
  per-segment rendering + the type cue, because FormBrowser highlights the
  active segment in place and a full-lane widget overlay would mask that
  editing feedback.

New renderer entry points ModernUiRenderOrderedList / ModernUiRenderDateTime
on both backends (GOP draws themed field boxes -- no regression). Display-only
throughout: edk2 FormBrowser still owns the reorder popup and segment editing.

Validated: OVMF X64 lvgl + GOP (DriverSample) builds clean, smoke PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ght band

When the highlighted statement is the last visible row (e.g. Reset on the
front page), the form's 'clean the remain field' loop cleared the rows below
the menu while the highlight display attribute was still set. The modern
renderer paints cleared rows with the current background, so that empty area
was filled with the highlight band color (SelectedBand) -- a large spurious
tan block below the menu.

Reset the display attribute to the normal field color (GetFieldTextColor,
which maps to Theme->Surface) before the clear loop, mirroring the reset
already done before the scroll down-arrow. Backend-agnostic; fixes any form
whose last visible row is highlighted.

Validated: OVMF X64 lvgl + GOP builds clean, smoke PASS, front-page
screendump with Reset highlighted shows a dark empty area (no tan block).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…own as ?)

FormBrowser builds an ordered-list value by joining options with
CHAR_CARRIAGE_RETURN (ProcessOptions.c). That 0x0D passes the >= 0xFFF0
glyph-width marker filter but is < 0x20, so LvglAsciiLabel rendered each
separator as '?' (<ATAPI CD>?<IDE HDD>?<PXE>).

Add a shared ModernUiNormalizeOrderedListText helper (in the common surface
compiled into both backends): strip glyph-width markers, collapse each run of
CR/LF into a single ' / ', and drop leading/trailing separators. Both the GOP
and LVGL ordered-list renderers normalize the value before drawing, so the box
reads '<ATAPI CD> / <IDE HDD> / <PXE>'. Display-only; minimal scope (no
multi-row list box).

Validated: OVMF X64 lvgl + GOP builds clean, smoke PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…empty label

Two in-setup widget issues found in live testing:

1. Numeric in-place editing and string/password input popups collided with the
   composited value-lane widget: native FormBrowser's initial editor draw is
   narrower than (and a different color from) the widget, so the widget box
   remnant showed beside/under the editor (digits looked invisible on the band;
   the string popup left the old-value widget peeking out to its left). Add
   ModernDisplayClearValueLane, called from the edit entry in the form key
   handler, to repaint the value lane to the plain field background
   (Theme->Surface, matching what native editing erases to) right before the
   editor runs. The full form repaint after editing restores the widget.

2. The lv_checkbox widget leaked a stray ']' from the raw '[X]' value text.
   Render the box indicator with an empty label -- the box conveys the state and
   the row's left prompt already names the control.

Validated: OVMF X64 lvgl + GOP builds clean, smoke PASS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the 'prefer LVGL built-in widgets' direction, replace the hand-composed
lv_obj+lv_label field with a real single-line lv_textarea for the
string/numeric/password value widgets (one-of already uses lv_dropdown,
checkbox uses lv_checkbox).

The earlier reason for the lv_obj+label fallback (textarea caret/scroll
obscuring short text at row height) is addressed by configuration:
- lv_textarea_set_one_line(true)
- scrollbar off (LV_SCROLLBAR_MODE_OFF)
- caret hidden (LV_PART_CURSOR opacity transparent) -- display-only snapshot
- native password masking (lv_textarea_set_password_mode) replaces the manual
  '*' loop
- cursor moved to position 0 so the start of the text shows, not the scrolled end

Display-only; edk2 still owns editing. Verified by an OVMF X64 lvgl DriverSample
screendump: [0]/[21]/[12]/[16]/[18] fields render cleanly at row height.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an OEM branding watermark slot to the modern DisplayEngine. A new renderer
entry point ModernUiDrawOemWatermark composites an original, theme-tinted brand
mark (document glyph + "MODERN UEFI SETUP * OPEN SOURCE" + the project URL) into
the content-area whitespace, low-opacity, so it fills empty space without
fighting the setup text.

The asset is 100% original geometry + the project URL -- no IBV/commercial
reuse. Assets/Branding/oem-watermark.svg is the scalable design source;
Scripts/gen-oem-watermark.py rasterizes it to an A8 coverage map
(OemWatermarkData.c/.h). The LVGL backend alpha-blends the A8 map (tinted with
the theme muted-text color) directly into the shadow canvas and BLTs the region;
the GOP backend is a no-op for now.

Because the native FormBrowser repaints the content area after the chrome (rows,
the empty-row clear, and the help-string update), drawing the mark at chrome
time gets wiped. So the DisplayEngine draws it *after* the statement rows via
ModernDisplayDrawOemWatermarkOverlay (invoked at the end of the CfRepaint pass)
and confines it to the statement column -- the help/right-rail columns are
repainted by later form states and would clip the mark.

Display-only throughout: parses no HII, owns no FormBrowser state, no
ConfigAccess. Verified by OVMF X64 lvgl screendump of a DriverSample sub-form
with content whitespace, plus smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In-setup confirm/error dialogs looked retro and clipped under the modern
renderer:

1. Text truncated with "..." -- the native DisplayEngine sizes popups in
   character columns, but the modern proportional font advances wider than a
   text-grid cell, so a budget derived from the char count clipped the dialog's
   own message ("Load default configurat..."). PrintInternal now grows the text
   budget to the measured proportional width when the caller imposes no column
   constraint (Width == 0), so unconstrained prints render in full.

2. Dashed-border seam -- the native engine frames popups and multi-string boxes
   with Unicode box-drawing glyphs (U+2500..U+257F), which the modern glyph
   renderer painted as dashes on top of the panel surface that already supplies
   the frame. ModernDisplayCopyPrintable now drops that block in modern mode.

Renderer-layer only: edk2 still owns the dialog control flow, key handling, and
message strings. Verified by OVMF X64 lvgl screendump of the F9 "Load defaults"
confirm dialog, plus smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A living, checklist-style read on how far the engine (GOP "modern" and LVGL
"beautiful" lines) and the ModernSetupApp front page are from shipping on real
platforms, with ordered remaining work, effort sizing, and the decisions that
gate the rest. Complements the existing feature/validation matrices rather than
duplicating them; records the LVGL backend graduation, CJK font, memory budget,
and hardware-validation thresholds as the real product gates, plus the known
watermark-on-dense-form regression and the LvglDisplayEngineDxe cleanup backlog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The watermark was anchored in the content region's bottom whitespace, but on a
form whose statement rows fill the content area it tinted faintly over the last
rows (a regression from the watermark feature). The DisplayEngine now passes the
first empty row (FirstEmptyRow, captured before the empty-row clear loop) to
ModernDisplayDrawOemWatermarkOverlay, which confines the mark to the whitespace
band from that row down to the content bottom. When the rows reach the content
bottom the band collapses below the renderer's minimum-size guard, so the mark
is suppressed instead of painting over a statement row. Placement on forms that
do have whitespace is unchanged.

Verified by OVMF X64 lvgl screendumps: dense DriverSample form (mark suppressed)
and short dynamic sub-form (mark still shown in whitespace), plus smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The modern DisplayEngine chrome (header product/mode names and the tab-hint
labels) was pinned to English even when the active language is Simplified
Chinese, so entering a setup form broke the localization the front-page app
already had. Route those labels through ModernUiGetString so the header reads
"现代UEFI设置工具 / 高级模式" with tabs "设置分类 / 设备 / 启动 / 安全 / 退出"
under the zh-Hans default (PcdModernSetupDefaultLanguage). The CJK glyphs come
from the existing embedded Noto Sans CJK SC subset.

Labels are a visual hint only -- no change to form navigation, HII GUID binding,
callbacks, or storage. To make this linkable from the DisplayEngine:
- ModernUiStringLib now declares DXE_DRIVER (was UEFI_APPLICATION only);
- it is added to the modern/lvgl DisplayEngine overlay library block (moved out
  of the app-only block, which is always co-applied) in all four target build
  scripts, and to the CustomizedDisplayLib LibraryClasses.

Verified by an OVMF X64 lvgl screendump of a DriverSample form with the chrome
in Chinese, the modern (GOP) build, and smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The highlighted statement row rendered as a flat, muddy SelectedBand band even
though ModernDisplayDrawStatementRow already paints a modern row-level selection
(inset bar, top/bottom sheen, left accent, framed border) underneath -- the
native per-cell text print buried it under a solid fill.

The per-cell print path (PrintInternal) now suppresses that flat highlight fill
on the single row that just received selection styling, tracked by
mModernStyledHighlightRow (set in ModernDisplayDrawStatementRow, cleared when
the row de-highlights). The skip is scoped to that exact row, so other
EFI_RED-background text -- a highlighted selectable-popup option, which has no
row-level styling beneath it -- still fills normally and keeps its highlight.

Display-only; no change to navigation, editing, or HII ownership. Applies to
both the GOP "modern" and LVGL backends. Verified by OVMF X64 lvgl screendumps:
selection at rest, after a highlight move (no stale band on the previous row),
and an open one-of popup (highlighted option unchanged), plus the modern build
and smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refine the selected row from a busy, muddy treatment (full SelectedBand band +
inset blend + top/bottom sheens + framed border + inner highlight line) into a
clean, single-intent look: a subtle warm-tinted fill plus one solid 6px left
accent stripe.

The selection color is sourced once in ModernUiGetSelectableRowBackground
(Surface blended ~30% toward AccentYellow instead of BackgroundBlack+SelectedBand
at 74%), so the row fill and the per-cell text anti-alias background stay matched
and there are no glyph halos. ModernUiDrawSelectableRow's selected branch is
reduced to the left accent stripe.

Shared visual primitive: applies to both the GOP and LVGL backends and to
selectable rows in both the front-page app and the in-setup browser. Verified by
OVMF X64 lvgl screendumps (selection clear and clean, text legible), the modern
build, and smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "CONTEXT HELP" label rendered as plain muted body text, reading as an
oversight next to the gold CPU/Memory/Voltage rail headers. Give it a soft
muted-gold accent (AccentYellow blended 50% toward MutedText) so it reads as a
styled section header while staying deliberately subdued below the primary
telemetry rail in the visual hierarchy.

Display-only label color; keeps the MutedText token the right-help-rail smoke
guard requires. Verified by an OVMF X64 lvgl screendump and smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ow reset

Two issues found in self-review of this branch:

1. ABI mismatch: ModernDisplayDrawOemWatermarkOverlay was declared EFIAPI in
   FormDisplay.h (and called through that prototype) but defined without EFIAPI.
   The defining TU does not include FormDisplay.h, so no diagnostic fired. On
   toolchains where EFIAPI differs from the default (e.g. GCC X64 ms_abi vs
   SysV), FirstEmptyRow would be read from the wrong register, defeating the
   watermark whitespace-confinement. Add EFIAPI to the definition to match the
   declaration and the sibling functions in the file.

2. Stale highlight-row tracking across popups: the per-cell print path skips the
   highlight fill on the row recorded by ModernDisplayDrawStatementRow. A
   selectable-option popup prints its highlighted option with an EFI_RED
   background via PrintStringAt without going through ModernDisplayDrawStatementRow,
   so if that option shares the recorded grid row its fill was wrongly skipped,
   leaving the option without a highlight. Add ModernDisplayResetHighlightRowTracking
   and call it at CreatePopup entry so popup lines are never mistaken for the
   styled statement row.

Verified by OVMF X64 lvgl screendumps: watermark still shown on a short form and
suppressed on a dense form (FirstEmptyRow passed correctly), and a one-of option
popup with the highlighted option correctly filled, plus smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MarsDoge MarsDoge changed the title feat(lvgl): map IFR value opcodes to real LVGL widgets + renderer-drawn input caret feat(lvgl): real widgets + modern dialogs, CJK chrome, selection polish & OEM branding slot Jun 7, 2026
@MarsDoge MarsDoge merged commit 4fcf547 into main Jun 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant