feat(tui): add fluent declarative UI DSL for SquidStd.Tui#15
Merged
Conversation
- Add BindMode enum (OneWay/TwoWay) under SquidStd.Tui.Types.Tui - Add StackOrientation enum (Vertical/Horizontal) under SquidStd.Tui.Types.Tui - Add abstract TuiNode<TViewModel> base class under SquidStd.Tui.Dsl - Add LabelNode<TViewModel>: one-way string property binding - Add TextFieldNode<TViewModel>: string property binding with BindMode - Add ButtonNode<TViewModel>: caption + ICommand expression binding - Add StackNode<TViewModel>: orientation + typed children list - Add TuiNodeTests covering all four node types (4 tests, all green)
… views - add TuiNodeMaterializer in SquidStd.Tui.Dsl; walks TuiNode<TViewModel> trees and produces View graphs - wire LabelNode via binder.OneWayText, TextFieldNode via TwoWay/OneWay binder overloads, ButtonNode via binder.Command - StackNode (Vertical/Horizontal) produces a container View using Pos.Bottom/Pos.Right and Dim.Fill for layout - TViewModel constrained to INotifyPropertyChanged so existing binder overloads resolve without new binding logic - add TuiNodeMaterializerTests covering VStack+Label one-way propagation and TextField two-way VM→widget; both pass headless - outer test class marked partial because nested ObservableObject partial class requires it (CommunityToolkit source generator)
Comment on lines
+69
to
+88
| foreach (var childNode in stack.Children) | ||
| { | ||
| var child = Materialize(childNode, viewModel, binder); | ||
|
|
||
| if (stack.Orientation == StackOrientation.Vertical) | ||
| { | ||
| child.X = 0; | ||
| child.Y = previous is null ? 0 : Pos.Bottom(previous); | ||
| child.Width = Dim.Fill(); | ||
| } | ||
| else | ||
| { | ||
| child.X = previous is null ? 0 : Pos.Right(previous); | ||
| child.Y = 0; | ||
| child.Height = Dim.Fill(); | ||
| } | ||
|
|
||
| container.Add(child); | ||
| previous = child; | ||
| } |
| private sealed partial class SampleViewModel : ObservableObject | ||
| { | ||
| [ObservableProperty] | ||
| private string _title = "start"; |
| private string _title = "start"; | ||
|
|
||
| [ObservableProperty] | ||
| private string _name = string.Empty; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A type-safe fluent C# DSL (markup-in-code) for building declarative UIs in
SquidStd.Tui, where one node = widget + binding. Reduces the imperativeBuildLayout+Bindboilerplate while keeping full type-safety (typed lambdas, no magic strings) and KISS.Architecture
Dsl/TuiNode<TVm>+LabelNode/TextFieldNode/ButtonNode/StackNode) — immutable, Terminal.Gui-free, fully unit-tested as plain data.UiFactory<TVm>— builds nodes from typed lambdas; fixesTVmper instance soUi.Label(x => x.Value)infers without annotations.TuiNodeMaterializer— the only Terminal.Gui-coupled piece; walks the tree and wires bindings exclusively through the existingViewBinder(no new binding logic, no extra subscriptions).TuiComposedView<TVm>— declarative view base: implementCompose()and return a node tree. Integrated via a new non-breakingOnInitialize(ViewBinder)hook onTuiView; imperativeTuiView<TVm>is unchanged.BindMode.OneWayoverrides a TextField. Enums underTypes.Tui.Usage
Notes
ViewBinder, disposed with the view (identical lifecycle to the imperative path).samples/SquidStd.Samples.Tuiported to the DSL; README documents both styles.Test plan
dotnet runof the DSL Counter sample in a real terminal (interactive render)