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
PopupWidget.cs
1using System.Drawing;
2using System.Numerics;
9using SkiaSharp;
10
12
22public sealed class PopupWidget : WidgetBase
23{
24 private bool _disposed;
25 private bool _isActive;
26 private float _elapsedSec;
27 private float _lifetimeSec = 1f;
28 private float _fadeInSec = 0.08f;
29 private float _fadeOutSec = 0.25f;
30 private long _lastAnimationTick;
31 private Func<Vector2> _sourceResolver;
32
36 public event Action? Completed;
37
47 RenderSurfaceHostBase renderSurfaceHost,
48 View view,
49 Rectangle screenBounds,
50 string text,
51 string? nickname = null)
52 : base(
53 renderSurfaceHost,
55 screenBounds.Location,
56 nickname)
57 {
58 ValidateBounds(screenBounds);
59
60 Text = CreateText(
61 renderSurfaceHost,
62 view,
63 screenBounds,
64 text,
65 $"{Nickname}.text");
66
67 Content = Text;
68 _sourceResolver = () => new Vector2(screenBounds.X, screenBounds.Y);
69
70 CompleteInitialization(Content);
71 }
72
82 RenderSurfaceHostBase renderSurfaceHost,
83 SceneLayer sceneLayer,
84 Rectangle worldBounds,
85 string text,
86 string? nickname = null)
87 : base(
88 renderSurfaceHost,
90 worldBounds.Location,
91 nickname)
92 {
93 ValidateBounds(worldBounds);
94
95 Text = CreateText(
96 renderSurfaceHost,
97 sceneLayer,
98 worldBounds,
99 text,
100 $"{Nickname}.text");
101
102 Content = Text;
103 _sourceResolver = () => new Vector2(worldBounds.X, worldBounds.Y);
104
105 CompleteInitialization(Content);
106 }
107
119 RenderSurfaceHostBase renderSurfaceHost,
120 Tile source,
121 Size size,
122 string text,
123 WidgetAnchor sourceAnchor = WidgetAnchor.Center,
124 Point? offsetPx = null,
125 string? nickname = null)
126 : this(
127 renderSurfaceHost,
128 source?.SceneLayer ?? throw new ArgumentNullException(nameof(source)),
129 new Rectangle(Point.Empty, size),
130 text,
131 nickname)
132 {
133 BindTo(source, sourceAnchor, offsetPx);
134 }
135
145 RenderSurfaceHostBase renderSurfaceHost,
146 View view,
147 Rectangle screenBounds,
148 SKImage image,
149 string? nickname = null)
150 : base(
151 renderSurfaceHost,
153 screenBounds.Location,
154 nickname)
155 {
156 ArgumentNullException.ThrowIfNull(image);
157 ValidateBounds(screenBounds);
158
159 Image = new DirectImage(
160 image,
161 renderSurfaceHost,
162 view,
163 screenBounds,
164 $"{Nickname}.image");
165
166 Content = Image;
167 _sourceResolver = () => new Vector2(screenBounds.X, screenBounds.Y);
168
169 CompleteInitialization(Content);
170 }
171
181 RenderSurfaceHostBase renderSurfaceHost,
182 SceneLayer sceneLayer,
183 Rectangle worldBounds,
184 SKImage image,
185 string? nickname = null)
186 : base(
187 renderSurfaceHost,
189 worldBounds.Location,
190 nickname)
191 {
192 ArgumentNullException.ThrowIfNull(image);
193 ValidateBounds(worldBounds);
194
195 Image = new DirectImage(
196 image,
197 renderSurfaceHost,
198 sceneLayer,
199 worldBounds,
200 $"{Nickname}.image");
201
202 Content = Image;
203 _sourceResolver = () => new Vector2(worldBounds.X, worldBounds.Y);
204
205 CompleteInitialization(Content);
206 }
207
217 RenderSurfaceHostBase renderSurfaceHost,
218 View view,
219 Rectangle screenBounds,
220 SKBitmap bitmap,
221 string? nickname = null)
222 : base(
223 renderSurfaceHost,
225 screenBounds.Location,
226 nickname)
227 {
228 ArgumentNullException.ThrowIfNull(bitmap);
229 ValidateBounds(screenBounds);
230
231 Image = new DirectImage(
232 bitmap,
233 renderSurfaceHost,
234 view,
235 screenBounds,
236 $"{Nickname}.image");
237
238 Content = Image;
239 _sourceResolver = () => new Vector2(screenBounds.X, screenBounds.Y);
240
241 CompleteInitialization(Content);
242 }
243
253 RenderSurfaceHostBase renderSurfaceHost,
254 SceneLayer sceneLayer,
255 Rectangle worldBounds,
256 SKBitmap bitmap,
257 string? nickname = null)
258 : base(
259 renderSurfaceHost,
261 worldBounds.Location,
262 nickname)
263 {
264 ArgumentNullException.ThrowIfNull(bitmap);
265 ValidateBounds(worldBounds);
266
267 Image = new DirectImage(
268 bitmap,
269 renderSurfaceHost,
270 sceneLayer,
271 worldBounds,
272 $"{Nickname}.image");
273
274 Content = Image;
275 _sourceResolver = () => new Vector2(worldBounds.X, worldBounds.Y);
276
277 CompleteInitialization(Content);
278 }
279
284
288 public TextBlock? Text { get; }
289
293 public DirectImage? Image { get; }
294
298 public Tile? SourceTile { get; private set; }
299
303 public Point? SourceGridLocation { get; private set; }
304
308 public float LifetimeSec
309 {
310 get => _lifetimeSec;
311 set => _lifetimeSec = value > 0f
312 ? value
313 : throw new ArgumentOutOfRangeException(nameof(value));
314 }
315
319 public float FadeInSec
320 {
321 get => _fadeInSec;
322 set => _fadeInSec = value >= 0f
323 ? value
324 : throw new ArgumentOutOfRangeException(nameof(value));
325 }
326
330 public float FadeOutSec
331 {
332 get => _fadeOutSec;
333 set => _fadeOutSec = value >= 0f
334 ? value
335 : throw new ArgumentOutOfRangeException(nameof(value));
336 }
337
342 public Vector2 VelocityPxPerSec { get; set; } = new(0f, -48f);
343
347 public Vector2 AccelerationPxPerSecSquared { get; set; }
348
352 public bool DisposeOnComplete { get; set; } = true;
353
359 {
360 Show();
361 return this;
362 }
363
369 public PopupWidget SetSourceLocation(Point locationPx)
370 {
371 SourceTile = null;
372 SourceGridLocation = null;
373 _sourceResolver = () => new Vector2(locationPx.X, locationPx.Y);
374 SetPosition(locationPx.X, locationPx.Y);
375 return this;
376 }
377
386 Tile source,
387 WidgetAnchor sourceAnchor = WidgetAnchor.Center,
388 Point? offsetPx = null)
389 {
390 ArgumentNullException.ThrowIfNull(source);
391
392 if (Mode != DirectDrawingMode.SceneLayer)
393 throw new InvalidOperationException("Only a scene-layer popup can bind to a tile.");
394
395 if (!ReferenceEquals(SceneLayer, source.SceneLayer))
396 throw new ArgumentException("The source tile must belong to the popup's SceneLayer.", nameof(source));
397
398 Point offset = offsetPx ?? Point.Empty;
399
400 SourceTile = source;
401 SourceGridLocation = source is SceneLayerTile fixedTile
402 ? fixedTile.GridCoordinatesAbs
403 : null;
404
405 _sourceResolver = () => GetCenteredSourcePosition(
406 source.DrawLocationWorld,
407 GetContentSize(),
408 sourceAnchor,
409 offset);
410
411 SetPosition(_sourceResolver());
412 return this;
413 }
414
424 SceneLayer sceneLayer,
425 Point gridLocation,
426 WidgetAnchor sourceAnchor = WidgetAnchor.Center,
427 Point? offsetPx = null)
428 {
429 ArgumentNullException.ThrowIfNull(sceneLayer);
430
431 SceneLayerTile tile = sceneLayer[gridLocation]
432 ?? throw new ArgumentOutOfRangeException(nameof(gridLocation), "The grid location is outside the SceneLayer.");
433
434 BindTo(tile, sourceAnchor, offsetPx);
435 SourceGridLocation = gridLocation;
436 return this;
437 }
438
444 public PopupWidget SetText(string text)
445 {
446 if (Text is null)
447 throw new InvalidOperationException("This popup contains an image, not text.");
448
449 Text.SetText(text);
450 return this;
451 }
452
461 SKTypeface typeface,
462 float size,
463 float? minSize = null)
464 {
465 if (Text is null)
466 throw new InvalidOperationException("This popup contains an image, not text.");
467
468 Text.SetFont(typeface, size, minSize);
469 return this;
470 }
471
477 public PopupWidget SetTextColor(SKColor color)
478 {
479 if (Text is null)
480 throw new InvalidOperationException("This popup contains an image, not text.");
481
482 Text.SetColors(color, SKColors.Transparent);
483 return this;
484 }
485
491 public PopupWidget SetPopupZOrder(int zOrder)
492 {
493 Content.SetZOrder(zOrder);
494 return this;
495 }
496
500 public void Dismiss()
501 {
502 if (!_isActive || _disposed)
503 return;
504
505 CompletePopup();
506 }
507
509 public override void Update(long tick)
510 {
511 if (_disposed || tick <= _lastAnimationTick)
512 return;
513
514 float elapsedSec = HighResTimer.GetDuration(_lastAnimationTick, tick);
515 _lastAnimationTick = tick;
516
517 base.Update(tick);
518
519 if (!_isActive)
520 return;
521
522 _elapsedSec += elapsedSec;
523
524 if (_elapsedSec >= LifetimeSec)
525 {
526 CompletePopup();
527 return;
528 }
529
530 float opacity = 1f;
531
532 if (FadeInSec > 0f && _elapsedSec < FadeInSec)
533 opacity = Math.Clamp(_elapsedSec / FadeInSec, 0f, 1f);
534
535 float remainingSec = LifetimeSec - _elapsedSec;
536
537 if (FadeOutSec > 0f && remainingSec < FadeOutSec)
538 opacity = Math.Min(opacity, Math.Clamp(remainingSec / FadeOutSec, 0f, 1f));
539
540 SetOpacity(opacity);
541 }
542
544 public override void Dispose()
545 {
546 if (_disposed)
547 return;
548
549 _disposed = true;
550 Completed = null;
551 base.Dispose();
552 }
553
555 protected override void ProcessShown()
556 {
557 base.ProcessShown();
558
559 _elapsedSec = 0f;
560 _lastAnimationTick = HighResTimer.GetCurrentTick();
561 _isActive = true;
562
563 Movement.StopAllMovement();
564 SetPosition(_sourceResolver());
565 SetOpacity(FadeInSec > 0f ? 0f : 1f);
566 Movement.SetVelocity(VelocityPxPerSec);
567 Movement.SetAcceleration(AccelerationPxPerSecSquared);
568 }
569
571 protected override void ProcessHidden()
572 {
573 _isActive = false;
574 Movement.StopAllMovement();
575 base.ProcessHidden();
576 }
577
578 private void CompleteInitialization(IDirectCompositeChild content)
579 {
580 IsInputEnabled = false;
581 IsPointerInputEnabled = false;
582
583 Add(
584 content,
585 keepCurrentOffset: false,
586 explicitLocalOffsetPx: Vector2.Zero);
587
588 SetPopupZOrder(int.MaxValue - 50);
589 SetIsVisible(false);
590 }
591
592 private void CompletePopup()
593 {
594 Hide();
595 Completed?.Invoke();
596
598 Dispose();
599 }
600
601 private Size GetContentSize()
602 {
603 Rectangle bounds = Mode == DirectDrawingMode.View
604 ? Content.ScreenBounds
606
607 return bounds.Size;
608 }
609
610 private static Vector2 GetCenteredSourcePosition(
611 Rectangle sourceBounds,
612 Size contentSize,
613 WidgetAnchor sourceAnchor,
614 Point offsetPx)
615 {
616 PointF anchor = sourceAnchor.GetPoint(sourceBounds);
617
618 return new Vector2(
619 anchor.X - contentSize.Width / 2f + offsetPx.X,
620 anchor.Y - contentSize.Height / 2f + offsetPx.Y);
621 }
622
623 private static TextBlock CreateText(
624 RenderSurfaceHostBase host,
625 View view,
626 Rectangle bounds,
627 string text,
628 string nickname)
629 {
630 return new TextBlock(
631 host,
632 view,
633 bounds,
634 nickname)
635 .SetText(text)
636 .SetFont(SKTypeface.Default, 22f, minSize: 10f)
637 .SetColors(SKColors.White, SKColors.Transparent)
638 .SetAlignment(SKTextAlign.Center, TextBlock.VerticalAlign.Center)
639 .EnableWrapping(false)
640 .UseShadow(true)
641 .UseOutline(true);
642 }
643
644 private static TextBlock CreateText(
645 RenderSurfaceHostBase host,
646 SceneLayer sceneLayer,
647 Rectangle bounds,
648 string text,
649 string nickname)
650 {
651 return new TextBlock(
652 host,
653 sceneLayer,
654 view: null,
655 worldBounds: bounds,
656 nickname: nickname)
657 .SetText(text)
658 .SetFont(SKTypeface.Default, 22f, minSize: 10f)
659 .SetColors(SKColors.White, SKColors.Transparent)
660 .SetAlignment(SKTextAlign.Center, TextBlock.VerticalAlign.Center)
661 .EnableWrapping(false)
662 .UseShadow(true)
663 .UseOutline(true);
664 }
665
666 private static void ValidateBounds(Rectangle bounds)
667 {
668 if (bounds.Width <= 0 || bounds.Height <= 0)
669 throw new ArgumentOutOfRangeException(nameof(bounds), "Popup bounds must have a positive width and height.");
670 }
671}
SceneLayer? SceneLayer
Gets the scene layer shared by all children in scene-layer mode.
DirectComposite SetOpacity(float opacity)
Applies one opacity to every descendant.
DirectComposite SetIsVisible(bool visible)
Applies one visibility value to every descendant.
View? View
Gets the view shared by all children in view mode.
MovementController Movement
Gets the movement controller used to move the 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.
DirectDrawingMode Mode
Gets the coordinate mode shared by the composite and its children.
Renders a bitmap or image to a rectangular region with support for scaling modes, source clipping,...
A retained-mode text drawable with wrapping, multi-line ( ) support, horizontal/vertical alignment,...
Definition TextBlock.cs:44
Represents an abstract base class for drawable tiles in the Gondwana engine. Provides core functional...
Definition Tile.cs:19
SceneLayer SceneLayer
Gets the scene layer that owns this tile.
Definition Tile.cs:91
Rectangle DrawLocationWorld
Gets the tile drawing bounds in world pixels.
Definition Tile.cs:83
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Represents the values stored at a single location on a SceneLayer.
PopupWidget SetText(string text)
Sets the popup text. Throws when this is an image popup.
PopupWidget SetPopupZOrder(int zOrder)
Sets the popup Z-order.
Tile? SourceTile
Gets the tile used to resolve the popup source, when one is configured.
float FadeInSec
Gets or sets the fade-in duration in seconds.
Action? Completed
Occurs after the popup reaches the end of its configured lifetime.
TextBlock? Text
Gets the popup's text drawing, or null for an image popup.
float FadeOutSec
Gets or sets the fade-out duration in seconds.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle worldBounds, SKBitmap bitmap, string? nickname=null)
Initializes a bitmap popup in scene-layer world coordinates.
override void ProcessHidden()
Performs processing when the widget is hidden before notification callbacks are invoked.
float LifetimeSec
Gets or sets the popup lifetime in seconds.
DirectImage? Image
Gets the popup's image drawing, or null for a text popup.
PopupWidget SetSourceLocation(Point locationPx)
Uses a fixed top-left location as the popup source.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle screenBounds, SKImage image, string? nickname=null)
Initializes an image popup in absolute view/screen coordinates.
override void Dispose()
Disposes the composite and every child it owns.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle screenBounds, string text, string? nickname=null)
Initializes a text popup in absolute view/screen coordinates.
Vector2 VelocityPxPerSec
Gets or sets the initial popup velocity in pixels per second. Scene-layer popups use world pixels; vi...
PopupWidget ShowPopup()
Shows the popup, resolves its current source, and starts its movement and fades.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle worldBounds, string text, string? nickname=null)
Initializes a text popup in scene-layer world coordinates.
Vector2 AccelerationPxPerSecSquared
Gets or sets popup acceleration in pixels per second squared.
PopupWidget BindTo(Tile source, WidgetAnchor sourceAnchor=WidgetAnchor.Center, Point? offsetPx=null)
Resolves the popup source from an anchor point on a tile when the popup is shown.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, Tile source, Size size, string text, WidgetAnchor sourceAnchor=WidgetAnchor.Center, Point? offsetPx=null, string? nickname=null)
Initializes a text popup whose source is resolved from a tile when shown.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, View view, Rectangle screenBounds, SKBitmap bitmap, string? nickname=null)
Initializes a bitmap popup in absolute view/screen coordinates.
override void ProcessShown()
Performs processing when the widget is shown before notification callbacks are invoked.
IDirectCompositeChild Content
Gets the direct drawing displayed by this popup.
bool DisposeOnComplete
Gets or sets whether the popup disposes itself after completion.
void Dismiss()
Completes and hides the popup immediately.
PopupWidget BindTo(SceneLayer sceneLayer, Point gridLocation, WidgetAnchor sourceAnchor=WidgetAnchor.Center, Point? offsetPx=null)
Resolves the popup source from a fixed grid cell when the popup is shown.
Point? SourceGridLocation
Gets the fixed grid coordinate used to resolve the popup source, when one is configured.
PopupWidget SetTextColor(SKColor color)
Sets the popup text color. Throws when this is an image popup.
PopupWidget(RenderSurfaceHostBase renderSurfaceHost, SceneLayer sceneLayer, Rectangle worldBounds, SKImage image, string? nickname=null)
Initializes an image popup in scene-layer world coordinates.
PopupWidget SetFont(SKTypeface typeface, float size, float? minSize=null)
Sets the popup font. Throws when this is an image popup.
override void Update(long tick)
Advances composite movement.This method uses the tick to calculate elapsed time and advance the movem...
WidgetBase Show()
Shows the widget and registers it for input routing.
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 IsInputEnabled
Gets or sets a value indicating whether the widget can participate in input routing.
Defines a movable direct drawable that may be owned by a DirectComposite.
Rectangle WorldBounds
The bounding rectangle of this drawable in WORLD space.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ View
Performs direct drawing at the view level using screen-space coordinates.
WidgetAnchor
Identifies one of the nine common anchor points on a rectangle.