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
LabelWidget.cs
1using System.Drawing;
2using System.Numerics;
7using SkiaSharp;
8
10
14public sealed class LabelWidget : WidgetBase
15{
16 private const int ScrollBarWidth = 12;
17 private const int ScrollBarMargin = 3;
18 private const int MinimumScrollBarThumbHeight = 18;
19
20 private SKColor _foregroundColor = SKColors.White;
21 private SKColor _backgroundColor = SKColors.Transparent;
22 private string _text;
23 private Size _size;
24 private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Never;
25 private int _mouseWheelScrollPixels = 48;
26 private float _verticalScrollOffsetPx;
27 private float _verticalScrollEndPaddingPx;
28 private float _maximumVerticalScrollOffsetPx;
29 private bool _isDraggingScrollBarThumb;
30 private float _scrollBarDragOffset;
31 private int? _scrollBarPointerId;
32
41 public LabelWidget(RenderSurfaceHostBase renderSurfaceHost,
42 View view,
43 Rectangle bounds,
44 string text = "",
45 string? nickname = null)
46 : base(renderSurfaceHost, DirectDrawingMode.View, ValidateBounds(bounds).Location, nickname)
47 {
48 ArgumentNullException.ThrowIfNull(view);
49
50 _text = text ?? string.Empty;
51 _size = bounds.Size;
52 TextBlock = new TextBlock(renderSurfaceHost, view, bounds, $"{Nickname}.text")
53 .SetText(_text)
54 .SetColors(_foregroundColor, _backgroundColor);
55 VerticalScrollBarTrack = CreateScrollBarTrack(renderSurfaceHost, view, bounds);
56 VerticalScrollBarThumb = CreateScrollBarThumb(renderSurfaceHost, view, bounds);
57
58 Add(TextBlock, keepCurrentOffset: false, explicitLocalOffsetPx: Vector2.Zero);
61 CompleteInitialization();
62 }
63
72 public LabelWidget(RenderSurfaceHostBase renderSurfaceHost,
73 SceneLayer sceneLayer,
74 Rectangle bounds,
75 string text = "",
76 string? nickname = null)
77 : base(renderSurfaceHost, DirectDrawingMode.SceneLayer, ValidateBounds(bounds).Location, nickname)
78 {
79 ArgumentNullException.ThrowIfNull(sceneLayer);
80
81 _text = text ?? string.Empty;
82 _size = bounds.Size;
83 TextBlock = new TextBlock(renderSurfaceHost, sceneLayer, view: null, worldBounds: bounds, nickname: $"{Nickname}.text")
84 .SetText(_text)
85 .SetColors(_foregroundColor, _backgroundColor);
86 VerticalScrollBarTrack = CreateScrollBarTrack(renderSurfaceHost, sceneLayer, bounds);
87 VerticalScrollBarThumb = CreateScrollBarThumb(renderSurfaceHost, sceneLayer, bounds);
88
89 Add(TextBlock, keepCurrentOffset: false, explicitLocalOffsetPx: Vector2.Zero);
92 CompleteInitialization();
93 }
94
98 public TextBlock TextBlock { get; }
99
102
105
109 public string Text => _text;
110
114 public Rectangle Bounds
115 {
116 get
117 {
118 Vector2 position = GetPosition();
119 return new Rectangle(
120 (int)MathF.Round(position.X),
121 (int)MathF.Round(position.Y),
122 _size.Width,
123 _size.Height);
124 }
125 }
126
130 public Size Size
131 {
132 get => Bounds.Size;
133 set
134 {
135 ValidateSize(value);
136 _size = value;
137 RefreshScrollState();
138 }
139 }
140
148 {
149 get => _verticalScrollBarVisibility;
150 set
151 {
152 if (!Enum.IsDefined(value))
153 throw new ArgumentOutOfRangeException(nameof(value));
154
155 if (_verticalScrollBarVisibility == value)
156 return;
157
158 _verticalScrollBarVisibility = value;
159 RefreshScrollState();
160 }
161 }
162
164 public bool IsVerticalScrollBarVisible => Visible && NeedsVerticalScrollBar;
165
168 {
169 get => _mouseWheelScrollPixels;
170 set => _mouseWheelScrollPixels = value > 0
171 ? value
172 : throw new ArgumentOutOfRangeException(nameof(value), "Mouse-wheel scrolling must advance at least one pixel.");
173 }
174
184 {
185 get => _verticalScrollEndPaddingPx;
186 set
187 {
188 if (!float.IsFinite(value) || value < 0f)
189 throw new ArgumentOutOfRangeException(nameof(value));
190
191 if (Math.Abs(_verticalScrollEndPaddingPx - value) < 0.001f)
192 return;
193
194 _verticalScrollEndPaddingPx = value;
196 RefreshScrollState();
197 }
198 }
199
202 {
203 get => _verticalScrollOffsetPx;
204 set
205 {
206 if (!float.IsFinite(value) || value < 0f)
207 throw new ArgumentOutOfRangeException(nameof(value));
208
209 SetVerticalScrollOffset(value);
210 }
211 }
212
214 public float MaximumVerticalScrollOffsetPx => _maximumVerticalScrollOffsetPx;
215
221 public LabelWidget SetText(string text)
222 {
223 _text = text ?? string.Empty;
224 TextBlock.SetText(_text);
225 RefreshScrollState();
226 return this;
227 }
228
236 public LabelWidget SetFont(SKTypeface typeface, float size, float? minSize = null)
237 {
238 ArgumentNullException.ThrowIfNull(typeface);
239 TextBlock.SetFont(typeface, size, minSize);
240 RefreshScrollState();
241 return this;
242 }
243
250 public LabelWidget SetColors(SKColor foreground, SKColor background)
251 {
252 _foregroundColor = foreground;
253 _backgroundColor = background;
254 TextBlock.SetColors(_foregroundColor, _backgroundColor);
255 return this;
256 }
257
263 public LabelWidget SetTextColor(SKColor color)
264 {
265 _foregroundColor = color;
266 TextBlock.SetColors(_foregroundColor, _backgroundColor);
267 return this;
268 }
269
275 public LabelWidget SetBackgroundColor(SKColor color)
276 {
277 _backgroundColor = color;
278 TextBlock.SetColors(_foregroundColor, _backgroundColor);
279 return this;
280 }
281
288 public LabelWidget SetAlignment(SKTextAlign horizontal, TextBlock.VerticalAlign vertical)
289 {
290 TextBlock.SetAlignment(horizontal, vertical);
291 return this;
292 }
293
299 public LabelWidget EnableWrapping(bool enabled = true)
300 {
301 TextBlock.EnableWrapping(enabled);
302 RefreshScrollState();
303 return this;
304 }
305
312 public LabelWidget SetPadding(float horizontal, float vertical)
313 {
314 if (!float.IsFinite(horizontal) || horizontal < 0f)
315 throw new ArgumentOutOfRangeException(nameof(horizontal));
316 if (!float.IsFinite(vertical) || vertical < 0f)
317 throw new ArgumentOutOfRangeException(nameof(vertical));
318
319 TextBlock.SetPadding(horizontal, vertical);
320 RefreshScrollState();
321 return this;
322 }
323
327 public LabelWidget SetLabelZOrder(int zOrder)
328 {
329 TextBlock.ZOrder = zOrder;
330 VerticalScrollBarTrack.ZOrder = zOrder + 1;
331 VerticalScrollBarThumb.ZOrder = zOrder + 2;
332 return this;
333 }
334
336 protected override void ProcessShown()
337 {
338 base.ProcessShown();
339 RefreshScrollState();
340 }
341
343 protected override void ProcessHidden()
344 {
345 _isDraggingScrollBarThumb = false;
346 _scrollBarPointerId = null;
347 base.ProcessHidden();
348 }
349
351 protected override void OnPointerDown(WidgetPointerEventArgs args)
352 {
353 base.OnPointerDown(args);
354
355 if (_scrollBarPointerId.HasValue || !args.IsPrimaryButton || !IsVerticalScrollBarVisible)
356 return;
357
358 PointF local = GetLocalPointerPosition(args);
359 Rectangle trackBounds = GetScrollBarTrackBounds();
360 Rectangle thumbBounds = GetScrollBarThumbBounds();
361
362 if (!trackBounds.Contains(Point.Round(local)))
363 return;
364
365 args.Handled = true;
366 _scrollBarPointerId = args.PointerId;
367
368 if (thumbBounds.Contains(Point.Round(local)))
369 {
370 _isDraggingScrollBarThumb = true;
371 _scrollBarDragOffset = local.Y - thumbBounds.Top;
372 return;
373 }
374
375 float page = Math.Max(1f, GetViewportContentHeight());
376 SetVerticalScrollOffset(_verticalScrollOffsetPx + (local.Y < thumbBounds.Top ? -page : page));
377 }
378
380 protected override void OnPointerMove(WidgetPointerEventArgs args)
381 {
382 base.OnPointerMove(args);
383
384 if (!_isDraggingScrollBarThumb ||
385 _scrollBarPointerId != args.PointerId ||
387 {
388 return;
389 }
390
391 Rectangle trackBounds = GetScrollBarTrackBounds();
392 Rectangle thumbBounds = GetScrollBarThumbBounds();
393 float travel = trackBounds.Height - thumbBounds.Height;
394 float maximumScroll = _maximumVerticalScrollOffsetPx;
395
396 if (travel <= 0f || maximumScroll <= 0f)
397 return;
398
399 float desiredTop = GetLocalPointerPosition(args).Y - _scrollBarDragOffset;
400 float fraction = Math.Clamp((desiredTop - trackBounds.Top) / travel, 0f, 1f);
401 SetVerticalScrollOffset(fraction * maximumScroll);
402 args.Handled = true;
403 }
404
406 protected override void OnPointerUp(WidgetPointerEventArgs args)
407 {
408 base.OnPointerUp(args);
409
410 if (_scrollBarPointerId != args.PointerId)
411 return;
412
413 _isDraggingScrollBarThumb = false;
414 _scrollBarPointerId = null;
415 args.Handled = true;
416 }
417
419 protected override void OnMouseWheel(WidgetMouseWheelEventArgs args)
420 {
421 base.OnMouseWheel(args);
422
423 float maximumScroll = _maximumVerticalScrollOffsetPx;
424 if (args.Delta == 0 || maximumScroll <= 0f)
425 return;
426
427 long notches = Math.Max(1, Math.Abs((long)args.Delta) / 120);
428 float change = notches * MouseWheelScrollPixels * (args.Delta > 0 ? -1f : 1f);
429 SetVerticalScrollOffset(_verticalScrollOffsetPx + change);
430 args.Handled = true;
431 }
432
433 private bool NeedsVerticalScrollBar =>
436 _maximumVerticalScrollOffsetPx > 0.5f);
437
438 private void CompleteInitialization()
439 {
440 VerticalScrollBarTrack.Visible = false;
441 VerticalScrollBarThumb.Visible = false;
443 RefreshScrollState();
444 }
445
446 private void RefreshScrollState()
447 {
448 bool scrollingEnabled = VerticalScrollBarVisibility != ScrollBarVisibility.Never;
449
450 Rectangle bounds = Bounds;
451 SetTextBlockBounds(bounds);
452
453 if (!scrollingEnabled)
454 {
455 _verticalScrollOffsetPx = 0f;
457 }
458
459 _maximumVerticalScrollOffsetPx = TextBlock.MeasureMaximumVerticalScrollOffsetPx();
460
461 bool showScrollBar = VerticalScrollBarVisibility == ScrollBarVisibility.Always ||
462 (VerticalScrollBarVisibility == ScrollBarVisibility.Auto &&
463 _maximumVerticalScrollOffsetPx > 0.5f);
464
465 if (showScrollBar)
466 {
467 Rectangle contentBounds = new(
468 bounds.Left,
469 bounds.Top,
470 Math.Max(1, bounds.Width - ScrollBarWidth - ScrollBarMargin * 2),
471 bounds.Height);
472 SetTextBlockBounds(contentBounds);
473 _maximumVerticalScrollOffsetPx = TextBlock.MeasureMaximumVerticalScrollOffsetPx();
474 }
475
476 IsInputEnabled = showScrollBar;
477 IsPointerInputEnabled = showScrollBar;
479 CanReceiveFocus = false;
480
481 SetVerticalScrollOffset(_verticalScrollOffsetPx);
482
483 bool visible = Visible && showScrollBar;
484 VerticalScrollBarTrack.Visible = visible;
485 VerticalScrollBarThumb.Visible = visible;
486
487 if (visible)
488 RefreshScrollBarBounds();
489 }
490
491 private void SetVerticalScrollOffset(float value)
492 {
493 _verticalScrollOffsetPx = Math.Clamp(value, 0f, _maximumVerticalScrollOffsetPx);
494 TextBlock.VerticalScrollOffsetPx = _verticalScrollOffsetPx;
495 RefreshScrollBarBounds();
496 }
497
498 private float GetViewportContentHeight()
499 {
500 Rectangle textBounds = Mode == DirectDrawingMode.View
501 ? TextBlock.ScreenBounds
502 : TextBlock.WorldBounds;
503
504 return Math.Max(0f, textBounds.Height - TextBlock.VerticalPadding * 2f);
505 }
506
507 private void RefreshScrollBarBounds()
508 {
510 return;
511
512 SetRectangleBounds(VerticalScrollBarTrack, GetScrollBarTrackBounds());
513 SetRectangleBounds(VerticalScrollBarThumb, GetScrollBarThumbBounds());
514 }
515
516 private Rectangle GetScrollBarTrackBounds()
517 {
518 return new Rectangle(
519 Bounds.Right - ScrollBarMargin - ScrollBarWidth,
520 Bounds.Top + ScrollBarMargin,
521 ScrollBarWidth,
522 Math.Max(1, Bounds.Height - ScrollBarMargin * 2));
523 }
524
525 private Rectangle GetScrollBarThumbBounds()
526 {
527 Rectangle trackBounds = GetScrollBarTrackBounds();
528 float maximumScroll = _maximumVerticalScrollOffsetPx;
529 float viewportHeight = GetViewportContentHeight();
530
531 if (maximumScroll <= 0f || viewportHeight <= 0f)
532 return trackBounds;
533
534 float contentHeight = viewportHeight + maximumScroll;
535 int thumbHeight = Math.Clamp(
536 (int)MathF.Round(trackBounds.Height * Math.Min(1f, viewportHeight / contentHeight)),
537 Math.Min(MinimumScrollBarThumbHeight, trackBounds.Height),
538 trackBounds.Height);
539 int travel = trackBounds.Height - thumbHeight;
540 int offset = maximumScroll <= 0f
541 ? 0
542 : (int)MathF.Round(travel * (_verticalScrollOffsetPx / maximumScroll));
543
544 return new Rectangle(
545 trackBounds.Left,
546 trackBounds.Top + offset,
547 trackBounds.Width,
548 thumbHeight);
549 }
550
551 private PointF GetLocalPointerPosition(WidgetPointerEventArgs args)
552 {
553 RectangleF screenBounds = TextBlock.GetDrawLocationScreen(args.View);
554 screenBounds.Offset(
555 args.WrappedOffsetWorldPx.X * args.View.Viewport.Zoom,
556 args.WrappedOffsetWorldPx.Y * args.View.Viewport.Zoom);
557
558 Rectangle textBounds = Mode == DirectDrawingMode.View
559 ? TextBlock.ScreenBounds
560 : TextBlock.WorldBounds;
561
562 float scaleX = screenBounds.Width / textBounds.Width;
563 float scaleY = screenBounds.Height / textBounds.Height;
564
565 return new PointF(
566 Bounds.Left + (args.ScreenPositionPx.X - screenBounds.Left) / scaleX,
567 Bounds.Top + (args.ScreenPositionPx.Y - screenBounds.Top) / scaleY);
568 }
569
570 private void SetTextBlockBounds(Rectangle bounds)
571 {
572 // Use TextBlock's size API so width changes invalidate wrapping/layout.
573 TextBlock.SetSize(bounds.Size);
574 TextBlock.SetPosition(new Vector2(bounds.X, bounds.Y));
575
576 Rectangle overallBounds = Bounds;
578 TextBlock,
579 new Vector2(bounds.X - overallBounds.X, bounds.Y - overallBounds.Y));
580 }
581
582 private void SetRectangleBounds(DirectRectangle rectangle, Rectangle bounds)
583 {
584 if (Mode == DirectDrawingMode.View)
585 rectangle.ScreenBounds = bounds;
586 else
587 rectangle.WorldBounds = bounds;
588
590 rectangle,
591 new Vector2(bounds.X - Bounds.X, bounds.Y - Bounds.Y));
592 }
593
594 private static DirectRectangle CreateScrollBarTrack(
595 RenderSurfaceHostBase host,
596 View view,
597 Rectangle bounds)
598 => ConfigureScrollBarTrack(new DirectRectangle(Color.FromArgb(255, 26, 26, 32), host, view, bounds));
599
600 private static DirectRectangle CreateScrollBarTrack(
601 RenderSurfaceHostBase host,
602 SceneLayer sceneLayer,
603 Rectangle bounds)
604 => ConfigureScrollBarTrack(new DirectRectangle(Color.FromArgb(255, 26, 26, 32), host, sceneLayer, bounds));
605
606 private static DirectRectangle ConfigureScrollBarTrack(DirectRectangle track)
607 => track.SetFilled(true).SetStrokeWidth(0f).SetCornerRadius(3f);
608
609 private static DirectRectangle CreateScrollBarThumb(
610 RenderSurfaceHostBase host,
611 View view,
612 Rectangle bounds)
613 => ConfigureScrollBarThumb(new DirectRectangle(Color.FromArgb(255, 126, 126, 142), host, view, bounds));
614
615 private static DirectRectangle CreateScrollBarThumb(
616 RenderSurfaceHostBase host,
617 SceneLayer sceneLayer,
618 Rectangle bounds)
619 => ConfigureScrollBarThumb(new DirectRectangle(Color.FromArgb(255, 126, 126, 142), host, sceneLayer, bounds));
620
621 private static DirectRectangle ConfigureScrollBarThumb(DirectRectangle thumb)
622 => thumb.SetFilled(true).SetStrokeWidth(0f).SetCornerRadius(3f);
623
624 private static Rectangle ValidateBounds(Rectangle bounds)
625 {
626 ValidateSize(bounds.Size);
627 return bounds;
628 }
629
630 private static void ValidateSize(Size size)
631 {
632 if (size.Width <= 0 || size.Height <= 0)
633 throw new ArgumentOutOfRangeException(nameof(size), size, "Label size must be positive.");
634 }
635}
bool Visible
Gets or sets the visibility of all descendants.
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
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.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
int ZOrder
Gets or sets the Z-order (depth) used for sorting this direct drawing relative to other drawables.
Renders a configurable rectangle with support for fill, border, rounded corners, dash patterns,...
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
float VerticalScrollEndPaddingPx
Gets or sets extra scrollable space after the final laid-out line.
Definition TextBlock.cs:241
float MeasureMaximumVerticalScrollOffsetPx()
Measures the maximum vertical scroll offset required to expose all currently laid-out text.
Definition TextBlock.cs:263
float VerticalScrollOffsetPx
Gets or sets the vertical text-content scroll offset in native pixels.
Definition TextBlock.cs:218
TextBlock SetText(string text)
Sets the current text content and rebuilds layout as needed. If no reveal animation is active,...
Definition TextBlock.cs:358
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
float VerticalScrollOffsetPx
Gets or sets the current vertical text-content scroll offset.
bool IsVerticalScrollBarVisible
Gets whether the vertical scrollbar is currently visible.
DirectRectangle VerticalScrollBarTrack
Gets the vertical scrollbar track drawing.
LabelWidget SetText(string text)
Changes the displayed text.
override void OnPointerMove(WidgetPointerEventArgs args)
Called when pointer movement is routed to the widget.
override void OnPointerDown(WidgetPointerEventArgs args)
Called when a pointer press begins on the widget.
float MaximumVerticalScrollOffsetPx
Gets the maximum vertical text-content scroll offset for the current layout.
override void OnPointerUp(WidgetPointerEventArgs args)
Called when a pointer press is released for the widget.
LabelWidget SetTextColor(SKColor color)
Sets the foreground text color while preserving the background color.
LabelWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle bounds, string text="", string? nickname=null)
Creates a scene-layer label.
ScrollBarVisibility VerticalScrollBarVisibility
Gets or sets when the label displays its vertical scrollbar.
override void ProcessShown()
Performs processing when the widget is shown before notification callbacks are invoked.
Rectangle Bounds
Gets the label bounds in its native coordinate space.
override void ProcessHidden()
Performs processing when the widget is hidden before notification callbacks are invoked.
string Text
Gets the current label text.
LabelWidget SetFont(SKTypeface typeface, float size, float? minSize=null)
Configures the label typeface and size.
float VerticalScrollEndPaddingPx
Gets or sets additional space exposed after the last line when scrolled fully down.
LabelWidget SetColors(SKColor foreground, SKColor background)
Sets foreground and background colors.
override void OnMouseWheel(WidgetMouseWheelEventArgs args)
Called when mouse-wheel input is routed to the widget.
DirectRectangle VerticalScrollBarThumb
Gets the vertical scrollbar thumb drawing.
Size Size
Gets or sets the label size while preserving its current position.
LabelWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle bounds, string text="", string? nickname=null)
Creates a view-level label.
TextBlock TextBlock
Gets the underlying text drawing.
LabelWidget EnableWrapping(bool enabled=true)
Enables or disables wrapping.
int MouseWheelScrollPixels
Gets or sets the number of native pixels scrolled for one mouse-wheel notch.
LabelWidget SetPadding(float horizontal, float vertical)
Sets symmetric horizontal and vertical padding.
LabelWidget SetBackgroundColor(SKColor color)
Sets the text-block background color while preserving the foreground color.
LabelWidget SetAlignment(SKTextAlign horizontal, TextBlock.VerticalAlign vertical)
Configures horizontal and vertical text alignment.
LabelWidget SetLabelZOrder(int zOrder)
Sets the base Z-order used by the label text and optional scrollbar.
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.
Event arguments for mouse-wheel input routed to a widget.
int Delta
Gets the wheel delta. Positive values scroll upward; negative values scroll downward.
Event arguments for pointer-based widget callbacks.
bool IsPrimaryButton
Gets whether the pointer event represents a primary-button interaction.
int PointerId
Gets the identifier of the pointer that produced the event.
@ 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.
ScrollBarVisibility
Specifies when a scrollable widget displays its vertical scrollbar.
@ Always
Always displays the scrollbar while the widget is visible.