Skip to content

Widgets & Nodes ​

Hex1b uses a two-layer architecture inspired by React: Widgets describe what to render, while Nodes manage state and perform actual rendering.

The Separation ​

LayerTypeMutabilityPurpose
WidgetrecordImmutableDescribes the desired UI
NodeclassMutableManages state, renders to terminal

This separation enables efficient reconciliation—Hex1b can diff widgets and update only what changed.

Widgets: The Declaration ​

Widgets are simple, immutable data structures:

csharp
// A widget holds configuration with fluent methods for event handlers
public record ButtonWidget(string Label) : Hex1bWidget
{
    public ButtonWidget OnClick(Action<ButtonClickedEventArgs> handler) => /* ... */;
    public ButtonWidget OnClick(Func<ButtonClickedEventArgs, Task> handler) => /* ... */;
}

public record TextBlockWidget(string Text) : Hex1bWidget;

public record VStackWidget(Hex1bWidget[] Children) : Hex1bWidget;

When you build your UI, you're constructing a tree of widgets:

csharp
buildWidget: (ctx, ct) => 
    new VStackWidget([
        new TextBlockWidget("Hello"),
        new ButtonWidget("Click").OnClick(_ => Console.WriteLine("Clicked!"))
    ])

Nodes: The Reality ​

Nodes are the actual objects that get measured, arranged, and rendered:

csharp
public class ButtonNode : Hex1bNode
{
    // Properties updated from widget during reconciliation
    public string Label { get; set; } = "";
    public Func<InputBindingActionContext, Task>? ClickAction { get; set; }
    
    // Mutable state preserved across reconciliations
    public bool IsFocused { get; set; }
    
    public override void Measure(Constraints constraints)
    {
        // Calculate how much space we need
        DesiredSize = new Size(Label.Length + 4, 1);
    }
    
    public override void Arrange(Rect rect)
    {
        // Position ourselves in the given rectangle
        Bounds = rect;
    }
    
    public override void Render(Hex1bRenderContext context)
    {
        // Draw to the terminal
        var style = IsFocused ? "[▶ " : "[ ";
        context.Write(Bounds.X, Bounds.Y, $"{style}{Label} ]");
    }
}

Reconciliation ​

When you call SetState(), Hex1b:

  1. Builds a new widget tree from your buildWidget function
  2. Diffs the new tree against the existing node tree
  3. Updates existing nodes with new properties, or creates new nodes
  4. Preserves mutable state (focus, scroll position, cursor) on reused nodes
Widget Tree (new)          Node Tree (existing)
     VStack          →          VStackNode
       │                            │
  ┌────┴────┐               ┌───────┴───────┐
Text    Button    →    TextNode      ButtonNode
"Hi"    "Save"         ↓ update        ↓ update
                       Text="Hi"       Label="Save"
                                       IsFocused=true ← preserved!

Why This Matters ​

  1. State Preservation: Focus doesn't jump around when the UI re-renders
  2. Performance: Only changed parts of the tree get updated
  3. Simplicity: You describe the UI declaratively; Hex1b figures out the transitions

Creating Custom Widgets ​

To add a custom widget:

1. Define the Widget ​

csharp
public record ProgressBarWidget(
    double Value,       // 0.0 to 1.0
    int Width = 20
) : Hex1bWidget;

2. Define the Node ​

csharp
public class ProgressBarNode : Hex1bNode
{
    public double Value { get; set; }
    public int BarWidth { get; set; }
    
    public override void Measure(Constraints constraints)
    {
        DesiredSize = new Size(BarWidth, 1);
    }
    
    public override void Arrange(Rect rect)
    {
        Bounds = rect;
    }
    
    public override void Render(Hex1bRenderContext context)
    {
        var filled = (int)(Value * BarWidth);
        var bar = new string('█', filled) + new string('░', BarWidth - filled);
        context.Write(Bounds.X, Bounds.Y, bar);
    }
}

3. Register Reconciliation ​

In Hex1bApp.Reconcile(), add a case:

csharp
ProgressBarWidget pb => ReconcileProgressBar(pb, existingNode),

And the reconcile method:

csharp
Hex1bNode ReconcileProgressBar(ProgressBarWidget widget, Hex1bNode? existing)
{
    var node = existing as ProgressBarNode ?? new ProgressBarNode();
    node.Value = widget.Value;
    node.BarWidth = widget.Width;
    return node;
}

Next Steps ​

Released under the MIT License.