xref: /reactos/boot/freeldr/freeldr/ui/tuimenu.c (revision c25a0e19)
1 /*
2  * COPYRIGHT:       See COPYING in the top level directory
3  * PROJECT:         FreeLoader
4  * FILE:            boot/freeldr/freeldr/ui/tuimenu.c
5  * PURPOSE:         Text UI Menu Functions
6  * PROGRAMMERS:     Alex Ionescu (alex@relsoft.net)
7  *                  Brian Palmer (brianp@sginet.com)
8  */
9 
10 /* INCLUDES ******************************************************************/
11 
12 #include <freeldr.h>
13 
14 /* FUNCTIONS *****************************************************************/
15 
16 static VOID
17 TuiCalcMenuBoxSize(
18     _In_ PUI_MENU_INFO MenuInfo);
19 
20 static ULONG
21 TuiProcessMenuKeyboardEvent(
22     _In_ PUI_MENU_INFO MenuInfo,
23     _In_ UiMenuKeyPressFilterCallback KeyPressFilter);
24 
25 static VOID
26 TuiDrawMenuTimeout(
27     _In_ PUI_MENU_INFO MenuInfo);
28 
29 BOOLEAN
TuiDisplayMenu(IN PCSTR MenuHeader,IN PCSTR MenuFooter OPTIONAL,IN BOOLEAN ShowBootOptions,IN PCSTR MenuItemList[],IN ULONG MenuItemCount,IN ULONG DefaultMenuItem,IN LONG MenuTimeOut,OUT PULONG SelectedMenuItem,IN BOOLEAN CanEscape,IN UiMenuKeyPressFilterCallback KeyPressFilter OPTIONAL,IN PVOID Context OPTIONAL)30 TuiDisplayMenu(
31     IN PCSTR MenuHeader,
32     IN PCSTR MenuFooter OPTIONAL,
33     IN BOOLEAN ShowBootOptions,
34     IN PCSTR MenuItemList[],
35     IN ULONG MenuItemCount,
36     IN ULONG DefaultMenuItem,
37     IN LONG MenuTimeOut,
38     OUT PULONG SelectedMenuItem,
39     IN BOOLEAN CanEscape,
40     IN UiMenuKeyPressFilterCallback KeyPressFilter OPTIONAL,
41     IN PVOID Context OPTIONAL)
42 {
43     UI_MENU_INFO MenuInformation;
44     ULONG LastClockSecond;
45     ULONG CurrentClockSecond;
46     ULONG KeyPress;
47 
48     /*
49      * Before taking any default action if there is no timeout,
50      * check whether the supplied key filter callback function
51      * may handle a specific user keypress. If it does, the
52      * timeout is cancelled.
53      */
54     if (!MenuTimeOut && KeyPressFilter && MachConsKbHit())
55     {
56         /* Get the key (get the extended key if needed) */
57         KeyPress = MachConsGetCh();
58         if (KeyPress == KEY_EXTENDED)
59             KeyPress = MachConsGetCh();
60 
61         /*
62          * Call the supplied key filter callback function to see
63          * if it is going to handle this keypress.
64          */
65         if (KeyPressFilter(KeyPress, DefaultMenuItem, Context))
66         {
67             /* It processed the key character, cancel the timeout */
68             MenuTimeOut = -1;
69         }
70     }
71 
72     /* Check if there is no timeout */
73     if (!MenuTimeOut)
74     {
75         /* Return the default selected item */
76         if (SelectedMenuItem) *SelectedMenuItem = DefaultMenuItem;
77         return TRUE;
78     }
79 
80     /* Setup the MENU_INFO structure */
81     MenuInformation.MenuHeader = MenuHeader;
82     MenuInformation.MenuFooter = MenuFooter;
83     MenuInformation.ShowBootOptions = ShowBootOptions;
84     MenuInformation.MenuItemList = MenuItemList;
85     MenuInformation.MenuItemCount = MenuItemCount;
86     MenuInformation.MenuTimeRemaining = MenuTimeOut;
87     MenuInformation.SelectedMenuItem = DefaultMenuItem;
88     MenuInformation.Context = Context;
89 
90     /* Calculate the size of the menu box */
91     TuiCalcMenuBoxSize(&MenuInformation);
92 
93     /* Draw the menu */
94     UiVtbl.DrawMenu(&MenuInformation);
95 
96     /* Get the current second of time */
97     LastClockSecond = ArcGetTime()->Second;
98 
99     /* Process keys */
100     while (TRUE)
101     {
102         /* Process key presses */
103         KeyPress = TuiProcessMenuKeyboardEvent(&MenuInformation, KeyPressFilter);
104 
105         /* Check for ENTER or ESC */
106         if (KeyPress == KEY_ENTER) break;
107         if (CanEscape && KeyPress == KEY_ESC) return FALSE;
108 
109         /* Get the updated time, and check if more than a second has elapsed */
110         CurrentClockSecond = ArcGetTime()->Second;
111         if (CurrentClockSecond != LastClockSecond)
112         {
113             /* Update the time information */
114             LastClockSecond = CurrentClockSecond;
115 
116             // FIXME: Theme-specific
117             /* Update the date & time */
118             TuiUpdateDateTime();
119 
120             /* If there is a countdown, update it */
121             if (MenuInformation.MenuTimeRemaining > 0)
122             {
123                 MenuInformation.MenuTimeRemaining--;
124                 TuiDrawMenuTimeout(&MenuInformation);
125             }
126             else if (MenuInformation.MenuTimeRemaining == 0)
127             {
128                 /* A timeout occurred, exit this loop and return selection */
129                 VideoCopyOffScreenBufferToVRAM();
130                 break;
131             }
132             VideoCopyOffScreenBufferToVRAM();
133         }
134 
135         MachHwIdle();
136     }
137 
138     /* Return the selected item */
139     if (SelectedMenuItem) *SelectedMenuItem = MenuInformation.SelectedMenuItem;
140     return TRUE;
141 }
142 
143 static VOID
TuiCalcMenuBoxSize(_In_ PUI_MENU_INFO MenuInfo)144 TuiCalcMenuBoxSize(
145     _In_ PUI_MENU_INFO MenuInfo)
146 {
147     ULONG i;
148     ULONG Width = 0;
149     ULONG Height;
150     ULONG Length;
151 
152     /* Height is the menu item count plus 2 (top border & bottom border) */
153     Height = MenuInfo->MenuItemCount + 2;
154 
155     /* Loop every item */
156     for (i = 0; i < MenuInfo->MenuItemCount; ++i)
157     {
158         /* Get the string length and make it become the new width if necessary */
159         if (MenuInfo->MenuItemList[i])
160         {
161             Length = (ULONG)strlen(MenuInfo->MenuItemList[i]);
162             Width = max(Width, Length);
163         }
164     }
165 
166     /* Allow room for left & right borders, plus 4 spaces on each side */
167     Width += 10;
168 
169     /* Check if we're drawing a centered menu */
170     if (UiCenterMenu)
171     {
172         /* Calculate the centered menu box area, also ensuring that the top-left
173          * corner is always visible if the borders are partly off-screen */
174         MenuInfo->Left = (UiScreenWidth - min(Width, UiScreenWidth)) / 2;
175         if (Height <= UiScreenHeight - TUI_TITLE_BOX_CHAR_HEIGHT - 1)
176         {
177             /* Exclude the header and the status bar */
178             // MenuInfo->Top = (UiScreenHeight - TUI_TITLE_BOX_CHAR_HEIGHT - 1 - Height) / 2
179             //                 + TUI_TITLE_BOX_CHAR_HEIGHT;
180             MenuInfo->Top = (UiScreenHeight + TUI_TITLE_BOX_CHAR_HEIGHT - 1 - Height) / 2;
181         }
182         else
183         {
184             MenuInfo->Top = (UiScreenHeight - min(Height, UiScreenHeight)) / 2;
185         }
186     }
187     else
188     {
189         /* Put the menu in the default left-corner position */
190         MenuInfo->Left = -1;
191         MenuInfo->Top = 4;
192     }
193 
194     /* The other margins are the same */
195     MenuInfo->Right = MenuInfo->Left + Width - 1;
196     MenuInfo->Bottom = MenuInfo->Top + Height - 1;
197 }
198 
199 VOID
TuiDrawMenu(_In_ PUI_MENU_INFO MenuInfo)200 TuiDrawMenu(
201     _In_ PUI_MENU_INFO MenuInfo)
202 {
203     ULONG i;
204 
205     // FIXME: Theme-specific
206     /* Draw the backdrop */
207     UiDrawBackdrop();
208 
209     /* Draw the menu box */
210     TuiDrawMenuBox(MenuInfo);
211 
212     /* Draw each line of the menu */
213     for (i = 0; i < MenuInfo->MenuItemCount; ++i)
214     {
215         TuiDrawMenuItem(MenuInfo, i);
216     }
217 
218     // FIXME: Theme-specific
219     /* Update the status bar */
220     UiVtbl.DrawStatusText("Use \x18 and \x19 to select, then press ENTER.");
221 
222     /* Display the boot options if needed */
223     if (MenuInfo->ShowBootOptions)
224     {
225         DisplayBootTimeOptions();
226     }
227 
228     VideoCopyOffScreenBufferToVRAM();
229 }
230 
231 static VOID
TuiDrawMenuTimeout(_In_ PUI_MENU_INFO MenuInfo)232 TuiDrawMenuTimeout(
233     _In_ PUI_MENU_INFO MenuInfo)
234 {
235     ULONG Length;
236     CHAR MenuLineText[80];
237 
238     /* If there is a timeout, draw the time remaining */
239     if (MenuInfo->MenuTimeRemaining >= 0)
240     {
241         /* Find whether the time text string is escaped
242          * with %d for specific countdown insertion. */
243         PCHAR ptr = UiTimeText;
244         while ((ptr = strchr(ptr, '%')) && (ptr[1] != 'd'))
245         {
246             /* Ignore any following character (including a following
247              * '%' that would be escaped), thus skip two characters.
248              * If this is the last character, ignore it and stop. */
249             if (*++ptr)
250                 ++ptr;
251         }
252         ASSERT(!ptr || (ptr[0] == '%' && ptr[1] == 'd'));
253 
254         if (ptr)
255         {
256             /* Copy the time text string up to the '%d' insertion point and
257              * skip it, add the remaining time and the rest of the string. */
258             RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
259                                "%.*s%d%s",
260                                ptr - UiTimeText, UiTimeText,
261                                MenuInfo->MenuTimeRemaining,
262                                ptr + 2);
263         }
264         else
265         {
266             /* Copy the time text string, append a separating blank,
267              * and add the remaining time. */
268             RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
269                                "%s %d",
270                                UiTimeText,
271                                MenuInfo->MenuTimeRemaining);
272         }
273 
274         Length = (ULONG)strlen(MenuLineText);
275     }
276     else
277     {
278         /* Erase the timeout with blanks */
279         Length = 0;
280     }
281 
282     /**
283      * How to pad/fill:
284      *
285      *  Center  Box     What to do:
286      *  0       0 or 1  Pad on the right with blanks.
287      *  1       0       Pad on the left with blanks.
288      *  1       1       Pad on the left with blanks + box bottom border.
289      **/
290 
291     if (UiCenterMenu)
292     {
293         /* In boxed menu mode, pad on the left with blanks and box border,
294          * otherwise, pad over all the box length until its right edge. */
295         TuiFillArea(0,
296                     MenuInfo->Bottom,
297                     UiMenuBox
298                         ? MenuInfo->Left - 1 /* Left side of the box bottom */
299                         : MenuInfo->Right,   /* Left side + all box length  */
300                     MenuInfo->Bottom,
301                     UiBackdropFillStyle,
302                     ATTR(UiBackdropFgColor, UiBackdropBgColor));
303 
304         if (UiMenuBox)
305         {
306             /* Fill with box bottom border */
307             TuiDrawBoxBottomLine(MenuInfo->Left,
308                                  MenuInfo->Bottom,
309                                  MenuInfo->Right,
310                                  D_VERT,
311                                  D_HORZ,
312                                  ATTR(UiMenuFgColor, UiMenuBgColor));
313 
314             /* In centered boxed menu mode, the timeout string
315              * does not go past the right border, in principle... */
316         }
317 
318         if (Length > 0)
319         {
320             /* Display the timeout at the bottom-right part of the menu */
321             UiDrawText(MenuInfo->Right - Length - 1,
322                        MenuInfo->Bottom,
323                        MenuLineText,
324                        ATTR(UiMenuFgColor, UiMenuBgColor));
325         }
326     }
327     else
328     {
329         if (Length > 0)
330         {
331             /* Display the timeout under the menu directly */
332             UiDrawText(0,
333                        MenuInfo->Bottom + 4,
334                        MenuLineText,
335                        ATTR(UiMenuFgColor, UiMenuBgColor));
336         }
337 
338         /* Pad on the right with blanks, to erase
339          * characters when the string length decreases. */
340         TuiFillArea(Length,
341                     MenuInfo->Bottom + 4,
342                     Length ? (Length + 1) : (UiScreenWidth - 1),
343                     MenuInfo->Bottom + 4,
344                     UiBackdropFillStyle,
345                     ATTR(UiBackdropFgColor, UiBackdropBgColor)
346                     );
347     }
348 }
349 
350 VOID
TuiDrawMenuBox(_In_ PUI_MENU_INFO MenuInfo)351 TuiDrawMenuBox(
352     _In_ PUI_MENU_INFO MenuInfo)
353 {
354     // FIXME: Theme-specific
355     /* Draw the menu box if requested */
356     if (UiMenuBox)
357     {
358         UiDrawBox(MenuInfo->Left,
359                   MenuInfo->Top,
360                   MenuInfo->Right,
361                   MenuInfo->Bottom,
362                   D_VERT,
363                   D_HORZ,
364                   FALSE,    // Filled
365                   TRUE,     // Shadow
366                   ATTR(UiMenuFgColor, UiMenuBgColor));
367     }
368 
369     /* Update the date & time */
370     TuiUpdateDateTime();
371     TuiDrawMenuTimeout(MenuInfo);
372 }
373 
374 VOID
TuiDrawMenuItem(_In_ PUI_MENU_INFO MenuInfo,_In_ ULONG MenuItemNumber)375 TuiDrawMenuItem(
376     _In_ PUI_MENU_INFO MenuInfo,
377     _In_ ULONG MenuItemNumber)
378 {
379     ULONG SpaceLeft;
380     ULONG SpaceRight;
381     UCHAR Attribute;
382     CHAR MenuLineText[80];
383 
384     /* If this is a separator */
385     if (MenuInfo->MenuItemList[MenuItemNumber] == NULL)
386     {
387         // FIXME: Theme-specific
388         /* Draw its left box corner */
389         if (UiMenuBox)
390         {
391             UiDrawText(MenuInfo->Left,
392                        MenuInfo->Top + 1 + MenuItemNumber,
393                        "\xC7",
394                        ATTR(UiMenuFgColor, UiMenuBgColor));
395         }
396 
397         /* Make it a separator line and use menu colors */
398         RtlZeroMemory(MenuLineText, sizeof(MenuLineText));
399         RtlFillMemory(MenuLineText,
400                       min(sizeof(MenuLineText), (MenuInfo->Right - MenuInfo->Left - 1)),
401                       0xC4);
402 
403         /* Draw the item */
404         UiDrawText(MenuInfo->Left + 1,
405                    MenuInfo->Top + 1 + MenuItemNumber,
406                    MenuLineText,
407                    ATTR(UiMenuFgColor, UiMenuBgColor));
408 
409         // FIXME: Theme-specific
410         /* Draw its right box corner */
411         if (UiMenuBox)
412         {
413             UiDrawText(MenuInfo->Right,
414                        MenuInfo->Top + 1 + MenuItemNumber,
415                        "\xB6",
416                        ATTR(UiMenuFgColor, UiMenuBgColor));
417         }
418 
419         /* We are done */
420         return;
421     }
422 
423     /* This is not a separator */
424     ASSERT(MenuInfo->MenuItemList[MenuItemNumber]);
425 
426     /* Check if using centered menu */
427     if (UiCenterMenu)
428     {
429         /*
430          * We will want the string centered so calculate
431          * how many spaces will be to the left and right.
432          */
433         ULONG SpaceTotal =
434             (MenuInfo->Right - MenuInfo->Left - 2) -
435             (ULONG)strlen(MenuInfo->MenuItemList[MenuItemNumber]);
436         SpaceLeft  = (SpaceTotal / 2) + 1;
437         SpaceRight = (SpaceTotal - SpaceLeft) + 1;
438     }
439     else
440     {
441         /* Simply left-align it */
442         SpaceLeft  = 4;
443         SpaceRight = 0;
444     }
445 
446     /* Format the item text string */
447     RtlStringCbPrintfA(MenuLineText, sizeof(MenuLineText),
448                        "%*s%s%*s",
449                        SpaceLeft, "",   // Left padding
450                        MenuInfo->MenuItemList[MenuItemNumber],
451                        SpaceRight, ""); // Right padding
452 
453     if (MenuItemNumber == MenuInfo->SelectedMenuItem)
454     {
455         /* If this is the selected item, use the selected colors */
456         Attribute = ATTR(UiSelectedTextColor, UiSelectedTextBgColor);
457     }
458     else
459     {
460         /* Normal item colors */
461         Attribute = ATTR(UiTextColor, UiMenuBgColor);
462     }
463 
464     /* Draw the item */
465     UiDrawText(MenuInfo->Left + 1,
466                MenuInfo->Top + 1 + MenuItemNumber,
467                MenuLineText,
468                Attribute);
469 }
470 
471 static ULONG
TuiProcessMenuKeyboardEvent(_In_ PUI_MENU_INFO MenuInfo,_In_ UiMenuKeyPressFilterCallback KeyPressFilter)472 TuiProcessMenuKeyboardEvent(
473     _In_ PUI_MENU_INFO MenuInfo,
474     _In_ UiMenuKeyPressFilterCallback KeyPressFilter)
475 {
476     ULONG KeyEvent = 0;
477     ULONG Selected, Count;
478 
479     /* Check for a keypress */
480     if (!MachConsKbHit())
481         return 0; // None, bail out
482 
483     /* Check if the timeout is not already complete */
484     if (MenuInfo->MenuTimeRemaining != -1)
485     {
486         /* Cancel it and remove it */
487         MenuInfo->MenuTimeRemaining = -1;
488         TuiDrawMenuTimeout(MenuInfo);
489     }
490 
491     /* Get the key (get the extended key if needed) */
492     KeyEvent = MachConsGetCh();
493     if (KeyEvent == KEY_EXTENDED)
494         KeyEvent = MachConsGetCh();
495 
496     /*
497      * Call the supplied key filter callback function to see
498      * if it is going to handle this keypress.
499      */
500     if (KeyPressFilter &&
501         KeyPressFilter(KeyEvent, MenuInfo->SelectedMenuItem, MenuInfo->Context))
502     {
503         /* It processed the key character, so redraw and exit */
504         UiVtbl.DrawMenu(MenuInfo);
505         return 0;
506     }
507 
508     /* Process the key */
509     if ((KeyEvent == KEY_UP  ) || (KeyEvent == KEY_DOWN) ||
510         (KeyEvent == KEY_HOME) || (KeyEvent == KEY_END ))
511     {
512         /* Get the current selected item and count */
513         Selected = MenuInfo->SelectedMenuItem;
514         Count = MenuInfo->MenuItemCount - 1;
515 
516         /* Check the key and change the selected menu item */
517         if ((KeyEvent == KEY_UP) && (Selected > 0))
518         {
519             /* Deselect previous item and go up */
520             MenuInfo->SelectedMenuItem--;
521             TuiDrawMenuItem(MenuInfo, Selected);
522             Selected--;
523 
524             /* Skip past any separators */
525             if ((Selected > 0) &&
526                 (MenuInfo->MenuItemList[Selected] == NULL))
527             {
528                 MenuInfo->SelectedMenuItem--;
529             }
530         }
531         else if ( ((KeyEvent == KEY_UP) && (Selected == 0)) ||
532                    (KeyEvent == KEY_END) )
533         {
534             /* Go to the end */
535             MenuInfo->SelectedMenuItem = Count;
536             TuiDrawMenuItem(MenuInfo, Selected);
537         }
538         else if ((KeyEvent == KEY_DOWN) && (Selected < Count))
539         {
540             /* Deselect previous item and go down */
541             MenuInfo->SelectedMenuItem++;
542             TuiDrawMenuItem(MenuInfo, Selected);
543             Selected++;
544 
545             /* Skip past any separators */
546             if ((Selected < Count) &&
547                 (MenuInfo->MenuItemList[Selected] == NULL))
548             {
549                 MenuInfo->SelectedMenuItem++;
550             }
551         }
552         else if ( ((KeyEvent == KEY_DOWN) && (Selected == Count)) ||
553                    (KeyEvent == KEY_HOME) )
554         {
555             /* Go to the beginning */
556             MenuInfo->SelectedMenuItem = 0;
557             TuiDrawMenuItem(MenuInfo, Selected);
558         }
559 
560         /* Select new item and update video buffer */
561         TuiDrawMenuItem(MenuInfo, MenuInfo->SelectedMenuItem);
562         VideoCopyOffScreenBufferToVRAM();
563     }
564 
565     /* Return the pressed key */
566     return KeyEvent;
567 }
568