15 private readonly
View _view;
16 private readonly Rectangle _bounds;
18 private readonly DismissLayerWidget _dismissLayer;
19 private readonly List<MenuBarMenu> _menus =
new();
21 private readonly Dictionary<string, MenuItemWidget> _itemsByKey =
new(StringComparer.Ordinal);
22 private readonly Dictionary<KeyGesture, MenuItemWidget> _shortcuts =
new();
37 _itemsByKey.TryGetValue(key, out item);
39 internal void ValidateRegistration(
string? key,
KeyGesture? shortcut)
41 ObjectDisposedException.ThrowIf(_disposed,
this);
44 ArgumentException.ThrowIfNullOrWhiteSpace(key);
45 if (_itemsByKey.ContainsKey(key))
throw new ArgumentException($
"Duplicate menu item key '{key}'.", nameof(key));
47 if (shortcut is { } gesture)
49 if (gesture.Key <= 0)
throw new ArgumentException(
"A shortcut requires a nonzero key.", nameof(shortcut));
50 if (_shortcuts.ContainsKey(gesture))
throw new ArgumentException($
"Duplicate menu shortcut '{gesture}'.", nameof(shortcut));
56 if (item.
Key is not
null) _itemsByKey.Add(item.
Key, item);
57 if (item.
Shortcut is { } gesture) _shortcuts.Add(gesture, item);
60 internal void UnregisterItem(MenuItemWidget item)
62 if (item.Key is not
null && _itemsByKey.GetValueOrDefault(item.Key) == item) _itemsByKey.Remove(item.Key);
63 if (item.Shortcut is { } gesture && _shortcuts.GetValueOrDefault(gesture) == item) _shortcuts.Remove(gesture);
67 private int _openMenuIndex = -1;
68 private int _nextHeaderX;
69 private int _menuZOrder = 20_000;
70 private bool _disposed;
93 string? nickname =
null)
96 ArgumentNullException.ThrowIfNull(view);
98 if (bounds.Width <= 0 || bounds.Height <= 0)
100 throw new ArgumentOutOfRangeException(
103 "Menu bar bounds must have positive width and height.");
109 _nextHeaderX = bounds.X;
112 _theme.BarBackgroundColor,
116 $
"{Nickname}.background")
122 _dismissLayer =
new DismissLayerWidget(
126 $
"{Nickname}.dismiss");
128 _dismissLayer.DismissRequested += OnDismissRequested;
129 _dismissLayer.SetDismissEnabled(
false);
134 dismissBounds.X - bounds.X,
135 dismissBounds.Y - bounds.Y));
150 public IReadOnlyList<MenuBarMenu>
Menus => _menus.AsReadOnly();
157 _openMenuIndex >= 0 && _openMenuIndex < _menus.Count
158 ? _menus[_openMenuIndex]
177 Action<MenuDropDownWidget>? configure =
null,
178 int? headerWidth =
null,
179 char? mnemonic =
null)
181 ObjectDisposedException.ThrowIf(_disposed,
this);
182 ArgumentNullException.ThrowIfNull(text);
183 if (mnemonic is
char character && _menus.Any(menu => menu.Header.Mnemonic is
char existing &&
184 char.ToUpperInvariant(existing) ==
char.ToUpperInvariant(character)))
185 throw new ArgumentException($
"Duplicate top-level mnemonic '{character}'.", nameof(mnemonic));
187 int resolvedHeaderWidth = headerWidth ?? EstimateHeaderWidth(text);
189 if (resolvedHeaderWidth <= 0)
191 throw new ArgumentOutOfRangeException(
194 "Menu header width must be positive.");
197 var headerBounds =
new Rectangle(
203 int menuIndex = _menus.Count;
211 $
"{Nickname}.header.{menuIndex}", mnemonic);
216 new Point(headerBounds.X, _bounds.Bottom),
218 $
"{Nickname}.dropdown.{menuIndex}",
this);
222 configure?.Invoke(dropDown);
230 PositionDropDown(headerBounds, dropDown);
235 header.Invoked += OnHeaderInvoked;
236 header.Hovered += OnHeaderHovered;
241 headerBounds.X - _bounds.X,
242 headerBounds.Y - _bounds.Y));
247 dropDown.GetPosition().X - _bounds.X,
248 dropDown.GetPosition().Y - _bounds.Y));
253 _nextHeaderX += resolvedHeaderWidth;
254 ApplyMenuZOrder(_menuZOrder);
263 if (index < 0 || index >= _menus.Count)
264 throw new ArgumentOutOfRangeException(nameof(index));
266 if (!IsAvailable || _openMenuIndex == index)
269 CloseMenuInternal(immediate:
true);
271 _previousFocus = WidgetInputRouterRegistry.GetFocusedWidget(
this);
272 _openMenuIndex = index;
275 menu.
Header.SetOpen(
true);
278 candidate.
Header.SetMenuBarActive(
true);
280 _dismissLayer.SetDismissEnabled(
true);
284 WidgetInputRouterRegistry.TryBringToFront(
this);
285 _dismissLayer.Activate();
290 PositionDropDown(menu.Header.Background.ScreenBounds, menu.DropDown);
291 WidgetInputRouterRegistry.TryFocus(menu.Header);
293 menu.DropDown.OpenAnimated(
297 menu.DropDown.Activate();
298 menu.DropDown.SelectFirstEnabled();
306 CloseMenuInternal(immediate:
false);
313 if (_openMenuIndex == index)
324 _menuZOrder = zOrder;
325 ApplyMenuZOrder(_menuZOrder);
334 _dismissLayer.SetDismissEnabled(
false);
339 menu.
Header.SetOpen(
false);
340 menu.
Header.SetMenuBarActive(
false);
348 CloseMenuInternal(immediate:
true);
349 _dismissLayer.SetDismissEnabled(
false);
350 base.ProcessHidden();
356 base.OnKeyboardInput(args);
357 HandleKeyboard(args);
360 void IWidgetKeyboardFallback.HandleUnhandledKeyboardInput(
WidgetKeyboardEventArgs args) => HandleKeyboard(args);
365 if (args.
Key > 0 && _shortcuts.TryGetValue(
new KeyGesture(args.
Key, args.
Modifiers), out var command) && command.CanInvoke)
368 command.PerformClick();
373 int index = _menus.FindIndex(menu => MatchesMnemonic(menu.
Header.
Mnemonic, args.
Key));
378 _menus[index].DropDown.SelectFirstEnabled();
383 var dropDown = _menus[_openMenuIndex].DropDown.ActiveMenu;
387 if (dropDown.ParentMenu is { } parent) parent.CloseChild();
391 if (dropDown.ParentMenu is { } previous) previous.CloseChild();
392 else OpenMenuAt((_openMenuIndex - 1 + _menus.Count) % _menus.Count);
395 if (dropDown.SelectedItem is { SubMenu: not null } owner)
396 dropDown.OpenSubMenu(owner, selectFirst:
true);
397 else if (dropDown.ParentMenu is
null)
398 OpenMenuAt((_openMenuIndex + 1) % _menus.Count);
400 case 38: dropDown.SelectPreviousEnabled();
break;
401 case 40: dropDown.SelectNextEnabled();
break;
403 case 32: dropDown.InvokeSelectedItem();
break;
405 var item = dropDown.Items.FirstOrDefault(item => item.CanInvoke && MatchesMnemonic(item.Mnemonic, args.
Key));
406 if (item is
null)
return;
413 private static bool MatchesMnemonic(
char? mnemonic,
int key) => mnemonic is
char character &&
414 key <= char.MaxValue && key >= 0 &&
char.ToUpperInvariant(character) ==
char.ToUpperInvariant((
char)key);
423 CloseMenuInternal(immediate:
true);
425 _dismissLayer.DismissRequested -= OnDismissRequested;
429 menu.Header.Invoked -= OnHeaderInvoked;
430 menu.Header.Hovered -= OnHeaderHovered;
441 int index = _menus.FindIndex(menu => ReferenceEquals(menu.
Header, header));
447 private void OnHeaderHovered(MenuHeaderWidget header)
449 if (_openMenuIndex < 0)
452 int index = _menus.FindIndex(menu => ReferenceEquals(menu.
Header, header));
454 if (index >= 0 && index != _openMenuIndex)
458 internal void OnItemInvoked(MenuItemWidget item)
460 CloseMenuInternal(immediate:
true);
464 private void OnDismissRequested()
469 private void CloseMenuInternal(
bool immediate)
471 if (_openMenuIndex < 0 || _openMenuIndex >= _menus.Count)
473 _dismissLayer.SetDismissEnabled(
false);
478 MenuBarMenu menu = _menus[_openMenuIndex];
481 menu.
Header.SetOpen(
false);
483 foreach (MenuBarMenu candidate
in _menus)
484 candidate.Header.SetMenuBarActive(
false);
486 _dismissLayer.SetDismissEnabled(
false);
488 menu.DropDown.CloseAnimated(
493 var focused = WidgetInputRouterRegistry.GetFocusedWidget(
this);
494 if (focused is
null || ContainsWidget(
this, focused))
495 WidgetInputRouterRegistry.RestoreFocus(
this, _previousFocus);
496 _previousFocus =
null;
501 container.ChildWidgets.Any(child => ReferenceEquals(child, widget) ||
504 private void PositionDropDown(Rectangle headerBounds,
505 MenuDropDownWidget dropDown)
507 Rectangle viewport = _view.Viewport.TargetRectPx;
511 Math.Max(viewport.Left, viewport.Right - dropDown.Width));
513 int y = Math.Clamp(_bounds.Bottom, viewport.Top, Math.Max(viewport.Top, viewport.Bottom - dropDown.Height));
517 dropDown.SetPosition(x, y);
520 private int EstimateHeaderWidth(
string text)
522 int estimatedTextWidth = (int)Math.Ceiling(
523 text.Length * _theme.EstimatedGlyphWidth);
526 _theme.MinimumHeaderWidth,
527 estimatedTextWidth + (_theme.HeaderHorizontalPadding * 2));
530 private void ApplyMenuZOrder(
int zOrder)
532 _dismissLayer.SetLayerZOrder(zOrder);
533 BarBackground.ZOrder = zOrder + 1;
535 foreach (MenuBarMenu menu
in _menus)
537 menu.Header.SetHeaderZOrder(zOrder + 2);
538 menu.DropDown.SetDropDownZOrder(zOrder + 10);
542 private sealed class DismissLayerWidget :
WidgetBase
544 internal event Action? DismissRequested;
546 internal DismissLayerWidget(RenderSurfaceHostBase host,
552 HitArea =
new DirectRectangle(
557 $
"{Nickname}.hit-area")
567 internal DirectRectangle HitArea {
get; }
569 internal void SetDismissEnabled(
bool enabled)
580 internal void SetLayerZOrder(
int zOrder)
582 HitArea.ZOrder = zOrder;
586 protected override void OnPointerClick(WidgetPointerEventArgs args)
588 base.OnPointerClick(args);
590 if (!args.IsPrimaryButton)
594 DismissRequested?.Invoke();