Skip to content

Commit 045749d

Browse files
bdachHiddenNode
andcommitted
Fix TextFlowContainer not laying text out properly on some TextAnchor settings
- Closes ppy#5084 - Closes ppy#5499 - Closes ppy#2073 - Closes ppy/osu#8580 - Supersedes / closes ppy#5507 You might ask why I'm bothering to try this now. Well, this came up when I wanted to use text flow for BSS purposes (and forced me to hack around it), and came up again in ppy/osu#31970, and I'm sick of it. The actual fix is taken verbatim from ppy#5507, it's just restructured using the idea of a single nested flow taken from ppy#5507 (comment). As such: Co-authored-by: HiddenNode <101936124+HiddenNode@users.noreply.github.com>
1 parent 640bc40 commit 045749d

File tree

5 files changed

+117
-80
lines changed

5 files changed

+117
-80
lines changed

osu.Framework.Tests/Visual/Containers/TestSceneTextFlowContainer.cs

+1-10
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public void Setup() => Schedule(() =>
6161
public void TestChangeTextAnchor(Anchor anchor)
6262
{
6363
AddStep("change text anchor", () => textContainer.TextAnchor = anchor);
64-
AddAssert("children have correct anchors", () => textContainer.Children.All(c => c.Anchor == anchor && c.Origin == anchor));
6564
AddAssert("children are positioned correctly", () =>
6665
{
6766
string result = string.Concat(textContainer.Children
@@ -72,14 +71,6 @@ public void TestChangeTextAnchor(Anchor anchor)
7271
});
7372
}
7473

75-
[Test]
76-
public void TestAddTextWithTextAnchor()
77-
{
78-
AddStep("change text anchor", () => textContainer.TextAnchor = Anchor.TopCentre);
79-
AddStep("add text", () => textContainer.AddText("added text"));
80-
AddAssert("children have correct anchors", () => textContainer.Children.All(c => c.Anchor == Anchor.TopCentre && c.Origin == Anchor.TopCentre));
81-
}
82-
8374
[Test]
8475
public void TestSetText()
8576
{
@@ -136,6 +127,6 @@ private void assertSpriteTextCount(int count)
136127
=> AddAssert($"text flow has {count} sprite texts", () => textContainer.ChildrenOfType<SpriteText>().Count() == count);
137128

138129
private void assertTotalChildCount(int count)
139-
=> AddAssert($"text flow has {count} children", () => textContainer.Count == count);
130+
=> AddAssert($"text flow has {count} children", () => textContainer.Children.Count() == count);
140131
}
141132
}

osu.Framework/Graphics/Containers/CustomizableTextContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ protected override void RecreateAllParts()
9898
// placeholders via AddPlaceholder() are similar to manual text parts
9999
// in that they were added/registered externally and cannot be recreated.
100100
// remove them before proceeding with part recreation to avoid accidentally disposing them in the process.
101-
RemoveRange(Placeholders, false);
101+
Flow.RemoveRange(Placeholders, false);
102102

103103
base.RecreateAllParts();
104104
}

osu.Framework/Graphics/Containers/FlowContainer.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,16 @@ public Vector2 MaximumSize
6868

6969
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || !layout.IsValid;
7070

71+
internal event Action OnLayoutInvalidated;
72+
7173
/// <summary>
7274
/// Invoked when layout should be invalidated.
7375
/// </summary>
74-
protected virtual void InvalidateLayout() => layout.Invalidate();
76+
protected virtual void InvalidateLayout()
77+
{
78+
layout.Invalidate();
79+
OnLayoutInvalidated?.Invoke();
80+
}
7581

7682
private readonly Dictionary<Drawable, float> layoutChildren = new Dictionary<Drawable, float>();
7783

osu.Framework/Graphics/Containers/Markdown/MarkdownTextFlowContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace osu.Framework.Graphics.Containers.Markdown
2222
/// </summary>
2323
public partial class MarkdownTextFlowContainer : CustomizableTextContainer, IMarkdownTextComponent
2424
{
25-
public float TotalTextWidth => Padding.TotalHorizontal + FlowingChildren.Sum(x => x.BoundingBox.Size.X);
25+
public float TotalTextWidth => Padding.TotalHorizontal + Flow.FlowingChildren.Sum(x => x.BoundingBox.Size.X);
2626

2727
[Resolved]
2828
private IMarkdownTextComponent parentTextComponent { get; set; }

osu.Framework/Graphics/Containers/TextFlowContainer.cs

+107-67
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.Linq;
11+
using JetBrains.Annotations;
1112
using osu.Framework.Allocation;
1213
using osu.Framework.Bindables;
1314
using osu.Framework.Extensions.EnumExtensions;
1415
using osu.Framework.Extensions.IEnumerableExtensions;
1516
using osu.Framework.Localisation;
17+
using osuTK;
1618

1719
namespace osu.Framework.Graphics.Containers
1820
{
1921
/// <summary>
2022
/// A drawable text object that supports more advanced text formatting.
2123
/// </summary>
22-
public partial class TextFlowContainer : FillFlowContainer
24+
public partial class TextFlowContainer : CompositeDrawable
2325
{
2426
private float firstLineIndent;
2527
private readonly Action<SpriteText> defaultCreationParameters;
@@ -115,6 +117,9 @@ public Anchor TextAnchor
115117

116118
textAnchor = value;
117119

120+
Flow.Anchor = value;
121+
Flow.Origin = value;
122+
118123
layout.Invalidate();
119124
}
120125
}
@@ -127,23 +132,104 @@ public LocalisableString Text
127132
{
128133
set
129134
{
130-
Clear();
135+
Flow.Clear();
131136
parts.Clear();
132137

133138
AddText(value);
134139
}
135140
}
136141

142+
public new Axes RelativeSizeAxes
143+
{
144+
get => base.RelativeSizeAxes;
145+
set
146+
{
147+
base.RelativeSizeAxes = value;
148+
setFlowSizing();
149+
}
150+
}
151+
152+
public new Axes AutoSizeAxes
153+
{
154+
get => base.AutoSizeAxes;
155+
set
156+
{
157+
base.AutoSizeAxes = value;
158+
setFlowSizing();
159+
}
160+
}
161+
162+
public override float Width
163+
{
164+
get => base.Width;
165+
set
166+
{
167+
base.Width = value;
168+
setFlowSizing();
169+
}
170+
}
171+
172+
private void setFlowSizing()
173+
{
174+
// if the user has imposed `RelativeSizeAxes` or a fixed size on the X axis on the entire flow,
175+
// we want the child flow that actually does the layout here to match that.
176+
// however, the child flow must always be auto-sized in the Y axis
177+
// to correctly respect `TextAnchor`.
178+
Flow.AutoSizeAxes = (AutoSizeAxes & ~RelativeSizeAxes) | Axes.Y;
179+
Flow.RelativeSizeAxes = RelativeSizeAxes & ~Flow.AutoSizeAxes;
180+
if ((Flow.AutoSizeAxes & Axes.X) == 0)
181+
Flow.Width = Width;
182+
}
183+
184+
public new MarginPadding Padding
185+
{
186+
get => base.Padding;
187+
set => base.Padding = value;
188+
}
189+
190+
public Vector2 Spacing
191+
{
192+
get => Flow.Spacing;
193+
set => Flow.Spacing = value;
194+
}
195+
196+
public Vector2 MaximumSize
197+
{
198+
get => Flow.MaximumSize;
199+
set => Flow.MaximumSize = value;
200+
}
201+
202+
public new bool Masking
203+
{
204+
get => base.Masking;
205+
set => base.Masking = value;
206+
}
207+
208+
public FillDirection Direction
209+
{
210+
get => Flow.Direction;
211+
set => Flow.Direction = value;
212+
}
213+
214+
public IEnumerable<Drawable> Children => Flow.Children;
215+
137216
[Resolved]
138217
internal LocalisationManager Localisation { get; private set; }
139218

219+
protected readonly FillFlowContainer Flow;
140220
private readonly Bindable<LocalisationParameters> localisationParameters = new Bindable<LocalisationParameters>();
141221

142222
public TextFlowContainer(Action<SpriteText> defaultCreationParameters = null)
143223
{
144224
this.defaultCreationParameters = defaultCreationParameters;
225+
226+
InternalChild = Flow = CreateFlow().With(f => f.AutoSizeAxes = Axes.Both);
227+
Flow.OnLayoutInvalidated += () => layout.Invalidate();
145228
}
146229

230+
[Pure]
231+
protected virtual FillFlowContainer CreateFlow() => new InnerFlow();
232+
147233
protected override void LoadAsyncComplete()
148234
{
149235
base.LoadAsyncComplete();
@@ -160,12 +246,6 @@ protected override void LoadComplete()
160246
((IBindable<LocalisationParameters>)localisationParameters).BindTo(Localisation.CurrentParameters);
161247
}
162248

163-
protected override void InvalidateLayout()
164-
{
165-
base.InvalidateLayout();
166-
layout.Invalidate();
167-
}
168-
169249
protected override void Update()
170250
{
171251
base.Update();
@@ -174,24 +254,6 @@ protected override void Update()
174254
RecreateAllParts();
175255
}
176256

177-
public override IEnumerable<Drawable> FlowingChildren
178-
{
179-
get
180-
{
181-
if ((TextAnchor & (Anchor.x2 | Anchor.y2)) == 0)
182-
return base.FlowingChildren;
183-
184-
var childArray = base.FlowingChildren.ToArray();
185-
186-
if ((TextAnchor & Anchor.x2) > 0)
187-
reverseHorizontal(childArray);
188-
if ((TextAnchor & Anchor.y2) > 0)
189-
reverseVertical(childArray);
190-
191-
return childArray;
192-
}
193-
}
194-
195257
protected override void UpdateAfterChildren()
196258
{
197259
if (!layout.IsValid)
@@ -279,14 +341,9 @@ protected internal virtual TextChunk<TSpriteText> CreateChunkFor<TSpriteText>(Lo
279341

280342
internal void ApplyDefaultCreationParameters(SpriteText spriteText) => defaultCreationParameters?.Invoke(spriteText);
281343

282-
public override void Add(Drawable drawable)
344+
public void Clear(bool disposeChildren = true)
283345
{
284-
throw new InvalidOperationException($"Use {nameof(AddText)} to add text to a {nameof(TextFlowContainer)}.");
285-
}
286-
287-
public override void Clear(bool disposeChildren)
288-
{
289-
base.Clear(disposeChildren);
346+
Flow.Clear(disposeChildren);
290347
parts.Clear();
291348
}
292349

@@ -322,10 +379,10 @@ protected virtual void RecreateAllParts()
322379
// manual parts need to be manually removed before clearing contents,
323380
// to avoid accidentally disposing of them in the process.
324381
foreach (var manualPart in parts.OfType<TextPartManual>())
325-
RemoveRange(manualPart.Drawables, false);
382+
Flow.RemoveRange(manualPart.Drawables, false);
326383

327384
// make sure not to clear the list of parts by accident.
328-
base.Clear(true);
385+
Flow.Clear(true);
329386

330387
foreach (var part in parts)
331388
recreatePart(part);
@@ -337,33 +394,7 @@ private void recreatePart(ITextPart part)
337394
{
338395
part.RecreateDrawablesFor(this);
339396
foreach (var drawable in part.Drawables)
340-
base.Add(drawable);
341-
}
342-
343-
private void reverseHorizontal(Drawable[] children)
344-
{
345-
int reverseStartIndex = 0;
346-
347-
// Inverse the order of all children when displaying backwards, stopping at newline boundaries
348-
for (int i = 0; i < children.Length; i++)
349-
{
350-
if (!(children[i] is NewLineContainer))
351-
continue;
352-
353-
Array.Reverse(children, reverseStartIndex, i - reverseStartIndex);
354-
reverseStartIndex = i + 1;
355-
}
356-
357-
// Extra loop for the last newline boundary (or all children if there are no newlines)
358-
Array.Reverse(children, reverseStartIndex, children.Length - reverseStartIndex);
359-
}
360-
361-
private void reverseVertical(Drawable[] children)
362-
{
363-
// A vertical reverse reverses the order of the newline sections, but not the order within the newline sections
364-
// For code clarity this is done by reversing the entire array, and then reversing within the newline sections to restore horizontal order
365-
Array.Reverse(children);
366-
reverseHorizontal(children);
397+
Flow.Add(drawable);
367398
}
368399

369400
private readonly Cached layout = new Cached();
@@ -373,11 +404,8 @@ private void computeLayout()
373404
var childrenByLine = new List<List<Drawable>>();
374405
var curLine = new List<Drawable>();
375406

376-
foreach (var c in Children)
407+
foreach (var c in Flow.Children)
377408
{
378-
c.Anchor = TextAnchor;
379-
c.Origin = TextAnchor;
380-
381409
if (c is NewLineContainer nlc)
382410
{
383411
curLine.Add(nlc);
@@ -411,6 +439,10 @@ private void computeLayout()
411439
float currentLineHeight = 0f;
412440
float lineSpacingValue = lastLineHeight * LineSpacing;
413441

442+
// Compute the offset of this line from the right
443+
Drawable lastTextPartInLine = (line[^1] is NewLineContainer && line.Count >= 2) ? line[^2] : line[^1];
444+
float lineOffsetFromRight = Flow.ChildSize.X - (lastTextPartInLine.X + lastTextPartInLine.DrawWidth);
445+
414446
foreach (Drawable c in line)
415447
{
416448
if (c is NewLineContainer nlc)
@@ -431,6 +463,11 @@ private void computeLayout()
431463
if (c.Height > currentLineHeight)
432464
currentLineHeight = c.Height;
433465

466+
if ((TextAnchor & Anchor.x1) != 0)
467+
c.X += lineOffsetFromRight / 2;
468+
else if ((TextAnchor & Anchor.x2) != 0)
469+
c.X += lineOffsetFromRight;
470+
434471
isFirstChild = false;
435472
}
436473

@@ -441,7 +478,10 @@ private void computeLayout()
441478
}
442479
}
443480

444-
protected override bool ForceNewRow(Drawable child) => child is NewLineContainer;
481+
protected partial class InnerFlow : FillFlowContainer
482+
{
483+
protected override bool ForceNewRow(Drawable child) => child is NewLineContainer;
484+
}
445485

446486
public partial class NewLineContainer : Container
447487
{

0 commit comments

Comments
 (0)