diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abfec0..90ebb56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,41 @@ this file as both a release log and a lightweight development progress record. ### Added +- Phase36 architecture guard: documented the PEI -> DXE services -> + ModernSetupApp/DisplayEngine dynamic data/configuration pipeline in English and + zh-CN, with smoke coverage that keeps DisplayEngine focused on rendering state + rather than owning hardware probing or config mutation. +- Phase36 UX iteration: Modern footer hotkey help now reserves left-side text + columns for the GOP status chip, preventing key-help text from colliding with + live/unsaved/reboot-required state indicators. +- Phase36 UX iteration: highlighted interactive rows now draw a subtle right-side + value lane, making prompt/value separation clearer without taking ownership of + FormBrowser text or value semantics. +- Phase36 UX iteration: the Modern UI header time now includes seconds, making + redraw/refresh activity visible without adding a new timer path or changing + FormBrowser event ownership. +- Phase36 UX iteration: DisplayEngine page status now normalizes through a + private page-state enum with an explicit future `REBOOT REQUIRED` state, so + reboot-after-save policy can be surfaced later without conflating it with + generic unsaved changes. +- Phase36 UX iteration: the shared Modern UI footer now renders page status as a + compact color-coded chip, giving live/refresh/unsaved/modal states a durable + visual slot for future PEI/DXE/App data handoff and reboot-required flows. +- Phase36 UX iteration: DisplayEngine page chrome now surfaces FormBrowser-owned + page state in the footer (`LIVE VIEW`, `LIVE REFRESH`, `UNSAVED CHANGES`, or + `MODAL VIEW`), establishing a presentation slot for future PEI/DXE/App data + handoff and dynamic refresh without adding renderer-owned policy/storage logic. +- Phase36 UX iteration: the modern DisplayEngine page chrome now adds a subtle + right-rail divider, clarifying the split between the actionable statement list + and contextual help without changing FormBrowser help text placement. +- Phase36 UX iteration: native FormBrowser prompt/value glyphs printed inside + the modern statement list now receive a small GOP-only inset so text no longer + crowds the accent rail or rounded row surface while preserving text-mode cursor + accounting and FormBrowser ownership. +- Phase36 DisplayEngine row visual polish: FormModel-driven row surfaces now add + conservative GOP accents for editable/action rows, changed settings, + invalid/warning feedback, and disabled/read-only states without changing + FormBrowser text/value rendering or HII/config ownership. - Phase35 native-vs-modern DisplayEngine visual validation foundation for OVMF X64: `Scripts/capture-displayengine-ovmf-x64.sh` creates separated native and modern overlay/build/capture artifact paths under a safe TMPDIR default, diff --git a/Docs/DisplayEngineDynamicDataFlow.md b/Docs/DisplayEngineDynamicDataFlow.md new file mode 100644 index 0000000..137175a --- /dev/null +++ b/Docs/DisplayEngineDynamicDataFlow.md @@ -0,0 +1,91 @@ +# DisplayEngine Dynamic Data Flow Contract + +This document defines the intended product direction for ModernSetupPkg dynamic platform data, setup configuration, and refresh UX. + +## Goal + +ModernSetupApp and the Modern DisplayEngine must be able to show current platform information, reflect configuration changes, and update dynamic data such as time or sensor-like values without turning the renderer into a policy or hardware owner. + +## Intended pipeline + +```text +PEI platform discovery / defaults + -> HOB / PCD / Variable / protocol handoff + -> DXE platform services + -> ModernSetupApp / DisplayEngine view model + -> Modern UI display + dynamic refresh + -> app writes selected config through platform-owned PCD / Variable / protocol paths + -> immediate effect where safe, or reboot-required effect + -> next PEI consumes non-default config and republishes latest state +``` + +## Responsibilities + +### PEI / platform discovery + +- Collect early platform information. +- Read default and non-default configuration inputs. +- Publish handoff data through platform-owned mechanisms. +- Consume reboot-persistent configuration on the next boot. + +### DXE platform services + +- Normalize platform data for applications. +- Own policy, validation, persistence, and reset requirements. +- Expose current data and update notifications to the app/display layer. + +### ModernSetupApp + +- Presents product-level setup pages. +- Initiates configuration changes through approved platform services or existing setup mechanisms. +- Shows whether changes are live, unsaved, or require reboot. + +### Modern DisplayEngine + +- Renders FormBrowser-owned form state and ModernSetup page state. +- Shows live/refresh/unsaved/reboot-required/status affordances. +- Supports redraw-friendly dynamic fields such as time. +- Must not own hardware probing, policy decisions, or storage writes. + +## DisplayEngine constraints + +Allowed in DisplayEngine/UI code: + +- Consume already-materialized FormBrowser display data. +- Consume future app/platform view-model state. +- Render row kind/state, status chips, refresh indicators, and dynamic values. +- Repaint on FormBrowser refresh events or app-driven redraws. + +Forbidden in DisplayEngine/UI renderer code: + +- Direct hardware probing. +- Independent IFR parsing. +- ConfigAccess semantics. +- Direct `SetVariable`, `RouteConfig`, `ExtractConfig`, or `HiiSetBrowserData` ownership. +- Treating generic unsaved changes as reboot-required without a platform/FormBrowser source. + +## UX states + +Current private DisplayEngine status slots: + +```text +LIVE VIEW default live page surface +LIVE REFRESH FormBrowser/page has a refresh event or equivalent update source +UNSAVED CHANGES changed state exists but is not committed +REBOOT REQUIRED future platform/FormBrowser source says reboot is required +MODAL VIEW modal FormBrowser state +``` + +`REBOOT REQUIRED` is intentionally a reserved UX state until a real source is wired. The UI must not infer it from generic changed state. + +## Validation expectation + +Routine UX iteration should stay Modern DisplayEngine focused: + +```bash +python3 Tests/Smoke/smoke_validate.py +git diff --check +TARGET=RELEASE MODERN_SETUP_DISPLAY_ENGINE=modern MODERN_SETUP_REPLACE_UIAPP=1 Scripts/build-ovmf-x64.sh +``` + +Use native-vs-modern capture only for milestones or PR review baselines, not every iteration. diff --git a/Docs/DisplayEngineDynamicDataFlow.zh-CN.md b/Docs/DisplayEngineDynamicDataFlow.zh-CN.md new file mode 100644 index 0000000..5fa1e48 --- /dev/null +++ b/Docs/DisplayEngineDynamicDataFlow.zh-CN.md @@ -0,0 +1,91 @@ +# DisplayEngine 动态数据流约束 + +本文档定义 ModernSetupPkg 后续动态平台数据、Setup 配置和刷新 UX 的产品方向。 + +## 目标 + +ModernSetupApp 和 Modern DisplayEngine 需要能显示当前平台信息、反映配置变更,并更新时间或类似 sensor 的动态数据;但 renderer 不能变成 policy owner 或 hardware owner。 + +## 目标链路 + +```text +PEI 平台发现 / 默认策略 + -> HOB / PCD / Variable / protocol handoff + -> DXE platform services + -> ModernSetupApp / DisplayEngine view model + -> Modern UI display + dynamic refresh + -> app 通过平台拥有的 PCD / Variable / protocol 路径写入配置 + -> 安全时立即生效,或标记 reboot-required + -> 下一次 PEI 消费非默认配置并重新发布最新状态 +``` + +## 职责边界 + +### PEI / platform discovery + +- 收集早期平台信息。 +- 读取默认和非默认配置输入。 +- 通过平台拥有的机制发布 handoff 数据。 +- 下一次启动时消费 reboot-persistent 配置。 + +### DXE platform services + +- 为 app 归一化平台数据。 +- 拥有 policy、validation、persistence 和 reset requirement。 +- 向 app/display 层暴露当前数据和更新通知。 + +### ModernSetupApp + +- 展示产品级 Setup 页面。 +- 通过批准的平台服务或现有 setup 机制发起配置变更。 +- 显示变更是 live、unsaved,还是 require reboot。 + +### Modern DisplayEngine + +- 渲染 FormBrowser-owned form state 和 ModernSetup page state。 +- 显示 live/refresh/unsaved/reboot-required/status affordances。 +- 支持 redraw-friendly 的动态字段,例如时间。 +- 不拥有硬件探测、policy decision 或 storage writes。 + +## DisplayEngine 约束 + +DisplayEngine/UI code 允许: + +- 消费已经 materialized 的 FormBrowser display data。 +- 消费未来 app/platform view-model state。 +- 渲染 row kind/state、status chips、refresh indicators 和 dynamic values。 +- 在 FormBrowser refresh event 或 app-driven redraw 时重绘。 + +DisplayEngine/UI renderer code 禁止: + +- 直接硬件探测。 +- 独立 IFR parsing。 +- ConfigAccess 语义。 +- 直接拥有 `SetVariable`、`RouteConfig`、`ExtractConfig` 或 `HiiSetBrowserData`。 +- 没有 platform/FormBrowser source 时,把普通 unsaved changes 当成 reboot-required。 + +## UX 状态 + +当前私有 DisplayEngine status slots: + +```text +LIVE VIEW 默认 live page surface +LIVE REFRESH FormBrowser/page 有 refresh event 或等价 update source +UNSAVED CHANGES 存在 changed state,但尚未提交 +REBOOT REQUIRED 未来 platform/FormBrowser source 明确要求重启 +MODAL VIEW modal FormBrowser state +``` + +`REBOOT REQUIRED` 现在只是预留 UX state,直到真实来源接入前不能伪造。UI 不能从 generic changed state 推断它。 + +## 验证预期 + +日常 UX iteration 保持 Modern DisplayEngine focused: + +```bash +python3 Tests/Smoke/smoke_validate.py +git diff --check +TARGET=RELEASE MODERN_SETUP_DISPLAY_ENGINE=modern MODERN_SETUP_REPLACE_UIAPP=1 Scripts/build-ovmf-x64.sh +``` + +native-vs-modern capture 只在 milestone 或 PR review baseline 使用,不要每轮 iteration 都跑。 diff --git a/Library/ModernUiCustomizedDisplayLib/CustomizedDisplayLibInternal.c b/Library/ModernUiCustomizedDisplayLib/CustomizedDisplayLibInternal.c index 88fa582..db36cd1 100644 --- a/Library/ModernUiCustomizedDisplayLib/CustomizedDisplayLibInternal.c +++ b/Library/ModernUiCustomizedDisplayLib/CustomizedDisplayLibInternal.c @@ -24,6 +24,14 @@ STATIC BOOLEAN mModernRenderReady; STATIC UINTN mModernCursorColumn; STATIC UINTN mModernCursorRow; +typedef enum { + ModernDisplayPageStateLive, + ModernDisplayPageStateLiveRefresh, + ModernDisplayPageStateUnsaved, + ModernDisplayPageStateRebootRequired, + ModernDisplayPageStateModal +} MODERN_DISPLAY_PAGE_STATE; + STATIC UINTN ModernDisplayColumns ( @@ -36,6 +44,58 @@ ModernDisplayRows ( VOID ); +STATIC +VOID +ModernDisplayDrawStatementRowAccents ( + IN CONST MODERN_UI_RECT *RowRect, + IN CONST MODERN_DISPLAY_FORM_ROW *FormRow, + IN CONST MODERN_UI_THEME *Theme + ); + +STATIC +VOID +ModernDisplayDrawStatementValueLane ( + IN CONST MODERN_UI_RECT *RowRect, + IN CONST MODERN_DISPLAY_FORM_ROW *FormRow, + IN CONST MODERN_UI_THEME *Theme + ); + +STATIC +UINTN +ModernDisplayStatementTextInset ( + IN UINTN Column, + IN UINTN Row, + IN UINTN Width, + IN UINTN CellWidth + ); + +STATIC +VOID +ModernDisplayDrawRightRailDivider ( + IN CONST MODERN_DISPLAY_LAYOUT *Layout, + IN CONST MODERN_UI_THEME *Theme, + IN UINTN CellWidth, + IN UINTN CellHeight + ); + +STATIC +MODERN_DISPLAY_PAGE_STATE +ModernDisplayPageState ( + IN CONST FORM_DISPLAY_ENGINE_FORM *FormData + ); + +STATIC +CONST CHAR16 * +ModernDisplayPageStatusText ( + IN CONST FORM_DISPLAY_ENGINE_FORM *FormData + ); + +STATIC +UINTN +ModernDisplayFooterStatusReservedColumns ( + VOID + ); + /** Return GOP cell metrics that match the active text-mode grid. @@ -204,6 +264,172 @@ ModernDisplayCalculateLayout ( return EFI_SUCCESS; } +/** + Return a small GOP text inset for native FormBrowser text printed inside the + modern statement list. + + The DisplayEngine still owns prompt/value text placement in text-grid cells. + This helper only nudges the GOP glyph draw position inside the already assigned + cells so the text does not visually collide with the FormModel accent rail or + the rounded row surface. Cursor accounting and text-mode semantics stay + unchanged. + + @param[in] Column Text-grid column where the string starts. + @param[in] Row Text-grid row where the string is printed. + @param[in] Width Text-grid column count assigned to the print. + @param[in] CellWidth Pixel width for one text column. + + @return Pixel inset to add to the GOP text X coordinate. +**/ +STATIC +UINTN +ModernDisplayStatementTextInset ( + IN UINTN Column, + IN UINTN Row, + IN UINTN Width, + IN UINTN CellWidth + ) +{ + MODERN_DISPLAY_LAYOUT Layout; + UINTN EndColumn; + + if ((Width == 0) || EFI_ERROR (ModernDisplayCalculateLayout (&Layout))) { + return 0; + } + + EndColumn = Column + Width; + if ((Row < Layout.Statement.TopRow) || (Row >= Layout.Statement.BottomRow) || + (EndColumn <= Layout.Statement.LeftColumn) || (Column >= Layout.Statement.RightColumn)) + { + return 0; + } + + if (Column <= (Layout.Statement.LeftColumn + 2)) { + return MIN (10, MAX (4, CellWidth / 2)); + } + + return MIN (6, MAX (2, CellWidth / 3)); +} + +/** + Draw a lightweight divider between the statement list and right-side help rail. + + This is a visual grouping hint only. FormBrowser still owns where help text is + printed and how it wraps; the divider simply makes the modern chrome read as + two regions: actionable statements on the left, contextual help on the right. + + @param[in] Layout Calculated DisplayEngine layout. Must not be NULL. + @param[in] Theme Theme token table. Must not be NULL. + @param[in] CellWidth Pixel width for one text column. + @param[in] CellHeight Pixel height for one text row. +**/ +STATIC +VOID +ModernDisplayDrawRightRailDivider ( + IN CONST MODERN_DISPLAY_LAYOUT *Layout, + IN CONST MODERN_UI_THEME *Theme, + IN UINTN CellWidth, + IN UINTN CellHeight + ) +{ + UINTN X; + UINTN Y; + UINTN Height; + + if ((Layout == NULL) || (Theme == NULL) || !Layout->RightRailVisible || (CellWidth == 0) || (CellHeight == 0)) { + return; + } + + X = (Layout->RightRailLeftColumn * CellWidth > 8) ? (Layout->RightRailLeftColumn * CellWidth - 8) : 0; + Y = Layout->ContentTopRow * CellHeight; + Height = (Layout->ContentBottomRow > Layout->ContentTopRow) ? + ((Layout->ContentBottomRow - Layout->ContentTopRow) * CellHeight) : + 0; + + if (Height < 8) { + return; + } + + ModernUiFillRect ( + &mModernRenderContext, + (MODERN_UI_RECT){ X, Y + 8, 2, Height - 16 }, + ModernUiBlendColor (Theme->AccentOrange, Theme->Background, 32) + ); + + ModernUiFillRect ( + &mModernRenderContext, + (MODERN_UI_RECT){ X - 2, Y + 8, 6, 1 }, + ModernUiBlendColor (Theme->AccentOrange, Theme->BackgroundBlack, 45) + ); +} + +/** + Return normalized page-level state for the Modern DisplayEngine footer. + + This is intentionally presentation-only. It reflects FormBrowser-owned page + state so future PEI/DXE/App data handoff and refresh flows have a stable UI + place to surface "live", "changed", or "modal" state without moving policy or + storage semantics into the renderer. `RebootRequired` is kept as an explicit + UI state for a future platform/FormBrowser source; this helper does not infer + it from generic changed state. + + @param[in] FormData DisplayEngine form currently shown. May be NULL. + + @return Normalized page status state. +**/ +STATIC +MODERN_DISPLAY_PAGE_STATE +ModernDisplayPageState ( + IN CONST FORM_DISPLAY_ENGINE_FORM *FormData + ) +{ + if (FormData == NULL) { + return ModernDisplayPageStateLive; + } + + if ((FormData->Attribute & HII_DISPLAY_MODAL) != 0) { + return ModernDisplayPageStateModal; + } + + if (FormData->SettingChangedFlag) { + return ModernDisplayPageStateUnsaved; + } + + if (FormData->FormRefreshEvent != NULL) { + return ModernDisplayPageStateLiveRefresh; + } + + return ModernDisplayPageStateLive; +} + +/** + Return concise page-level status text for the Modern DisplayEngine footer. + + @param[in] FormData DisplayEngine form currently shown. May be NULL. + + @return Static status string, or NULL when there is no status to surface. +**/ +STATIC +CONST CHAR16 * +ModernDisplayPageStatusText ( + IN CONST FORM_DISPLAY_ENGINE_FORM *FormData + ) +{ + switch (ModernDisplayPageState (FormData)) { + case ModernDisplayPageStateModal: + return L"MODAL VIEW"; + case ModernDisplayPageStateRebootRequired: + return L"REBOOT REQUIRED"; + case ModernDisplayPageStateUnsaved: + return L"UNSAVED CHANGES"; + case ModernDisplayPageStateLiveRefresh: + return L"LIVE REFRESH"; + case ModernDisplayPageStateLive: + default: + return L"LIVE VIEW"; + } +} + /** Convert an EFI text attribute into a foreground pixel color. @@ -397,6 +623,184 @@ ModernDisplayDrawPopupSurface ( ModernUiEngineDrawPopup (&mModernRenderContext, &Popup, Theme); } +/** + Select the accent color for a FormBrowser statement row. + + The color is a visual hint only. It is derived from the private FormModel row + kind/state after FormBrowser has already materialized the statement. The helper + does not inspect IFR packages, route storage, or change browser behavior. + + @param[in] FormRow Private row model to inspect. May be NULL. + @param[in] Theme Theme token table. Must not be NULL. + + @return Accent color. NULL Theme returns zero. +**/ +STATIC +EFI_GRAPHICS_OUTPUT_BLT_PIXEL +ModernDisplayFormRowAccentColor ( + IN CONST MODERN_DISPLAY_FORM_ROW *FormRow OPTIONAL, + IN CONST MODERN_UI_THEME *Theme + ) +{ + EFI_GRAPHICS_OUTPUT_BLT_PIXEL Accent; + + ZeroMem (&Accent, sizeof (Accent)); + if (Theme == NULL) { + return Accent; + } + + if (FormRow == NULL) { + return Theme->Border; + } + + if ((FormRow->State & ModernDisplayFormRowStateInvalid) != 0) { + return Theme->WarningText; + } + + if ((FormRow->State & ModernDisplayFormRowStateDisabled) != 0) { + return Theme->MutedText; + } + + switch (FormRow->Kind) { + case ModernDisplayFormRowChoice: + case ModernDisplayFormRowOrderedList: + case ModernDisplayFormRowNumeric: + case ModernDisplayFormRowDate: + case ModernDisplayFormRowTime: + return Theme->AccentOrange; + + case ModernDisplayFormRowCheckbox: + case ModernDisplayFormRowPassword: + case ModernDisplayFormRowString: + return Theme->AccentYellow; + + case ModernDisplayFormRowReference: + case ModernDisplayFormRowAction: + case ModernDisplayFormRowResetButton: + return Theme->Success; + + case ModernDisplayFormRowSubtitle: + case ModernDisplayFormRowText: + case ModernDisplayFormRowUnknown: + default: + return Theme->Border; + } +} + +/** + Draw a subtle value lane on highlighted/editable rows. + + Native FormBrowser still prints prompt/value text. This helper only paints a + GOP background hint on the right side of interactive rows so users can + distinguish the value/edit region from the prompt region. + + @param[in] RowRect Pixel row rectangle. Must not be NULL. + @param[in] FormRow Private row model. Must not be NULL. + @param[in] Theme Theme token table. Must not be NULL. +**/ +STATIC +VOID +ModernDisplayDrawStatementValueLane ( + IN CONST MODERN_UI_RECT *RowRect, + IN CONST MODERN_DISPLAY_FORM_ROW *FormRow, + IN CONST MODERN_UI_THEME *Theme + ) +{ + EFI_GRAPHICS_OUTPUT_BLT_PIXEL LaneColor; + UINTN LaneWidth; + UINTN LaneX; + UINTN LaneY; + UINTN LaneHeight; + + if ((RowRect == NULL) || (FormRow == NULL) || (Theme == NULL) || (RowRect->Width < 160) || (RowRect->Height < 12)) { + return; + } + + if (ModernDisplayFormRowIsTextOnly (FormRow->Kind) || ((FormRow->State & ModernDisplayFormRowStateHighlighted) == 0)) { + return; + } + + if (((FormRow->State & ModernDisplayFormRowStateDisabled) != 0) || ((FormRow->State & ModernDisplayFormRowStateReadOnly) != 0)) { + return; + } + + LaneWidth = MAX (72, RowRect->Width / 3); + LaneWidth = MIN (LaneWidth, RowRect->Width - 48); + LaneX = RowRect->X + RowRect->Width - LaneWidth - 10; + LaneY = RowRect->Y + 4; + LaneHeight = RowRect->Height - 8; + LaneColor = ((FormRow->State & ModernDisplayFormRowStateSelected) != 0) ? + ModernUiBlendColor (Theme->AccentOrange, Theme->BackgroundBlack, 32) : + ModernUiBlendColor (Theme->SurfaceRaised, Theme->BackgroundBlack, 55); + + ModernUiFillRect (&mModernRenderContext, (MODERN_UI_RECT){ LaneX, LaneY, LaneWidth, LaneHeight }, LaneColor); + ModernUiFillRect (&mModernRenderContext, (MODERN_UI_RECT){ LaneX, LaneY, 2, LaneHeight }, ModernDisplayFormRowAccentColor (FormRow, Theme)); +} + +/** + Draw lightweight FormModel-driven accents over one statement row surface. + + The native DisplayEngine still prints the prompt/value text. This function only + paints non-semantic GOP hints: editable/action accent rails, changed markers, + invalid borders, and disabled/read-only separators. Any draw failure leaves the + already-painted row surface in place and is intentionally ignored by callers. + + @param[in] RowRect Pixel row rectangle. Must not be NULL. + @param[in] FormRow Private row model. Must not be NULL. + @param[in] Theme Theme token table. Must not be NULL. +**/ +STATIC +VOID +ModernDisplayDrawStatementRowAccents ( + IN CONST MODERN_UI_RECT *RowRect, + IN CONST MODERN_DISPLAY_FORM_ROW *FormRow, + IN CONST MODERN_UI_THEME *Theme + ) +{ + EFI_GRAPHICS_OUTPUT_BLT_PIXEL Accent; + UINTN AccentWidth; + UINTN MarkerSize; + + if ((RowRect == NULL) || (FormRow == NULL) || (Theme == NULL) || (RowRect->Width == 0) || (RowRect->Height == 0)) { + return; + } + + Accent = ModernDisplayFormRowAccentColor (FormRow, Theme); + AccentWidth = ((FormRow->State & ModernDisplayFormRowStateHighlighted) != 0) ? 6 : 3; + + if (!ModernDisplayFormRowIsTextOnly (FormRow->Kind) && (RowRect->Width > (AccentWidth + 4)) && (RowRect->Height > 6)) { + ModernUiFillRect ( + &mModernRenderContext, + (MODERN_UI_RECT){ RowRect->X, RowRect->Y + 2, AccentWidth, RowRect->Height - 4 }, + Accent + ); + } + + if ((FormRow->State & ModernDisplayFormRowStateChanged) != 0) { + MarkerSize = (RowRect->Height > 18) ? 6 : 4; + if ((RowRect->Width > (MarkerSize + 8)) && (RowRect->Height > (MarkerSize + 6))) { + ModernUiFillRect ( + &mModernRenderContext, + (MODERN_UI_RECT){ RowRect->X + RowRect->Width - MarkerSize - 6, RowRect->Y + 4, MarkerSize, MarkerSize }, + Theme->AccentYellow + ); + } + } + + if ((FormRow->State & ModernDisplayFormRowStateInvalid) != 0) { + ModernUiStrokeRect (&mModernRenderContext, *RowRect, Theme->WarningText); + return; + } + + if (((FormRow->State & ModernDisplayFormRowStateDisabled) != 0) || ((FormRow->State & ModernDisplayFormRowStateReadOnly) != 0)) { + ModernUiFillRect ( + &mModernRenderContext, + (MODERN_UI_RECT){ RowRect->X, RowRect->Y + RowRect->Height - 1, RowRect->Width, 1 }, + ModernUiBlendColor (Theme->Border, Theme->Background, 50) + ); + } +} + /** Draw a ModernSetup row background for one FormBrowser statement. @@ -455,6 +859,8 @@ ModernDisplayDrawStatementRow ( } ModernUiEngineDrawRows (&mModernRenderContext, &RowModel, 1, Theme); + ModernDisplayDrawStatementValueLane (&RowRect, &FormRow, Theme); + ModernDisplayDrawStatementRowAccents (&RowRect, &FormRow, Theme); } /** @@ -543,9 +949,10 @@ ModernDisplayDrawPageChrome ( PageModel.SelectedTab = ModernDisplaySelectChromeTab (PrintableTitle); PageModel.ProductName = L"MODERN SETUP"; PageModel.ModeName = L"ADVANCED MODE"; - PageModel.StatusText = NULL; + PageModel.StatusText = ModernDisplayPageStatusText (FormData); PageModel.DrawRightRail = TRUE; ModernUiEngineDrawPage (&mModernRenderContext, &PageModel, Theme); + ModernDisplayDrawRightRailDivider (&Layout, Theme, CellWidth, CellHeight); if (PrintableTitle != NULL) { FreePool (PrintableTitle); @@ -1032,6 +1439,27 @@ LibGetStringWidth ( return Count * sizeof (CHAR16); } +/** + Reserve text columns used by the Modern footer status chip. + + Native hotkey help still owns its text output. When the GOP Modern renderer is + active, keep the left-most hotkey column from colliding with the status chip. + + @return Number of text columns reserved at the left of the footer. +**/ +STATIC +UINTN +ModernDisplayFooterStatusReservedColumns ( + VOID + ) +{ + if (!mModernRenderReady) { + return 0; + } + + return 18; +} + /** Show all registered HotKey help strings on bottom Rows. @@ -1051,6 +1479,7 @@ PrintHotKeyHelpString ( UINTN ColumnIndexWidth; UINTN ColumnWidth; UINTN ColumnIndex; + UINTN FooterReservedColumns; UINTN Index; EFI_SCREEN_DESCRIPTOR LocalScreen; LIST_ENTRY *Link; @@ -1061,6 +1490,7 @@ PrintHotKeyHelpString ( CopyMem (&LocalScreen, &gScreenDimensions, sizeof (EFI_SCREEN_DESCRIPTOR)); ColumnWidth = (LocalScreen.RightColumn - LocalScreen.LeftColumn) / 3; BottomRowOfHotKeyHelp = LocalScreen.BottomRow - STATUS_BAR_HEIGHT - 3; + FooterReservedColumns = ModernDisplayFooterStatusReservedColumns (); ColumnStr = gLibEmptyString; // @@ -1081,8 +1511,14 @@ PrintHotKeyHelpString ( CurrentCol = LocalScreen.LeftColumn + ColumnWidth; ColumnIndexWidth = ColumnWidth; } else { - CurrentCol = LocalScreen.LeftColumn + 2; - ColumnIndexWidth = ColumnWidth - 2; + CurrentCol = LocalScreen.LeftColumn + 2 + FooterReservedColumns; + ColumnIndexWidth = (ColumnWidth > (FooterReservedColumns + 2)) ? (ColumnWidth - FooterReservedColumns - 2) : 0; + } + + if (ColumnIndexWidth == 0) { + Link = GetNextNode (&FormData->HotKeyListHead, Link); + Index++; + continue; } CurrentRow = BottomRowOfHotKeyHelp - Index / 3; @@ -1350,6 +1786,7 @@ PrintInternal ( UINTN TextX; UINTN TextY; UINTN TextMaxWidth; + UINTN TextInset; CHAR16 *Printable; EFI_GRAPHICS_OUTPUT_BLT_PIXEL Foreground; EFI_GRAPHICS_OUTPUT_BLT_PIXEL Background; @@ -1423,10 +1860,11 @@ PrintInternal ( Printable = AllocateZeroPool ((StrLen (Buffer) + 1) * sizeof (CHAR16)); if (Printable != NULL) { ModernDisplayCopyPrintable (Printable, StrLen (Buffer) + 1, Buffer); - TextX = DrawColumn * CellWidth + 2; + TextInset = ModernDisplayStatementTextInset (DrawColumn, DrawRow, DrawWidth, CellWidth); + TextX = DrawColumn * CellWidth + 2 + TextInset; TextY = DrawRow * CellHeight + ((CellHeight > 18) ? ((CellHeight - 18) / 2) : 0); - TextMaxWidth = (DrawWidth * CellWidth > 4) ? - (DrawWidth * CellWidth - 4) : + TextMaxWidth = (DrawWidth * CellWidth > (4 + TextInset)) ? + (DrawWidth * CellWidth - 4 - TextInset) : DrawWidth * CellWidth; ModernUiDrawTextFit ( &mModernRenderContext, diff --git a/Library/ModernUiEngineLib/ModernUiEngineLib.c b/Library/ModernUiEngineLib/ModernUiEngineLib.c index a44917f..5dd9503 100644 --- a/Library/ModernUiEngineLib/ModernUiEngineLib.c +++ b/Library/ModernUiEngineLib/ModernUiEngineLib.c @@ -422,6 +422,32 @@ ModernUiEngineDrawTabs ( return EFI_SUCCESS; } +STATIC +EFI_GRAPHICS_OUTPUT_BLT_PIXEL +ModernUiEngineStatusColor ( + IN CONST CHAR16 *StatusText, + IN CONST MODERN_UI_THEME *Theme + ) +{ + if ((StatusText == NULL) || (Theme == NULL)) { + return (EFI_GRAPHICS_OUTPUT_BLT_PIXEL){ 0, 0, 0, 0 }; + } + + if ((StrCmp (StatusText, L"UNSAVED CHANGES") == 0) || (StrCmp (StatusText, L"REBOOT REQUIRED") == 0)) { + return Theme->Warning; + } + + if (StrCmp (StatusText, L"LIVE REFRESH") == 0) { + return Theme->Success; + } + + if (StrCmp (StatusText, L"MODAL VIEW") == 0) { + return Theme->AccentYellow; + } + + return Theme->AccentOrange; +} + EFI_STATUS EFIAPI ModernUiEngineDrawFooter ( @@ -431,7 +457,11 @@ ModernUiEngineDrawFooter ( IN CONST MODERN_UI_THEME *Theme ) { - EFI_STATUS Status; + EFI_STATUS Status; + EFI_GRAPHICS_OUTPUT_BLT_PIXEL StatusColor; + UINTN ChipWidth; + UINTN TextWidth; + UINTN MaxChipWidth; if ((Context == NULL) || (Theme == NULL) || (Rect.Width == 0) || (Rect.Height == 0)) { return EFI_INVALID_PARAMETER; @@ -458,8 +488,48 @@ ModernUiEngineDrawFooter ( } } - if ((StatusText != NULL) && (StatusText[0] != CHAR_NULL)) { - return ModernUiDrawText (Context, Rect.X + 24, Rect.Y + 10, StatusText, Theme->Warning, Theme->BackgroundBlack); + if ((StatusText != NULL) && (StatusText[0] != CHAR_NULL) && (Rect.Width > 80) && (Rect.Height > 22)) { + StatusColor = ModernUiEngineStatusColor (StatusText, Theme); + TextWidth = ModernUiMeasureText (StatusText); + MaxChipWidth = (Rect.Width > 96) ? (Rect.Width - 48) : Rect.Width; + ChipWidth = MIN (MaxChipWidth, TextWidth + 42); + + Status = ModernUiFillRect ( + Context, + (MODERN_UI_RECT){ Rect.X + 18, Rect.Y + 7, ChipWidth, 20 }, + ModernUiBlendColor (Theme->BackgroundBlack, StatusColor, 16) + ); + if (EFI_ERROR (Status)) { + return Status; + } + + Status = ModernUiFillRect ( + Context, + (MODERN_UI_RECT){ Rect.X + 18, Rect.Y + 7, 4, 20 }, + StatusColor + ); + if (EFI_ERROR (Status)) { + return Status; + } + + Status = ModernUiFillRect ( + Context, + (MODERN_UI_RECT){ Rect.X + 22, Rect.Y + 7, ChipWidth - 4, 1 }, + ModernUiBlendColor (StatusColor, Theme->BackgroundBlack, 55) + ); + if (EFI_ERROR (Status)) { + return Status; + } + + return ModernUiDrawTextFit ( + Context, + Rect.X + 34, + Rect.Y + 11, + (ChipWidth > 30) ? (ChipWidth - 30) : ChipWidth, + StatusText, + StatusColor, + ModernUiBlendColor (Theme->BackgroundBlack, StatusColor, 16) + ); } return EFI_SUCCESS; @@ -523,19 +593,20 @@ ModernUiEngineDrawPage ( } if (!EFI_ERROR (gRT->GetTime (&Time, NULL))) { - TimeX = (Context->Width > 210) ? (Context->Width - 210) : (Model->Rect.X + 26); + TimeX = (Context->Width > 238) ? (Context->Width - 238) : (Model->Rect.X + 26); Status = ModernUiDrawTextFormatted ( Context, TimeX, Model->Rect.Y + 6, Theme->Text, Theme->HeaderPattern, - L"%02d/%02d/%04d %02d:%02d", + L"%02d/%02d/%04d %02d:%02d:%02d", Time.Month, Time.Day, Time.Year, Time.Hour, - Time.Minute + Time.Minute, + Time.Second ); if (EFI_ERROR (Status)) { return Status; diff --git a/Tests/Smoke/smoke_validate.py b/Tests/Smoke/smoke_validate.py index 196116f..423d0e5 100755 --- a/Tests/Smoke/smoke_validate.py +++ b/Tests/Smoke/smoke_validate.py @@ -2031,9 +2031,45 @@ def check_phase33_display_form_view_model_boundary(root: Path) -> list[str]: "ModernDisplayClassifyStatementForForm", "ModernDisplayFormRowGetVisualRole", "MODERN_DISPLAY_FORM_ROW", + "ModernDisplayDrawStatementRowAccents", + "ModernDisplayDrawStatementValueLane", ): if token not in row_surface_body: - raise SmokeFailure(f"DisplayEngine row surface must consume the Phase34 row model/helper: {token}") + raise SmokeFailure(f"DisplayEngine row surface must consume the Phase34/36 row model/helper: {token}") + + internal_text = strip_c_comments(internal_c.read_text(encoding="utf-8")) + for token in ( + "ModernDisplayFormRowAccentColor", + "ModernDisplayFormRowStateChanged", + "ModernDisplayFormRowStateInvalid", + "ModernDisplayFormRowStateDisabled", + "ModernDisplayFormRowStateReadOnly", + "ModernDisplayFormRowIsTextOnly", + "ModernUiStrokeRect", + "ModernDisplayStatementTextInset", + "ModernDisplayDrawRightRailDivider", + "ModernDisplayPageStatusText", + "ModernDisplayDrawStatementValueLane", + "ModernDisplayFooterStatusReservedColumns", + "ModernDisplayPageState", + "ModernDisplayPageStateRebootRequired", + "TextInset", + ): + if token not in internal_text: + raise SmokeFailure(f"Phase36 DisplayEngine row polish missing FormModel-driven accent token: {token}") + + engine_text = strip_c_comments((root / "Library" / "ModernUiEngineLib" / "ModernUiEngineLib.c").read_text(encoding="utf-8")) + for token in ( + "ModernUiEngineStatusColor", + "LIVE REFRESH", + "REBOOT REQUIRED", + "UNSAVED CHANGES", + "MODAL VIEW", + "ModernUiDrawTextFit", + "Time.Second", + ): + if token not in engine_text: + raise SmokeFailure(f"Phase36 DisplayEngine footer status chip missing token: {token}") form_display = root / "Universal" / "ModernDisplayEngineDxe" / "FormDisplay.c" form_display_text = strip_c_comments(form_display.read_text(encoding="utf-8")) @@ -2048,7 +2084,30 @@ def check_phase33_display_form_view_model_boundary(root: Path) -> list[str]: if token in row_surface_body: raise SmokeFailure(f"Phase34 row rendering hook contains prohibited browser/storage token: {token}") - return ["PASS Phase33/34 private DisplayEngine form view-model boundary"] + data_flow_doc = root / "Docs" / "DisplayEngineDynamicDataFlow.md" + data_flow_doc_zh = root / "Docs" / "DisplayEngineDynamicDataFlow.zh-CN.md" + for doc in (data_flow_doc, data_flow_doc_zh): + if not doc.exists(): + raise SmokeFailure(f"missing DisplayEngine dynamic data flow contract: {doc}") + doc_text = doc.read_text(encoding="utf-8") + for token in ( + "PEI", + "DXE", + "ModernSetupApp", + "DisplayEngine", + "REBOOT REQUIRED", + "SetVariable", + "RouteConfig", + ): + if token not in doc_text: + raise SmokeFailure(f"DisplayEngine dynamic data flow contract missing token {token}: {doc}") + + display_renderer_text = "\n".join((internal_text, engine_text)) + for token in ("PciIo", "IoRead", "MmioRead", "PcdSet", "SetVariable", "RouteConfig", "ExtractConfig", "HiiSetBrowserData"): + if token in display_renderer_text: + raise SmokeFailure(f"DisplayEngine renderer must not own hardware/config mutation token: {token}") + + return ["PASS Phase33/34/36 private DisplayEngine form view-model and dynamic data boundary"] def check_modern_ui_builtin_glyph_subset(root: Path) -> list[str]: