diff --git a/TUI.Test/UIContainerBaseTests.cs b/TUI.Test/UIContainerBaseTests.cs index b398b24..e75f856 100644 --- a/TUI.Test/UIContainerBaseTests.cs +++ b/TUI.Test/UIContainerBaseTests.cs @@ -67,4 +67,54 @@ public void AMovedChildNoLongerInvalidatesItsPreviousContainer() Assert.AreEqual(0, firstInvalidations); } + + /// + /// A change deep in the tree reaches the root once, not once per route at every level + /// (ktsu-dev/TUI#148) + /// + [TestMethod] + public void AChangeNestedManyLevelsDeepInvalidatesTheRootOnce() + { + StackPanel root = []; + StackPanel current = root; + for (int i = 0; i < 15; i++) + { + StackPanel next = []; + current.AddChild(next); + current = next; + } + + TextElement leaf = new() { Text = "x" }; + current.AddChild(leaf); + + int count = 0; + root.Invalidated += (_, _) => count++; + + leaf.Text = "y"; + + Assert.AreEqual(1, count); + } + + /// + /// Each ancestor between the change and the root is invalidated exactly once + /// + [TestMethod] + public void AChangeInvalidatesEachAncestorOnce() + { + StackPanel root = []; + StackPanel middle = []; + TextElement leaf = new() { Text = "x" }; + root.AddChild(middle); + middle.AddChild(leaf); + + int rootCount = 0; + int middleCount = 0; + root.Invalidated += (_, _) => rootCount++; + middle.Invalidated += (_, _) => middleCount++; + + leaf.Text = "y"; + + Assert.AreEqual(1, middleCount); + Assert.AreEqual(1, rootCount); + } } diff --git a/TUI/Contracts/IUIElement.cs b/TUI/Contracts/IUIElement.cs index 5d257c2..cceda6d 100644 --- a/TUI/Contracts/IUIElement.cs +++ b/TUI/Contracts/IUIElement.cs @@ -67,8 +67,8 @@ public interface IUIElement public Dimensions CalculateRequiredDimensions(int availableWidth) => CalculateRequiredDimensions(); /// - /// Marks the element as changed since its last draw, notifies its parent, and raises the - /// invalidated event + /// Marks the element as changed since its last draw and raises the invalidated event, which is + /// how the containing parent hears about the change /// /// /// Rendering redraws every visible element on every pass, so this does not decide whether an diff --git a/TUI/Elements/UIElementBase.cs b/TUI/Elements/UIElementBase.cs index 96108aa..f281a7c 100644 --- a/TUI/Elements/UIElementBase.cs +++ b/TUI/Elements/UIElementBase.cs @@ -103,10 +103,14 @@ public virtual void Render(IConsoleProvider provider) public virtual Dimensions CalculateRequiredDimensions(int availableWidth) => OnCalculateRequiredDimensions(availableWidth); /// + /// + /// The parent hears about this through , which every container + /// subscribes to when it adopts a child. Calling the parent directly as well would reach it by + /// two routes, doubling the count at every level (ktsu-dev/TUI#148). + /// public void Invalidate() { IsDirty = true; - Parent?.Invalidate(); Invalidated?.Invoke(this, EventArgs.Empty); }