diff --git a/CHANGELOG.md b/CHANGELOG.md index fea90c7..7316032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Additions Fixes - Fixed TextInput not working correctly when using surrogate pairs +### MLEM.Ui +Improvements +- Allow scrolling panels to contain other scrolling panels + ## 6.2.0 ### MLEM diff --git a/Demos/UiDemo.cs b/Demos/UiDemo.cs index a3381c0..934d010 100644 --- a/Demos/UiDemo.cs +++ b/Demos/UiDemo.cs @@ -223,6 +223,15 @@ namespace Demos { PositionOffset = new Vector2(0, 1) }); + var subPanel = this.root.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1, 25), Vector2.Zero, false, true) { + PositionOffset = new Vector2(0, 1), + Texture = null, + ChildPadding = Padding.Empty + }); + subPanel.AddChild(new Paragraph(Anchor.AutoLeft, 1, "This is a nested scrolling panel!")); + for (var i = 1; i <= 5; i++) + subPanel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 10), $"Button {i}") {PositionOffset = new Vector2(0, 1)}); + const string alignText = "Paragraphs can have left aligned text, right aligned text and center aligned text."; this.root.AddChild(new VerticalSpace(3)); var alignPar = this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, alignText)); diff --git a/MLEM.Ui/Elements/Panel.cs b/MLEM.Ui/Elements/Panel.cs index c40a565..526426d 100644 --- a/MLEM.Ui/Elements/Panel.cs +++ b/MLEM.Ui/Elements/Panel.cs @@ -61,6 +61,7 @@ namespace MLEM.Ui.Elements { private bool relevantChildrenDirty; private float scrollBarChildOffset; private StyleProp scrollBarOffset; + private float lastScrollOffset; /// /// Creates a new panel with the given settings. @@ -70,7 +71,7 @@ namespace MLEM.Ui.Elements { /// The panel's offset from its anchor point /// Whether the panel should automatically calculate its height based on its children's size /// Whether this panel should automatically add a scroll bar to scroll towards elements that are beyond the area this panel covers - /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling + /// Whether the scroll bar should be hidden automatically if the panel does not contain enough children to allow for scrolling. This only has an effect if is . public Panel(Anchor anchor, Vector2 size, Vector2 positionOffset, bool setHeightBasedOnChildren = false, bool scrollOverflow = false, bool autoHideScrollbar = true) : base(anchor, size) { this.PositionOffset = positionOffset; this.SetHeightBasedOnChildren = setHeightBasedOnChildren; @@ -278,9 +279,9 @@ namespace MLEM.Ui.Elements { // update child padding based on whether the scroll bar is visible var childOffset = this.ScrollBar.IsHidden ? 0 : this.ScrollerSize.Value.X + this.ScrollBarOffset; if (!this.scrollBarChildOffset.Equals(childOffset, Element.Epsilon)) { + // this implicitly sets our area dirty! this.ChildPadding += new Padding(0, -this.scrollBarChildOffset + childOffset, 0, 0); this.scrollBarChildOffset = childOffset; - this.SetAreaDirty(); } // the scroller height has the same relation to the scroll bar height as the visible area has to the total height of the panel's content @@ -288,15 +289,15 @@ namespace MLEM.Ui.Elements { this.ScrollBar.ScrollerSize = new Vector2(this.ScrollerSize.Value.X, Math.Max(this.ScrollerSize.Value.Y, scrollerHeight)); // update the render target - var targetArea = (Rectangle) this.GetRenderTargetArea(); - if (targetArea.Width <= 0 || targetArea.Height <= 0) { + var (_, _, width, height) = (Rectangle) this.GetRenderTargetArea(); + if (width <= 0 || height <= 0) { this.renderTarget?.Dispose(); this.renderTarget = null; return; } - if (this.renderTarget == null || targetArea.Width != this.renderTarget.Width || targetArea.Height != this.renderTarget.Height) { + if (this.renderTarget == null || width != this.renderTarget.Width || height != this.renderTarget.Height) { this.renderTarget?.Dispose(); - this.renderTarget = targetArea.IsEmpty ? null : new RenderTarget2D(this.System.Game.GraphicsDevice, targetArea.Width, targetArea.Height, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); + this.renderTarget = new RenderTarget2D(this.System.Game.GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); this.relevantChildrenDirty = true; } } @@ -328,7 +329,7 @@ namespace MLEM.Ui.Elements { } private RectangleF GetRenderTargetArea() { - var area = this.ChildPaddedArea; + var area = this.ChildPaddedArea.OffsetCopy(this.ScaledScrollOffset); area.X = this.DisplayArea.X; area.Width = this.DisplayArea.Width; return area; @@ -339,7 +340,8 @@ namespace MLEM.Ui.Elements { return; // we ignore false grandchildren so that the children of the scroll bar stay in place foreach (var child in this.GetChildren(c => c != this.ScrollBar, true, true)) - child.ScrollOffset.Y = -this.ScrollBar.CurrentValue; + child.ScrollOffset.Y += (this.lastScrollOffset - this.ScrollBar.CurrentValue); + this.lastScrollOffset = this.ScrollBar.CurrentValue; this.relevantChildrenDirty = true; } diff --git a/MLEM.Ui/Elements/ScrollBar.cs b/MLEM.Ui/Elements/ScrollBar.cs index 6fce5bc..0b2e2d7 100644 --- a/MLEM.Ui/Elements/ScrollBar.cs +++ b/MLEM.Ui/Elements/ScrollBar.cs @@ -158,7 +158,7 @@ namespace MLEM.Ui.Elements { if (this.isMouseScrolling) this.ScrollToPos(this.TransformInverseAll(this.Input.ViewportMousePosition.ToVector2())); if (!this.Horizontal) { - if (moused != null && (moused == this.Parent || moused.GetParentTree().Contains(this.Parent))) { + if (this.IsMousedForScrolling(moused)) { var scroll = this.Input.LastScrollWheel - this.Input.ScrollWheel; if (scroll != 0) this.CurrentValue += this.StepPerScroll * Math.Sign(scroll); @@ -244,6 +244,23 @@ namespace MLEM.Ui.Elements { this.SmoothScrollFactor = this.SmoothScrollFactor.OrStyle(style.ScrollBarSmoothScrollFactor); } + private bool IsMousedForScrolling(Element moused) { + if (moused == null || (moused != this.Parent && !moused.GetParentTree().Contains(this.Parent))) + return false; + // if we're moused, check if there are any scroll bars deeper than us that should take precedence + var foundMe = false; + foreach (var child in this.Parent.GetChildren(regardGrandchildren: true)) { + if (foundMe) { + if (child is ScrollBar b && !b.Horizontal && b.IsMousedForScrolling(moused)) + return false; + } else if (child == this) { + // once we found ourselves, all subsequent children are deeper/older! + foundMe = true; + } + } + return true; + } + /// /// A delegate method used for ///