diff --git a/Application/ModernSetupApp/ModernSetupApp.c b/Application/ModernSetupApp/ModernSetupApp.c index 3cd62d7..ac49438 100644 --- a/Application/ModernSetupApp/ModernSetupApp.c +++ b/Application/ModernSetupApp/ModernSetupApp.c @@ -92,6 +92,18 @@ 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; + UINTN ListRowHit; + UINTN *ListSelPtr; + BOOLEAN ListRowWasActive; gBS->SetWatchdogTimer (0, 0, 0, NULL); mModernSetupImageHandle = ImageHandle; @@ -136,11 +148,28 @@ 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); + // + // 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) { + ModernSetupMovePointerCursor (&Ui, Theme, PointerX, PointerY); + LastCursorX = PointerX; + LastCursorY = PointerY; + } + Redraw = FALSE; } @@ -195,6 +224,114 @@ 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: 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) || (PointerY != LastCursorY)) { + ModernSetupMovePointerCursor (&Ui, Theme, PointerX, PointerY); + LastCursorX = PointerX; + LastCursorY = PointerY; + } + + 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 if (ModernSetupHitTestPageListRow (&Ui, Page, PointerX, PointerY, &ListRowHit)) { + // + // 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: + ListSelPtr = &BootSelection; + break; + case PageDevices: + ListSelPtr = &DeviceSelection; + break; + case PagePreferences: + ListSelPtr = &PreferencesSelection; + break; + default: + break; + } + + 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; + } + } + 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..bb5fcd4 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; @@ -724,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/ModernSetupAppChrome.c b/Application/ModernSetupApp/ModernSetupAppChrome.c index b2629b1..32b1bd8 100644 --- a/Application/ModernSetupApp/ModernSetupAppChrome.c +++ b/Application/ModernSetupApp/ModernSetupAppChrome.c @@ -165,6 +165,217 @@ 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; +} + +// +// 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; +} + +/** + 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 (clamped to keep the arrow + fully on screen). + @param[in] Y Cursor hotspot Y in pixels (clamped likewise). +**/ +VOID +ModernSetupMovePointerCursor ( + IN MODERN_UI_RENDER_CONTEXT *Ui, + IN CONST MODERN_UI_THEME *Theme, + IN UINTN X, + IN UINTN Y + ) +{ + 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 + // 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 +398,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..55c6df8 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,141 @@ 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 + ); + +/** + Move (or first-draw) the pointer cursor using save-under compositing. + + 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 (clamped on-screen). + @param[in] Y Cursor hotspot Y in pixels (clamped on-screen). +**/ +VOID +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. + + 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 + ); + +/** + 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/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/Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png b/Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png new file mode 100644 index 0000000..4463346 Binary files /dev/null and b/Assets/Screenshots/modern-ovmf-x64-devices-real-hii.png differ 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 0000000..e1b0bf3 Binary files /dev/null and b/Assets/Screenshots/modern-ovmf-x64-secureboot-form.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 066ec11..2c1b8a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,104 @@ this file as both a release log and a lightweight development progress record. ## Unreleased +### 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. 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 + 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 + 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 + 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 + 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 + (`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/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..87adf9b 100644 --- a/Docs/ProductizationValidationMatrix.md +++ b/Docs/ProductizationValidationMatrix.md @@ -49,6 +49,27 @@ 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. | + +| **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. + ## 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. @@ -61,6 +82,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..07ae02d 100644 --- a/Docs/ProductizationValidationMatrix.zh-CN.md +++ b/Docs/ProductizationValidationMatrix.zh-CN.md @@ -49,6 +49,25 @@ 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` -> 重新渲染。 | + +| **完整 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 行证明它如实复现原生行为(包括驱动自有的不持久化)。 + ## Phase32 响应式页面布局矩阵 Phase32(`ModernSetupGetPageListLayout`,`Application/ModernSetupApp/ModernSetupAppActions.c`,已在 `038a156` 落地)让 Boot/Devices/provider 摘要页的列表行高、padding、可见行上限以及 Devices 预览分栏跟随 app 自有的 `DashboardDensity` 偏好和当前内容矩形;绘制与键盘行数共用同一 helper,smoke 固化其 compact/comfortable 分支。 @@ -61,6 +80,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/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/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/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c b/Library/ModernUiLvglRendererLib/ModernUiRendererLib.c index 9b135ca..40f3932 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; +} + +/** + 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. + + @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; + } + + 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; +} + /** - Convert a UCS-2 run to an ASCII label for LVGL's Latin fonts. + 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. - 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). + @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; + } - @param[out] Out Destination ASCII buffer. + 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; } - Out[DstIdx++] = ((Src[SrcIdx] >= 0x20) && (Src[SrcIdx] < 0x7F)) ? (CHAR8)Src[SrcIdx] : '?'; + if ((Ch >= 0x20) && (Ch < 0x7F)) { + Out[DstIdx++] = (CHAR8)Ch; + continue; + } + + 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. @@ -1429,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) + ); +} 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 diff --git a/Scripts/build-ovmf-x64.sh b/Scripts/build-ovmf-x64.sh index 9d4d2e6..ca4ae8b 100755 --- a/Scripts/build-ovmf-x64.sh +++ b/Scripts/build-ovmf-x64.sh @@ -21,6 +21,22 @@ 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}" +# 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" @@ -47,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 @@ -58,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", @@ -120,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. @@ -226,12 +255,46 @@ 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", + ) +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 + # 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" @@ -266,11 +329,19 @@ 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", + ) +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 @@ -279,6 +350,8 @@ 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}" +echo "Video resolution override: ${MODERN_SETUP_VIDEO_RES:-(upstream default)}" if [[ "${GENERATE_ONLY}" == "1" ]]; then exit 0 @@ -295,12 +368,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 """, )