Skip to content

Commit d071b16

Browse files
committed
Add DatePicker and TimePicker controls with locale support and calendar popup
DatePickerControl: segmented date editing (month/day/year) with culture-aware format parsing, calendar popup via portal system, min/max date constraints, digit entry with auto-advance, and full keyboard/mouse support. TimePickerControl: segmented time editing with configurable 12h/24h modes, optional seconds, AM/PM toggle, culture-derived separators and designators, min/max time constraints, and digit entry with auto-advance. Both controls include fluent builders, theme integration (17 new ITheme properties), ColorResolver helpers, 155 unit tests, a DemoApp showcase window, and per-control documentation. Also fixes PortalContentBase to clip inner content to border bounds, preventing portal content from overwriting border characters.
1 parent 8930d3a commit d071b16

28 files changed

Lines changed: 6133 additions & 4 deletions
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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 System.Globalization;
10+
using SharpConsoleUI;
11+
using SharpConsoleUI.Builders;
12+
using SharpConsoleUI.Controls;
13+
using SharpConsoleUI.Helpers;
14+
using SharpConsoleUI.Layout;
15+
using SharpConsoleUI.Rendering;
16+
17+
namespace DemoApp.DemoWindows;
18+
19+
public static class DateTimeDemo
20+
{
21+
private const int WindowWidth = 100;
22+
private const int WindowHeight = 35;
23+
private const int LeftColumnWidth = 50;
24+
25+
public static Window Create(ConsoleWindowSystem ws)
26+
{
27+
var statusMarkup = Controls.Markup()
28+
.AddLines("[bold cyan]Status[/]", "", "[dim]Select a date or time to see values here[/]")
29+
.WithMargin(1, 1, 1, 1)
30+
.Build();
31+
32+
// --- Date Pickers ---
33+
34+
var dateIso = Controls.DatePicker("ISO:")
35+
.WithFormat("yyyy-MM-dd")
36+
.WithSelectedDate(DateTime.Today)
37+
.WithMargin(1, 0, 1, 1)
38+
.Build();
39+
40+
var dateUs = Controls.DatePicker("US:")
41+
.WithFormat("MM/dd/yyyy")
42+
.WithCulture(new CultureInfo("en-US"))
43+
.WithSelectedDate(DateTime.Today)
44+
.WithMargin(1, 0, 1, 1)
45+
.Build();
46+
47+
var dateEu = Controls.DatePicker("EU:")
48+
.WithFormat("dd.MM.yyyy")
49+
.WithCulture(new CultureInfo("de-DE"))
50+
.WithSelectedDate(DateTime.Today)
51+
.WithMargin(1, 0, 1, 1)
52+
.Build();
53+
54+
var dateConstrained = Controls.DatePicker("Range:")
55+
.WithFormat("yyyy-MM-dd")
56+
.WithMinDate(new DateTime(2020, 1, 1))
57+
.WithMaxDate(new DateTime(2030, 12, 31))
58+
.WithSelectedDate(DateTime.Today)
59+
.WithMargin(1, 0, 1, 1)
60+
.Build();
61+
62+
// --- Time Pickers ---
63+
64+
var time24 = Controls.TimePicker("24h:")
65+
.With24HourFormat()
66+
.WithSelectedTime(new TimeSpan(14, 30, 0))
67+
.WithMargin(1, 0, 1, 1)
68+
.Build();
69+
70+
var time24Sec = Controls.TimePicker("24h+s:")
71+
.With24HourFormat()
72+
.WithSeconds()
73+
.WithSelectedTime(new TimeSpan(14, 30, 45))
74+
.WithMargin(1, 0, 1, 1)
75+
.Build();
76+
77+
var time12 = Controls.TimePicker("12h:")
78+
.With12HourFormat()
79+
.WithCulture(new CultureInfo("en-US"))
80+
.WithSelectedTime(new TimeSpan(14, 30, 0))
81+
.WithMargin(1, 0, 1, 1)
82+
.Build();
83+
84+
var time12Sec = Controls.TimePicker("12h+s:")
85+
.With12HourFormat()
86+
.WithCulture(new CultureInfo("en-US"))
87+
.WithSeconds()
88+
.WithSelectedTime(new TimeSpan(9, 15, 30))
89+
.WithMargin(1, 0, 1, 1)
90+
.Build();
91+
92+
var timeConstrained = Controls.TimePicker("Work:")
93+
.With24HourFormat()
94+
.WithMinTime(new TimeSpan(9, 0, 0))
95+
.WithMaxTime(new TimeSpan(17, 0, 0))
96+
.WithSelectedTime(new TimeSpan(9, 0, 0))
97+
.WithMargin(1, 0, 1, 1)
98+
.Build();
99+
100+
// --- Combined Example ---
101+
102+
var eventStartDate = Controls.DatePicker("Start:")
103+
.WithFormat("yyyy-MM-dd")
104+
.WithSelectedDate(DateTime.Today)
105+
.WithMargin(1, 0, 1, 0)
106+
.Build();
107+
108+
var eventStartTime = Controls.TimePicker("")
109+
.With24HourFormat()
110+
.WithSelectedTime(new TimeSpan(10, 0, 0))
111+
.WithMargin(1, 0, 1, 1)
112+
.Build();
113+
114+
var eventEndDate = Controls.DatePicker("End:")
115+
.WithFormat("yyyy-MM-dd")
116+
.WithSelectedDate(DateTime.Today)
117+
.WithMargin(1, 0, 1, 0)
118+
.Build();
119+
120+
var eventEndTime = Controls.TimePicker("")
121+
.With24HourFormat()
122+
.WithSelectedTime(new TimeSpan(11, 0, 0))
123+
.WithMargin(1, 0, 1, 1)
124+
.Build();
125+
126+
// --- Update status on any change ---
127+
128+
void UpdateStatus(object? s, object? e)
129+
{
130+
string FormatDate(DatePickerControl dp) =>
131+
dp.SelectedDate?.ToString("yyyy-MM-dd") ?? "[dim]none[/]";
132+
133+
string FormatTime(TimePickerControl tp) =>
134+
tp.SelectedTime.HasValue
135+
? $"{tp.SelectedTime.Value.Hours:D2}:{tp.SelectedTime.Value.Minutes:D2}:{tp.SelectedTime.Value.Seconds:D2}"
136+
: "[dim]none[/]";
137+
138+
statusMarkup.SetContent(new List<string>
139+
{
140+
"[bold cyan]Current Values[/]",
141+
"",
142+
"[bold]Date Pickers[/]",
143+
$" [dim]ISO:[/] {FormatDate(dateIso)}",
144+
$" [dim]US:[/] {FormatDate(dateUs)}",
145+
$" [dim]EU:[/] {FormatDate(dateEu)}",
146+
$" [dim]Range:[/] {FormatDate(dateConstrained)}",
147+
"",
148+
"[bold]Time Pickers[/]",
149+
$" [dim]24h:[/] {FormatTime(time24)}",
150+
$" [dim]24h+s:[/] {FormatTime(time24Sec)}",
151+
$" [dim]12h:[/] {FormatTime(time12)}",
152+
$" [dim]12h+s:[/] {FormatTime(time12Sec)}",
153+
$" [dim]Work:[/] {FormatTime(timeConstrained)}",
154+
"",
155+
"[bold]Event[/]",
156+
$" [dim]Start:[/] {FormatDate(eventStartDate)} {FormatTime(eventStartTime)}",
157+
$" [dim]End:[/] {FormatDate(eventEndDate)} {FormatTime(eventEndTime)}",
158+
});
159+
}
160+
161+
dateIso.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
162+
dateUs.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
163+
dateEu.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
164+
dateConstrained.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
165+
time24.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
166+
time24Sec.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
167+
time12.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
168+
time12Sec.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
169+
timeConstrained.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
170+
eventStartDate.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
171+
eventStartTime.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
172+
eventEndDate.SelectedDateChanged += (s, _) => UpdateStatus(s, null);
173+
eventEndTime.SelectedTimeChanged += (s, _) => UpdateStatus(s, null);
174+
175+
UpdateStatus(null, null);
176+
177+
// --- Layout ---
178+
179+
var leftPanel = Controls.ScrollablePanel()
180+
.AddControl(Controls.Rule("Date Pickers"))
181+
.AddControl(Controls.Markup("[dim]ISO format (yyyy-MM-dd)[/]").WithMargin(1, 0, 1, 0).Build())
182+
.AddControl(dateIso)
183+
.AddControl(Controls.Markup("[dim]US format (MM/dd/yyyy)[/]").WithMargin(1, 0, 1, 0).Build())
184+
.AddControl(dateUs)
185+
.AddControl(Controls.Markup("[dim]European format (dd.MM.yyyy)[/]").WithMargin(1, 0, 1, 0).Build())
186+
.AddControl(dateEu)
187+
.AddControl(Controls.Markup("[dim]Constrained 2020-2030[/]").WithMargin(1, 0, 1, 0).Build())
188+
.AddControl(dateConstrained)
189+
.AddControl(Controls.Rule("Time Pickers"))
190+
.AddControl(Controls.Markup("[dim]24-hour (HH:MM)[/]").WithMargin(1, 0, 1, 0).Build())
191+
.AddControl(time24)
192+
.AddControl(Controls.Markup("[dim]24-hour with seconds[/]").WithMargin(1, 0, 1, 0).Build())
193+
.AddControl(time24Sec)
194+
.AddControl(Controls.Markup("[dim]12-hour AM/PM[/]").WithMargin(1, 0, 1, 0).Build())
195+
.AddControl(time12)
196+
.AddControl(Controls.Markup("[dim]12-hour with seconds[/]").WithMargin(1, 0, 1, 0).Build())
197+
.AddControl(time12Sec)
198+
.AddControl(Controls.Markup("[dim]Work hours (09:00-17:00)[/]").WithMargin(1, 0, 1, 0).Build())
199+
.AddControl(timeConstrained)
200+
.AddControl(Controls.Rule("Combined Example"))
201+
.AddControl(Controls.Markup("[dim]Event scheduling[/]").WithMargin(1, 0, 1, 0).Build())
202+
.AddControl(eventStartDate)
203+
.AddControl(eventStartTime)
204+
.AddControl(eventEndDate)
205+
.AddControl(eventEndTime)
206+
.WithVerticalAlignment(VerticalAlignment.Fill)
207+
.Build();
208+
209+
var rightPanel = Controls.ScrollablePanel()
210+
.AddControl(statusMarkup)
211+
.WithVerticalAlignment(VerticalAlignment.Fill)
212+
.Build();
213+
214+
var grid = Controls.HorizontalGrid()
215+
.Column(col => col.Width(LeftColumnWidth).Add(leftPanel))
216+
.Column(col => col.Flex().Add(rightPanel))
217+
.WithSplitterAfter(0)
218+
.WithAlignment(HorizontalAlignment.Stretch)
219+
.WithVerticalAlignment(VerticalAlignment.Fill)
220+
.Build();
221+
222+
var header = Controls.Markup("[bold yellow] Date & Time Controls[/]")
223+
.StickyTop()
224+
.Build();
225+
226+
var statusBar = Controls.Markup("[dim]Tab: next field | Digits: enter value | Esc: close[/]")
227+
.StickyBottom()
228+
.Build();
229+
230+
var gradient = ColorGradient.FromColors(
231+
new Color(20, 30, 60),
232+
new Color(10, 10, 25));
233+
234+
return new WindowBuilder(ws)
235+
.WithTitle("Date & Time")
236+
.WithSize(WindowWidth, WindowHeight)
237+
.Centered()
238+
.WithBackgroundGradient(gradient, GradientDirection.Vertical)
239+
.AddControls(header, grid, statusBar)
240+
.OnKeyPressed((sender, e) =>
241+
{
242+
if (e.KeyInfo.Key == ConsoleKey.Escape)
243+
{
244+
ws.CloseWindow((Window)sender!);
245+
e.Handled = true;
246+
}
247+
})
248+
.BuildAndShow();
249+
}
250+
}

Examples/DemoApp/DemoWindows/LauncherWindow.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public static Window Create(ConsoleWindowSystem ws)
3333
.AddItem("Nerd Fonts", subtitle: "NerdFont icon showcase", content: MakeInfoPanel("Nerd Fonts"))
3434
.AddItem("Markup Syntax", subtitle: "Rich markup system demo", content: MakeInfoPanel("Markup Syntax"))
3535
.AddItem("International & Emoji", subtitle: "Unicode & emoji support", content: MakeInfoPanel("International & Emoji"))
36-
.AddItem("Data Binding", subtitle: "MVVM data binding", content: MakeInfoPanel("Data Binding")))
36+
.AddItem("Data Binding", subtitle: "MVVM data binding", content: MakeInfoPanel("Data Binding"))
37+
.AddItem("Date & Time", subtitle: "DatePicker and TimePicker controls", content: MakeInfoPanel("Date & Time")))
3738
.AddHeader("Data Visualization", Color.Yellow, header => header
3839
.AddItem("Graphs & Charts", subtitle: "Live sparklines & bar graphs", content: MakeInfoPanel("Graphs & Charts")))
3940
.AddHeader("Rendering", Color.Orange1, header => header
@@ -125,6 +126,7 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
125126
"Markup Syntax" => MarkupSyntaxWindow.Create(ws),
126127
"International & Emoji" => InternationalWindow.Create(ws),
127128
"Data Binding" => DataBindingWindow.Create(ws),
129+
"Date & Time" => DateTimeDemo.Create(ws),
128130
"Graphs & Charts" => GraphsWindow.Create(ws),
129131
"Gradients" => GradientDemoWindow.Create(ws),
130132
"Animations" => AnimationDemoWindow.Create(ws),
@@ -349,6 +351,27 @@ private static void LaunchDemo(ConsoleWindowSystem ws, string demoName)
349351
" - ProgressBarControl, MarkupControl",
350352
" - CheckboxControl",
351353
},
354+
"Date & Time" => new List<string>
355+
{
356+
"[bold cyan]Date & Time[/]",
357+
"",
358+
"DatePicker and TimePicker controls with multiple",
359+
"format and culture configurations, constraints,",
360+
"and a combined event scheduling example.",
361+
"",
362+
"[dim]Features:[/]",
363+
" - ISO, US, and European date formats",
364+
" - 12h and 24h time formats with seconds",
365+
" - Culture-aware formatting (en-US, de-DE)",
366+
" - Min/max date and time constraints",
367+
" - Combined date+time event scheduling",
368+
" - Live status panel showing all values",
369+
"",
370+
"[dim]Controls used:[/]",
371+
" - DatePickerControl, TimePickerControl",
372+
" - MarkupControl, RuleControl",
373+
" - HorizontalGridControl, ScrollablePanelControl",
374+
},
352375
"Graphs & Charts" => new List<string>
353376
{
354377
"[bold cyan]Graphs & Charts[/]",

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ SharpConsoleUI is actively maintained and driven by real-world usage in producti
1313
- .NET 8.0 + 9.0 multi-targeting
1414
- SourceLink and symbol packages for debugging
1515
- Compositor effects — PreBufferPaint/PostBufferPaint hooks for custom rendering
16+
- DatePicker — locale-aware date selection with segmented editing and calendar popup
17+
- TimePicker — locale-aware time selection with 12h/24h modes, optional seconds, AM/PM
1618

1719
## Next
1820

19-
- **DatePicker** — calendar-based date selection control
20-
- **TimePicker** — time selection with hour/minute/second
2121
- **Slider / RangeControl** — horizontal and vertical value sliders
2222
- **StatusBarControl** — per-window status bar (distinct from the system-level status bars)
2323
- **Instant input response** — replace polling-based input loop with event-driven wake for zero-latency keypress handling

0 commit comments

Comments
 (0)