Skip to content

Commit

Permalink
Fixed newly introduced issues since #28 (#30)
Browse files Browse the repository at this point in the history
* fixed newly introduced stack overflow when adding/removing from scrolling panel

* added second part of issue as test

* fixed TestIssue29Inconsistencies
  • Loading branch information
Ellpeck authored Oct 29, 2024
1 parent b59fe6b commit dbd52d5
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 23 deletions.
36 changes: 23 additions & 13 deletions MLEM.Ui/Elements/Panel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeight
this.CanBeSelected = false;

if (scrollOverflow) {
this.scrollBarMaxHistory = new float[3];
this.ResetScrollBarMaxHistory();

this.ScrollBar = new ScrollBar(Anchor.TopRight, Vector2.Zero, 0, 0) {
OnValueChanged = (element, value) => this.ScrollChildren(),
CanAutoAnchorsAttach = false,
Expand All @@ -98,10 +101,6 @@ public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeight
this.ScrollToElement(e);
};
this.AddChild(this.ScrollBar);

this.scrollBarMaxHistory = new float[3];
for (var i = 0; i < this.scrollBarMaxHistory.Length; i++)
this.scrollBarMaxHistory[i] = -1;
}
}

Expand Down Expand Up @@ -150,6 +149,8 @@ public override void RemoveChild(Element element) {
throw new NotSupportedException("A panel that scrolls overflow cannot have its scroll bar removed from its list of children");
base.RemoveChild(element);

this.ResetScrollBarMaxHistory();

// when removing children, our scroll bar might have to be hidden
// if we don't do this before adding children again, they might incorrectly assume that the scroll bar will still be visible and adjust their size accordingly
this.childrenDirtyForScroll = true;
Expand All @@ -161,6 +162,8 @@ public override T AddChild<T>(T element, int index = -1) {
if (this.childrenDirtyForScroll && this.System != null)
this.ScrollSetup();

this.ResetScrollBarMaxHistory();

return base.AddChild(element, index);
}

Expand Down Expand Up @@ -324,16 +327,16 @@ protected virtual void ScrollSetup() {

// the max value of the scroll bar is the amount of non-scaled pixels taken up by overflowing components
var scrollBarMax = Math.Max(0, (childrenHeight - this.ChildPaddedArea.Height) / this.Scale);
// avoid an infinite show/hide oscillation that occurs while updating our area by simply using the maximum recent height in that case
if (this.scrollBarMaxHistory[0].Equals(this.scrollBarMaxHistory[2], Element.Epsilon) && this.scrollBarMaxHistory[1].Equals(scrollBarMax, Element.Epsilon))
scrollBarMax = Math.Max(scrollBarMax, this.scrollBarMaxHistory.Max());
if (!this.ScrollBar.MaxValue.Equals(scrollBarMax, Element.Epsilon)) {
// avoid a show/hide oscillation that occurs while updating our area with children that can lose height when the scroll bar is shown (like long paragraphs)
if (!this.scrollBarMaxHistory[0].Equals(this.scrollBarMaxHistory[2], Element.Epsilon) || !this.scrollBarMaxHistory[1].Equals(scrollBarMax, Element.Epsilon)) {
this.scrollBarMaxHistory[0] = this.scrollBarMaxHistory[1];
this.scrollBarMaxHistory[1] = this.scrollBarMaxHistory[2];
this.scrollBarMaxHistory[2] = scrollBarMax;

this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true;
}
this.scrollBarMaxHistory[0] = this.scrollBarMaxHistory[1];
this.scrollBarMaxHistory[1] = this.scrollBarMaxHistory[2];
this.scrollBarMaxHistory[2] = scrollBarMax;

this.ScrollBar.MaxValue = scrollBarMax;
this.relevantChildrenDirty = true;
}

// update child padding based on whether the scroll bar is visible
Expand Down Expand Up @@ -415,5 +418,12 @@ private void ScrollChildren() {
this.relevantChildrenDirty = true;
}

private void ResetScrollBarMaxHistory() {
if (this.scrollOverflow) {
for (var i = 0; i < this.scrollBarMaxHistory.Length; i++)
this.scrollBarMaxHistory[i] = -1;
}
}

}
}
33 changes: 29 additions & 4 deletions Sandbox/GameImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ protected override void LoadContent() {
//var font = new GenericBitmapFont(LoadContent<BitmapFont>("Fonts/Regular"));
var font = new GenericStashFont(system.GetFont(32));
var spriteFont = new GenericSpriteFont(MlemGame.LoadContent<SpriteFont>("Fonts/TestFont"));
this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
/*this.UiSystem.Style = new UntexturedStyle(this.SpriteBatch) {
Font = font,
TextScale = 0.5F,
PanelTexture = new NinePatch(new TextureRegion(tex, 0, 8, 24, 24), 8),
ButtonTexture = new NinePatch(new TextureRegion(tex, 24, 8, 16, 16), 4)
};
this.UiSystem.AutoScaleReferenceSize = new Point(1280, 720);
this.UiSystem.AutoScaleWithScreen = true;
this.UiSystem.GlobalScale = 5;
this.UiSystem.GlobalScale = 5;*/

/*this.OnDraw += (g, time) => {
const string strg = "This is a test string\nto test things\n\nMany things are being tested, like the ability\nfor this font to agree\n\nwith newlines";
Expand Down Expand Up @@ -398,7 +398,7 @@ protected override void LoadContent() {
Console.WriteLine("Buttons: " + string.Join(", ", GenericInput.AllButtons));
Console.WriteLine("Inputs: " + string.Join(", ", GenericInput.AllInputs));*/

var hsv = new Panel(Anchor.Center, new Vector2(100), true);
/*var hsv = new Panel(Anchor.Center, new Vector2(100), true);
var color = Color.Pink.ToHsv();
hsv.AddChild(new Paragraph(Anchor.AutoLeft, 1, "H"));
hsv.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 10), 5, 1) {
Expand All @@ -418,9 +418,27 @@ protected override void LoadContent() {
hsv.AddChild(new Group(Anchor.AutoLeft, new Vector2(1, 40), false) {
OnDrawn = (e, _, batch, _, _) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHsv(color))
});
this.UiSystem.Add("HSV", hsv);
this.UiSystem.Add("HSV", hsv);*/

var group = new SquishingGroup(Anchor.TopLeft, Vector2.One);
var root = this.UiSystem.Add("UI", group);

var centerGroup = new ScissorGroup(Anchor.TopCenter, Vector2.One);
var centerPanel = new Panel(Anchor.TopRight, Vector2.One);
centerPanel.DrawColor = Color.Red;
centerPanel.Padding = new MLEM.Maths.Padding(5);
centerGroup.AddChild(centerPanel);
group.AddChild(centerGroup);

this.listView = new Panel(Anchor.TopLeft, new Vector2(1, 1), false, true);
group.AddChild(this.listView);

var bottomPane = new Panel(Anchor.BottomCenter, new Vector2(1, 500));
group.AddChild(bottomPane);
}

private Panel listView;

protected override void DoUpdate(GameTime gameTime) {
base.DoUpdate(gameTime);
if (this.InputHandler.IsPressed(Keys.F11))
Expand All @@ -431,6 +449,13 @@ protected override void DoUpdate(GameTime gameTime) {
this.camera.Zoom(0.1F * Math.Sign(delta), this.InputHandler.ViewportMousePosition.ToVector2());
}

if (this.InputHandler.TryConsumePressed(Keys.Space)) {
var c = new Panel(Anchor.AutoLeft, new Vector2(1, 50), false);
c.DrawColor = Color.Green;
c.Padding = new MLEM.Maths.Padding(5);
this.listView.AddChild(c);
}

/*if (Input.InputsDown.Length > 0)
Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown));*/
/*if (MlemGame.Input.InputsPressed.Length > 0)
Expand Down
8 changes: 6 additions & 2 deletions Tests/TestGame.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Data.Content;
using MLEM.Font;
Expand All @@ -24,7 +27,6 @@ protected override void LoadContent() {

// make sure that the viewport is always the same size, since RunOneFrame doesn't ensure window size is correct
this.UiSystem.Viewport = new Rectangle(0, 0, 1280, 720);

// we use precompiled fonts and kni uses a different asset compilation system, so we just have both stored
this.UiSystem.Style.Font = new GenericSpriteFont(MlemGame.LoadContent<SpriteFont>(
#if KNI
Expand All @@ -33,6 +35,8 @@ protected override void LoadContent() {
"TestFont"
#endif
));
// make sure we catch a potential ui stack overflow as part of the tests by ensuring a sufficient execution stack
this.UiSystem.OnElementAreaUpdated += _ => RuntimeHelpers.EnsureSufficientExecutionStack();
}

public static TestGame Create() {
Expand Down
91 changes: 87 additions & 4 deletions Tests/UiTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Xna.Framework;
using MLEM.Maths;
using MLEM.Ui;
Expand Down Expand Up @@ -147,10 +148,9 @@ public void TestAutoAreaPerformanceRandom() {
}
}

// Stack overflow related to panel scrolling and scrollbar auto-hiding
[Test]
public void TestIssue27([Values(5, 50, 15)] int numChildren) {
// Stack overflow related to panel scrolling and scrollbar auto-hiding

var group = new SquishingGroup(Anchor.TopLeft, Vector2.One);

var centerGroup = new ScissorGroup(Anchor.TopCenter, Vector2.One);
Expand All @@ -172,7 +172,86 @@ public void TestIssue27([Values(5, 50, 15)] int numChildren) {
var bottomPane = new Panel(Anchor.BottomCenter, new Vector2(1, 500));
group.AddChild(bottomPane);

this.AddAndUpdate(group, out _, out _);
Assert.DoesNotThrow(() => this.AddAndUpdate(group, out _, out _));
}

// Removing and re-adding to a scrolling panel causes a stack overflow
[Test]
public void TestIssue29StackOverflow([Values(5, 50, 15)] int numChildren) {
var group = new SquishingGroup(Anchor.TopLeft, Vector2.One);

var centerGroup = new ScissorGroup(Anchor.TopCenter, Vector2.One);
var centerPanel = new Panel(Anchor.TopRight, Vector2.One);
centerPanel.DrawColor = Color.Red;
centerPanel.Padding = new MLEM.Maths.Padding(5);
centerGroup.AddChild(centerPanel);
group.AddChild(centerGroup);

var leftColumn = new SquishingGroup(Anchor.TopLeft, new Vector2(500, 1));
group.AddChild(leftColumn);
var namePanel = new Panel(Anchor.TopLeft, new Vector2(1, 50), true);
var test = new Panel(Anchor.TopCenter, new Vector2(1, 30));
test.DrawColor = Color.Red;
namePanel.AddChild(test);
var listView = new Panel(Anchor.TopLeft, new Vector2(1, 1), false, true);
leftColumn.AddChild(listView);
leftColumn.AddChild(namePanel);

var bottomPane = new Panel(Anchor.BottomCenter, new Vector2(1, 500));
group.AddChild(bottomPane);

Repopulate();
Assert.DoesNotThrow(() => this.AddAndUpdate(group, out _, out _));
Repopulate();
Assert.DoesNotThrow(() => UiTests.ForceUpdate(group, out _));

void Repopulate() {
listView.RemoveChildren();
for (var i = 0; i < numChildren; i++) {
var c = new Panel(Anchor.AutoLeft, new Vector2(1, 30));
c.DrawColor = Color.Green;
c.Padding = new MLEM.Maths.Padding(5);
listView.AddChild(c);
}
}
}

// Adding children causes the scroll bar to disappear when it shouldn't
[Test]
public void TestIssue29Inconsistencies() {
var group = new SquishingGroup(Anchor.TopLeft, Vector2.One);

var centerGroup = new ScissorGroup(Anchor.TopCenter, Vector2.One);
var centerPanel = new Panel(Anchor.TopRight, Vector2.One);
centerPanel.DrawColor = Color.Red;
centerPanel.Padding = new MLEM.Maths.Padding(5);
centerGroup.AddChild(centerPanel);
group.AddChild(centerGroup);

var listView = new Panel(Anchor.TopLeft, new Vector2(1, 1), false, true);
group.AddChild(listView);

var bottomPane = new Panel(Anchor.BottomCenter, new Vector2(1, 500));
group.AddChild(bottomPane);

Assert.DoesNotThrow(() => this.AddAndUpdate(group, out _, out _));

var appeared = false;
for (var i = 0; i < 100; i++) {
var c = new Panel(Anchor.AutoLeft, new Vector2(1, 50));
c.DrawColor = Color.Green;
c.Padding = new MLEM.Maths.Padding(5);
listView.AddChild(c);
Console.WriteLine($"Adding child, up to {i}");

Assert.DoesNotThrow(() => UiTests.ForceUpdate(group, out _));
if (appeared) {
Assert.False(listView.ScrollBar.IsHidden, $"Fail bar was hidden after {i} children");
} else if (!listView.ScrollBar.IsHidden) {
appeared = true;
}
}
Assert.True(appeared, "Scroll bar never appeared");
}

private void AddAndUpdate(Element element, out TimeSpan addTime, out TimeSpan updateTime) {
Expand All @@ -185,7 +264,11 @@ private void AddAndUpdate(Element element, out TimeSpan addTime, out TimeSpan up
stopwatch.Stop();
addTime = stopwatch.Elapsed;

stopwatch.Restart();
UiTests.ForceUpdate(element, out updateTime);
}

private static void ForceUpdate(Element element, out TimeSpan updateTime) {
var stopwatch = Stopwatch.StartNew();
element.ForceUpdateArea();
stopwatch.Stop();
updateTime = stopwatch.Elapsed;
Expand Down

0 comments on commit dbd52d5

Please sign in to comment.