Skip to content

Commit 416376d

Browse files
committed
More work on DialogBox
1 parent d673446 commit 416376d

20 files changed

Lines changed: 1791 additions & 59 deletions

Gum/Wireframe/CustomSetPropertyOnRenderable.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,23 @@ public class CustomSetPropertyOnRenderable
5151
{
5252
public static ILocalizationService? LocalizationService { get; set; }
5353

54+
#if !FRB
5455
/// <summary>
5556
/// Optional font service used for on-demand font creation. In the Gum tool this is
5657
/// assigned at startup; game runtimes can assign their own implementation.
5758
/// </summary>
58-
#if !FRB
5959
public static IRuntimeFontService? FontService { get; set; }
6060
#endif
6161

62+
#if !RAYLIB
6263
/// <summary>
6364
/// Optional in-memory font creator. When set, font generation bypasses disk entirely —
6465
/// the creator produces a <see cref="BitmapFont"/> directly from raw pixel data and
6566
/// .fnt metadata. If null or if creation fails, falls back to the disk-based
6667
/// <see cref="FontService"/> path.
6768
/// </summary>
6869
public static IInMemoryFontCreator? InMemoryFontCreator { get; set; }
70+
#endif
6971

7072
public static event Action<string>? PropertyAssignmentError;
7173

GumCommon/GumCommon.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
</Compile>
136136
<Compile Include="..\GumRuntime\GraphicalUiElement.cs" Link="GraphicalUiElement.cs" />
137137
<Compile Include="..\GumRuntime\GraphicalUiElement.Binding.cs" Link="GraphicalUiElement.Binding.cs" />
138+
<Compile Include="..\GumRuntime\IUpdateEveryFrame.cs" Link="IUpdateEveryFrame.cs" />
138139
<Compile Include="..\GumRuntime\LayoutExporter.cs" Link="Runtime\LayoutExporter.cs" />
139140

140141

GumRuntime/GraphicalUiElement.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7033,6 +7033,12 @@ public void AnimateSelf(double secondDifference)
70337033
UpdateTextureValuesFrom(asSprite!);
70347034
}
70357035

7036+
if (this is InteractiveGue interactive
7037+
&& interactive.FormsControlAsObject is IUpdateEveryFrame updatable)
7038+
{
7039+
updatable.Activity(secondDifference);
7040+
}
7041+
70367042
if (Children != null)
70377043
{
70387044
for (int i = 0; i < this.Children.Count; i++)

GumRuntime/IUpdateEveryFrame.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Gum.Wireframe;
2+
3+
/// <summary>
4+
/// Implemented by Forms controls (or other objects assigned to
5+
/// <see cref="InteractiveGue.FormsControlAsObject"/>) that need a per-frame
6+
/// callback regardless of focus. <see cref="Activity"/> is invoked from
7+
/// <see cref="GraphicalUiElement.AnimateSelf"/>, so it follows the same
8+
/// visibility and tree-membership rules as sprite animation: hidden or
9+
/// detached elements do not tick.
10+
/// </summary>
11+
public interface IUpdateEveryFrame
12+
{
13+
void Activity(double secondDifference);
14+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
using Gum.DataTypes;
2+
using Gum.Forms.Controls.Games;
3+
using Gum.Wireframe;
4+
using MonoGameGum.GueDeriving;
5+
using RenderingLibrary.Graphics;
6+
using Shouldly;
7+
using Xunit;
8+
9+
namespace MonoGameGum.Tests.Forms;
10+
11+
public class DialogBoxTests : BaseTestClass
12+
{
13+
sealed class TestDialogBoxVisual : InteractiveGue
14+
{
15+
public TextRuntime TextInstance { get; }
16+
public ContainerRuntime ContinueIndicatorInstance { get; }
17+
18+
public TestDialogBoxVisual(bool paginating = false) : base(new InvisibleRenderable())
19+
{
20+
HasEvents = true;
21+
Width = 400;
22+
Height = 100;
23+
24+
TextInstance = new TextRuntime { Name = "TextInstance", Text = string.Empty };
25+
if (paginating)
26+
{
27+
// Fixed pixel size + TruncateLine triggers DialogBox.ConvertToPages's
28+
// pagination path. Width 200 / Height ~42 fits roughly 2 lines at the
29+
// default font's 21px line height.
30+
TextInstance.Width = 200;
31+
TextInstance.Height = 42;
32+
TextInstance.WidthUnits = DimensionUnitType.Absolute;
33+
TextInstance.HeightUnits = DimensionUnitType.Absolute;
34+
TextInstance.TextOverflowVerticalMode = TextOverflowVerticalMode.TruncateLine;
35+
}
36+
AddChild(TextInstance);
37+
38+
ContinueIndicatorInstance = new ContainerRuntime { Name = "ContinueIndicatorInstance", Visible = false };
39+
AddChild(ContinueIndicatorInstance);
40+
41+
FormsControlAsObject = new DialogBox(this);
42+
}
43+
}
44+
45+
static (DialogBox dialogBox, TestDialogBoxVisual visual) CreateDialogBox(bool paginating = false)
46+
{
47+
var visual = new TestDialogBoxVisual(paginating);
48+
return ((DialogBox)visual.FormsControlAsObject, visual);
49+
}
50+
51+
static void Tick(DialogBox dialogBox, double secondDifference) =>
52+
((IUpdateEveryFrame)dialogBox).Activity(secondDifference);
53+
54+
[Fact]
55+
public void Constructor_ShouldWireUpDialogBoxToVisual()
56+
{
57+
var (dialogBox, visual) = CreateDialogBox();
58+
59+
dialogBox.ShouldNotBeNull();
60+
dialogBox.Visual.ShouldBe(visual);
61+
visual.TextInstance.ShouldNotBeNull();
62+
visual.ContinueIndicatorInstance.ShouldNotBeNull();
63+
visual.ContinueIndicatorInstance.Visible.ShouldBeFalse();
64+
}
65+
66+
[Fact]
67+
public void Show_WithLettersPerSecond_ShouldStartAtZeroLetters()
68+
{
69+
var (dialogBox, visual) = CreateDialogBox();
70+
dialogBox.LettersPerSecond = 10;
71+
72+
dialogBox.Show("hello");
73+
74+
visual.TextInstance.MaxLettersToShow.ShouldBe(0);
75+
visual.ContinueIndicatorInstance.Visible.ShouldBeFalse();
76+
}
77+
78+
[Fact]
79+
public void Show_WithZeroLettersPerSecond_ShouldShowAllLettersImmediately()
80+
{
81+
var (dialogBox, visual) = CreateDialogBox();
82+
dialogBox.LettersPerSecond = 0;
83+
84+
dialogBox.Show("hello");
85+
86+
visual.TextInstance.MaxLettersToShow.ShouldBe(5);
87+
}
88+
89+
[Fact]
90+
public void Activity_ShouldAdvanceMaxLettersToShow_ProportionalToElapsedTime()
91+
{
92+
var (dialogBox, visual) = CreateDialogBox();
93+
dialogBox.LettersPerSecond = 10;
94+
dialogBox.Show("hello world");
95+
96+
Tick(dialogBox, 0.5);
97+
98+
visual.TextInstance.MaxLettersToShow.ShouldBe(5);
99+
}
100+
101+
[Fact]
102+
public void Activity_ShouldFireFinishedTypingPage_WhenAllLettersRevealed()
103+
{
104+
var (dialogBox, visual) = CreateDialogBox();
105+
dialogBox.LettersPerSecond = 10;
106+
int finishedFireCount = 0;
107+
dialogBox.FinishedTypingPage += (_, _) => finishedFireCount++;
108+
109+
dialogBox.Show("hello");
110+
Tick(dialogBox, 1.0);
111+
112+
visual.TextInstance.MaxLettersToShow.ShouldBe(5);
113+
finishedFireCount.ShouldBe(1);
114+
visual.ContinueIndicatorInstance.Visible.ShouldBeTrue();
115+
}
116+
117+
[Fact]
118+
public void Activity_ShouldNotFireFinishedTypingPageMultipleTimes()
119+
{
120+
var (dialogBox, visual) = CreateDialogBox();
121+
dialogBox.LettersPerSecond = 10;
122+
int finishedFireCount = 0;
123+
dialogBox.FinishedTypingPage += (_, _) => finishedFireCount++;
124+
125+
dialogBox.Show("hello");
126+
Tick(dialogBox, 1.0);
127+
Tick(dialogBox, 1.0);
128+
Tick(dialogBox, 1.0);
129+
130+
finishedFireCount.ShouldBe(1);
131+
}
132+
133+
[Fact]
134+
public void Activity_BeforeShow_ShouldBeNoOp()
135+
{
136+
var (dialogBox, visual) = CreateDialogBox();
137+
int finishedFireCount = 0;
138+
dialogBox.FinishedTypingPage += (_, _) => finishedFireCount++;
139+
140+
Tick(dialogBox, 1.0);
141+
142+
finishedFireCount.ShouldBe(0);
143+
}
144+
145+
[Fact]
146+
public void Activity_ShouldClampMaxLettersToShow_AtTargetCount()
147+
{
148+
var (dialogBox, visual) = CreateDialogBox();
149+
dialogBox.LettersPerSecond = 1000;
150+
dialogBox.Show("hi");
151+
152+
Tick(dialogBox, 10.0);
153+
154+
visual.TextInstance.MaxLettersToShow.ShouldBe(2);
155+
}
156+
157+
[Fact]
158+
public void Activity_OnTwoDialogBoxes_ShouldAdvanceIndependently()
159+
{
160+
var (dialogBoxA, visualA) = CreateDialogBox();
161+
var (dialogBoxB, visualB) = CreateDialogBox();
162+
dialogBoxA.LettersPerSecond = 10;
163+
dialogBoxB.LettersPerSecond = 10;
164+
165+
dialogBoxA.Show("hello");
166+
dialogBoxB.Show("hello");
167+
Tick(dialogBoxA, 0.3);
168+
169+
visualA.TextInstance.MaxLettersToShow.ShouldBe(3);
170+
visualB.TextInstance.MaxLettersToShow.ShouldBe(0);
171+
}
172+
173+
[Fact]
174+
public void AnimateSelf_OnVisual_ShouldDispatchToActivity()
175+
{
176+
var (dialogBox, visual) = CreateDialogBox();
177+
dialogBox.LettersPerSecond = 10;
178+
dialogBox.Show("hello");
179+
180+
visual.AnimateSelf(0.5);
181+
182+
visual.TextInstance.MaxLettersToShow.ShouldBe(5);
183+
}
184+
185+
const string LongText =
186+
"This is sentence one of a fairly long passage. " +
187+
"This is sentence two which keeps going on and on. " +
188+
"Sentence three adds even more text to push past the height. " +
189+
"Sentence four ensures we definitely overflow the visible area. " +
190+
"And sentence five is here to be very sure of pagination.";
191+
192+
[Fact]
193+
public void Show_String_ShouldSplitLongTextAcrossMultiplePages()
194+
{
195+
var (dialogBox, _) = CreateDialogBox(paginating: true);
196+
dialogBox.LettersPerSecond = 0;
197+
198+
dialogBox.Show(LongText);
199+
200+
// First page is shown (popped); remaining pages should still be queued.
201+
dialogBox.PagesRemaining.ShouldBeGreaterThan(0);
202+
}
203+
204+
[Fact]
205+
public void Show_Enumerable_ShouldSplitEachLongEntryAcrossMultiplePages()
206+
{
207+
var (dialogBoxA, _) = CreateDialogBox(paginating: true);
208+
var (dialogBoxB, _) = CreateDialogBox(paginating: true);
209+
dialogBoxA.LettersPerSecond = 0;
210+
dialogBoxB.LettersPerSecond = 0;
211+
212+
// Two long entries should produce strictly more pages than one long entry —
213+
// confirming each entry runs through ConvertToPages, not just appended verbatim.
214+
dialogBoxA.Show(new[] { LongText });
215+
dialogBoxB.Show(new[] { LongText, LongText });
216+
217+
dialogBoxB.PagesRemaining.ShouldBeGreaterThan(dialogBoxA.PagesRemaining);
218+
}
219+
220+
[Fact]
221+
public void Show_String_AndShow_SingleEntryArray_ShouldProduceSamePageCount()
222+
{
223+
var (dialogBoxA, _) = CreateDialogBox(paginating: true);
224+
var (dialogBoxB, _) = CreateDialogBox(paginating: true);
225+
dialogBoxA.LettersPerSecond = 0;
226+
dialogBoxB.LettersPerSecond = 0;
227+
228+
dialogBoxA.Show(LongText);
229+
dialogBoxB.Show(new[] { LongText });
230+
231+
dialogBoxA.PagesRemaining.ShouldBe(dialogBoxB.PagesRemaining);
232+
}
233+
234+
[Fact]
235+
public void Show_Enumerable_TwoIdenticalLongEntries_ShouldProduceTwiceTheSplitPagesOfOne()
236+
{
237+
var (dialogBoxA, _) = CreateDialogBox(paginating: true);
238+
var (dialogBoxB, _) = CreateDialogBox(paginating: true);
239+
dialogBoxA.LettersPerSecond = 0;
240+
dialogBoxB.LettersPerSecond = 0;
241+
242+
dialogBoxA.Show(new[] { LongText });
243+
dialogBoxB.Show(new[] { LongText, LongText });
244+
245+
// PagesRemaining = pages queued AFTER ShowNextPage popped one,
246+
// so the two-entry case should have (2 * (A.PagesRemaining + 1)) - 1
247+
// = 2 * A.PagesRemaining + 1 pages remaining.
248+
dialogBoxB.PagesRemaining.ShouldBe(2 * dialogBoxA.PagesRemaining + 1);
249+
}
250+
251+
[Fact]
252+
public void Show_Enumerable_ShouldKeepShortEntriesAsLiteralPages()
253+
{
254+
var (dialogBox, _) = CreateDialogBox(paginating: true);
255+
dialogBox.LettersPerSecond = 0;
256+
257+
dialogBox.Show(new[] { "one", "two", "three" });
258+
259+
// Three short entries -> three pages, first popped -> two remaining.
260+
dialogBox.PagesRemaining.ShouldBe(2);
261+
}
262+
}

0 commit comments

Comments
 (0)