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);
}