feat: add mutable composite component lifecycle contract

- add generic child change events for dynamic composite components

- update runtime mount tracking to subscribe through the common contract

- cover third-party dynamic composite keybinding theme and context injection
This commit is contained in:
chuan
2026-06-04 03:47:00 +08:00
Unverified
parent 3154cc4ebf
commit 7c254fd9d7
5 changed files with 177 additions and 25 deletions
@@ -287,6 +287,39 @@ public sealed class TuiRuntimeInputListenerTests
Assert.Equal(1, component.SetThemeCount);
}
[Fact]
public void RuntimeInjectsSharedContextIntoDynamicMutableCompositeChildren()
{
var terminal = new TestTerminalSession();
var theme = new TestTheme("third-party-mutable");
var keybindings = KeybindingRegistry.CreateDefault();
keybindings.SetUserBinding(TuiKeybindings.InputSubmit, KeyNames.Tab);
using var runtime = new TuiRuntime(terminal, new DefaultInputParser(), new CountingRenderer(), keybindings, theme);
var composite = new ThirdPartyMutableComposite();
var input = new TinyTUI.Components.Input();
var themeComponent = new RecordingThemeComponent();
var contextComponent = new RecordingRuntimeContextComponent();
var submitted = false;
input.OnSubmitted = _ => submitted = true;
runtime.Add(composite);
runtime.SetFocus(input);
runtime.Start();
composite.Add(new ThirdPartyComposite(new Box(input), themeComponent, contextComponent));
terminal.Receive("\t");
composite.Remove(composite.Children[0]);
Assert.True(submitted);
Assert.Same(theme, themeComponent.Theme);
Assert.Equal(1, themeComponent.SetThemeCount);
Assert.Equal(1, contextComponent.MountedCount);
Assert.Equal(1, contextComponent.UnmountedCount);
Assert.Same(runtime, contextComponent.LastMountedContext?.Runtime);
Assert.Same(theme, contextComponent.LastMountedContext?.Theme);
Assert.Same(keybindings, contextComponent.LastMountedContext?.Keybindings);
}
[Fact]
public void RuntimeInjectedEditorKeybindingsReachAutocompleteList()
{
@@ -618,6 +651,47 @@ public sealed class TuiRuntimeInputListenerTests
public IEnumerable<IComponent> GetChildren() => Children;
}
private sealed class ThirdPartyMutableComposite : IMutableCompositeComponent
{
private readonly List<IComponent> _children = [];
public event EventHandler<CompositeComponentChildChangedEventArgs>? ChildAdded;
public event EventHandler<CompositeComponentChildChangedEventArgs>? ChildRemoved;
public IReadOnlyList<IComponent> Children => _children;
public void Add(IComponent component)
{
_children.Add(component);
ChildAdded?.Invoke(this, new CompositeComponentChildChangedEventArgs(component));
}
public void Remove(IComponent component)
{
if (_children.Remove(component))
ChildRemoved?.Invoke(this, new CompositeComponentChildChangedEventArgs(component));
}
public IReadOnlyList<string> Render(int width)
{
var lines = new List<string>();
foreach (var child in _children)
lines.AddRange(child.Render(width));
return lines;
}
public void Invalidate()
{
foreach (var child in _children)
child.Invalidate();
}
public IEnumerable<IComponent> GetChildren() => _children;
}
private sealed class TestTheme(string name) : ITuiTheme
{
public string Cursor => $"{name}> ";