Skip to content

Commit bffca77

Browse files
committed
Add multi-line wrapping mode to CommandBar
1 parent 37192da commit bffca77

File tree

5 files changed

+340
-59
lines changed

5 files changed

+340
-59
lines changed

samples/ControlsDemo/Demos/CommandBarDemo.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using XenoAtom.Terminal.UI;
33
using XenoAtom.Terminal.UI.Commands;
44
using XenoAtom.Terminal.UI.Controls;
5+
using XenoAtom.Terminal.UI.Geometry;
56
using XenoAtom.Terminal.UI.Input;
67

78
namespace XenoAtom.Terminal.UI.ControlsDemo.Demos;
@@ -19,20 +20,26 @@ public override Visual Build(DemoContext context)
1920

2021
var counter = new State<int>(0);
2122
var enabled = new State<bool>(true);
23+
var multiLine = new State<bool>(false);
2224

2325
var editor = new TextBox().Placeholder("Focus me to populate the command bar…");
2426
editor.AutoFocus(true);
27+
var commandBar = new CommandBar()
28+
.MultiLine(() => multiLine.Value)
29+
.MaxWidth(34);
2530

2631
var root = new VStack(
2732
DemoUi.Hint("CommandBar surfaces commands registered on the focused visual (and its parents), plus app-level commands."),
2833
editor,
2934
new HStack(
3035
new Button("Increment").Click(() => counter.Value++),
31-
new CheckBox("Enabled").IsChecked(enabled))
36+
new CheckBox("Enabled").IsChecked(enabled),
37+
new CheckBox("Multi-line bar").IsChecked(multiLine))
3238
.Spacing(2),
3339
new TextBlock(() => $"Counter: {counter.Value}"),
40+
DemoUi.Hint("The command bar below is width-limited to make clipping vs wrapping easy to compare."),
3441
new Rule(),
35-
new CommandBar())
42+
new Border(commandBar).Padding(new Thickness(1, 0, 1, 0)))
3643
.Spacing(1);
3744

3845
root.AddCommand(new Command
@@ -56,7 +63,26 @@ public override Visual Build(DemoContext context)
5663
Execute = _ => counter.Value = 0,
5764
});
5865

66+
root.AddCommand(new Command
67+
{
68+
Id = "Demo.Randomize",
69+
LabelMarkup = "Randomize value",
70+
Gesture = new KeyGesture(TerminalChar.CtrlD, TerminalModifiers.Ctrl),
71+
Importance = CommandImportance.Primary,
72+
Presentation = CommandPresentation.CommandBar,
73+
Execute = _ => counter.Value = (counter.Value + 7) % 10,
74+
});
75+
76+
root.AddCommand(new Command
77+
{
78+
Id = "Demo.Export",
79+
LabelMarkup = "Export snapshot",
80+
Gesture = new KeyGesture(TerminalChar.CtrlS, TerminalModifiers.Ctrl),
81+
Importance = CommandImportance.Secondary,
82+
Presentation = CommandPresentation.CommandBar,
83+
Execute = _ => { },
84+
});
85+
5986
return root;
6087
}
6188
}
62-

site/docs/controls/commandbar.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: CommandBar
44

55
# CommandBar
66

7-
`CommandBar` displays a single-row “key hints” strip for the current focus context.
7+
`CommandBar` displays a “key hints” strip for the current focus context.
88

99
It collects `Command` instances registered on the focused visual (and its parents) plus app-level commands, then renders
1010
them as a sequence of keycaps and labels.
@@ -31,9 +31,24 @@ var root = new DockLayout()
3131
Terminal.Run(root);
3232
```
3333

34+
## Multi-line wrapping
35+
36+
By default, `CommandBar` keeps the existing single-row behavior and clips entries that do not fit.
37+
38+
If you want commands to wrap onto additional rows instead, enable `MultiLine`:
39+
40+
```csharp
41+
var bar = new CommandBar()
42+
.MultiLine(true);
43+
```
44+
45+
In multi-line mode, a command entry is moved to the next row when it does not fit in the remaining space on the current row.
46+
The default remains `false`.
47+
3448
## Defaults
3549

3650
- Default alignment: `HorizontalAlignment = Align.Start`, `VerticalAlignment = Align.Start`
51+
- `MultiLine = false`
3752

3853
## Styling
3954
Use `CommandBarStyle` to change bar/keycap colors:

site/docs/specs/controls/commandbar.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This file intentionally focuses on *implementation-specific* notes for `CommandB
1818

1919
## Goals
2020

21-
- Provide a lightweight, single-row “key hints” surface intended for app chrome (footer/header).
21+
- Provide a lightweight “key hints” surface intended for app chrome (footer/header).
2222
- Surface commands relevant to the current focus context and app/global commands.
2323
- Stay allocation-conscious and compatible with the binding dirty model.
2424

@@ -36,9 +36,14 @@ This file intentionally focuses on *implementation-specific* notes for `CommandB
3636

3737
## Layout & rendering (current behavior)
3838

39-
- The control measures to a single row (`Height = 1`) and to the *current* content width (focused context), while allowing clipping when the available width is smaller.
40-
- Render always clears the entire bar row using `CommandBarStyle.Resolve(theme).BarStyle` (chrome must not “inherit” background/attributes from content behind it).
41-
- When a key sequence prefix is active (`TerminalApp.PendingCommandSequenceCount > 0`), `CommandBar` renders the pending prefix + `` before regular entries.
39+
- `MultiLine = false` is the default and preserves the existing single-row behavior:
40+
- the control measures to a single row (`Height = 1`) and to the *current* content width (focused context), while allowing clipping when the available width is smaller
41+
- render clears the entire bar row using `CommandBarStyle.Resolve(theme).BarStyle` (chrome must not “inherit” background/attributes from content behind it)
42+
- `MultiLine = true` allows wrapped layout:
43+
- the control may measure to multiple rows when the available width is bounded
44+
- command entries wrap as atomic units; if an entry does not fit in the remaining space on the current row, it starts on the next row
45+
- separators are not rendered at the start of a wrapped row
46+
- In both modes, when a key sequence prefix is active (`TerminalApp.PendingCommandSequenceCount > 0`), `CommandBar` renders the pending prefix + `` before regular entries.
4247

4348
## Command collection
4449

@@ -58,6 +63,11 @@ This file intentionally focuses on *implementation-specific* notes for `CommandB
5863
- separator text
5964
- Labels are rendered from `Command.LabelMarkup` via `MarkupTextParser` using the current theme markup styles.
6065

66+
## Public API
67+
68+
- `Presentation : CommandPresentation`
69+
- `MultiLine : bool` (default `false`)
70+
6171
## Future ideas
6272

6373
- Optional “More…” entry that opens a help/palette surface when there is insufficient space.

src/XenoAtom.Terminal.UI.Tests/CommandBarRenderingTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the BSD-Clause 2 license.
33
// See license.txt file in the project root for full license information.
44

5+
using System.Linq;
56
using XenoAtom.Terminal.UI.Commands;
67
using XenoAtom.Terminal.UI.Controls;
78
using XenoAtom.Terminal.UI.Hosting;
@@ -39,6 +40,68 @@ public void CommandBar_Renders_Local_And_Global_Commands()
3940
StringAssert.Contains(outText, "Probe");
4041
}
4142

43+
[TestMethod]
44+
public void CommandBar_Default_Mode_Remains_Single_Row_When_Commands_Do_Not_Fit()
45+
{
46+
var probe = new WrappingProbe();
47+
var bar = new CommandBar();
48+
var layout = new DockLayout { Content = probe, Bottom = bar };
49+
50+
using var driver = new TerminalAppTestDriver(layout, TerminalHostKind.Fullscreen, new TerminalSize(20, 6));
51+
driver.App.Focus(probe);
52+
driver.Tick();
53+
54+
Assert.AreEqual(1, bar.Bounds.Height);
55+
56+
var lines = GetScreenLines(driver, 20, 6);
57+
var renderedCommandRows = lines.Count(static line => line.Contains("Alpha", StringComparison.Ordinal) || line.Contains("Beta", StringComparison.Ordinal) || line.Contains("Gamma", StringComparison.Ordinal));
58+
Assert.AreEqual(1, renderedCommandRows);
59+
}
60+
61+
[TestMethod]
62+
public void CommandBar_MultiLine_Wraps_Commands_To_Additional_Rows()
63+
{
64+
var probe = new WrappingProbe();
65+
var bar = new CommandBar().MultiLine(true);
66+
var layout = new DockLayout { Content = probe, Bottom = bar };
67+
68+
using var driver = new TerminalAppTestDriver(layout, TerminalHostKind.Fullscreen, new TerminalSize(20, 6));
69+
driver.App.Focus(probe);
70+
driver.Tick();
71+
72+
Assert.IsTrue(bar.Bounds.Height > 1, $"Expected wrapped command bar to request multiple rows, actual height={bar.Bounds.Height}.");
73+
74+
var lines = GetScreenLines(driver, 20, 6);
75+
var alphaRow = FindLine(lines, "Alpha");
76+
var betaRow = FindLine(lines, "Beta");
77+
var gammaRow = FindLine(lines, "Gamma");
78+
79+
Assert.IsTrue(alphaRow >= 0, "Expected Alpha to render.");
80+
Assert.IsTrue(betaRow >= 0, "Expected Beta to render.");
81+
Assert.IsTrue(gammaRow >= 0, "Expected Gamma to render.");
82+
Assert.IsTrue(alphaRow != betaRow || betaRow != gammaRow, "Expected wrapped commands to span multiple rows.");
83+
}
84+
85+
private static string[] GetScreenLines(TerminalAppTestDriver driver, int width, int height)
86+
{
87+
var screen = new AnsiTestScreen(width, height);
88+
screen.Apply(driver.Backend.GetOutText());
89+
return screen.GetText().Split('\n');
90+
}
91+
92+
private static int FindLine(string[] lines, string text)
93+
{
94+
for (var i = 0; i < lines.Length; i++)
95+
{
96+
if (lines[i].Contains(text, StringComparison.Ordinal))
97+
{
98+
return i;
99+
}
100+
}
101+
102+
return -1;
103+
}
104+
42105
private sealed class CommandProbe : Visual
43106
{
44107
public CommandProbe()
@@ -61,4 +124,39 @@ protected override void RenderOverride(Rendering.CellBuffer buffer)
61124
{
62125
}
63126
}
127+
128+
private sealed class WrappingProbe : Visual
129+
{
130+
public WrappingProbe()
131+
{
132+
Focusable = true;
133+
AddCommand(new Command
134+
{
135+
Id = "alpha",
136+
LabelMarkup = "Alpha",
137+
Gesture = new KeyGesture(TerminalChar.CtrlA, TerminalModifiers.Ctrl),
138+
Execute = _ => { },
139+
});
140+
AddCommand(new Command
141+
{
142+
Id = "beta",
143+
LabelMarkup = "Beta",
144+
Gesture = new KeyGesture(TerminalChar.CtrlB, TerminalModifiers.Ctrl),
145+
Execute = _ => { },
146+
});
147+
AddCommand(new Command
148+
{
149+
Id = "gamma",
150+
LabelMarkup = "Gamma",
151+
Gesture = new KeyGesture(TerminalChar.CtrlG, TerminalModifiers.Ctrl),
152+
Execute = _ => { },
153+
});
154+
}
155+
156+
protected override SizeHints MeasureCore(in LayoutConstraints constraints) => SizeHints.Fixed(constraints.Clamp(new Geometry.Size(10, 1)));
157+
158+
protected override void RenderOverride(Rendering.CellBuffer buffer)
159+
{
160+
}
161+
}
64162
}

0 commit comments

Comments
 (0)