From aed1a3e25fdb456dff51b2c00c3da1fe49b127b9 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Wed, 10 Jun 2026 14:12:10 +0800 Subject: [PATCH 01/10] feat(build): real Secure Boot HII validation scenario for OVMF X64 Add MODERN_SETUP_SECURE_BOOT=1 to Scripts/build-ovmf-x64.sh: passes -D SECURE_BOOT_ENABLE=TRUE through to the upstream OVMF DSC/FDF !if blocks so SecurityPkg's real Secure Boot Configuration formset (SecureBootConfigDxe) is included. This gives the App Devices page and the modern DisplayEngine a production VFR surface (not just DriverSampleDxe). Display-only validation aid, off by default, no upstream edits -- the define flows through the overlay's preserved !if conditions. Also fix MODERN_SETUP_DEMO_DRIVER_SAMPLE so it combines with MODERN_SETUP_REPLACE_UIAPP: the DriverSample DSC/FDF insertion now anchors on QemuKernelLoaderFsDxe (stable; same anchor BootManagerMenuApp uses) instead of the UiApp component that REPLACE_UIAPP may have already replaced. The smoke OVMF fixture gains the matching anchor component/INF. Verified under QEMU (lvgl backend, app front page): Devices page lists the real formsets (32 entries / 10 HII: Secure Boot, RAM Disk, OVMF Platform Configuration, Driver Health, File Explorer, DriverSample) with the read-only preview; Enter opens the real Secure Boot Configuration form through native FormBrowser -- checkbox, mode dropdown, goto row, context-help rail all render modern. Evidence captures committed as Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png and modern-ovmf-x64-secureboot-form.png. Smoke PASS. Co-Authored-By: Claude Opus 4.8 --- .../modern-ovmf-x64-devices-real-hii.png | Bin 0 -> 39514 bytes .../modern-ovmf-x64-secureboot-form.png | Bin 0 -> 54906 bytes CHANGELOG.md | 21 +++++++++++++ Scripts/build-ovmf-x64.sh | 29 +++++++++++++++--- Tests/Smoke/smoke_validate.py | 3 ++ 5 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png create mode 100644 Assets/Screenshots/modern-ovmf-x64-secureboot-form.png diff --git a/Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png b/Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png new file mode 100644 index 0000000000000000000000000000000000000000..4463346dd506c0922ebe165a211a8ced555517ca GIT binary patch literal 39514 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 literal 0 HcmV?d00001 diff --git a/Assets/Screenshots/modern-ovmf-x64-secureboot-form.png b/Assets/Screenshots/modern-ovmf-x64-secureboot-form.png new file mode 100644 index 0000000000000000000000000000000000000000..e1b0bf371077ece8e2f3f7be94b639ef73884026 GIT binary patch literal 54906 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 literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md index 066ec11..7ad8c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,27 @@ this file as both a release log and a lightweight development progress record. ## Unreleased +### Added + +- New `MODERN_SETUP_SECURE_BOOT=1` switch on `Scripts/build-ovmf-x64.sh`: passes + `-D SECURE_BOOT_ENABLE=TRUE` through to the upstream OVMF DSC/FDF `!if` blocks + so SecurityPkg's real **Secure Boot Configuration** formset + (`SecureBootConfigDxe`) is included -- a production VFR surface for validating + the App Devices page and the modern DisplayEngine beyond `DriverSampleDxe`. + Display-only validation aid; off by default; no upstream file is edited. + Verified end-to-end under QEMU: the Devices page lists the real formsets + (Secure Boot, RAM Disk, OVMF Platform Configuration, Driver Health, File + Explorer) and the Secure Boot form renders through native FormBrowser with the + LVGL backend (checkbox, mode dropdown, goto, context help). + +### Fixed + +- `MODERN_SETUP_DEMO_DRIVER_SAMPLE=1` can now be combined with + `MODERN_SETUP_REPLACE_UIAPP=1` on OVMF X64: the DriverSample DSC/FDF insertion + is re-anchored on `QemuKernelLoaderFsDxe` (stable) instead of the UiApp + component, which `MODERN_SETUP_REPLACE_UIAPP` may already have replaced. The + smoke OVMF fixture gains the same anchor. + ### Changed - Refreshed the GitHub showcase screenshots (`Assets/Screenshots/`) to the current diff --git a/Scripts/build-ovmf-x64.sh b/Scripts/build-ovmf-x64.sh index 9d4d2e6..9d21b9e 100755 --- a/Scripts/build-ovmf-x64.sh +++ b/Scripts/build-ovmf-x64.sh @@ -21,6 +21,12 @@ MODERN_SETUP_REPLACE_UIAPP="${MODERN_SETUP_REPLACE_UIAPP:-0}" # password/ordered-list), reachable via Device Manager. Used to exercise the # DisplayEngine control affordances; off by default. MODERN_SETUP_DEMO_DRIVER_SAMPLE="${MODERN_SETUP_DEMO_DRIVER_SAMPLE:-0}" +# Opt-in real-platform HII validation: passes -D SECURE_BOOT_ENABLE=TRUE through +# to the upstream OVMF DSC/FDF !if blocks so SecurityPkg's SecureBootConfigDxe +# (the real Secure Boot configuration formset) is included. This exercises the +# App Devices page and the modern DisplayEngine against a production VFR surface +# instead of only DriverSampleDxe. Display-only validation aid; off by default. +MODERN_SETUP_SECURE_BOOT="${MODERN_SETUP_SECURE_BOOT:-0}" GENERATE_ONLY="${GENERATE_ONLY:-0}" OVERLAY_DIR="${WORKSPACE}/Build/ModernSetupPkgOverlay" @@ -226,11 +232,14 @@ if (display_engine == "modern" or display_engine == "lvgl"): f" gModernSetupPkgTokenSpaceGuid.PcdModernSetupTheme|{theme_pcd}\n" ) if enable_driver_sample and driver_sample_component not in dsc: + # Anchor on QemuKernelLoaderFsDxe (also used for BootManagerMenuApp): it is + # stable whether or not MODERN_SETUP_REPLACE_UIAPP has already replaced the + # UiApp component above. dsc = replace_regex_once( dsc, - r"^( MdeModulePkg/Application/UiApp/UiApp\.inf)", + r"^(\s*OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe\.inf \{)", driver_sample_component + r"\n\1", - "UiApp DSC component anchor for DriverSample", + "QemuKernelLoaderFsDxe DSC component anchor for DriverSample", ) (overlay / "OvmfX64ModernSetup.dsc").write_text(dsc) @@ -266,11 +275,12 @@ if replace_uiapp: " }\n" ) if enable_driver_sample and driver_sample_fdf_inf not in fdf: + # Same stable anchor as the DSC side (UiApp may already be replaced). fdf = replace_regex_once( fdf, - r"^(\s*INF\s+MdeModulePkg/Application/UiApp/UiApp\.inf\s*)$", + r"^(\s*INF\s+OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe\.inf\s*)$", driver_sample_fdf_inf + r"\n\1", - "UiApp FDF INF anchor for DriverSample", + "QemuKernelLoaderFsDxe FDF INF anchor for DriverSample", ) (overlay / "OvmfX64ModernSetup.fdf").write_text(fdf) PY @@ -279,6 +289,7 @@ echo "Generated: ${OVERLAY_DIR}/OvmfX64ModernSetup.dsc" echo "Generated: ${OVERLAY_DIR}/OvmfX64ModernSetup.fdf" echo "DisplayEngine: ${MODERN_SETUP_DISPLAY_ENGINE}" echo "Replace UiApp with ModernSetupApp: ${MODERN_SETUP_REPLACE_UIAPP}" +echo "Secure Boot HII (SecureBootConfigDxe): ${MODERN_SETUP_SECURE_BOOT}" if [[ "${GENERATE_ONLY}" == "1" ]]; then exit 0 @@ -295,12 +306,20 @@ set +u source edksetup.sh set -u +# Extra -D defines flow into the upstream !if blocks preserved by the overlay +# DSC/FDF; no upstream file is edited. +EXTRA_BUILD_DEFINES=() +if [[ "${MODERN_SETUP_SECURE_BOOT}" =~ ^(1|true|yes)$ ]]; then + EXTRA_BUILD_DEFINES+=( -D SECURE_BOOT_ENABLE=TRUE ) +fi + build \ -a X64 \ -t "${TOOL_CHAIN_TAG}" \ -p Build/ModernSetupPkgOverlay/OvmfX64ModernSetup.dsc \ -b "${TARGET}" \ - -n "${JOBS}" + -n "${JOBS}" \ + ${EXTRA_BUILD_DEFINES[@]+"${EXTRA_BUILD_DEFINES[@]}"} echo "Built: ${WORKSPACE}/Build/OvmfX64/${TARGET}_${TOOL_CHAIN_TAG}/FV/OVMF_CODE.fd" echo "Vars: ${WORKSPACE}/Build/OvmfX64/${TARGET}_${TOOL_CHAIN_TAG}/FV/OVMF_VARS.fd" diff --git a/Tests/Smoke/smoke_validate.py b/Tests/Smoke/smoke_validate.py index 283a3c2..8ff9189 100755 --- a/Tests/Smoke/smoke_validate.py +++ b/Tests/Smoke/smoke_validate.py @@ -644,12 +644,15 @@ def ovmf_x64_fixture(workspace: Path) -> None: MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe.inf MdeModulePkg/Application/UiApp/UiApp.inf { } + OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe.inf { + } """, ) write( workspace / "OvmfPkg" / "OvmfPkgX64.fdf", """INF MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe.inf INF MdeModulePkg/Application/UiApp/UiApp.inf +INF OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe.inf """, ) From 65ab7c3be79a35181ddb925321c4eb3128afd4e6 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Wed, 10 Jun 2026 15:03:51 +0800 Subject: [PATCH 02/10] feat(validation): close Gate 4 with the resolution-matrix evidence Run the LVGL productization Gate 4 resolution matrix on OVMF X64 (lvgl backend, app front page) and record the evidence: - 1920x1080: kept (above floor) -- full 13-tab nav row, 3-column quick cards with detail lines, Display row reads 1920 x 1080; no clipping/overlap. - 1024x768: kept (equals floor) -- tab scroll chevron, compact quick-card reflow (height guard drops detail lines), ellipsis truncation on long values. - 800x600: auto-promoted to 1024x768 by SelectPreferredGopMode (smallest qualifying mode); render identical to the native 1024x768 case. Method finding recorded in both the validation matrix and the build script: OVMF's QemuVideoDxe adopts the QEMU EDID preferred mode and overwrites the display PCDs at runtime (PcdVideoResolutionSource==0), so the DSC PCD default is not the effective lever under modern QEMU -- the matrix is driven with `-vga none -device VGA,edid=on,xres=,yres=`. Add an optional MODERN_SETUP_VIDEO_RES=x switch to Scripts/build-ovmf-x64.sh that rewrites the overlay's display-PCD include inline (overlay-only; upstream untouched) for the edid=off case. Evidence tables added to Docs/ProductizationValidationMatrix.md (+ zh mirror) under Phase32; Gate 4 marked closed in Docs/LvglProductizationPlan.md. Smoke PASS. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++++ Docs/LvglProductizationPlan.md | 10 ++--- Docs/ProductizationValidationMatrix.md | 15 +++++++ Docs/ProductizationValidationMatrix.zh-CN.md | 15 +++++++ Scripts/build-ovmf-x64.sh | 44 +++++++++++++++++++- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad8c15..c4a360e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ this file as both a release log and a lightweight development progress record. ### Added +- Resolution-matrix validation (LVGL productization Gate 4 closure): the OVMF X64 + app front page is visually verified at 1920x1080 (kept; full 13-tab nav, + 3-column cards), 1024x768 (kept; tab scroll chevron, compact cards, ellipsis + truncation), and 800x600 (auto-promoted to 1024x768 by + `SelectPreferredGopMode`). Evidence recorded in + `Docs/ProductizationValidationMatrix.md` (+ zh mirror); Gate 4 is now closed in + `Docs/LvglProductizationPlan.md`. A new optional `MODERN_SETUP_VIDEO_RES=x` + switch on `Scripts/build-ovmf-x64.sh` rewrites the overlay's display-PCD + defaults for the `edid=off` case; under modern QEMU the effective lever is the + QEMU EDID (`-vga none -device VGA,edid=on,xres=,yres=`) because OVMF's + `QemuVideoDxe` adopts the EDID preferred mode over the DSC PCDs at runtime. - New `MODERN_SETUP_SECURE_BOOT=1` switch on `Scripts/build-ovmf-x64.sh`: passes `-D SECURE_BOOT_ENABLE=TRUE` through to the upstream OVMF DSC/FDF `!if` blocks so SecurityPkg's real **Secure Boot Configuration** formset diff --git a/Docs/LvglProductizationPlan.md b/Docs/LvglProductizationPlan.md index 7ac4646..f19b239 100644 --- a/Docs/LvglProductizationPlan.md +++ b/Docs/LvglProductizationPlan.md @@ -136,17 +136,17 @@ failures or leaks under sustained navigation. **Current:** canvas sizes to the active GOP mode and re-inits on change; guards skip draws when a region is too small. Graceful degradation (GOP-absent + -degenerate-mode) and mode-change re-init are now defined and fixed; mode-matrix -visual validation is the remaining open item. +degenerate-mode), mode-change re-init, and the resolution-matrix visual +validation are all done — Gate 4 is closed. | ✓ | Step | Effort | Note | | --- | --- | --- | --- | -| [ ] | Resolution matrix | M | Validate 1024×768, 1280×800, 1920×1080, and a small mode (e.g. 800×600). Confirm chrome/rows/right-rail/popups/watermark scale and the size guards behave. Needs a per-resolution OVMF build (resolution PCDs) + capture loop. | +| [x] | Resolution matrix | M | Validated 2026-06-10 on OVMF X64 (lvgl, app front page): 1920×1080 kept (13-tab nav, 3-column cards), 1024×768 kept (tab scroll chevron, compact cards, ellipsis truncation), 800×600 auto-promoted to 1024×768 by `SelectPreferredGopMode`. Driven via QEMU EDID (`-vga none -device VGA,edid=on,xres=…,yres=…`) — OVMF `QemuVideoDxe` adopts EDID over the DSC display PCDs; `MODERN_SETUP_VIDEO_RES` on `build-ovmf-x64.sh` covers the `edid=off` case. Evidence table in `Docs/ProductizationValidationMatrix.md` (Phase32 → Resolution matrix). | | [x] | Re-init correctness on mode change | S | LVGL re-init now syncs `lv_display_set_resolution` to the reallocated canvas and creates the canvas object once (rebinding its buffer) instead of re-creating it each change (which orphaned the prior canvas + its freed buffer). Commit `fix(displayengine): degrade gracefully on absent/degenerate GOP mode`. | | [x] | GOP-absent / degenerate fallback | M | Both `ModernUiRendererInit` backends refuse modes below `MODERN_UI_MIN_RENDER_WIDTH`×`_HEIGHT` (640×480); the in-setup display engine's `PrintInternal` now falls back to text-console `OutputString` (padded) when the renderer is unavailable instead of emitting nothing (blank screen), and the front-page app exits to the native shell. | -**Acceptance:** correct rendering across the matrix (open); graceful degradation -with no GOP or an unusably small mode (done). +**Acceptance:** correct rendering across the matrix (done); graceful degradation +with no GOP or an unusably small mode (done). **Gate 4 closed 2026-06-10.** ## Gate 5 — Interaction completeness & polish diff --git a/Docs/ProductizationValidationMatrix.md b/Docs/ProductizationValidationMatrix.md index d371b5a..71388c8 100644 --- a/Docs/ProductizationValidationMatrix.md +++ b/Docs/ProductizationValidationMatrix.md @@ -61,6 +61,21 @@ Resolution floor (applies to every row below): `SelectPreferredGopMode` (`Librar | Devices | Density rows plus the `>=720`-width native-setup preview split | 1280x800 | `Captured` | Left list and preview pane both render; no missing-glyph squares or value-lane overlap. | | Firmware (provider summary) | Density rows for the read-only provider summary | 1280x800 | `Captured` | Localized zh labels and `N/A`/read-only states render cleanly. | +### Resolution matrix (Gate 4 closure, 2026-06-10) + +Dashboard captured per active GOP mode, driven from the QEMU side +(`-vga none -device VGA,edid=on,xres=,yres=`). Finding: OVMF's +`QemuVideoDxe` adopts the EDID preferred mode and overwrites the display PCDs at +runtime when `PcdVideoResolutionSource==0`, so the DSC PCD default is **not** +the effective lever under modern QEMU; `Scripts/build-ovmf-x64.sh` documents +this and its `MODERN_SETUP_VIDEO_RES` override applies only with `edid=off`. + +| Requested (EDID) | Active mode rendered | Evidence | Result | +| --- | --- | --- | --- | +| 1920x1080 | 1920x1080 (kept; above floor) | `Captured` | Full 13-tab nav row (no scroll chevron), 3-column quick cards with detail lines, dashboard `Display` row reads `1920 x 1080`; no clipping/overlap. | +| 1024x768 | 1024x768 (kept; equals floor) | `Captured` | Tab row scrolls with `>` chevron, quick cards reflow to a compact layout (detail lines dropped by the height guard), long values truncate with ellipsis; no overlap. | +| 800x600 | 1024x768 (auto-promoted) | `Captured` | `SelectPreferredGopMode` promotes the sub-floor EDID mode to the smallest qualifying mode; the render is identical to the native 1024x768 case and the `Display` row reads `1024 x 768`. | + Captured via `Scripts/capture-ovmf-x64.sh` (`BOOT_APP=1` plus a tab `SENDKEY_SEQUENCE`) after rebuilding the App ESP at the current `main` HEAD; inspected as modern-App-only artifacts, which is **not** a native-vs-modern maintainer `Visual reviewed` sign-off. Captures default to `${TMPDIR:-/tmp}/modernsetup-qemu` and are not committed as assets. ## Product Class Validation Matrix diff --git a/Docs/ProductizationValidationMatrix.zh-CN.md b/Docs/ProductizationValidationMatrix.zh-CN.md index ee6334d..0e3818f 100644 --- a/Docs/ProductizationValidationMatrix.zh-CN.md +++ b/Docs/ProductizationValidationMatrix.zh-CN.md @@ -61,6 +61,21 @@ Phase32(`ModernSetupGetPageListLayout`,`Application/ModernSetupApp/ModernSet | Devices | 密度行加 `>=720` 宽度的 native-setup 预览分栏 | 1280x800 | `Captured` | 左列表与预览栏均渲染;无缺字方块、无值列重叠。 | | Firmware(provider 摘要) | 只读 provider 摘要的密度行 | 1280x800 | `Captured` | 本地化 zh 标签与 `N/A`/只读状态渲染干净。 | +### 分辨率矩阵(Gate 4 收尾,2026-06-10) + +按活动 GOP 模式逐档捕获仪表盘,从 QEMU 侧驱动 +(`-vga none -device VGA,edid=on,xres=,yres=`)。发现:当 +`PcdVideoResolutionSource==0` 时 OVMF 的 `QemuVideoDxe` 会采纳 EDID 首选模式并 +在运行时覆写显示 PCD,因此在现代 QEMU 下 DSC 的 PCD 默认值**不是**有效杠杆; +`Scripts/build-ovmf-x64.sh` 已记录此事,其 `MODERN_SETUP_VIDEO_RES` 覆盖仅在 +`edid=off` 时生效。 + +| 请求(EDID) | 实际渲染模式 | 证据 | 结果 | +| --- | --- | --- | --- | +| 1920x1080 | 1920x1080(保持;高于下限) | `Captured` | 13 个导航标签全展开(无滚动符),快捷卡三列含详情行,仪表盘「显示」行读数 `1920 x 1080`;无截断/重叠。 | +| 1024x768 | 1024x768(保持;等于下限) | `Captured` | 标签行带 `>` 滚动符,快捷卡回流为紧凑布局(高度守卫剪掉详情行),长值省略号截断;无重叠。 | +| 800x600 | 1024x768(自动升档) | `Captured` | `SelectPreferredGopMode` 把低于下限的 EDID 模式升到最小合格模式;渲染与原生 1024x768 一致,「显示」行读数 `1024 x 768`。 | + 经 `Scripts/capture-ovmf-x64.sh`(`BOOT_APP=1` 加 tab `SENDKEY_SEQUENCE`)在重建当前 `main` HEAD 的 App ESP 后捕获;作为「仅 modern App」产物审阅,**不是** native-vs-modern 的 maintainer `Visual reviewed` 签署。截图默认输出到 `${TMPDIR:-/tmp}/modernsetup-qemu`,不作为资产提交。 ## 产品类别验证矩阵 diff --git a/Scripts/build-ovmf-x64.sh b/Scripts/build-ovmf-x64.sh index 9d21b9e..7b38b03 100755 --- a/Scripts/build-ovmf-x64.sh +++ b/Scripts/build-ovmf-x64.sh @@ -27,6 +27,16 @@ MODERN_SETUP_DEMO_DRIVER_SAMPLE="${MODERN_SETUP_DEMO_DRIVER_SAMPLE:-0}" # App Devices page and the modern DisplayEngine against a production VFR surface # instead of only DriverSampleDxe. Display-only validation aid; off by default. MODERN_SETUP_SECURE_BOOT="${MODERN_SETUP_SECURE_BOOT:-0}" +# Opt-in firmware video resolution override (e.g. MODERN_SETUP_VIDEO_RES=1920x1080) +# for the Gate 4 / Phase32 resolution-matrix validation. Rewrites the overlay +# DSC's display-PCD include with inline values; empty keeps the upstream default +# (1280x800). Only the generated overlay is written; upstream files are not edited. +# NOTE: when QEMU presents an EDID (modern QEMU std VGA default), OVMF's +# QemuVideoDxe overwrites these PCDs at runtime from the EDID preferred mode +# (PcdVideoResolutionSource==0 path). To drive the matrix from QEMU instead, use +# `-vga none -device VGA,edid=on,xres=,yres=`; this PCD override only +# decides the resolution when no EDID hint is present (e.g. `edid=off`). +MODERN_SETUP_VIDEO_RES="${MODERN_SETUP_VIDEO_RES:-}" GENERATE_ONLY="${GENERATE_ONLY:-0}" OVERLAY_DIR="${WORKSPACE}/Build/ModernSetupPkgOverlay" @@ -53,7 +63,7 @@ fi mkdir -p "${OVERLAY_DIR}" -python3 - <<'PY' "${WORKSPACE}" "${OVERLAY_DIR}" "${MODERN_SETUP_THEME}" "${MODERN_SETUP_DISPLAY_ENGINE}" "${MODERN_SETUP_REPLACE_UIAPP}" "${MODERN_SETUP_DEMO_DRIVER_SAMPLE}" +python3 - <<'PY' "${WORKSPACE}" "${OVERLAY_DIR}" "${MODERN_SETUP_THEME}" "${MODERN_SETUP_DISPLAY_ENGINE}" "${MODERN_SETUP_REPLACE_UIAPP}" "${MODERN_SETUP_DEMO_DRIVER_SAMPLE}" "${MODERN_SETUP_VIDEO_RES}" from pathlib import Path import re import sys @@ -64,6 +74,15 @@ theme_name = sys.argv[3].strip().lower() display_engine = sys.argv[4].strip().lower() replace_uiapp_flag = sys.argv[5].strip().lower() enable_driver_sample = sys.argv[6].strip().lower() in {"1", "true", "yes"} +video_res = sys.argv[7].strip().lower() +video_width = video_height = None +if video_res: + match = re.fullmatch(r"(\d{3,4})x(\d{3,4})", video_res) + if match is None: + raise SystemExit( + f"Unsupported MODERN_SETUP_VIDEO_RES={video_res!r}; use x, e.g. 1920x1080" + ) + video_width, video_height = match.group(1), match.group(2) theme_pcd = { "orange": "0x00", "amber": "0x00", @@ -241,6 +260,28 @@ if enable_driver_sample and driver_sample_component not in dsc: driver_sample_component + r"\n\1", "QemuKernelLoaderFsDxe DSC component anchor for DriverSample", ) +if video_width is not None: + # Replace the upstream display-PCD include with its expanded content carrying + # the requested resolution; this avoids duplicate PCD assignments and leaves + # the upstream .inc untouched (resolution-matrix validation builds). + video_pcd_block = ( + f" gEfiMdeModulePkgTokenSpaceGuid.PcdVideoHorizontalResolution|{video_width}\n" + f" gEfiMdeModulePkgTokenSpaceGuid.PcdVideoVerticalResolution|{video_height}\n" + f" gEfiMdeModulePkgTokenSpaceGuid.PcdSetupVideoHorizontalResolution|{video_width}\n" + f" gEfiMdeModulePkgTokenSpaceGuid.PcdSetupVideoVerticalResolution|{video_height}\n" + " gEfiMdeModulePkgTokenSpaceGuid.PcdConOutRow|0\n" + " gEfiMdeModulePkgTokenSpaceGuid.PcdConOutColumn|0\n" + " gEfiMdeModulePkgTokenSpaceGuid.PcdSetupConOutRow|0\n" + " gEfiMdeModulePkgTokenSpaceGuid.PcdSetupConOutColumn|0\n" + " gUefiOvmfPkgTokenSpaceGuid.PcdVideoResolutionSource|0" + ) + dsc = replace_regex_once( + dsc, + r"^!include OvmfPkg/Include/Dsc/OvmfDisplayPcds\.dsc\.inc\s*$", + video_pcd_block, + "OvmfDisplayPcds include", + ) + (overlay / "OvmfX64ModernSetup.dsc").write_text(dsc) fdf_path = workspace / "OvmfPkg/OvmfPkgX64.fdf" @@ -290,6 +331,7 @@ echo "Generated: ${OVERLAY_DIR}/OvmfX64ModernSetup.fdf" echo "DisplayEngine: ${MODERN_SETUP_DISPLAY_ENGINE}" echo "Replace UiApp with ModernSetupApp: ${MODERN_SETUP_REPLACE_UIAPP}" echo "Secure Boot HII (SecureBootConfigDxe): ${MODERN_SETUP_SECURE_BOOT}" +echo "Video resolution override: ${MODERN_SETUP_VIDEO_RES:-(upstream default)}" if [[ "${GENERATE_ONLY}" == "1" ]]; then exit 0 From b41cdadf34d539ed9ff39744033f92102048e1fd Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Wed, 10 Jun 2026 16:10:56 +0800 Subject: [PATCH 03/10] fix(lvgl): render subset CJK in LVGL widgets via embedded-bitmap font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported: the language dropdown showed "中文" as "??" on the LoongArch REPLACE_UIAPP firmware. Root cause: the LVGL backend's widget label path (LvglAsciiLabel) ASCII-folded every non-ASCII code point to '?', so any CJK value text in the lv_dropdown/checkbox/textarea/ordered-list widgets degraded to question marks. (The standalone ESP app links the GOP renderer, which is why the same dropdown rendered correctly there -- the bug only shows when the app renders through ModernUiLvglRendererLib, e.g. REPLACE_UIAPP lvgl firmware.) Fix at the font level rather than per-widget special cases: - Register a custom lv_font_t (LvglCjkFont) whose get_glyph_dsc/get_glyph_bitmap callbacks serve the embedded Noto Sans CJK SC 18x18 A8 subset (ModernUiFindBuiltinGlyph -- the same bitmaps the primitive text path composites), copying rows into the renderer draw buffer with the A8 stride per the upstream fmt_txt contract. The stock Latin font (LV_FONT_DEFAULT) is the fallback, so ASCII keeps the stock widget look. - Apply the font in LvglStyleControl (single shared styling point for all five widget paths). - Replace LvglAsciiLabel with LvglWidgetLabel: UTF-8 emission for subset-covered CJK; anything outside the subset still degrades to '?' so LVGL never draws a placeholder/tofu box (graceful-fallback policy). Verified under QEMU on the REPLACE_UIAPP lvgl firmware (the exact reported surface): the Exit-page language dropdown renders 中文 in both the value box and the open option list. Smoke PASS. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 13 ++ .../ModernUiRendererLib.c | 188 ++++++++++++++++-- 2 files changed, 187 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a360e..039e21b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ this file as both a release log and a lightweight development progress record. ## Unreleased +### Fixed + +- LVGL widget text no longer renders CJK as `?`. The LVGL backend's widget paths + (one-of dropdown value, checkbox label, numeric/string field, ordered list) + previously ASCII-folded all non-ASCII to `'?'` ("中文" showed as "??", e.g. the + front-page Exit language dropdown when the app renders through the LVGL + backend, as in `MODERN_SETUP_REPLACE_UIAPP=1` lvgl firmware). The backend now + registers a custom `lv_font_t` backed by the embedded Noto Sans CJK SC A8 + subset (the same bitmaps the primitive text path composites) with the stock + Latin font as fallback, and converts widget labels to UTF-8. Subset CJK renders + natively in widgets; out-of-subset code points still degrade to `'?'` (never an + LVGL placeholder/tofu box), per the graceful-fallback policy. + ### Added - Resolution-matrix validation (LVGL productization Gate 4 closure): the OVMF X64 diff --git a/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c b/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c index 9b135ca..8824146 100644 --- a/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c +++ b/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c @@ -790,36 +790,191 @@ ModernUiDrawText ( } +// +// Baseline (from the bottom of line_height) used by the widget fallback font. +// Chosen so Latin fallback glyphs keep a sane baseline while the full-cell +// 18x18 CJK bitmaps stay top-aligned within the 18px line (ofs_y compensates). +// +#define LVGL_CJK_FONT_BASE_LINE 2 + +STATIC lv_font_t mLvglCjkFont; +STATIC BOOLEAN mLvglCjkFontReady = FALSE; + +/** + lv_font_t get_glyph_dsc callback serving the embedded builtin glyph subset. + + Returns FALSE for any code point outside the subset so LVGL consults the + configured fallback (the default Latin font) — ASCII intentionally lives in + the fallback, this font only serves the embedded CJK/punctuation bitmaps. + + @param[in] Font Font being queried (this font). Not used. + @param[out] DscOut Glyph descriptor to fill. Must not be NULL. + @param[in] Letter Unicode code point being resolved. + @param[in] LetterNext Following code point (kerning); unused. + + @retval TRUE Glyph is served by the embedded subset; DscOut is filled. + @retval FALSE Glyph is not in the subset (fallback font is consulted). +**/ +STATIC +bool +LvglCjkGetGlyphDsc ( + const lv_font_t *Font, + lv_font_glyph_dsc_t *DscOut, + uint32_t Letter, + uint32_t LetterNext + ) +{ + CONST MODERN_UI_BUILTIN_GLYPH *Glyph; + + (void)Font; + (void)LetterNext; + + if (Letter > 0xFFFF) { + return false; + } + + Glyph = ModernUiFindBuiltinGlyph ((CHAR16)Letter); + if (Glyph == NULL) { + return false; + } + + DscOut->gid.index = Letter; + DscOut->adv_w = Glyph->Advance; + DscOut->box_w = Glyph->Width; + DscOut->box_h = Glyph->Height; + DscOut->ofs_x = 0; + DscOut->ofs_y = -LVGL_CJK_FONT_BASE_LINE; + DscOut->format = LV_FONT_GLYPH_FORMAT_A8; + DscOut->is_placeholder = 0; + DscOut->stride = 0; + return true; +} + /** - Convert a UCS-2 run to an ASCII label for LVGL's Latin fonts. + lv_font_t get_glyph_bitmap callback for the embedded builtin glyph subset. + + Copies the glyph's A8 rows into the renderer-provided draw buffer using the + stride LVGL expects for A8 (mirroring the upstream fmt_txt contract) and + returns the draw buffer. + + @param[in] GlyphDsc Glyph descriptor previously filled by LvglCjkGetGlyphDsc. + @param[in] DrawBuf Renderer-provided destination buffer. May be NULL. - Glyph-width markers and non-ASCII code points are dropped/replaced; CJK in a - widget label is a known limitation of the widget path (handled elsewhere for - primitive text). + @retval NULL Glyph or destination unavailable. + @retval others DrawBuf with the A8 bitmap copied in. +**/ +STATIC +const void * +LvglCjkGetGlyphBitmap ( + lv_font_glyph_dsc_t *GlyphDsc, + lv_draw_buf_t *DrawBuf + ) +{ + CONST MODERN_UI_BUILTIN_GLYPH *Glyph; + uint8_t *Out; + uint32_t StrideOut; + UINTN Row; + + if ((GlyphDsc == NULL) || (DrawBuf == NULL) || (GlyphDsc->gid.index > 0xFFFF)) { + return NULL; + } - @param[out] Out Destination ASCII buffer. + Glyph = ModernUiFindBuiltinGlyph ((CHAR16)GlyphDsc->gid.index); + if (Glyph == NULL) { + return NULL; + } + + Out = DrawBuf->data; + StrideOut = lv_draw_buf_width_to_stride (Glyph->Width, LV_COLOR_FORMAT_A8); + for (Row = 0; Row < Glyph->Height; Row++) { + CopyMem ( + Out + (Row * StrideOut), + &Glyph->Bitmap[Row * MODERN_UI_BUILTIN_GLYPH_WIDTH], + Glyph->Width + ); + } + + return DrawBuf; +} + +/** + Return the widget text font: the embedded CJK subset with the default Latin + font as fallback. + + Lazily initializes a static lv_font_t on first use. ASCII resolves through + the fallback (this font serves only the embedded subset), so Latin text in + widgets keeps the stock LVGL look while subset CJK renders from the same + bitmaps the primitive text path uses. + + @return Non-NULL font usable with lv_obj_set_style_text_font(). +**/ +STATIC +CONST lv_font_t * +LvglCjkFont ( + VOID + ) +{ + if (!mLvglCjkFontReady) { + ZeroMem (&mLvglCjkFont, sizeof (mLvglCjkFont)); + mLvglCjkFont.get_glyph_dsc = LvglCjkGetGlyphDsc; + mLvglCjkFont.get_glyph_bitmap = LvglCjkGetGlyphBitmap; + mLvglCjkFont.line_height = MODERN_UI_BUILTIN_GLYPH_HEIGHT; + mLvglCjkFont.base_line = LVGL_CJK_FONT_BASE_LINE; + mLvglCjkFont.fallback = LV_FONT_DEFAULT; + mLvglCjkFontReady = TRUE; + } + + return &mLvglCjkFont; +} + +/** + Convert a UCS-2 run to a UTF-8 label for LVGL widget text. + + Glyph-width markers are dropped. CJK code points covered by the embedded + builtin subset are emitted as UTF-8 (rendered by the widget font's subset + path); any other non-ASCII code point degrades to '?' so LVGL never draws a + placeholder/tofu box (graceful-fallback policy). + + @param[out] Out Destination UTF-8 buffer. @param[in] Cap Capacity of Out in bytes (>= 1). @param[in] Src Null-terminated UCS-2 source. May be NULL (empty result). **/ STATIC VOID -LvglAsciiLabel ( +LvglWidgetLabel ( OUT CHAR8 *Out, IN UINTN Cap, IN CONST CHAR16 *Src ) { - UINTN SrcIdx; - UINTN DstIdx; + UINTN SrcIdx; + UINTN DstIdx; + CHAR16 Ch; DstIdx = 0; if (Src != NULL) { for (SrcIdx = 0; (Src[SrcIdx] != CHAR_NULL) && (DstIdx < (Cap - 1)); SrcIdx++) { - if (Src[SrcIdx] >= 0xFFF0) { + Ch = Src[SrcIdx]; + if (Ch >= 0xFFF0) { + continue; + } + + if ((Ch >= 0x20) && (Ch < 0x7F)) { + Out[DstIdx++] = (CHAR8)Ch; continue; } - Out[DstIdx++] = ((Src[SrcIdx] >= 0x20) && (Src[SrcIdx] < 0x7F)) ? (CHAR8)Src[SrcIdx] : '?'; + if ((Ch >= 0x80) && (ModernUiFindBuiltinGlyph (Ch) != NULL) && ((DstIdx + 3) < Cap)) { + // + // Emit as UTF-8 (all subset code points are >= U+0800: 3 bytes). + // + Out[DstIdx++] = (CHAR8)(0xE0 | ((Ch >> 12) & 0x0F)); + Out[DstIdx++] = (CHAR8)(0x80 | ((Ch >> 6) & 0x3F)); + Out[DstIdx++] = (CHAR8)(0x80 | (Ch & 0x3F)); + continue; + } + + Out[DstIdx++] = '?'; } } @@ -851,6 +1006,11 @@ LvglStyleControl ( lv_obj_set_style_bg_color (Obj, ToLvColor (Selected ? Theme->SelectedBand : Theme->Surface), 0); lv_obj_set_style_bg_opa (Obj, LV_OPA_COVER, 0); lv_obj_set_style_border_color (Obj, ToLvColor (Selected ? Theme->PopupBorder : Theme->Border), 0); + // + // Embedded-subset CJK with the stock Latin font as fallback, so localized + // HII value text renders in widgets instead of degrading to '?'. + // + lv_obj_set_style_text_font (Obj, LvglCjkFont (), 0); } /** @@ -986,7 +1146,7 @@ ModernUiRenderOneOf ( return ModernUiDrawValueBox (Context, Rect, Value, Selected, Theme); } - LvglAsciiLabel (Label, sizeof (Label), Value); + LvglWidgetLabel (Label, sizeof (Label), Value); Dropdown = lv_dropdown_create (lv_display_get_screen_active (mDisplay)); if (Dropdown == NULL) { @@ -1101,7 +1261,7 @@ LvglRenderTextField ( return ModernUiDrawFieldBox (Context, Rect, Value, Selected, Theme); } - LvglAsciiLabel (Text, sizeof (Text), Value); + LvglWidgetLabel (Text, sizeof (Text), Value); // // Display-only single-line lv_textarea: a real LVGL input control. one-line @@ -1228,7 +1388,7 @@ ModernUiRenderOrderedList ( return ModernUiDrawFieldBox (Context, Rect, Norm, Selected, Theme); } - LvglAsciiLabel (Text, sizeof (Text), Norm); + LvglWidgetLabel (Text, sizeof (Text), Norm); // // LV_SYMBOL_LIST is a UTF-8 glyph from the bundled Montserrat symbol set; it is // copied verbatim ahead of the ASCII order text to mark the field as a list. @@ -1291,7 +1451,7 @@ ModernUiRenderDateTime ( return ModernUiDrawFieldBox (Context, Rect, Value, Selected, Theme); } - LvglAsciiLabel (Raw, sizeof (Raw), Value); + LvglWidgetLabel (Raw, sizeof (Raw), Value); // // Pad each date/time delimiter with surrounding spaces so the segments read as // discrete cells (e.g. "06 / 05 / 2026"), without parsing the field layout. From 19211451d4e2a275ec91e2ac23a8d9f39f0d63f9 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Wed, 10 Jun 2026 20:20:30 +0800 Subject: [PATCH 04/10] docs(validation): record VFR write-chain interaction evidence Add the end-to-end interaction evidence table (EN + zh mirror): one-of popup select/commit with live IFR conditional re-evaluation on the real Secure Boot form; F10 save dialog + Y; native-vs-modern A/B proving the Secure Boot Mode revert is SecureBootConfigDxe's own RouteConfig semantics (persists only AttemptSecureBoot; CustomMode written only by key-enrollment flows), identical under both engines; grayed-control fidelity (Attempt checkbox, no PK); and the full NV persistence proof on DriverSampleDxe -- change one-of, F10/Y, cold reboot (RESET_VARS=0), value persists and dependent grayoutif/suppressif re-evaluate correctly. Co-Authored-By: Claude Opus 4.8 --- Docs/ProductizationValidationMatrix.md | 19 +++++++++++++++++++ Docs/ProductizationValidationMatrix.zh-CN.md | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Docs/ProductizationValidationMatrix.md b/Docs/ProductizationValidationMatrix.md index 71388c8..83c97e3 100644 --- a/Docs/ProductizationValidationMatrix.md +++ b/Docs/ProductizationValidationMatrix.md @@ -49,6 +49,25 @@ The validation terms below describe current evidence only: Current Phase35 status in this matrix is `Script`/`Manual` foundation only. Static smoke can check that the helper and manual workflow exist; `--mode generate-only` can check overlay snapshots; `--mode build` can check firmware FD snapshots; only `--mode capture` with successful QEMU `screendump` output creates visual screenshot evidence, and the helper does not inspect pixels or mark visual equivalence as verified. +## VFR Write-Chain Interaction Evidence (2026-06-10) + +End-to-end interaction validation on OVMF X64 (lvgl backend, app front page, +`MODERN_SETUP_SECURE_BOOT=1` + `MODERN_SETUP_DEMO_DRIVER_SAMPLE=1` + +`MODERN_SETUP_REPLACE_UIAPP=1`), driven by QEMU `sendkey` with screendump +evidence at each step: + +| Step | Surface | Evidence | Result | +| --- | --- | --- | --- | +| One-of popup open/select/commit | `SecureBootConfigDxe` "Secure Boot Mode" | `Captured` | Popup renders as a modern panel; selecting Custom updates the value lane to `` and the form **live-reevaluates IFR conditionals** — the suppressed "Custom Secure Boot Options" row appears. | +| F10 save dialog + Y confirm | Same form | `Captured` | "Save configuration changes?" dialog renders and Y dismisses it with the changed value retained in-browser. | +| Driver-owned no-persist semantics | Same form, exit + re-enter | `Captured` | Mode reverts to `` after re-entry — **identical under the native DisplayEngine** (A/B re-run with `MODERN_SETUP_DISPLAY_ENGINE=native`). Driver source confirms `SecureBootRouteConfig` persists only `AttemptSecureBoot`; the `CustomMode` variable is written exclusively by the key-enrollment flows. Not an engine defect. | +| Grayed-out control fidelity | "Attempt Secure Boot" checkbox | `Captured` | Renders grayed and non-editable (no PK enrolled, `SetupMode != USER_MODE`), matching native semantics. | +| **NV persistence across cold reboot** | `DriverSampleDxe` "My one-of prompt #1" | `Captured` | Change option (popup) -> F10 -> Y -> cold reboot (`RESET_VARS=0`) -> re-enter form: the changed value (``) persists, the dependent checkbox renders **grayed** (grayoutif on the new value) and the suppressed "Pick 1" ordered list appears (suppressif released). Full chain: modern engine input -> FormBrowser -> ConfigRouting -> driver `RouteConfig` -> `SetVariable` (NV) -> reboot -> `ExtractConfig` -> re-render. | + +Boundary note: all semantics above are owned by native FormBrowser/ConfigAccess; +the modern engine contributes display and input only, and the A/B row shows it +reproduces native behavior including driver-owned non-persistence. + ## Phase32 Responsive Page Layout Matrix Phase32 (`ModernSetupGetPageListLayout`, `Application/ModernSetupApp/ModernSetupAppActions.c`, landed in `038a156`) drives Boot/Devices/provider-summary list rows, padding, the visible row cap, and the Devices preview split from the app-owned `DashboardDensity` preference and the active content rect; drawing and keyboard row counts share the helper, and smoke fixes its compact/comfortable branches. diff --git a/Docs/ProductizationValidationMatrix.zh-CN.md b/Docs/ProductizationValidationMatrix.zh-CN.md index 0e3818f..04b5738 100644 --- a/Docs/ProductizationValidationMatrix.zh-CN.md +++ b/Docs/ProductizationValidationMatrix.zh-CN.md @@ -49,6 +49,23 @@ XArch 是 ModernSetupPkg 的跨架构验证/产品化术语。XArch 不会替代 本矩阵中的 Phase35 当前状态仅为 `Script`/`Manual` foundation。Static smoke 可检查 helper 和手动工作流存在;`--mode generate-only` 可检查 overlay snapshot;`--mode build` 可检查 firmware FD snapshot;只有 `--mode capture` 成功产出 QEMU `screendump` 后才形成视觉截图证据,并且该 helper 不检查像素,也不会将视觉等价标记为 verified。 +## VFR 写链交互证据(2026-06-10) + +OVMF X64 端到端交互验证(lvgl 后端、App 首页、`MODERN_SETUP_SECURE_BOOT=1` + +`MODERN_SETUP_DEMO_DRIVER_SAMPLE=1` + `MODERN_SETUP_REPLACE_UIAPP=1`),由 QEMU +`sendkey` 驱动并逐步截图取证: + +| 步骤 | 验证面 | 证据 | 结果 | +| --- | --- | --- | --- | +| oneof 弹窗打开/选择/提交 | `SecureBootConfigDxe`「Secure Boot Mode」 | `Captured` | 弹窗以现代面板渲染;选 Custom 后值栏变为 ``,且表单**实时重评估 IFR 条件**——被 suppress 的「Custom Secure Boot Options」行出现。 | +| F10 保存对话框 + Y 确认 | 同一表单 | `Captured` | 「Save configuration changes?」对话框渲染正常,Y 关闭后浏览器内保留改值。 | +| 驱动自有的不持久化语义 | 同一表单,退出后重进 | `Captured` | 重进后 Mode 回退 ``——**原生 DisplayEngine 下行为完全一致**(`MODERN_SETUP_DISPLAY_ENGINE=native` A/B 复跑)。驱动源码证实 `SecureBootRouteConfig` 只持久化 `AttemptSecureBoot`;`CustomMode` 变量仅由密钥注册流程写入。非引擎缺陷。 | +| 灰禁控件保真 | 「Attempt Secure Boot」复选框 | `Captured` | 渲染为灰禁不可编辑(未注册 PK,`SetupMode != USER_MODE`),与原生语义一致。 | +| **跨冷重启 NV 持久化** | `DriverSampleDxe`「My one-of prompt #1」 | `Captured` | 弹窗改值 -> F10 -> Y -> 冷重启(`RESET_VARS=0`)-> 重进表单:改值(``)保持,依赖的复选框按新值**变灰**(grayoutif),被 suppress 的「Pick 1」有序列表出现(suppressif 释放)。完整链路:现代引擎输入 -> FormBrowser -> ConfigRouting -> 驱动 `RouteConfig` -> `SetVariable`(NV)-> 重启 -> `ExtractConfig` -> 重新渲染。 | + +边界说明:上述全部语义归原生 FormBrowser/ConfigAccess 所有;现代引擎只贡献显示与 +输入,A/B 行证明它如实复现原生行为(包括驱动自有的不持久化)。 + ## Phase32 响应式页面布局矩阵 Phase32(`ModernSetupGetPageListLayout`,`Application/ModernSetupApp/ModernSetupAppActions.c`,已在 `038a156` 落地)让 Boot/Devices/provider 摘要页的列表行高、padding、可见行上限以及 Devices 预览分栏跟随 app 自有的 `DashboardDensity` 偏好和当前内容矩形;绘制与键盘行数共用同一 helper,smoke 固化其 compact/comfortable 分支。 From a19bebb7b7bf05cd82cdef1afac237a278ddbb7f Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Thu, 11 Jun 2026 16:58:59 +0800 Subject: [PATCH 05/10] docs(validation): record full PK enrollment -> Secure Boot enforcement evidence Extend the VFR write-chain table (EN + zh) with the complete Secure Boot enablement flow driven end-to-end through the modern LVGL engine under QEMU: Custom mode -> Custom Secure Boot Options -> PK Options -> Enroll PK -> file explorer (volume -> root -> DER X509 pk.cer from the ESP) -> Commit Changes and Exit. Same boot: Current Secure Boot State flips Enabled and the Attempt Secure Boot checkbox un-grays/checks. Cold reboot: Secure Boot actively enforces -- the unsigned ESP BOOTX64.EFI is rejected ("Access Denied -- rejected probably by Secure Boot" in serial) and BDS falls back to the FV-embedded app. This exercises the deepest remaining VFR surfaces: nested goto subform navigation, the dynamic file-explorer formset, action gotos with RESET_REQUIRED, conditional un-graying on SetupMode transition, and real security enforcement across reboot. Test PK is an openssl self-signed cert; QEMU-only. Co-Authored-By: Claude Opus 4.8 --- Docs/ProductizationValidationMatrix.md | 2 ++ Docs/ProductizationValidationMatrix.zh-CN.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Docs/ProductizationValidationMatrix.md b/Docs/ProductizationValidationMatrix.md index 83c97e3..87adf9b 100644 --- a/Docs/ProductizationValidationMatrix.md +++ b/Docs/ProductizationValidationMatrix.md @@ -64,6 +64,8 @@ evidence at each step: | Grayed-out control fidelity | "Attempt Secure Boot" checkbox | `Captured` | Renders grayed and non-editable (no PK enrolled, `SetupMode != USER_MODE`), matching native semantics. | | **NV persistence across cold reboot** | `DriverSampleDxe` "My one-of prompt #1" | `Captured` | Change option (popup) -> F10 -> Y -> cold reboot (`RESET_VARS=0`) -> re-enter form: the changed value (``) persists, the dependent checkbox renders **grayed** (grayoutif on the new value) and the suppressed "Pick 1" ordered list appears (suppressif released). Full chain: modern engine input -> FormBrowser -> ConfigRouting -> driver `RouteConfig` -> `SetVariable` (NV) -> reboot -> `ExtractConfig` -> re-render. | +| **Full PK enrollment -> Secure Boot enabled** | `SecureBootConfigDxe` key-enrollment flow | `Captured` + serial | Complete flow driven through the modern engine in one session: Custom mode -> Custom Secure Boot Options -> PK Options -> Enroll PK -> **file explorer** (volume select -> root listing -> pick a DER X509 `pk.cer` from the ESP) -> Commit Changes and Exit. Same boot: "Current Secure Boot State" flips **Enabled**, "Attempt Secure Boot" un-grays and shows checked. Cold reboot (`RESET_VARS=0`): Secure Boot **enforces** -- the unsigned ESP `BOOTX64.EFI` is rejected (`BdsDxe: ... Access Denied -- rejected probably by Secure Boot` in the serial log) and BDS falls back to the FV-embedded app. Test PK generated via openssl (self-signed, CN=ModernSetup Test PK); QEMU-only, no real platform touched. | + Boundary note: all semantics above are owned by native FormBrowser/ConfigAccess; the modern engine contributes display and input only, and the A/B row shows it reproduces native behavior including driver-owned non-persistence. diff --git a/Docs/ProductizationValidationMatrix.zh-CN.md b/Docs/ProductizationValidationMatrix.zh-CN.md index 04b5738..07ae02d 100644 --- a/Docs/ProductizationValidationMatrix.zh-CN.md +++ b/Docs/ProductizationValidationMatrix.zh-CN.md @@ -63,6 +63,8 @@ OVMF X64 端到端交互验证(lvgl 后端、App 首页、`MODERN_SETUP_SECURE | 灰禁控件保真 | 「Attempt Secure Boot」复选框 | `Captured` | 渲染为灰禁不可编辑(未注册 PK,`SetupMode != USER_MODE`),与原生语义一致。 | | **跨冷重启 NV 持久化** | `DriverSampleDxe`「My one-of prompt #1」 | `Captured` | 弹窗改值 -> F10 -> Y -> 冷重启(`RESET_VARS=0`)-> 重进表单:改值(``)保持,依赖的复选框按新值**变灰**(grayoutif),被 suppress 的「Pick 1」有序列表出现(suppressif 释放)。完整链路:现代引擎输入 -> FormBrowser -> ConfigRouting -> 驱动 `RouteConfig` -> `SetVariable`(NV)-> 重启 -> `ExtractConfig` -> 重新渲染。 | +| **完整 PK 注册 -> Secure Boot 开启** | `SecureBootConfigDxe` 密钥注册流程 | `Captured` + 串口 | 单次会话内全程经现代引擎驱动:Custom 模式 -> Custom Secure Boot Options -> PK Options -> Enroll PK -> **文件浏览器**(选卷 -> 根目录列表 -> 从 ESP 选择 DER X509 `pk.cer`)-> Commit Changes and Exit。同一次启动内:「Current Secure Boot State」翻转 **Enabled**,「Attempt Secure Boot」解除灰禁并显示勾选。冷重启(`RESET_VARS=0`):Secure Boot **真实执法**——未签名的 ESP `BOOTX64.EFI` 被拒载(串口日志 `BdsDxe: ... Access Denied -- rejected probably by Secure Boot`),BDS 回退至固件内嵌 App。测试 PK 由 openssl 生成(自签名,CN=ModernSetup Test PK);仅 QEMU,不触碰真实平台。 | + 边界说明:上述全部语义归原生 FormBrowser/ConfigAccess 所有;现代引擎只贡献显示与 输入,A/B 行证明它如实复现原生行为(包括驱动自有的不持久化)。 From 2793a5703359ba8f64a743ccc0ccddbd805b7c65 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Thu, 11 Jun 2026 19:11:06 +0800 Subject: [PATCH 06/10] feat(app): mouse support -- cursor, click-to-navigate, validated under QEMU Wire the existing (but never consumed) pointer pipeline end to end and validate it under QEMU: - App main loop handles ModernUiInputPointer: scales the absolute report into framebuffer pixels via the device Mode range, tracks the cursor, throttles motion repaints (>=6px), and routes clicks. A successful hit updates the selection state and synthesizes an Enter event so the existing keyboard Enter handling stays the single owner of activation semantics. - Hit-test helpers share the painters' layout math (single source of truth): ModernSetupHitTestTab reuses a new ModernSetupGetTabWindow extracted from ModernSetupDrawTabs (scroll window + chevron inset identical); the dashboard card test reuses the quick-grid contract and the platform-visible card count (hidden cards are unclickable); the Exit-row/dropdown test shares new MODERN_SETUP_EXIT_ROW_* constants with DrawExit. - Original arrow cursor (two ModernUiFillTriangle primitives) composites last on every frame. - Fix ModernUiReadInput losing pointer reports to double-waiting: poll GetState non-blocking first, so the App's tick pre-wait consuming the WaitForInput signal no longer swallows the report. Keyboard was unaffected (buffered). - OVMF X64 overlay now always includes upstream UsbMouseAbsolutePointerDxe (upstream OVMF ships no pointer driver). Note: that edk2 driver integrates relative HID mice into its own 0..1024 absolute space -- QEMU validation uses `-device usb-mouse`; a usb-tablet's absolute reports are not understood by it. Validated end-to-end under QEMU via monitor mouse injection: cursor renders and follows; clicking a dashboard card routes to Devices; clicking tabs switches pages (scrolled-window hit-testing correct); clicking the Exit Language row opens the dropdown; clicking "English" commits and the live UI switches language. X64 + AARCH64 app builds -Werror clean; smoke PASS. Co-Authored-By: Claude Opus 4.8 --- Application/ModernSetupApp/ModernSetupApp.c | 92 +++++++++ .../ModernSetupApp/ModernSetupAppActions.c | 131 +++++++++++++ .../ModernSetupApp/ModernSetupAppChrome.c | 183 +++++++++++++++--- .../ModernSetupApp/ModernSetupAppInternal.h | 105 ++++++++++ .../ModernSetupApp/ModernSetupAppPages.c | 6 +- CHANGELOG.md | 27 +++ Library/ModernUiInputLib/ModernUiInputLib.c | 20 ++ Scripts/build-ovmf-x64.sh | 20 ++ 8 files changed, 551 insertions(+), 33 deletions(-) diff --git a/Application/ModernSetupApp/ModernSetupApp.c b/Application/ModernSetupApp/ModernSetupApp.c index 3cd62d7..97f047c 100644 --- a/Application/ModernSetupApp/ModernSetupApp.c +++ b/Application/ModernSetupApp/ModernSetupApp.c @@ -92,6 +92,15 @@ UefiMain ( EFI_EVENT KeyEvent; UINTN WaitCount; UINTN WaitIndex; + UINTN PointerX; + UINTN PointerY; + BOOLEAN PointerVisible; + UINTN LastCursorX; + UINTN LastCursorY; + SETUP_PAGE TabHit; + UINTN CardHit; + UINTN ExitRowHit; + UINTN ExitOptionHit; gBS->SetWatchdogTimer (0, 0, 0, NULL); mModernSetupImageHandle = ImageHandle; @@ -136,11 +145,25 @@ UefiMain ( StatusMessage[0] = L'\0'; Redraw = TRUE; ResetConfirmationPending = FALSE; + PointerX = 0; + PointerY = 0; + PointerVisible = FALSE; + LastCursorX = 0; + LastCursorY = 0; for (;;) { if (Redraw) { Theme = ModernUiGetThemeForPreference (mModernSetupPreferences.ThemeId); ModernSetupDrawCurrentPage (&Ui, Theme, Page, Focus, DashboardSelection, BootSelection, DeviceSelection, PreferencesSelection, ExitSelection, StatusMessage); + // + // Composite the pointer cursor last so it rides on top of the frame. + // + if (PointerVisible) { + ModernSetupDrawPointerCursor (&Ui, Theme, PointerX, PointerY); + LastCursorX = PointerX; + LastCursorY = PointerY; + } + Redraw = FALSE; } @@ -195,6 +218,75 @@ UefiMain ( CopyMem (&OldPreferences, &mModernSetupPreferences, sizeof (OldPreferences)); CopyMem (OldStatusMessage, StatusMessage, sizeof (OldStatusMessage)); + if ((Event.Type == ModernUiInputPointer) && Event.PointerValid) { + // + // Scale the absolute pointer report into framebuffer pixels. When the + // device reports no usable range the raw values are taken as pixels. + // + PointerX = Event.PointerX; + PointerY = Event.PointerY; + if ((Input.Pointer != NULL) && (Input.Pointer->Mode != NULL) && + (Input.Pointer->Mode->AbsoluteMaxX > Input.Pointer->Mode->AbsoluteMinX) && + (Input.Pointer->Mode->AbsoluteMaxY > Input.Pointer->Mode->AbsoluteMinY) && + (Ui.Width > 0) && (Ui.Height > 0)) + { + PointerX = (UINTN)(((Event.PointerX - (UINTN)Input.Pointer->Mode->AbsoluteMinX) * (Ui.Width - 1)) / + (UINTN)(Input.Pointer->Mode->AbsoluteMaxX - Input.Pointer->Mode->AbsoluteMinX)); + PointerY = (UINTN)(((Event.PointerY - (UINTN)Input.Pointer->Mode->AbsoluteMinY) * (Ui.Height - 1)) / + (UINTN)(Input.Pointer->Mode->AbsoluteMaxY - Input.Pointer->Mode->AbsoluteMinY)); + } + + PointerVisible = TRUE; + + if (!Event.PointerPressed) { + // + // Motion only: repaint when the cursor moved far enough to matter, so + // sustained motion does not flood the frame with full redraws. + // + if (((PointerX > LastCursorX) ? (PointerX - LastCursorX) : (LastCursorX - PointerX)) >= 6 || + ((PointerY > LastCursorY) ? (PointerY - LastCursorY) : (LastCursorY - PointerY)) >= 6) + { + Redraw = TRUE; + } + + continue; + } + + // + // Click. Route through the same activation paths the keyboard uses: + // a successful hit updates the selection state and synthesizes an Enter + // event, so the shared Enter handling below stays the single owner of + // activation semantics. + // + if (ModernSetupHitTestTab (&Ui, Page, PointerX, PointerY, &TabHit)) { + Page = TabHit; + Focus = SetupFocusNav; + mModernSetupLanguageDropdownOpen = FALSE; + ModernSetupCancelPreferencePopup (); + StatusMessage[0] = L'\0'; + Redraw = TRUE; + continue; + } + + if ((Page == PageDashboard) && ModernSetupHitTestDashboardCard (&Ui, PointerX, PointerY, &CardHit)) { + DashboardSelection = CardHit; + Focus = SetupFocusContent; + Event.Type = ModernUiInputEnter; + } else if ((Page == PageExit) && ModernSetupHitTestExitRow (&Ui, PointerX, PointerY, &ExitRowHit, &ExitOptionHit)) { + if (ExitOptionHit != (UINTN)-1) { + mModernSetupLanguageDropdownSelection = ExitOptionHit; + } else { + ExitSelection = ExitRowHit; + } + + Focus = SetupFocusContent; + Event.Type = ModernUiInputEnter; + } else { + Redraw = TRUE; + continue; + } + } + if ((Focus == SetupFocusContent) && (Page == PagePreferences) && mModernSetupPreferencePopupOpen && (Event.Type == ModernUiInputOther)) { ModernSetupHandlePreferenceInputKey (&Event, StatusMessage, sizeof (StatusMessage)); ResetConfirmationPending = FALSE; diff --git a/Application/ModernSetupApp/ModernSetupAppActions.c b/Application/ModernSetupApp/ModernSetupAppActions.c index b94ce04..7c6428e 100644 --- a/Application/ModernSetupApp/ModernSetupAppActions.c +++ b/Application/ModernSetupApp/ModernSetupAppActions.c @@ -113,6 +113,137 @@ ModernSetupDashboardVisibleQuickCardCount ( return MAX (Count, (UINTN)1); } +/** + Hit-test the dashboard quick-card grid for a pointer click. See + ModernSetupAppInternal.h. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Card Receives the catalog index of the clicked card. + + @retval TRUE (X,Y) lies on a visible quick card; *Card is set. + @retval FALSE No card at this position. +**/ +BOOLEAN +ModernSetupHitTestDashboardCard ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Card + ) +{ + MODERN_SETUP_DASHBOARD_QUICK_GRID Grid; + UINTN VisibleCount; + UINTN Index; + UINTN CardX; + UINTN CardY; + + if ((Ui == NULL) || (Card == NULL)) { + return FALSE; + } + + if (!ModernSetupGetDashboardQuickGrid (Ui, mModernSetupPreferences.DashboardDensity, &Grid) || + !Grid.Visible || (Grid.CardsPerRow == 0)) + { + return FALSE; + } + + // + // Same placement formula the dashboard drawing loop uses; bounded by the + // platform-visible count so a hidden card can never be clicked. + // + VisibleCount = ModernSetupDashboardVisibleQuickCardCount (); + for (Index = 0; Index < VisibleCount; Index++) { + CardX = Grid.Panel.X + 20 + ((Index % Grid.CardsPerRow) * (Grid.CardWidth + Grid.CardGap)); + CardY = Grid.Panel.Y + Grid.CardTop + ((Index / Grid.CardsPerRow) * (Grid.CardHeight + Grid.CardGap)); + if ((X >= CardX) && (X < (CardX + Grid.CardWidth)) && + (Y >= CardY) && (Y < (CardY + Grid.CardHeight))) + { + *Card = Index; + return TRUE; + } + } + + return FALSE; +} + +/** + Hit-test the Exit page rows and the open language dropdown. See + ModernSetupAppInternal.h. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Row Receives the clicked row index. + @param[out] DropdownOption Receives the clicked dropdown option, or + (UINTN)-1 when the click is on a row. + + @retval TRUE (X,Y) lies on an Exit row or an open dropdown option. + @retval FALSE Nothing clickable at this position. +**/ +BOOLEAN +ModernSetupHitTestExitRow ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Row, + OUT UINTN *DropdownOption + ) +{ + MODERN_UI_RECT Panel; + UINTN RowX; + UINTN RowWidth; + UINTN Index; + UINTN RowTop; + UINTN DropdownX; + UINTN DropdownY; + UINTN Option; + UINTN OptionTop; + + if ((Ui == NULL) || (Row == NULL) || (DropdownOption == NULL)) { + return FALSE; + } + + Panel = ModernSetupContentRect (Ui); + RowX = Panel.X + 26; + RowWidth = (Panel.Width > 52) ? (Panel.Width - 52) : Panel.Width; + + // + // The open dropdown floats above the rows, so test it first. Geometry mirrors + // DrawExit's dropdown block. + // + if (mModernSetupLanguageDropdownOpen) { + DropdownX = RowX + RowWidth - MODERN_SETUP_EXIT_VALUE_WIDTH - 12; + DropdownY = Panel.Y + MODERN_SETUP_EXIT_ROW_TOP + MODERN_SETUP_EXIT_ROW_COUNT * MODERN_SETUP_EXIT_ROW_STRIDE - 8; + for (Option = 0; Option < 2; Option++) { + OptionTop = DropdownY + 7 + Option * 34; + if ((X >= (DropdownX + 6)) && (X < (DropdownX + MODERN_SETUP_EXIT_VALUE_WIDTH - 6)) && + (Y >= OptionTop) && (Y < (OptionTop + 30))) + { + *Row = MODERN_SETUP_EXIT_ROW_COUNT - 1; + *DropdownOption = Option; + return TRUE; + } + } + } + + if ((X < RowX) || (X >= (RowX + RowWidth))) { + return FALSE; + } + + for (Index = 0; Index < MODERN_SETUP_EXIT_ROW_COUNT; Index++) { + RowTop = Panel.Y + MODERN_SETUP_EXIT_ROW_TOP + Index * MODERN_SETUP_EXIT_ROW_STRIDE - 10; + if ((Y >= RowTop) && (Y < (RowTop + MODERN_SETUP_EXIT_ROW_HEIGHT))) { + *Row = Index; + *DropdownOption = (UINTN)-1; + return TRUE; + } + } + + return FALSE; +} + BOOLEAN mModernSetupLanguageDropdownOpen; UINTN mModernSetupLanguageDropdownSelection; BOOLEAN mModernSetupPreferencePopupOpen; diff --git a/Application/ModernSetupApp/ModernSetupAppChrome.c b/Application/ModernSetupApp/ModernSetupAppChrome.c index b2629b1..b76f240 100644 --- a/Application/ModernSetupApp/ModernSetupAppChrome.c +++ b/Application/ModernSetupApp/ModernSetupAppChrome.c @@ -165,6 +165,158 @@ ModernSetupRefreshHeaderClock ( ModernUiDrawText (Ui, TimeStart, 6, TimeText, Theme->Text, Theme->HeaderPattern); } +/** + Compute the visible tab window and strip rectangles for the current page. + + Single source of layout truth shared by ModernSetupDrawTabs (painting) and + ModernSetupHitTestTab (pointer routing): the scroll window selection and the + chevron inset are identical in both, so click targets always match the + painted tabs. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Page Currently selected page. + @param[out] SelectedTab Receives the absolute selected tab index. + @param[out] FirstVisibleTab Receives the first visible tab index. + @param[out] VisibleTabCount Receives the visible tab count (>= 1). + @param[out] TabRect Receives the full strip rectangle. + @param[out] DrawTabRect Receives the strip rectangle after the scrolled + chevron inset (the rect tabs are painted in). +**/ +STATIC +VOID +ModernSetupGetTabWindow ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN SETUP_PAGE Page, + OUT UINTN *SelectedTab, + OUT UINTN *FirstVisibleTab, + OUT UINTN *VisibleTabCount, + OUT MODERN_UI_RECT *TabRect, + OUT MODERN_UI_RECT *DrawTabRect + ) +{ + UINTN Index; + UINTN TabCapacity; + + *SelectedTab = 0; + for (Index = 0; Index < ARRAY_SIZE (mPages); Index++) { + if (mPages[Index].Page == Page) { + *SelectedTab = Index; + } + } + + *TabRect = (MODERN_UI_RECT){ SCREEN_MARGIN, TOP_BAR_HEIGHT, (Ui->Width > (SCREEN_MARGIN * 2)) ? (Ui->Width - (SCREEN_MARGIN * 2)) : Ui->Width, TAB_BAR_HEIGHT }; + *DrawTabRect = *TabRect; + *VisibleTabCount = ARRAY_SIZE (mPages); + *FirstVisibleTab = 0; + if ((*VisibleTabCount > 0) && ((TabRect->Width / *VisibleTabCount) < 118)) { + TabCapacity = TabRect->Width / 132; + if (TabCapacity < 5) { + TabCapacity = 5; + } + + if (TabCapacity < *VisibleTabCount) { + *VisibleTabCount = TabCapacity; + *FirstVisibleTab = (*SelectedTab > (*VisibleTabCount / 2)) ? (*SelectedTab - (*VisibleTabCount / 2)) : 0; + if ((*FirstVisibleTab + *VisibleTabCount) > ARRAY_SIZE (mPages)) { + *FirstVisibleTab = ARRAY_SIZE (mPages) - *VisibleTabCount; + } + } + } + + if (((*FirstVisibleTab > 0) || ((*FirstVisibleTab + *VisibleTabCount) < ARRAY_SIZE (mPages))) && (DrawTabRect->Width > 48)) { + DrawTabRect->X += 18; + DrawTabRect->Width -= 36; + } +} + +/** + Hit-test the top tab strip for a pointer click. See ModernSetupAppInternal.h. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Page Currently selected page (determines the scroll window). + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Hit Receives the page of the clicked tab on success. + + @retval TRUE (X,Y) lies on a visible tab; *Hit is set. + @retval FALSE No tab at this position. +**/ +BOOLEAN +ModernSetupHitTestTab ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN SETUP_PAGE Page, + IN UINTN X, + IN UINTN Y, + OUT SETUP_PAGE *Hit + ) +{ + UINTN SelectedTab; + UINTN FirstVisibleTab; + UINTN VisibleTabCount; + MODERN_UI_RECT TabRect; + MODERN_UI_RECT DrawTabRect; + UINTN TabWidth; + UINTN Index; + + if ((Ui == NULL) || (Hit == NULL)) { + return FALSE; + } + + if ((Y < TOP_BAR_HEIGHT) || (Y >= (TOP_BAR_HEIGHT + TAB_BAR_HEIGHT))) { + return FALSE; + } + + ModernSetupGetTabWindow (Ui, Page, &SelectedTab, &FirstVisibleTab, &VisibleTabCount, &TabRect, &DrawTabRect); + if ((VisibleTabCount == 0) || (DrawTabRect.Width == 0) || + (X < DrawTabRect.X) || (X >= (DrawTabRect.X + DrawTabRect.Width))) + { + return FALSE; + } + + TabWidth = DrawTabRect.Width / VisibleTabCount; + if (TabWidth == 0) { + return FALSE; + } + + Index = (X - DrawTabRect.X) / TabWidth; + if (Index >= VisibleTabCount) { + Index = VisibleTabCount - 1; + } + + *Hit = mPages[FirstVisibleTab + Index].Page; + return TRUE; +} + +/** + Draw the pointer cursor at the given pixel position. See + ModernSetupAppInternal.h. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Theme Theme token table. Must not be NULL. + @param[in] X Cursor hotspot X in pixels. + @param[in] Y Cursor hotspot Y in pixels. +**/ +VOID +ModernSetupDrawPointerCursor ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN CONST MODERN_UI_THEME *Theme, + IN UINTN X, + IN UINTN Y + ) +{ + if ((Ui == NULL) || (Theme == NULL) || (X >= Ui->Width) || (Y >= Ui->Height)) { + return; + } + + // + // Simple high-contrast arrow: a dark outline triangle with a lighter accent + // triangle inset, apex at the hotspot pointing right-down. Original artwork + // built from the shared primitive vocabulary (no bitmap asset). + // + ModernUiFillTriangle (Ui, (MODERN_UI_RECT){ X, Y, 16, 16 }, ModernUiTriRight, Theme->BackgroundBlack); + ModernUiFillTriangle (Ui, (MODERN_UI_RECT){ X + 1, Y + 2, 12, 12 }, ModernUiTriRight, Theme->AccentYellow); +} + /** Draw the top page tab bar. @@ -187,45 +339,16 @@ ModernSetupDrawTabs ( UINTN FirstVisibleTab; UINTN VisibleTabCount; UINTN LocalSelectedTab; - UINTN TabCapacity; MODERN_UI_RECT TabRect; MODERN_UI_RECT DrawTabRect; - SelectedTab = 0; - for (Index = 0; Index < ARRAY_SIZE (mPages); Index++) { - if (mPages[Index].Page == Page) { - SelectedTab = Index; - } - } - - TabRect = (MODERN_UI_RECT){ SCREEN_MARGIN, TOP_BAR_HEIGHT, (Ui->Width > (SCREEN_MARGIN * 2)) ? (Ui->Width - (SCREEN_MARGIN * 2)) : Ui->Width, TAB_BAR_HEIGHT }; - DrawTabRect = TabRect; - VisibleTabCount = ARRAY_SIZE (mPages); - FirstVisibleTab = 0; - if ((VisibleTabCount > 0) && ((TabRect.Width / VisibleTabCount) < 118)) { - TabCapacity = TabRect.Width / 132; - if (TabCapacity < 5) { - TabCapacity = 5; - } - - if (TabCapacity < VisibleTabCount) { - VisibleTabCount = TabCapacity; - FirstVisibleTab = (SelectedTab > (VisibleTabCount / 2)) ? (SelectedTab - (VisibleTabCount / 2)) : 0; - if ((FirstVisibleTab + VisibleTabCount) > ARRAY_SIZE (mPages)) { - FirstVisibleTab = ARRAY_SIZE (mPages) - VisibleTabCount; - } - } - } + ModernSetupGetTabWindow (Ui, Page, &SelectedTab, &FirstVisibleTab, &VisibleTabCount, &TabRect, &DrawTabRect); for (Index = 0; Index < VisibleTabCount; Index++) { Tabs[Index].Text = ModernSetupGetCompactTabLabel (FirstVisibleTab + Index); } LocalSelectedTab = SelectedTab - FirstVisibleTab; - if (((FirstVisibleTab > 0) || ((FirstVisibleTab + VisibleTabCount) < ARRAY_SIZE (mPages))) && (DrawTabRect.Width > 48)) { - DrawTabRect.X += 18; - DrawTabRect.Width -= 36; - } ModernUiEngineDrawTabs ( Ui, diff --git a/Application/ModernSetupApp/ModernSetupAppInternal.h b/Application/ModernSetupApp/ModernSetupAppInternal.h index 6410c7a..e567250 100644 --- a/Application/ModernSetupApp/ModernSetupAppInternal.h +++ b/Application/ModernSetupApp/ModernSetupAppInternal.h @@ -67,6 +67,15 @@ #define DASHBOARD_QUICK_GROUP_LABEL_OFFSET 24 #define DASHBOARD_QUICK_CARD_BOTTOM 10 #define DASHBOARD_QUICK_VALUE_MIN_HEIGHT 36 +// +// Exit-page row layout, shared by DrawExit (drawing) and the pointer hit-test +// (input routing) so click targets always match the painted rows. +// +#define MODERN_SETUP_EXIT_ROW_TOP 72 +#define MODERN_SETUP_EXIT_ROW_STRIDE 54 +#define MODERN_SETUP_EXIT_ROW_HEIGHT 40 +#define MODERN_SETUP_EXIT_ROW_COUNT 4 +#define MODERN_SETUP_EXIT_VALUE_WIDTH 220 typedef enum { PageDashboard = 0, @@ -386,6 +395,102 @@ ModernSetupDashboardQuickCardApplicable ( IN UINTN CardIndex ); +/** + Hit-test the top tab strip for a pointer click. + + Mirrors the same visible-tab window math ModernSetupDrawTabs paints with + (including the scrolled chevron inset), so the click targets always match the + painted tabs. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Page Currently selected page (determines the scroll window). + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Hit Receives the page of the clicked tab on success. Must not + be NULL. + + @retval TRUE (X,Y) lies on a visible tab; *Hit is set. + @retval FALSE No tab at this position; *Hit is unchanged. +**/ +BOOLEAN +ModernSetupHitTestTab ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN SETUP_PAGE Page, + IN UINTN X, + IN UINTN Y, + OUT SETUP_PAGE *Hit + ); + +/** + Draw the pointer cursor at the given pixel position. + + Drawn last in the frame so it composites on top of the page. Display-only; + a no-op when Ui or Theme is NULL or the position is outside the screen. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Theme Theme token table. Must not be NULL. + @param[in] X Cursor hotspot X in pixels. + @param[in] Y Cursor hotspot Y in pixels. +**/ +VOID +ModernSetupDrawPointerCursor ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN CONST MODERN_UI_THEME *Theme, + IN UINTN X, + IN UINTN Y + ); + +/** + Hit-test the dashboard quick-card grid for a pointer click. + + Uses the same grid contract as drawing and keyboard navigation + (ModernSetupGetDashboardQuickGrid + the platform-visible card count), so a + hidden card can never be clicked. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Card Receives the catalog index of the clicked card. Must not + be NULL. + + @retval TRUE (X,Y) lies on a visible quick card; *Card is set. + @retval FALSE No card at this position; *Card is unchanged. +**/ +BOOLEAN +ModernSetupHitTestDashboardCard ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Card + ); + +/** + Hit-test the Exit page rows (and the language dropdown when open) for a + pointer click. + + Uses the shared MODERN_SETUP_EXIT_ROW_* layout constants so click targets + always match DrawExit's painted rows. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Row Receives the clicked row index. Must not be NULL. + @param[out] DropdownOption Receives the clicked open-dropdown option, or + (UINTN)-1 when the click is on a row instead. + Must not be NULL. + + @retval TRUE (X,Y) lies on an Exit row or an open dropdown option. + @retval FALSE Nothing clickable at this position. +**/ +BOOLEAN +ModernSetupHitTestExitRow ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Row, + OUT UINTN *DropdownOption + ); + /** Return the number of dashboard quick-cards visible on the current platform. diff --git a/Application/ModernSetupApp/ModernSetupAppPages.c b/Application/ModernSetupApp/ModernSetupAppPages.c index 1159fe3..3e2bb9f 100644 --- a/Application/ModernSetupApp/ModernSetupAppPages.c +++ b/Application/ModernSetupApp/ModernSetupAppPages.c @@ -2005,13 +2005,13 @@ DrawExit ( Panel = ModernSetupContentRect (Ui); RowX = Panel.X + 26; RowWidth = Panel.Width - 52; - ValueWidth = 220; + ValueWidth = MODERN_SETUP_EXIT_VALUE_WIDTH; ModernUiDrawPanel (Ui, Panel, Theme); ModernUiDrawFocusFrame (Ui, Panel, (BOOLEAN)(Focus == SetupFocusContent), Theme); ModernUiDrawText (Ui, Panel.X + 20, Panel.Y + 20, ModernUiGetString (ModernUiStringExitInstruction), Theme->MutedText, Theme->Surface); for (Index = 0; Index < ARRAY_SIZE (Items); Index++) { - Y = Panel.Y + 72 + Index * 54; + Y = Panel.Y + MODERN_SETUP_EXIT_ROW_TOP + Index * MODERN_SETUP_EXIT_ROW_STRIDE; IsSelected = (BOOLEAN)((Focus == SetupFocusContent) && (Index == Selected)); RowModel.Rect = (MODERN_UI_RECT){ RowX, Y - 10, RowWidth, 40 }; RowModel.Prompt = Items[Index]; @@ -2027,7 +2027,7 @@ DrawExit ( UINTN Option; DropdownX = RowX + RowWidth - ValueWidth - 12; - DropdownY = Panel.Y + 72 + 4 * 54 - 8; + DropdownY = Panel.Y + MODERN_SETUP_EXIT_ROW_TOP + MODERN_SETUP_EXIT_ROW_COUNT * MODERN_SETUP_EXIT_ROW_STRIDE - 8; PopupModel.Rect = (MODERN_UI_RECT){ DropdownX, DropdownY, ValueWidth, 80 }; PopupModel.Title = NULL; ModernUiEngineDrawPopup (Ui, &PopupModel, Theme); diff --git a/CHANGELOG.md b/CHANGELOG.md index 039e21b..bb89750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,35 @@ this file as both a release log and a lightweight development progress record. ## Unreleased +### Added + +- **Mouse support in the front-page App.** A USB mouse now drives the App: an + original arrow cursor composites on top of every frame, moving the mouse + repaints (throttled), and clicking activates -- top tabs switch pages + (hit-testing shares the exact scrolled-window math the tab painter uses), + dashboard quick cards select-and-route (bounded by the platform-visible card + count, so a hidden card can never be clicked), and Exit-page rows / the open + language dropdown select-and-activate. Clicks reuse the keyboard's Enter + handling (the hit updates the selection and synthesizes an Enter event), so + activation semantics stay single-owner. Validated end-to-end under QEMU + (`-device usb-mouse` + monitor injection): card click routes to Devices, tab + click switches pages, and clicking "English" in the language dropdown switches + the live UI language. +- The OVMF X64 overlay now always includes the upstream + `UsbMouseAbsolutePointerDxe` driver (upstream OVMF ships no pointer driver at + all), so `EFI_ABSOLUTE_POINTER_PROTOCOL` exists for the App's pointer input. + Note the edk2 driver integrates *relative* HID mice into its own 0..1024 + absolute space (`-device usb-mouse` under QEMU; a `usb-tablet`'s absolute + reports are not understood by it). + ### Fixed +- `ModernUiReadInput` no longer loses pointer reports to double-waiting: it now + polls `GetState` non-blocking first, so a caller that pre-waited on the same + `WaitForInput` event (the App's clock-tick wait does) and consumed its signal + still receives the pending pointer event instead of blocking until the next + one. Keyboard input was never affected (key strokes are buffered). + - LVGL widget text no longer renders CJK as `?`. The LVGL backend's widget paths (one-of dropdown value, checkbox label, numeric/string field, ordered list) previously ASCII-folded all non-ASCII to `'?'` ("中文" showed as "??", e.g. the diff --git a/Library/ModernUiInputLib/ModernUiInputLib.c b/Library/ModernUiInputLib/ModernUiInputLib.c index 6d04516..4991447 100644 --- a/Library/ModernUiInputLib/ModernUiInputLib.c +++ b/Library/ModernUiInputLib/ModernUiInputLib.c @@ -133,6 +133,26 @@ ModernUiReadInput ( ZeroMem (Event, sizeof (*Event)); Event->Type = ModernUiInputNone; + // + // Poll the pointer first, non-blocking. Callers that pre-wait on the same + // WaitForInput event (e.g. alongside a periodic tick) consume its signaled + // state before reaching this function; GetState still reports the pending + // movement/button data, so polling here keeps that report from being lost to + // the second blocking wait below. GetState returns EFI_NOT_READY when there + // is nothing new, in which case we fall through to the normal wait. + // + if (Context->Pointer != NULL) { + Status = Context->Pointer->GetState (Context->Pointer, &PointerState); + if (!EFI_ERROR (Status)) { + Event->Type = ModernUiInputPointer; + Event->PointerValid = TRUE; + Event->PointerX = (UINTN)PointerState.CurrentX; + Event->PointerY = (UINTN)PointerState.CurrentY; + Event->PointerPressed = (BOOLEAN)((PointerState.ActiveButtons & EFI_ABSP_TouchActive) != 0); + return EFI_SUCCESS; + } + } + EventCount = 0; if (Context->TextInEx != NULL) { Events[EventCount++] = Context->TextInEx->WaitForKeyEx; diff --git a/Scripts/build-ovmf-x64.sh b/Scripts/build-ovmf-x64.sh index 7b38b03..ca4ae8b 100755 --- a/Scripts/build-ovmf-x64.sh +++ b/Scripts/build-ovmf-x64.sh @@ -145,6 +145,10 @@ boot_manager_menu_fdf_inf = "INF MdeModulePkg/Application/BootManagerMenuApp/Bo # Opt-in control-rich VFR test driver (reachable via Device Manager). driver_sample_component = " MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf" driver_sample_fdf_inf = "INF MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf" +# Upstream USB absolute-pointer driver (usb-tablet -> EFI_ABSOLUTE_POINTER): the +# app's pointer input consumes it; upstream OVMF ships no pointer driver at all. +usb_pointer_component = " MdeModulePkg/Bus/Usb/UsbMouseAbsolutePointerDxe/UsbMouseAbsolutePointerDxe.inf" +usb_pointer_fdf_inf = "INF MdeModulePkg/Bus/Usb/UsbMouseAbsolutePointerDxe/UsbMouseAbsolutePointerDxe.inf" # The renderer library class resolves to the LVGL-backed implementation in lvgl # mode and to the hand-rolled GOP rasterizer otherwise. Both expose the identical # ModernUiRenderer.h API, so ModernUiEngineLib and its consumers are unchanged. @@ -260,6 +264,15 @@ if enable_driver_sample and driver_sample_component not in dsc: driver_sample_component + r"\n\1", "QemuKernelLoaderFsDxe DSC component anchor for DriverSample", ) +if usb_pointer_component not in dsc: + # Pointer input for the app: upstream OVMF ships no USB pointer driver, so + # the overlay always adds the absolute-pointer driver (same stable anchor). + dsc = replace_regex_once( + dsc, + r"^(\s*OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe\.inf \{)", + usb_pointer_component + r"\n\1", + "QemuKernelLoaderFsDxe DSC component anchor for UsbMouseAbsolutePointer", + ) if video_width is not None: # Replace the upstream display-PCD include with its expanded content carrying # the requested resolution; this avoids duplicate PCD assignments and leaves @@ -323,6 +336,13 @@ if enable_driver_sample and driver_sample_fdf_inf not in fdf: driver_sample_fdf_inf + r"\n\1", "QemuKernelLoaderFsDxe FDF INF anchor for DriverSample", ) +if usb_pointer_fdf_inf not in fdf: + fdf = replace_regex_once( + fdf, + r"^(\s*INF\s+OvmfPkg/QemuKernelLoaderFsDxe/QemuKernelLoaderFsDxe\.inf\s*)$", + usb_pointer_fdf_inf + r"\n\1", + "QemuKernelLoaderFsDxe FDF INF anchor for UsbMouseAbsolutePointer", + ) (overlay / "OvmfX64ModernSetup.fdf").write_text(fdf) PY From cec0a34d0552833defcce0928bd84e96e4051045 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Fri, 12 Jun 2026 09:57:54 +0800 Subject: [PATCH 07/10] feat(build): include USB pointer driver in LoongArch overlay Mirror the OVMF X64 change: the LoongArchVirtQemu overlay now adds upstream UsbMouseAbsolutePointerDxe next to UsbKbDxe (the platform ships only the keyboard driver), so the app's mouse support has EFI_ABSOLUTE_POINTER on LoongArch too. Anchored replacements no-op gracefully when absent. Smoke PASS. Co-Authored-By: Claude Opus 4.8 --- Scripts/build-loongarchvirt.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Scripts/build-loongarchvirt.sh b/Scripts/build-loongarchvirt.sh index 319f9f1..2890e97 100755 --- a/Scripts/build-loongarchvirt.sh +++ b/Scripts/build-loongarchvirt.sh @@ -131,6 +131,10 @@ boot_manager_menu_component = " MdeModulePkg/Application/BootManagerMenuApp/Boo boot_manager_menu_fdf_inf = "INF MdeModulePkg/Application/BootManagerMenuApp/BootManagerMenuApp.inf" driver_sample_component = " MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf" driver_sample_fdf_inf = "INF MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf" +# Upstream USB absolute-pointer driver (relative HID mouse -> EFI_ABSOLUTE_POINTER): +# the app's mouse support consumes it; the upstream platform ships only UsbKbDxe. +usb_pointer_component = " MdeModulePkg/Bus/Usb/UsbMouseAbsolutePointerDxe/UsbMouseAbsolutePointerDxe.inf" +usb_pointer_fdf_inf = "INF MdeModulePkg/Bus/Usb/UsbMouseAbsolutePointerDxe/UsbMouseAbsolutePointerDxe.inf" # LVGL core (upstream lvgl sources as a BASE library); resolved only in lvgl mode # and consumed transitively through the LVGL renderer library. lvgl_library_block = " LvglCoreLib|LvglSpikePkg/Library/LvglLib/LvglCoreLib.inf\n" @@ -228,6 +232,14 @@ if enable_driver_sample and driver_sample_component not in dsc: driver_sample_component + "\n MdeModulePkg/Application/UiApp/UiApp.inf {", 1, ) +if usb_pointer_component not in dsc: + # Pointer input for the app: anchor on the existing USB keyboard driver so + # the pointer driver sits with the rest of the USB stack. + dsc = dsc.replace( + " MdeModulePkg/Bus/Usb/UsbKbDxe/UsbKbDxe.inf", + " MdeModulePkg/Bus/Usb/UsbKbDxe/UsbKbDxe.inf\n" + usb_pointer_component, + 1, + ) if (display_engine == "modern" or display_engine == "lvgl"): dsc += ( "\n[PcdsFixedAtBuild]\n" @@ -293,6 +305,12 @@ if replace_uiapp and "[Rule.Common.UEFI_APPLICATION.MODERN_SETUP_UIAPP]" not in " UI STRING=\"ModernSetupApp\" Optional\n" " }\n" ) +if usb_pointer_fdf_inf not in fdf: + fdf = fdf.replace( + "INF MdeModulePkg/Bus/Usb/UsbKbDxe/UsbKbDxe.inf", + "INF MdeModulePkg/Bus/Usb/UsbKbDxe/UsbKbDxe.inf\n" + usb_pointer_fdf_inf, + 1, + ) (overlay / "LoongArchVirtQemuModernSetup.fdf").write_text(fdf) PY From 5188f906486f181e55c01924b34d6e738ed09f37 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Fri, 12 Jun 2026 10:16:26 +0800 Subject: [PATCH 08/10] fix(app): flicker-free mouse motion via save-under cursor compositing User-reported: moving the mouse flickered the screen. Pointer motion previously set Redraw and repainted the whole frame per event -- through the LVGL backend that is a full-canvas composite + full-screen Blt each time. Replace it with classic save-under cursor compositing: - New additive renderer API pair, implemented by both backends per the ModernUiRenderer.h contract: ModernUiCaptureRect (GOP: EfiBltVideoToBltBuffer read-back; LVGL: shadow-canvas rows) and ModernUiRestoreRect (GOP: BufferToVideo; LVGL: canvas rows + BltCanvasRegion re-flush so later partial flushes cannot resurrect the overlay). - The app cursor becomes a save-under manager (ModernSetupMovePointerCursor / ModernSetupInvalidatePointerCursor in Chrome.c): restore the previous 16x16 rect, capture the new one, draw the arrow; position clamped so the fixed-size capture stays on screen. Motion events now touch only that rectangle -- no full-frame repaint, no flicker. Full repaints (clicks, page switches) invalidate the saved pixels first and re-composite with a fresh capture. Verified under QEMU (GOP path): multi-hop motion leaves a single cursor with no trails, including across click-driven full repaints. X64 + AARCH64 builds -Werror clean; smoke PASS. Co-Authored-By: Claude Opus 4.8 --- Application/ModernSetupApp/ModernSetupApp.c | 22 +++-- .../ModernSetupApp/ModernSetupAppChrome.c | 69 ++++++++++++- .../ModernSetupApp/ModernSetupAppInternal.h | 25 +++-- CHANGELOG.md | 15 +++ Include/ModernUi/ModernUiRenderer.h | 55 +++++++++++ .../ModernUiRendererLib.c | 99 +++++++++++++++++++ .../ModernUiRendererLib/ModernUiRendererLib.c | 84 ++++++++++++++++ 7 files changed, 349 insertions(+), 20 deletions(-) diff --git a/Application/ModernSetupApp/ModernSetupApp.c b/Application/ModernSetupApp/ModernSetupApp.c index 97f047c..44ef9bc 100644 --- a/Application/ModernSetupApp/ModernSetupApp.c +++ b/Application/ModernSetupApp/ModernSetupApp.c @@ -154,12 +154,15 @@ UefiMain ( for (;;) { if (Redraw) { Theme = ModernUiGetThemeForPreference (mModernSetupPreferences.ThemeId); - ModernSetupDrawCurrentPage (&Ui, Theme, Page, Focus, DashboardSelection, BootSelection, DeviceSelection, PreferencesSelection, ExitSelection, StatusMessage); // - // Composite the pointer cursor last so it rides on top of the frame. + // The full repaint below invalidates any saved under-cursor pixels; the + // cursor is then re-composited (with a fresh capture) on top of the new + // frame. // + ModernSetupInvalidatePointerCursor (); + ModernSetupDrawCurrentPage (&Ui, Theme, Page, Focus, DashboardSelection, BootSelection, DeviceSelection, PreferencesSelection, ExitSelection, StatusMessage); if (PointerVisible) { - ModernSetupDrawPointerCursor (&Ui, Theme, PointerX, PointerY); + ModernSetupMovePointerCursor (&Ui, Theme, PointerX, PointerY); LastCursorX = PointerX; LastCursorY = PointerY; } @@ -240,13 +243,14 @@ UefiMain ( if (!Event.PointerPressed) { // - // Motion only: repaint when the cursor moved far enough to matter, so - // sustained motion does not flood the frame with full redraws. + // Motion only: composite the cursor with save-under (restore the old + // 16x16 rect, capture and draw at the new position). No full-frame + // repaint -- this is what keeps mouse motion flicker-free. // - if (((PointerX > LastCursorX) ? (PointerX - LastCursorX) : (LastCursorX - PointerX)) >= 6 || - ((PointerY > LastCursorY) ? (PointerY - LastCursorY) : (LastCursorY - PointerY)) >= 6) - { - Redraw = TRUE; + if ((PointerX != LastCursorX) || (PointerY != LastCursorY)) { + ModernSetupMovePointerCursor (&Ui, Theme, PointerX, PointerY); + LastCursorX = PointerX; + LastCursorY = PointerY; } continue; diff --git a/Application/ModernSetupApp/ModernSetupAppChrome.c b/Application/ModernSetupApp/ModernSetupAppChrome.c index b76f240..32b1bd8 100644 --- a/Application/ModernSetupApp/ModernSetupAppChrome.c +++ b/Application/ModernSetupApp/ModernSetupAppChrome.c @@ -287,27 +287,86 @@ ModernSetupHitTestTab ( return TRUE; } +// +// Save-under state for the pointer cursor: the pixels beneath the cursor are +// captured before the arrow is drawn and restored when it moves, so pointer +// motion repaints only this small rectangle instead of the whole frame. +// +#define MODERN_SETUP_CURSOR_SIZE 16 + +STATIC EFI_GRAPHICS_OUTPUT_BLT_PIXEL mCursorSave[MODERN_SETUP_CURSOR_SIZE * MODERN_SETUP_CURSOR_SIZE]; +STATIC BOOLEAN mCursorSaveValid = FALSE; +STATIC UINTN mCursorSaveX; +STATIC UINTN mCursorSaveY; + +/** + Forget the saved under-cursor pixels. See ModernSetupAppInternal.h. + + Call after any full-frame repaint: the saved pixels describe the old frame + and must not be restored on the next cursor move. +**/ +VOID +ModernSetupInvalidatePointerCursor ( + VOID + ) +{ + mCursorSaveValid = FALSE; +} + /** - Draw the pointer cursor at the given pixel position. See + Move (or first-draw) the pointer cursor using save-under compositing. See ModernSetupAppInternal.h. @param[in] Ui Initialized render context. Must not be NULL. @param[in] Theme Theme token table. Must not be NULL. - @param[in] X Cursor hotspot X in pixels. - @param[in] Y Cursor hotspot Y in pixels. + @param[in] X Cursor hotspot X in pixels (clamped to keep the arrow + fully on screen). + @param[in] Y Cursor hotspot Y in pixels (clamped likewise). **/ VOID -ModernSetupDrawPointerCursor ( +ModernSetupMovePointerCursor ( IN MODERN_UI_RENDER_CONTEXT *Ui, IN CONST MODERN_UI_THEME *Theme, IN UINTN X, IN UINTN Y ) { - if ((Ui == NULL) || (Theme == NULL) || (X >= Ui->Width) || (Y >= Ui->Height)) { + MODERN_UI_RECT Rect; + + if ((Ui == NULL) || (Theme == NULL) || + (Ui->Width < MODERN_SETUP_CURSOR_SIZE) || (Ui->Height < MODERN_SETUP_CURSOR_SIZE)) + { return; } + // + // Clamp so the full save rectangle stays on screen (fixed-size capture). + // + if (X > (Ui->Width - MODERN_SETUP_CURSOR_SIZE)) { + X = Ui->Width - MODERN_SETUP_CURSOR_SIZE; + } + + if (Y > (Ui->Height - MODERN_SETUP_CURSOR_SIZE)) { + Y = Ui->Height - MODERN_SETUP_CURSOR_SIZE; + } + + if (mCursorSaveValid) { + if ((X == mCursorSaveX) && (Y == mCursorSaveY)) { + return; + } + + Rect = (MODERN_UI_RECT){ mCursorSaveX, mCursorSaveY, MODERN_SETUP_CURSOR_SIZE, MODERN_SETUP_CURSOR_SIZE }; + ModernUiRestoreRect (Ui, Rect, mCursorSave); + mCursorSaveValid = FALSE; + } + + Rect = (MODERN_UI_RECT){ X, Y, MODERN_SETUP_CURSOR_SIZE, MODERN_SETUP_CURSOR_SIZE }; + if (!EFI_ERROR (ModernUiCaptureRect (Ui, Rect, mCursorSave))) { + mCursorSaveValid = TRUE; + mCursorSaveX = X; + mCursorSaveY = Y; + } + // // Simple high-contrast arrow: a dark outline triangle with a lighter accent // triangle inset, apex at the hotspot pointing right-down. Original artwork diff --git a/Application/ModernSetupApp/ModernSetupAppInternal.h b/Application/ModernSetupApp/ModernSetupAppInternal.h index e567250..8dd854a 100644 --- a/Application/ModernSetupApp/ModernSetupAppInternal.h +++ b/Application/ModernSetupApp/ModernSetupAppInternal.h @@ -422,24 +422,37 @@ ModernSetupHitTestTab ( ); /** - Draw the pointer cursor at the given pixel position. + Move (or first-draw) the pointer cursor using save-under compositing. - Drawn last in the frame so it composites on top of the page. Display-only; - a no-op when Ui or Theme is NULL or the position is outside the screen. + The pixels beneath the cursor are captured before the arrow is drawn and + restored when it moves, so pointer motion repaints only a small rectangle + instead of the whole frame (no full-screen flicker). Display-only; a no-op + when Ui or Theme is NULL or the screen is smaller than the cursor. @param[in] Ui Initialized render context. Must not be NULL. @param[in] Theme Theme token table. Must not be NULL. - @param[in] X Cursor hotspot X in pixels. - @param[in] Y Cursor hotspot Y in pixels. + @param[in] X Cursor hotspot X in pixels (clamped on-screen). + @param[in] Y Cursor hotspot Y in pixels (clamped on-screen). **/ VOID -ModernSetupDrawPointerCursor ( +ModernSetupMovePointerCursor ( IN MODERN_UI_RENDER_CONTEXT *Ui, IN CONST MODERN_UI_THEME *Theme, IN UINTN X, IN UINTN Y ); +/** + Forget the saved under-cursor pixels. + + Must be called after any full-frame repaint: the saved pixels describe the + previous frame and must not be restored onto the new one. +**/ +VOID +ModernSetupInvalidatePointerCursor ( + VOID + ); + /** Hit-test the dashboard quick-card grid for a pointer click. diff --git a/CHANGELOG.md b/CHANGELOG.md index bb89750..1404eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,21 @@ this file as both a release log and a lightweight development progress record. ## Unreleased +### Fixed + +- Mouse motion no longer flickers the screen. Pointer movement previously + triggered a full-frame repaint per motion event (visible flicker, especially + through the LVGL backend's full-canvas composite). The cursor now uses + classic save-under compositing: the 16x16 pixels beneath the arrow are + captured before drawing and restored on move, so motion repaints only that + small rectangle. Backed by a new additive renderer API pair -- + `ModernUiCaptureRect` / `ModernUiRestoreRect` (GOP backend: framebuffer + read-back/write; LVGL backend: shadow-canvas read/write plus a region + re-flush so later partial flushes cannot resurrect the cursor). Full-frame + repaints (page switches, clicks) invalidate the saved pixels and re-composite + the cursor with a fresh capture. Verified under QEMU: multi-hop motion leaves + no trails, before and after click-driven full repaints. + ### Added - **Mouse support in the front-page App.** A USB mouse now drives the App: an diff --git a/Include/ModernUi/ModernUiRenderer.h b/Include/ModernUi/ModernUiRenderer.h index 0c18c20..6ed29e9 100644 --- a/Include/ModernUi/ModernUiRenderer.h +++ b/Include/ModernUi/ModernUiRenderer.h @@ -677,4 +677,59 @@ ModernUiDrawOemWatermark ( IN CONST MODERN_UI_THEME *Theme ); +/** + Capture the current on-screen pixels of a rectangle into a caller buffer. + + Used for small save-under overlays (e.g. the pointer cursor): capture before + drawing the overlay, restore on move, so motion does not require a full-frame + repaint. The buffer is tightly packed Rect.Width x Rect.Height pixels and the + caller owns its sizing; the rectangle must lie fully inside the screen. + + - GOP backend: reads back from the framebuffer (EfiBltVideoToBltBuffer). + - LVGL backend: reads from the shadow canvas (which mirrors the screen). + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Source rectangle; must lie fully on screen and be + non-empty. + @param[out] Buffer Receives Rect.Width*Rect.Height pixels. Must not be NULL. + + @retval EFI_SUCCESS Pixels captured. + @retval EFI_INVALID_PARAMETER Context/Buffer is NULL, Rect is empty, or Rect + exceeds the screen. + @retval EFI_NOT_READY The backend surface is not initialized. +**/ +EFI_STATUS +EFIAPI +ModernUiCaptureRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ); + +/** + Restore previously captured pixels back to the screen. + + Counterpart of ModernUiCaptureRect; Rect must match the capture. On the LVGL + backend the shadow canvas is updated and the region re-flushed so later + partial flushes cannot resurrect the overlay. + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Destination rectangle; must lie fully on screen and be + non-empty. + @param[in] Buffer Rect.Width*Rect.Height pixels from ModernUiCaptureRect. + Must not be NULL. + + @retval EFI_SUCCESS Pixels restored. + @retval EFI_INVALID_PARAMETER Context/Buffer is NULL, Rect is empty, or Rect + exceeds the screen. + @retval EFI_NOT_READY The backend surface is not initialized. +**/ +EFI_STATUS +EFIAPI +ModernUiRestoreRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + IN CONST EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ); + #endif diff --git a/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c b/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c index 8824146..40f3932 100644 --- a/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c +++ b/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c @@ -1589,3 +1589,102 @@ ModernUiDrawOemWatermark ( return EFI_SUCCESS; } + +/** + Capture the current on-screen pixels of a rectangle into a caller buffer. + See the contract in ModernUi/ModernUiRenderer.h. The LVGL backend reads from + the shadow canvas, which mirrors the screen (every primitive flushes through + it). + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Source rectangle; must lie fully on screen, non-empty. + @param[out] Buffer Receives Rect.Width*Rect.Height pixels. Must not be NULL. + + @retval EFI_SUCCESS Pixels captured. + @retval EFI_INVALID_PARAMETER Bad arguments or off-screen rectangle. + @retval EFI_NOT_READY The canvas bridge is not initialized. +**/ +EFI_STATUS +EFIAPI +ModernUiCaptureRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ) +{ + UINTN Row; + + if ((Context == NULL) || (Buffer == NULL) || + (Rect.Width == 0) || (Rect.Height == 0) || + ((Rect.X + Rect.Width) > Context->Width) || + ((Rect.Y + Rect.Height) > Context->Height)) + { + return EFI_INVALID_PARAMETER; + } + + if (!mLvglReady || (mCanvasBuf == NULL) || + ((Rect.X + Rect.Width) > mCanvasW) || ((Rect.Y + Rect.Height) > mCanvasH)) + { + return EFI_NOT_READY; + } + + for (Row = 0; Row < Rect.Height; Row++) { + CopyMem ( + &Buffer[Row * Rect.Width], + &mCanvasBuf[(Rect.Y + Row) * mCanvasW + Rect.X], + Rect.Width * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) + ); + } + + return EFI_SUCCESS; +} + +/** + Restore previously captured pixels back to the screen. + See the contract in ModernUi/ModernUiRenderer.h. The LVGL backend writes the + pixels into the shadow canvas and re-flushes the region, so later partial + flushes cannot resurrect the overlay that was drawn over them. + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Destination rectangle; must lie fully on screen. + @param[in] Buffer Pixels from ModernUiCaptureRect. Must not be NULL. + + @retval EFI_SUCCESS Pixels restored. + @retval EFI_INVALID_PARAMETER Bad arguments or off-screen rectangle. + @retval EFI_NOT_READY The canvas bridge is not initialized. +**/ +EFI_STATUS +EFIAPI +ModernUiRestoreRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + IN CONST EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ) +{ + UINTN Row; + + if ((Context == NULL) || (Buffer == NULL) || + (Rect.Width == 0) || (Rect.Height == 0) || + ((Rect.X + Rect.Width) > Context->Width) || + ((Rect.Y + Rect.Height) > Context->Height)) + { + return EFI_INVALID_PARAMETER; + } + + if (!mLvglReady || (mCanvasBuf == NULL) || + ((Rect.X + Rect.Width) > mCanvasW) || ((Rect.Y + Rect.Height) > mCanvasH)) + { + return EFI_NOT_READY; + } + + for (Row = 0; Row < Rect.Height; Row++) { + CopyMem ( + &mCanvasBuf[(Rect.Y + Row) * mCanvasW + Rect.X], + &Buffer[Row * Rect.Width], + Rect.Width * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) + ); + } + + BltCanvasRegion (Rect.X, Rect.Y, Rect.Width, Rect.Height); + return EFI_SUCCESS; +} diff --git a/Library/ModernUiRendererLib/ModernUiRendererLib.c b/Library/ModernUiRendererLib/ModernUiRendererLib.c index 0fa171a..e5748c8 100644 --- a/Library/ModernUiRendererLib/ModernUiRendererLib.c +++ b/Library/ModernUiRendererLib/ModernUiRendererLib.c @@ -567,3 +567,87 @@ ModernUiDrawOemWatermark ( return EFI_SUCCESS; } + +/** + Capture the current on-screen pixels of a rectangle into a caller buffer. + See the contract in ModernUi/ModernUiRenderer.h. + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Source rectangle; must lie fully on screen, non-empty. + @param[out] Buffer Receives Rect.Width*Rect.Height pixels. Must not be NULL. + + @retval EFI_SUCCESS Pixels captured. + @retval EFI_INVALID_PARAMETER Bad arguments or off-screen rectangle. + @retval others Status from the GOP Blt read-back. +**/ +EFI_STATUS +EFIAPI +ModernUiCaptureRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ) +{ + if ((Context == NULL) || (Context->Gop == NULL) || (Buffer == NULL) || + (Rect.Width == 0) || (Rect.Height == 0) || + ((Rect.X + Rect.Width) > Context->Width) || + ((Rect.Y + Rect.Height) > Context->Height)) + { + return EFI_INVALID_PARAMETER; + } + + return Context->Gop->Blt ( + Context->Gop, + Buffer, + EfiBltVideoToBltBuffer, + Rect.X, + Rect.Y, + 0, + 0, + Rect.Width, + Rect.Height, + Rect.Width * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) + ); +} + +/** + Restore previously captured pixels back to the screen. + See the contract in ModernUi/ModernUiRenderer.h. + + @param[in] Context Initialized render context. Must not be NULL. + @param[in] Rect Destination rectangle; must lie fully on screen. + @param[in] Buffer Pixels from ModernUiCaptureRect. Must not be NULL. + + @retval EFI_SUCCESS Pixels restored. + @retval EFI_INVALID_PARAMETER Bad arguments or off-screen rectangle. + @retval others Status from the GOP Blt write. +**/ +EFI_STATUS +EFIAPI +ModernUiRestoreRect ( + IN MODERN_UI_RENDER_CONTEXT *Context, + IN MODERN_UI_RECT Rect, + IN CONST EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer + ) +{ + if ((Context == NULL) || (Context->Gop == NULL) || (Buffer == NULL) || + (Rect.Width == 0) || (Rect.Height == 0) || + ((Rect.X + Rect.Width) > Context->Width) || + ((Rect.Y + Rect.Height) > Context->Height)) + { + return EFI_INVALID_PARAMETER; + } + + return Context->Gop->Blt ( + Context->Gop, + (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)Buffer, + EfiBltBufferToVideo, + 0, + 0, + Rect.X, + Rect.Y, + Rect.Width, + Rect.Height, + Rect.Width * sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) + ); +} From d62ab6d4890c0fdb2bb2a002b7dbb2d8cb6db6b8 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Mon, 15 Jun 2026 10:37:03 +0800 Subject: [PATCH 09/10] fix(app): mouse clicks on Boot/Devices/Preferences list rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported: on the validation FD the Boot ("启动项") page rows could not be operated with the mouse. The first mouse pass only hit-tested tabs, dashboard cards, and the Exit rows -- the list pages were never wired. Add ModernSetupHitTestPageListRow (Actions.c): it looks up the page's layout with the exact ModernSetupGetPageListLayout parameters the page draws with (Boot: MAX_BOOT_ROWS + native tools, no preview; Devices: MAX_DEVICE_ROWS, preview pane; Preferences: row count), bounds the click to the page's selectable count, and maps Y to a visible row via the row stride so a click anywhere on a row line selects it. The App click router sets the page's selection to the hit row and synthesizes Enter, so activation stays single-owner (launch boot option / SendForm the HII formset / open the preference popup). Verified under QEMU: click the Boot tab, click a boot row -> it launches through the same path as keyboard Enter. X64 + AARCH64 builds -Werror clean; smoke PASS. Co-Authored-By: Claude Opus 4.8 --- Application/ModernSetupApp/ModernSetupApp.c | 23 ++++++ .../ModernSetupApp/ModernSetupAppActions.c | 74 +++++++++++++++++++ .../ModernSetupApp/ModernSetupAppInternal.h | 26 +++++++ CHANGELOG.md | 8 ++ 4 files changed, 131 insertions(+) diff --git a/Application/ModernSetupApp/ModernSetupApp.c b/Application/ModernSetupApp/ModernSetupApp.c index 44ef9bc..d23efdf 100644 --- a/Application/ModernSetupApp/ModernSetupApp.c +++ b/Application/ModernSetupApp/ModernSetupApp.c @@ -101,6 +101,7 @@ UefiMain ( UINTN CardHit; UINTN ExitRowHit; UINTN ExitOptionHit; + UINTN ListRowHit; gBS->SetWatchdogTimer (0, 0, 0, NULL); mModernSetupImageHandle = ImageHandle; @@ -283,6 +284,28 @@ UefiMain ( ExitSelection = ExitRowHit; } + Focus = SetupFocusContent; + Event.Type = ModernUiInputEnter; + } else if (ModernSetupHitTestPageListRow (&Ui, Page, PointerX, PointerY, &ListRowHit)) { + // + // Boot / Devices / Preferences list rows: select the clicked row and + // activate it through the shared Enter handling (launch boot option, + // open the native HII form, or open the preference popup/toggle). + // + switch (Page) { + case PageBoot: + BootSelection = ListRowHit; + break; + case PageDevices: + DeviceSelection = ListRowHit; + break; + case PagePreferences: + PreferencesSelection = ListRowHit; + break; + default: + break; + } + Focus = SetupFocusContent; Event.Type = ModernUiInputEnter; } else { diff --git a/Application/ModernSetupApp/ModernSetupAppActions.c b/Application/ModernSetupApp/ModernSetupAppActions.c index 7c6428e..bb5fcd4 100644 --- a/Application/ModernSetupApp/ModernSetupAppActions.c +++ b/Application/ModernSetupApp/ModernSetupAppActions.c @@ -855,6 +855,80 @@ ModernSetupGetPageSelectableCount ( } } +/** + Hit-test a list-page (Boot / Devices / Preferences) row for a pointer click. + See ModernSetupAppInternal.h. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Page List page under test. + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Row Receives the clicked visible row index. Must not be NULL. + + @retval TRUE (X,Y) lies on a visible list row; *Row is set. + @retval FALSE Not a list page, or no row at this position. +**/ +BOOLEAN +ModernSetupHitTestPageListRow ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN SETUP_PAGE Page, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Row + ) +{ + MODERN_SETUP_PAGE_LIST_LAYOUT Layout; + UINTN HardRowCap; + BOOLEAN AllowPreviewPane; + UINTN VisibleCount; + UINTN Index; + + if ((Ui == NULL) || (Row == NULL)) { + return FALSE; + } + + // + // Same layout parameters the page's drawing and selectable-count use, so the + // click bands match the painted rows exactly. + // + switch (Page) { + case PageBoot: + HardRowCap = MAX_BOOT_ROWS + MODERN_SETUP_NATIVE_BOOT_TOOLS_ROW_COUNT; + AllowPreviewPane = FALSE; + break; + case PageDevices: + HardRowCap = MAX_DEVICE_ROWS; + AllowPreviewPane = TRUE; + break; + case PagePreferences: + HardRowCap = MODERN_SETUP_PREFERENCE_ROW_COUNT; + AllowPreviewPane = FALSE; + break; + default: + return FALSE; + } + + if (!ModernSetupGetPageListLayout (Ui, mModernSetupPreferences.DashboardDensity, HardRowCap, AllowPreviewPane, &Layout)) { + return FALSE; + } + + VisibleCount = ModernSetupGetPageSelectableCount (Ui, Page); + if ((VisibleCount == 0) || (Layout.RowStride == 0) || + (X < Layout.RowX) || (X >= (Layout.RowX + Layout.RowWidth)) || + (Y < Layout.FirstRowY)) + { + return FALSE; + } + + Index = (Y - Layout.FirstRowY) / Layout.RowStride; + if (Index >= VisibleCount) { + return FALSE; + } + + *Row = Index; + return TRUE; +} + /** Launch the selected visible Boot#### option. diff --git a/Application/ModernSetupApp/ModernSetupAppInternal.h b/Application/ModernSetupApp/ModernSetupAppInternal.h index 8dd854a..55c6df8 100644 --- a/Application/ModernSetupApp/ModernSetupAppInternal.h +++ b/Application/ModernSetupApp/ModernSetupAppInternal.h @@ -504,6 +504,32 @@ ModernSetupHitTestExitRow ( OUT UINTN *DropdownOption ); +/** + Hit-test a list-page (Boot / Devices / Preferences) row for a pointer click. + + Uses the same `ModernSetupGetPageListLayout` parameters and selectable count + the page's drawing uses, so click bands match the painted rows. The vertical + band is the row stride starting at the first row, so a click anywhere on a + row line selects it. + + @param[in] Ui Initialized render context. Must not be NULL. + @param[in] Page List page under test (non-list pages return FALSE). + @param[in] X Pointer X in pixels. + @param[in] Y Pointer Y in pixels. + @param[out] Row Receives the clicked visible row index. Must not be NULL. + + @retval TRUE (X,Y) lies on a visible list row; *Row is set. + @retval FALSE Not a list page, or no row at this position. +**/ +BOOLEAN +ModernSetupHitTestPageListRow ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN SETUP_PAGE Page, + IN UINTN X, + IN UINTN Y, + OUT UINTN *Row + ); + /** Return the number of dashboard quick-cards visible on the current platform. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1404eed..5d64d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ this file as both a release log and a lightweight development progress record. ### Fixed +- Mouse clicks now work on the Boot / Devices / Preferences list pages (the + first mouse pass only wired tabs, dashboard cards, and the Exit rows, so the + Boot page rows were unclickable). A new `ModernSetupHitTestPageListRow` maps a + click to a visible row using the same `ModernSetupGetPageListLayout` + parameters and selectable count each page draws with, then routes through the + shared Enter handling (launch boot option / open native HII form / open the + preference popup). Verified under QEMU: clicking a Boot row launches it. + - Mouse motion no longer flickers the screen. Pointer movement previously triggered a full-frame repaint per motion event (visible flicker, especially through the LVGL backend's full-canvas composite). The cursor now uses From fc78bbe63f030033446477bc190e83c4989bb498 Mon Sep 17 00:00:00 2001 From: MarsDoge Date: Mon, 15 Jun 2026 10:58:31 +0800 Subject: [PATCH 10/10] feat(app): two-stage click on list rows (select, then activate) Per user feedback, a single click launching a Boot entry is too eager. Make the Boot / Devices / Preferences list rows two-stage: the first click on a row only selects it (focus to content + highlight); a second click on the already-selected row activates it (the existing Enter path: launch boot option, SendForm the HII formset, or open the preference popup). A stray click can no longer launch anything. Tabs, dashboard cards, and Exit rows keep single-click navigation. Verified under QEMU: first click on a Boot row highlights it and stays on the page (no boot); second click on the same row launches it. X64 + AARCH64 builds -Werror clean; smoke PASS. Co-Authored-By: Claude Opus 4.8 --- Application/ModernSetupApp/ModernSetupApp.c | 34 ++++++++++++++++----- CHANGELOG.md | 9 ++++-- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Application/ModernSetupApp/ModernSetupApp.c b/Application/ModernSetupApp/ModernSetupApp.c index d23efdf..ac49438 100644 --- a/Application/ModernSetupApp/ModernSetupApp.c +++ b/Application/ModernSetupApp/ModernSetupApp.c @@ -102,6 +102,8 @@ UefiMain ( UINTN ExitRowHit; UINTN ExitOptionHit; UINTN ListRowHit; + UINTN *ListSelPtr; + BOOLEAN ListRowWasActive; gBS->SetWatchdogTimer (0, 0, 0, NULL); mModernSetupImageHandle = ImageHandle; @@ -288,26 +290,42 @@ UefiMain ( Event.Type = ModernUiInputEnter; } else if (ModernSetupHitTestPageListRow (&Ui, Page, PointerX, PointerY, &ListRowHit)) { // - // Boot / Devices / Preferences list rows: select the clicked row and - // activate it through the shared Enter handling (launch boot option, - // open the native HII form, or open the preference popup/toggle). + // Boot / Devices / Preferences list rows are two-stage: the first click + // only selects the row (focus + highlight), and a second click on the + // already-selected row activates it (launch boot option, open the + // native HII form, or open the preference popup) -- so a stray click + // never launches anything. Activation reuses the shared Enter handling. // + ListSelPtr = NULL; switch (Page) { case PageBoot: - BootSelection = ListRowHit; + ListSelPtr = &BootSelection; break; case PageDevices: - DeviceSelection = ListRowHit; + ListSelPtr = &DeviceSelection; break; case PagePreferences: - PreferencesSelection = ListRowHit; + ListSelPtr = &PreferencesSelection; break; default: break; } - Focus = SetupFocusContent; - Event.Type = ModernUiInputEnter; + if (ListSelPtr != NULL) { + ListRowWasActive = (BOOLEAN)((Focus == SetupFocusContent) && (*ListSelPtr == ListRowHit)); + *ListSelPtr = ListRowHit; + Focus = SetupFocusContent; + if (ListRowWasActive) { + Event.Type = ModernUiInputEnter; + } else { + StatusMessage[0] = L'\0'; + Redraw = TRUE; + continue; + } + } else { + Redraw = TRUE; + continue; + } } else { Redraw = TRUE; continue; diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d64d80..2c1b8a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,12 @@ this file as both a release log and a lightweight development progress record. first mouse pass only wired tabs, dashboard cards, and the Exit rows, so the Boot page rows were unclickable). A new `ModernSetupHitTestPageListRow` maps a click to a visible row using the same `ModernSetupGetPageListLayout` - parameters and selectable count each page draws with, then routes through the - shared Enter handling (launch boot option / open native HII form / open the - preference popup). Verified under QEMU: clicking a Boot row launches it. + parameters and selectable count each page draws with. List rows are + **two-stage**: the first click only selects (focus + highlight), and a second + click on the already-selected row activates it (launch boot option / open the + native HII form / open the preference popup), so a stray click never launches + anything. Activation reuses the shared Enter handling. Verified under QEMU: + first click selects the Boot row, second click launches it. - Mouse motion no longer flickers the screen. Pointer movement previously triggered a full-frame repaint per motion event (visible flicker, especially