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
ComboBoxWidget.cs
1using System.Drawing;
2using System.Numerics;
8using SkiaSharp;
9
11
15public sealed class ComboBoxWidget : ContainerWidget
16{
17 private const int DefaultDropDownHeight = 144;
18
19 private readonly DropDownChevronDrawing _chevron;
20 private bool _isDropDownOpen;
21 private bool _disposed;
22 private string _placeholder = "Select...";
23
27 public event Action<int>? SelectedIndexChanged;
28
38 public ComboBoxWidget(RenderSurfaceHostBase renderSurfaceHost,
39 View view,
40 Rectangle bounds,
41 IEnumerable<string>? items = null,
42 int dropDownHeight = DefaultDropDownHeight,
43 string? nickname = null)
44 : base(renderSurfaceHost, DirectDrawingMode.View, ValidateBounds(bounds).Location, nickname)
45 {
46 ArgumentNullException.ThrowIfNull(view);
47 ValidateDropDownHeight(dropDownHeight);
48
49 Header = new ButtonWidget(renderSurfaceHost, view, bounds, string.Empty, $"{Nickname}.header");
50 DropDown = new ListBoxWidget(renderSurfaceHost,
51 view,
52 new Rectangle(bounds.X, bounds.Bottom, bounds.Width, dropDownHeight),
53 items,
54 $"{Nickname}.list");
55 _chevron = new DropDownChevronDrawing(
56 renderSurfaceHost,
57 view,
58 ResolveChevronBounds(bounds),
59 $"{Nickname}.chevron");
60
61 CompleteInitialization(bounds);
62 }
63
73 public ComboBoxWidget(RenderSurfaceHostBase renderSurfaceHost,
74 SceneLayer sceneLayer,
75 Rectangle bounds,
76 IEnumerable<string>? items = null,
77 int dropDownHeight = DefaultDropDownHeight,
78 string? nickname = null)
79 : base(renderSurfaceHost, DirectDrawingMode.SceneLayer, ValidateBounds(bounds).Location, nickname)
80 {
81 ArgumentNullException.ThrowIfNull(sceneLayer);
82 ValidateDropDownHeight(dropDownHeight);
83
84 Header = new ButtonWidget(renderSurfaceHost, sceneLayer, bounds, string.Empty, $"{Nickname}.header");
85 DropDown = new ListBoxWidget(renderSurfaceHost,
86 sceneLayer,
87 new Rectangle(bounds.X, bounds.Bottom, bounds.Width, dropDownHeight),
88 items,
89 $"{Nickname}.list");
90 _chevron = new DropDownChevronDrawing(
91 renderSurfaceHost,
92 sceneLayer,
93 ResolveChevronBounds(bounds),
94 $"{Nickname}.chevron");
95
96 CompleteInitialization(bounds);
97 }
98
102 public ButtonWidget Header { get; }
103
107 public ListBoxWidget DropDown { get; }
108
112 public IReadOnlyList<string> Items => DropDown.Items;
113
117 public bool IsDropDownOpen => _isDropDownOpen;
118
122 public int SelectedIndex
123 {
124 get => DropDown.SelectedIndex;
125 set => DropDown.SelectedIndex = value;
126 }
127
131 public string? SelectedItem => DropDown.SelectedItem;
132
136 public string Placeholder
137 {
138 get => _placeholder;
139 set
140 {
141 _placeholder = value ?? string.Empty;
142 RefreshHeaderText();
143 }
144 }
145
151 public ComboBoxWidget SetItems(IEnumerable<string> items)
152 {
153 DropDown.SetItems(items);
154 RefreshHeaderText();
155 return this;
156 }
157
163 public ComboBoxWidget AddItem(string item)
164 {
165 DropDown.AddItem(item);
166 return this;
167 }
168
174 {
175 if (_isDropDownOpen || Items.Count == 0)
176 return this;
177
178 _isDropDownOpen = true;
179 DropDown.Show();
180 DropDown.Activate();
181 WidgetInputRouterRegistry.TryFocus(DropDown);
182 return this;
183 }
184
190 {
191 CollapseDropDown(restoreHeaderFocus: true);
192 return this;
193 }
194
203
210 {
211 Header.SetButtonZOrder(zOrder);
212 _chevron.ZOrder = zOrder + 2;
213 DropDown.SetListBoxZOrder(zOrder + 10);
214 return this;
215 }
216
223 {
224 Header.SetTextColor(color);
225 _chevron.SetColor(color);
226 return this;
227 }
228
230 public override void Dispose()
231 {
232 if (_disposed)
233 return;
234
235 _disposed = true;
236 Header.Clicked -= OnHeaderClicked;
237 DropDown.SelectedIndexChanged -= OnSelectedIndexChanged;
238 DropDown.SelectionCommitted -= OnDropDownSelectionCommitted;
239 DropDown.FocusLost -= OnDropDownFocusLost;
240 base.Dispose();
241 }
242
244 protected override void ProcessShown()
245 {
246 base.ProcessShown();
247
248 if (!_isDropDownOpen)
249 DropDown.Hide();
250 }
251
253 protected override void ProcessHidden()
254 {
255 _isDropDownOpen = false;
256 base.ProcessHidden();
257 }
258
259 private void CompleteInitialization(Rectangle bounds)
260 {
261 ConfigureHeaderContentBounds(bounds);
262
263 Add(Header, Vector2.Zero);
264 Add(_chevron);
265 Add(DropDown, new Vector2(0f, bounds.Height));
266 Header.Clicked += OnHeaderClicked;
267 DropDown.SelectedIndexChanged += OnSelectedIndexChanged;
268 DropDown.SelectionCommitted += OnDropDownSelectionCommitted;
269 DropDown.FocusLost += OnDropDownFocusLost;
270
271 IsInputEnabled = false;
272 IsPointerInputEnabled = false;
274 CanReceiveFocus = false;
275
276 DropDown.Hide();
278 RefreshHeaderText();
279 }
280
281 private void OnHeaderClicked()
282 {
284 }
285
286 private void OnSelectedIndexChanged(int index)
287 {
288 RefreshHeaderText();
289 SelectedIndexChanged?.Invoke(index);
290 }
291
292 private void OnDropDownSelectionCommitted(int index)
293 {
294 if (index >= 0)
296 }
297
298 private void OnDropDownFocusLost()
299 {
300 if (!_isDropDownOpen)
301 return;
302
303 // Pointer-down moves focus before the header click is dispatched. If focus
304 // moved back to this combo's own header, leave the list open long enough
305 // for that click to toggle it closed normally. Any other focus target means
306 // the user has moved on, so collapse without stealing focus back.
307 WidgetBase? focusedWidget = WidgetInputRouterRegistry.GetFocusedWidget(DropDown);
308 if (ReferenceEquals(focusedWidget, Header) || ReferenceEquals(focusedWidget, DropDown))
309 return;
310
311 CollapseDropDown(restoreHeaderFocus: false);
312 }
313
314 private void CollapseDropDown(bool restoreHeaderFocus)
315 {
316 if (!_isDropDownOpen)
317 return;
318
319 _isDropDownOpen = false;
320 DropDown.Hide();
321
322 if (!restoreHeaderFocus)
323 return;
324
325 Header.Activate();
326 WidgetInputRouterRegistry.TryFocus(Header);
327 }
328
329 private void RefreshHeaderText()
330 {
331 Header.SetText(SelectedItem ?? Placeholder);
332 }
333
334 private void ConfigureHeaderContentBounds(Rectangle bounds)
335 {
336 Rectangle labelBounds = new(
337 bounds.Left + 4,
338 bounds.Top,
339 Math.Max(1, bounds.Width - 28),
340 bounds.Height);
341
342 if (Mode == DirectDrawingMode.View)
343 Header.Label.ScreenBounds = labelBounds;
344 else
345 Header.Label.WorldBounds = labelBounds;
346 }
347
348 private static Rectangle ResolveChevronBounds(Rectangle bounds)
349 {
350 const int width = 18;
351 return new Rectangle(
352 Math.Max(bounds.Left, bounds.Right - width - 4),
353 bounds.Top,
354 Math.Min(width, bounds.Width),
355 bounds.Height);
356 }
357
358 private static Rectangle ValidateBounds(Rectangle bounds)
359 {
360 if (bounds.Width < 60 || bounds.Height < 20)
361 throw new ArgumentOutOfRangeException(nameof(bounds), bounds, "Combo-box bounds are too small.");
362
363 return bounds;
364 }
365
366 private static void ValidateDropDownHeight(int height)
367 {
368 if (height < 28)
369 throw new ArgumentOutOfRangeException(nameof(height), "Drop-down height must be at least 28 pixels.");
370 }
371
372 private sealed class DropDownChevronDrawing : DirectDrawingMovableBase
373 {
374 private SKColor _color = SKColors.White;
375
376 internal DropDownChevronDrawing(
377 RenderSurfaceHostBase host,
378 View view,
379 Rectangle bounds,
380 string nickname)
381 : base(host, DirectDrawingMode.View, null, view, bounds, null, nickname)
382 {
383 }
384
385 internal DropDownChevronDrawing(
386 RenderSurfaceHostBase host,
387 SceneLayer sceneLayer,
388 Rectangle bounds,
389 string nickname)
390 : base(host, DirectDrawingMode.SceneLayer, sceneLayer, null, null, bounds, nickname)
391 {
392 }
393
394 internal void SetColor(Color color)
395 {
396 _color = new SKColor(color.R, color.G, color.B, color.A);
397 ForceRefresh();
398 }
399
401 protected override void OnDraw(BackbufferBase backbuffer, RectangleF destRectScreen)
402 {
403 float width = Math.Min(8f, destRectScreen.Width - 4f);
404 float centerX = destRectScreen.Left + destRectScreen.Width / 2f;
405 float centerY = destRectScreen.Top + destRectScreen.Height / 2f;
406
407 using var paint = new SKPaint
408 {
409 Color = _color,
410 IsAntialias = true,
411 Style = SKPaintStyle.Stroke,
412 StrokeWidth = 1.75f,
413 StrokeCap = SKStrokeCap.Round,
414 StrokeJoin = SKStrokeJoin.Round
415 };
416 using var path = new SKPath();
417
418 path.MoveTo(centerX - width / 2f, centerY - 2f);
419 path.LineTo(centerX, centerY + 2f);
420 path.LineTo(centerX + width / 2f, centerY - 2f);
421
422 backbuffer.Canvas.DrawPath(path, paint);
423 }
424 }
425}
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
View? View
Gets the view shared by all children in view mode.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
View? View
Gets the view to which this direct drawing is attached, or null if the drawing is in scene-layer mode...
SceneLayer? SceneLayer
Gets the scene layer to which this direct drawing is attached, or null if the drawing is in view mode...
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
override DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.The current composite.
ContainerWidget(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the ContainerWidget class.
Represents a basic text button composed from a rectangle and text block.
string Placeholder
Gets or sets the text displayed while no item is selected.
ComboBoxWidget SetHeaderTextColor(Color color)
Sets the collapsed header text and chevron color.
string? SelectedItem
Gets the selected item text, or null.
override void ProcessHidden()
Performs processing when the widget is hidden before notification callbacks are invoked.
ComboBoxWidget AddItem(string item)
Adds an item to the end of the combo box.
ComboBoxWidget SetItems(IEnumerable< string > items)
Replaces all available items and clears the current selection.
ButtonWidget Header
Gets the collapsed button portion of the combo box.
bool IsDropDownOpen
Gets whether the drop-down list is currently open.
ListBoxWidget DropDown
Gets the list displayed while the combo box is expanded.
IReadOnlyList< string > Items
Gets the available items.
ComboBoxWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle bounds, IEnumerable< string >? items=null, int dropDownHeight=DefaultDropDownHeight, string? nickname=null)
Creates a view-level combo box.
override void ProcessShown()
Performs processing when the widget is shown before notification callbacks are invoked.
ComboBoxWidget CloseDropDown()
Closes the drop-down list.
ComboBoxWidget ToggleDropDown()
Toggles the drop-down list.
override void Dispose()
Disposes the composite and every child it owns.
Action< int >? SelectedIndexChanged
Occurs when SelectedIndex changes.
ComboBoxWidget SetComboBoxZOrder(int zOrder)
Sets the base Z-order used by the combo-box visuals.
ComboBoxWidget OpenDropDown()
Opens the drop-down list when items are available.
ComboBoxWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle bounds, IEnumerable< string >? items=null, int dropDownHeight=DefaultDropDownHeight, string? nickname=null)
Creates a scene-layer combo box.
int SelectedIndex
Gets or sets the selected item index. Use -1 to clear selection.
Provides a selectable, vertically scrolling list of text items.
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
WidgetBase Hide()
Hides the widget and releases any routed input state.
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.
@ 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.