Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
MenuItemWidget.cs
1using System.Drawing;
2using System.Numerics;
7using SkiaSharp;
8
10
12public sealed class MenuItemWidget : WidgetBase
13{
14 private readonly MenuBarTheme _theme;
15 private readonly MenuDropDownWidget _owner;
16 private readonly Action? _action;
17 private readonly Action<bool>? _checkedAction;
18 private readonly MenuIndicatorDrawing _marker;
19 private readonly MenuIndicatorDrawing _arrow;
20 private bool _disposed;
21 private bool _isPointerOver;
22 private bool _isSelected;
23 private bool _isEnabled = true;
24
25 internal event Action<MenuItemWidget>? Hovered;
27 public event Action<MenuItemWidget>? Invoked;
28
29 internal MenuItemWidget(RenderSurfaceHostBase host, View view, Rectangle bounds,
30 string text, string? shortcutText, Action? action, MenuBarTheme theme,
31 string nickname, MenuDropDownWidget owner, string? key, KeyGesture? shortcut,
32 char? mnemonic, SKImage? icon, bool checkable, string? radioGroup,
33 Action<bool>? checkedAction, MenuDropDownWidget? subMenu)
34 : base(host, DirectDrawingMode.View, bounds.Location, nickname)
35 {
36 _theme = theme;
37 _owner = owner;
38 _action = action;
39 _checkedAction = checkedAction;
40 Text = text;
41 Key = key;
42 Shortcut = shortcut;
43 ShortcutText = shortcut?.ToString() ?? shortcutText ?? string.Empty;
44 Mnemonic = mnemonic;
45 Icon = icon;
46 IsCheckable = checkable;
47 RadioGroup = radioGroup;
48 SubMenu = subMenu;
49 Background = new DirectRectangle(theme.ItemNormalColor, host, view, bounds, $"{Nickname}.background")
50 .SetFilled(true).SetStrokeWidth(0f);
51 Label = CreateText(host, view, bounds, "label", SKTextAlign.Left).SetText(Text);
52 ShortcutLabel = CreateText(host, view, bounds, "shortcut", SKTextAlign.Right).SetText(ShortcutText);
53 _marker = new MenuIndicatorDrawing(host, view, bounds,
54 radioGroup is null ? MenuIndicatorDrawing.Shape.Check : MenuIndicatorDrawing.Shape.Radio,
55 theme, $"{Nickname}.marker");
56 _arrow = new MenuIndicatorDrawing(host, view, bounds, MenuIndicatorDrawing.Shape.Arrow,
57 theme, $"{Nickname}.arrow");
59 Add(Label);
61 Add(Marker);
62 Add(Arrow);
63 if (icon is not null)
64 {
65 IconDrawing = new DirectImage(icon, host, view, bounds, $"{Nickname}.icon")
68 }
69 CanReceiveFocus = true;
72 UpdateVisualState();
73 }
74
76 public string? Key { get; }
78 public string Text { get; }
80 public KeyGesture? Shortcut { get; }
82 public string ShortcutText { get; }
84 public char? Mnemonic { get; }
86 public SKImage? Icon { get; }
88 public DirectImage? IconDrawing { get; }
90 public bool IsCheckable { get; }
92 public bool IsChecked { get; private set; }
94 public string? RadioGroup { get; }
96 public MenuDropDownWidget? SubMenu { get; }
100 public TextBlock Label { get; }
102 public TextBlock ShortcutLabel { get; }
108 public bool IsEnabled => _isEnabled;
110 public bool IsSelected => _isSelected;
111 internal bool CanInvoke => !_disposed && _isEnabled && _owner.CanInvoke;
112
116 public MenuItemWidget SetEnabled(bool enabled)
117 {
118 ObjectDisposedException.ThrowIf(_disposed, this);
119 _isEnabled = enabled;
120 ApplyInputState(_owner.IsOpen);
121 if (!enabled) _owner.OnItemDisabled(this);
122 UpdateVisualState();
123 return this;
124 }
125
129 public MenuItemWidget SetChecked(bool isChecked)
130 {
131 ObjectDisposedException.ThrowIf(_disposed, this);
132 if (!IsCheckable) throw new InvalidOperationException("This menu item is not checkable.");
133 if (isChecked && RadioGroup is not null) _owner.UncheckRadioPeers(this);
134 IsChecked = isChecked;
135 UpdateVisualState();
136 return this;
137 }
138
141 public void PerformClick()
142 {
143 if (!CanInvoke) return;
144 if (SubMenu is not null)
145 {
146 _owner.OpenSubMenu(this, selectFirst: true);
147 return;
148 }
149 if (IsCheckable) SetChecked(RadioGroup is not null || !IsChecked);
150 bool checkedState = IsChecked;
151 _owner.NotifyInvoked(this);
152 Invoked?.Invoke(this);
153 _checkedAction?.Invoke(checkedState);
154 _action?.Invoke();
155 }
156
157 internal void SetSelected(bool selected)
158 {
159 _isSelected = selected;
160 if (!selected) _isPointerOver = false;
161 UpdateVisualState();
162 }
163
164 internal void ApplyInputState(bool menuAcceptsInput)
165 {
166 bool enabled = menuAcceptsInput && _isEnabled && !_disposed;
167 IsInputEnabled = enabled;
168 IsPointerInputEnabled = enabled;
169 IsKeyboardInputEnabled = enabled;
170 CanReceiveFocus = enabled;
171 }
172
173 internal void SetItemZOrder(int zOrder)
174 {
175 Background.ZOrder = zOrder;
176 foreach (var drawing in new DirectDrawingBase[] { Label, ShortcutLabel, Marker, Arrow })
177 drawing.ZOrder = zOrder + 1;
178 if (IconDrawing is not null) IconDrawing.ZOrder = zOrder + 1;
179 }
180
181 internal void SetBounds(Rectangle bounds, int markerWidth, int iconWidth, int shortcutWidth, int arrowWidth)
182 {
183 SetPosition(bounds.X, bounds.Y);
184 Background.ScreenBounds = bounds;
185 int x = bounds.X + _theme.ItemHorizontalPadding;
186 Place(Marker, new Rectangle(x, bounds.Y, Math.Max(1, markerWidth), bounds.Height));
187 x += markerWidth;
188 if (IconDrawing is not null)
189 Place(IconDrawing, new Rectangle(x, bounds.Y + (bounds.Height - _theme.IconSize) / 2, _theme.IconSize, _theme.IconSize));
190 x += iconWidth;
191 int right = bounds.Right - _theme.ItemHorizontalPadding;
192 Place(Arrow, new Rectangle(right - arrowWidth, bounds.Y, Math.Max(1, arrowWidth), bounds.Height));
193 right -= arrowWidth;
194 Place(ShortcutLabel, new Rectangle(right - shortcutWidth, bounds.Y, Math.Max(1, shortcutWidth), bounds.Height));
195 if (shortcutWidth > 0) right -= shortcutWidth + _theme.ShortcutGap;
196 Place(Label, new Rectangle(x, bounds.Y, Math.Max(1, right - x), bounds.Height));
197 Label.HorizontalPadding = ShortcutLabel.HorizontalPadding = 0;
198 }
199
200 private void Place(DirectDrawingBase drawing, Rectangle bounds)
201 {
202 SetLocalOffset((IDirectCompositeChild)drawing, new Vector2(bounds.X - GetPosition().X, bounds.Y - GetPosition().Y));
203 drawing.ScreenBounds = bounds;
204 }
205
206 private TextBlock CreateText(RenderSurfaceHostBase host, View view, Rectangle bounds, string name, SKTextAlign alignment) =>
207 new TextBlock(host, view, bounds, $"{Nickname}.{name}")
208 .SetFont(SKTypeface.Default, _theme.FontSize, _theme.MinimumFontSize)
209 .SetAlignment(alignment, TextBlock.VerticalAlign.Center).EnableWrapping(false);
210
212 public override void Dispose()
213 {
214 if (_disposed) return;
215 _disposed = true;
216 _owner.UnregisterItem(this);
217 Hovered = null;
218 Invoked = null;
219 base.Dispose();
220 }
221
223 protected override void OnPointerEnter(WidgetPointerEventArgs args)
224 {
225 base.OnPointerEnter(args);
226 if (!_isEnabled) return;
227 _isPointerOver = true;
228 Hovered?.Invoke(this);
229 UpdateVisualState();
230 }
231
233 protected override void OnPointerLeave(WidgetPointerEventArgs args)
234 {
235 base.OnPointerLeave(args);
236 _isPointerOver = false;
237 UpdateVisualState();
238 }
239
241 protected override void OnPointerDown(WidgetPointerEventArgs args)
242 {
243 base.OnPointerDown(args);
244 if (_isEnabled && args.IsPrimaryButton) SetBackgroundColor(_theme.ItemPressedColor);
245 }
246
248 protected override void OnPointerUp(WidgetPointerEventArgs args)
249 {
250 base.OnPointerUp(args);
251 UpdateVisualState();
252 }
253
255 protected override void OnPointerClick(WidgetPointerEventArgs args)
256 {
257 base.OnPointerClick(args);
258 if (!_isEnabled || !args.IsPrimaryButton) return;
259 args.Handled = true;
260 PerformClick();
261 }
262
264 protected override void OnKeyboardInput(WidgetKeyboardEventArgs args)
265 {
266 base.OnKeyboardInput(args);
267 // Open-menu navigation bubbles to the bar, which knows the active depth.
268 if (_owner.IsOpen || args.Handled || args.KeyAction != KeyAction.Pressed || args.Key is not 13 and not 32) return;
269 if (!CanInvoke) return;
270 args.Handled = true;
271 PerformClick();
272 }
273
274 private void UpdateVisualState()
275 {
276 SetBackgroundColor(_isEnabled && (_isPointerOver || _isSelected) ? _theme.ItemHoverColor : _theme.ItemNormalColor);
277 Label.SetColors(ToSkColor(_isEnabled ? _theme.TextColor : _theme.DisabledTextColor), SKColors.Transparent);
278 ShortcutLabel.SetColors(ToSkColor(_isEnabled ? _theme.ShortcutTextColor : _theme.DisabledTextColor), SKColors.Transparent);
279 var indicator = ToSkColor(_isEnabled ? _theme.IndicatorColor : _theme.DisabledTextColor);
280 _marker.SetState(IsChecked, indicator);
281 _arrow.SetState(SubMenu is not null, indicator);
282 IconDrawing?.SetTint(_isEnabled ? SKColors.White : ToSkColor(_theme.DisabledTextColor));
283 }
284
285 private void SetBackgroundColor(Color color)
286 {
287 Background.SetColor(color);
289 }
290
291 private static SKColor ToSkColor(Color color) => new(color.R, color.G, color.B, color.A);
292}
DirectComposite SetLocalOffset(IDirectCompositeChild child, Vector2 newLocalOffsetPx)
Changes one child's local offset and reapplies its position.
View? View
Gets the view shared by all children in view mode.
Vector2 GetPosition()
Gets the current composite anchor.
virtual DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.
void SetPosition(Vector2 position)
Sets the composite anchor and repositions all children from their stored offsets.
Provides the base implementation for direct drawing objects that can be rendered to the backbuffer wi...
Provides an abstract base class for direct drawings that support physics-based movement.
Vector2 GetPosition()
Gets the current position of this direct drawing in pixel coordinates.
void SetPosition(Vector2 p)
Sets the position of this direct drawing to the specified pixel coordinates.
Renders a bitmap or image to a rectangular region with support for scaling modes, source clipping,...
DirectImage SetScaleMode(ScaleMode mode)
Sets how the image is scaled to fit within the destination rectangle.
DirectImage SetTint(SKColor color)
Applies a color tint (multiplicative blend) to the image.
ScaleMode
Defines how an image is scaled to fit within the destination rectangle.
Renders a configurable rectangle with support for fill, border, rounded corners, dash patterns,...
DirectRectangle SetFilled(bool isFilled)
Sets whether the rectangle interior is filled with the base color (or pattern).
DirectRectangle SetColor(Color color)
Sets the base color used for both fill and stroke (unless a distinct border color is specified).
DirectRectangle SetStrokeWidth(float width)
Sets the width (thickness) of the rectangle's stroke (outline) in pixels.
A retained-mode text drawable with wrapping, multi-line ( ) support, horizontal/vertical alignment,...
Definition TextBlock.cs:44
TextBlock SetColors(SKColor fg, SKColor bg)
Sets the foreground (text) and background colors using Skia colors.
Definition TextBlock.cs:410
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Represents a popup command list owned by a header or submenu item.
override void OnPointerDown(WidgetPointerEventArgs args)
Called when a pointer press begins on the widget.
char? Mnemonic
Gets the explicit, case-insensitive access character.
DirectDrawingMovableBase Marker
Gets the check/radio indicator drawing.
TextBlock ShortcutLabel
Gets the shortcut drawing.
string Text
Gets the command label.
bool IsCheckable
Gets whether this item has check or radio state.
override void OnPointerLeave(WidgetPointerEventArgs args)
Called when a pointer leaves the widget bounds.
MenuItemWidget SetEnabled(bool enabled)
Enables or disables the item.
override void OnPointerClick(WidgetPointerEventArgs args)
Called when a pointer click is dispatched to the widget.
override void OnPointerEnter(WidgetPointerEventArgs args)
Called when a pointer enters the widget bounds.
DirectRectangle Background
Gets the background drawing.
string? RadioGroup
Gets the radio group, scoped to the containing dropdown.
void PerformClick()
Invokes an enabled command, even while its dropdown is closed, or opens its submenu.
KeyGesture? Shortcut
Gets the executable gesture, if any.
Action< MenuItemWidget >? Invoked
Occurs after state changes and hierarchy closure, before the command callback.
override void OnKeyboardInput(WidgetKeyboardEventArgs args)
Called when keyboard input is routed to the widget.
bool IsEnabled
Gets whether this item is enabled.
DirectImage? IconDrawing
Gets the retained icon drawing, if configured.
bool IsChecked
Gets the current checked state.
string ShortcutText
Gets gesture-generated text, or the legacy display-only shortcut text.
MenuDropDownWidget? SubMenu
Gets the child dropdown, if this item owns a submenu.
override void Dispose()
Disposes the composite and every child it owns.
bool IsSelected
Gets whether this item is selected by pointer or keyboard.
SKImage? Icon
Gets the caller-owned image. Keep it alive until the item is disposed.
DirectDrawingMovableBase Arrow
Gets the submenu arrow drawing.
MenuItemWidget SetChecked(bool isChecked)
Changes state without invoking callbacks. Checking a radio item clears its group peers.
string? Key
Gets the immutable optional key, unique within the menu bar.
override void OnPointerUp(WidgetPointerEventArgs args)
Called when a pointer press is released for the widget.
TextBlock Label
Gets the label drawing.
bool CanReceiveFocus
Gets or sets a value indicating whether the widget can receive keyboard focus.
bool IsPointerInputEnabled
Gets or sets a value indicating whether the widget can receive pointer input.
WidgetBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the WidgetBase class.
Definition WidgetBase.cs:95
bool IsKeyboardInputEnabled
Gets or sets a value indicating whether the widget can receive keyboard input.
bool IsInputEnabled
Gets or sets a value indicating whether the widget can participate in input routing.
bool Handled
Gets or sets whether this widget interaction was handled.
Provides data for widget keyboard input events routed by WidgetInputRouter.
int Key
Gets the integer key identifier.
KeyAction KeyAction
Gets the action associated with this key event.
Event arguments for pointer-based widget callbacks.
bool IsPrimaryButton
Gets whether the pointer event represents a primary-button interaction.
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
KeyAction
Represents the type of action performed on a keyboard key, indicating whether the key was pressed,...
Definition KeyAction.cs:10
record MenuBarTheme
Defines the dimensions and colors used by menu bar widgets.
A host key code and an exact combination of keyboard modifiers.
Definition KeyGesture.cs:9
override string ToString()
Returns a display label such as Ctrl+O or Alt+F4.
Definition KeyGesture.cs:37