Skip to content

Commit d05f6c1

Browse files
committed
Add SliderControl and RangeSliderControl with end-caps, fix focus in nested containers
SliderControl: single-value slider with horizontal/vertical orientation, keyboard (arrows, Home/End, PageUp/Down), mouse drag with absolute positioning, step/large-step, value and min/max labels, theme integration, end-cap characters on track ends. RangeSliderControl: dual-thumb range slider with MinRange enforcement, Tab to switch active thumb (Low→High handled internally, High→exits control), push behavior when values cross, range events. Bug fixes: - ScrollablePanel: Tab navigation now discovers focusable controls inside containers with CanReceiveFocus=false (e.g. HorizontalGrid) via recursive CanChildReceiveFocus - ScrollablePanel: NotifyChildFocusChanged maps deeply nested controls back to their direct-child container via FindDirectChildContaining, preventing Tab position reset - ScrollablePanel: saved focused child reference before ProcessKey delegation to handle notification chains that clear _focusedChild during container exit - HorizontalGrid mouse hit-testing: use ActualWidth instead of GetContentWidth for correct click detection on Flex columns - HorizontalGrid FocusChanged: guard against re-entrant null _focusedContent - MarkupControl MeasureDOM: guard against invalid constraints during resize - NavigationView compact mode: show first letter of item text when no icon is set New files: SliderControl (3 partials), RangeSliderControl (3 partials), SliderRenderingHelper, SliderBuilder, RangeSliderBuilder, SliderDemoWindow, SliderControlTests (69 tests), 9 focus navigation tests, docs.
1 parent 356e08f commit d05f6c1

31 files changed

Lines changed: 5008 additions & 21 deletions

Examples/DemoApp/DemoWindows/LauncherWindow.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public static Window Create(ConsoleWindowSystem ws)
3737
.AddItem("Markup Syntax", subtitle: "Rich markup system demo", content: MakeInfoPanel("Markup Syntax"))
3838
.AddItem("International & Emoji", subtitle: "Unicode & emoji support", content: MakeInfoPanel("International & Emoji"))
3939
.AddItem("Data Binding", subtitle: "MVVM data binding", content: MakeInfoPanel("Data Binding"))
40-
.AddItem("Date & Time", subtitle: "DatePicker and TimePicker controls", content: MakeInfoPanel("Date & Time")))
40+
.AddItem("Date & Time", subtitle: "DatePicker and TimePicker controls", content: MakeInfoPanel("Date & Time"))
41+
.AddItem("Slider", subtitle: "Value and range slider controls", content: MakeInfoPanel("Slider")))
4142
.AddHeader("Data Visualization", Color.Yellow, header => header
4243
.AddItem("Graphs & Charts", subtitle: "Live sparklines & bar graphs", content: MakeInfoPanel("Graphs & Charts"))
4344
.AddItem("System Monitor", subtitle: "Real-time system dashboard", content: MakeInfoPanel("System Monitor")))
@@ -134,6 +135,7 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
134135
"International & Emoji" => InternationalWindow.Create(ws),
135136
"Data Binding" => DataBindingWindow.Create(ws),
136137
"Date & Time" => DateTimeDemo.Create(ws),
138+
"Slider" => SliderDemoWindow.Create(ws),
137139
"Graphs & Charts" => GraphsWindow.Create(ws),
138140
"System Monitor" => SystemMonitorWindow.Create(ws),
139141
"Container Backgrounds" => ContainerBgDemoWindow.Create(ws),
@@ -603,6 +605,25 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
603605
"[dim]Controls used:[/]",
604606
" - TerminalControl (PTY)",
605607
},
608+
"Slider" => new List<string>
609+
{
610+
"[bold cyan]Slider Controls[/]",
611+
"",
612+
"Single-value sliders and dual-thumb range sliders",
613+
"with keyboard and mouse interaction.",
614+
"",
615+
"[dim]Features:[/]",
616+
" - Horizontal and vertical orientations",
617+
" - Step and large step increments",
618+
" - Value labels and min/max labels",
619+
" - Mouse drag and click-to-jump",
620+
" - RangeSlider with MinRange constraint",
621+
" - Tab to switch active thumb (range)",
622+
"",
623+
"[dim]Controls used:[/]",
624+
" - SliderControl, RangeSliderControl",
625+
" - MarkupControl, HorizontalGridControl",
626+
},
606627
"Welcome Banner" => new List<string>
607628
{
608629
"[bold cyan]Welcome Banner[/]",
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// -----------------------------------------------------------------------
2+
// ConsoleEx - A simple console window system for .NET Core
3+
//
4+
// Author: Nikolaos Protopapas
5+
// Email: nikolaos.protopapas@gmail.com
6+
// License: MIT
7+
// -----------------------------------------------------------------------
8+
9+
using SharpConsoleUI;
10+
using SharpConsoleUI.Builders;
11+
using SharpConsoleUI.Controls;
12+
using SharpConsoleUI.Layout;
13+
using SharpConsoleUI.Helpers;
14+
using SharpConsoleUI.Rendering;
15+
16+
namespace DemoApp.DemoWindows;
17+
18+
public static class SliderDemoWindow
19+
{
20+
private const int WindowWidth = 80;
21+
private const int WindowHeight = 32;
22+
23+
public static Window Create(ConsoleWindowSystem ws)
24+
{
25+
// --- Section 1: Volume (basic horizontal slider) ---
26+
27+
var volumeStatus = Controls.Markup("[dim]Volume: 50[/]")
28+
.WithMargin(1, 0, 1, 0)
29+
.Build();
30+
31+
var volumeSlider = Controls.Slider()
32+
.WithRange(0, 100)
33+
.WithValue(50)
34+
.ShowValueLabel()
35+
.WithMargin(1, 0, 1, 1)
36+
.OnValueChanged((s, val) =>
37+
{
38+
volumeStatus.SetContent(new List<string>
39+
{
40+
$"[dim]Volume: [bold cyan]{val:F0}[/][/]"
41+
});
42+
})
43+
.Build();
44+
45+
// --- Section 2: Brightness (Step 5) ---
46+
47+
var brightnessSlider = Controls.Slider()
48+
.WithRange(0, 100)
49+
.WithValue(50)
50+
.WithStep(5)
51+
.ShowMinMaxLabels()
52+
.WithMargin(1, 0, 1, 1)
53+
.Build();
54+
55+
// --- Section 3: Custom Colors ---
56+
57+
var customColorSlider = Controls.Slider()
58+
.WithRange(0, 100)
59+
.WithValue(30)
60+
.WithTrackColor(Color.Green)
61+
.WithFilledTrackColor(Color.Red)
62+
.WithThumbColor(Color.Magenta1)
63+
.ShowValueLabel()
64+
.WithMargin(1, 0, 1, 1)
65+
.Build();
66+
67+
// --- Section 4: Vertical Sliders (Bass / Treble) ---
68+
69+
var bassSlider = Controls.Slider()
70+
.Vertical()
71+
.WithRange(0, 100)
72+
.WithValue(60)
73+
.WithHeight(10)
74+
.ShowValueLabel()
75+
.WithMargin(1, 0, 1, 0)
76+
.Build();
77+
78+
var trebleSlider = Controls.Slider()
79+
.Vertical()
80+
.WithRange(0, 100)
81+
.WithValue(40)
82+
.WithHeight(10)
83+
.ShowValueLabel()
84+
.WithMargin(1, 0, 1, 0)
85+
.Build();
86+
87+
var verticalGrid = Controls.HorizontalGrid()
88+
.Column(col => col.Flex()
89+
.Add(Controls.Header("Bass"))
90+
.Add(bassSlider))
91+
.Column(col => col.Flex()
92+
.Add(Controls.Header("Treble"))
93+
.Add(trebleSlider))
94+
.WithAlignment(HorizontalAlignment.Stretch)
95+
.WithMargin(1, 0, 1, 1)
96+
.Build();
97+
98+
// --- Section 5: Price Range (RangeSlider) ---
99+
100+
var priceStatus = Controls.Markup("[dim]Range: $0 - $1000[/]")
101+
.WithMargin(1, 0, 1, 0)
102+
.Build();
103+
104+
var priceRange = Controls.RangeSlider()
105+
.WithRange(0, 1000)
106+
.WithValues(200, 800)
107+
.WithStep(10)
108+
.ShowValueLabel()
109+
.WithMargin(1, 0, 1, 1)
110+
.OnRangeChanged((s, range) =>
111+
{
112+
priceStatus.SetContent(new List<string>
113+
{
114+
$"[dim]Range: [bold green]${range.Low:F0}[/] - [bold green]${range.High:F0}[/][/]"
115+
});
116+
})
117+
.Build();
118+
119+
// --- Section 6: Constrained Range ---
120+
121+
var constrainedRange = Controls.RangeSlider()
122+
.WithRange(0, 100)
123+
.WithValues(30, 70)
124+
.WithMinRange(20)
125+
.ShowMinMaxLabels()
126+
.WithMargin(1, 0, 1, 1)
127+
.Build();
128+
129+
// --- Layout ---
130+
131+
var panel = Controls.ScrollablePanel()
132+
.AddControl(Controls.Header("Volume"))
133+
.AddControl(Controls.Markup("[dim]Basic horizontal slider 0-100[/]").WithMargin(1, 0, 1, 0).Build())
134+
.AddControl(volumeSlider)
135+
.AddControl(volumeStatus)
136+
.AddControl(Controls.Rule(""))
137+
.AddControl(Controls.Header("Brightness (Step 5)"))
138+
.AddControl(Controls.Markup("[dim]Slider with step=5, showing min/max labels[/]").WithMargin(1, 0, 1, 0).Build())
139+
.AddControl(brightnessSlider)
140+
.AddControl(Controls.Rule(""))
141+
.AddControl(Controls.Header("Custom Colors"))
142+
.AddControl(Controls.Markup("[dim]Track=Green, Filled=Red, Thumb=Magenta[/]").WithMargin(1, 0, 1, 0).Build())
143+
.AddControl(customColorSlider)
144+
.AddControl(Controls.Rule(""))
145+
.AddControl(Controls.Header("Vertical Sliders"))
146+
.AddControl(verticalGrid)
147+
.AddControl(Controls.Rule(""))
148+
.AddControl(Controls.Header("Price Range"))
149+
.AddControl(Controls.Markup("[dim]RangeSlider 0-1000, step=10[/]").WithMargin(1, 0, 1, 0).Build())
150+
.AddControl(priceRange)
151+
.AddControl(priceStatus)
152+
.AddControl(Controls.Rule(""))
153+
.AddControl(Controls.Header("Constrained Range"))
154+
.AddControl(Controls.Markup("[dim]MinRange=20, cannot narrow below 20 units[/]").WithMargin(1, 0, 1, 0).Build())
155+
.AddControl(constrainedRange)
156+
.WithVerticalAlignment(VerticalAlignment.Fill)
157+
.Build();
158+
159+
var statusBar = Controls.Markup("[dim]Arrow keys: adjust | Tab: next slider | Esc: close[/]")
160+
.StickyBottom()
161+
.Build();
162+
163+
var gradient = ColorGradient.FromColors(
164+
new Color(10, 45, 30),
165+
new Color(25, 60, 55),
166+
new Color(15, 35, 50));
167+
168+
return new WindowBuilder(ws)
169+
.WithTitle("Slider Controls")
170+
.WithSize(WindowWidth, WindowHeight)
171+
.Centered()
172+
.WithBackgroundGradient(gradient, GradientDirection.Vertical)
173+
.AddControls(panel, statusBar)
174+
.OnKeyPressed((sender, e) =>
175+
{
176+
if (e.KeyInfo.Key == ConsoleKey.Escape)
177+
{
178+
ws.CloseWindow((Window)sender!);
179+
e.Handled = true;
180+
}
181+
})
182+
.BuildAndShow();
183+
}
184+
}

ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ SharpConsoleUI is actively maintained and driven by real-world usage in producti
1717
- TimePicker — locale-aware time selection with 12h/24h modes, optional seconds, AM/PM
1818
- HorizontalSplitterControl — drag-to-resize horizontal bar between vertically stacked controls, mouse and keyboard support, auto-hide on invisible neighbors
1919
- StatusBarControl — per-window status bar with left/center/right zones, clickable shortcut+label items, markup support, optional above line, theme integration
20+
- LineGraphControl — multi-series line graphs with braille (2×4 pixel grid) and ASCII rendering modes, color gradients, Y-axis labels, live data updates
21+
- SliderControl / RangeSliderControl — horizontal and vertical value sliders with keyboard, mouse drag, step/large-step, min/max labels, and dual-thumb range selection with MinRange enforcement
2022

2123
## Next
2224

23-
- **Slider / RangeControl** — horizontal and vertical value sliders
2425
- **Instant input response** — replace polling-based input loop with event-driven wake for zero-latency keypress handling
2526

2627
## Later

0 commit comments

Comments
 (0)