Skip to content

Commit 1d575eb

Browse files
committed
feat(start-menu): redesign as window-based panel with StartMenuOptions
Replace portal-based StartMenuControl with a borderless Window built via WindowBuilder. Add Window.CloseOnDeactivate, Window.ShowInTaskbar, Window.CanClose (rename misleading TryClose). Extract StartMenuOptions record from StatusBarOptions for cleaner configuration. Add configurable app name, version, header icon, gradient background, and icon visibility.
1 parent 1814e3b commit 1d575eb

23 files changed

Lines changed: 733 additions & 167 deletions

Examples/DemoApp/DemoWindows/VideoDemoWindow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static Window Create(ConsoleWindowSystem ws)
3939
filePath = samplePath;
4040
else
4141
{
42-
ws.EnqueueOnUIThread(() => win.TryClose(force: true));
42+
ws.EnqueueOnUIThread(() => win.Close(force: true));
4343
return;
4444
}
4545
}

Examples/StartMenuDemo/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ static async Task<int> Main(string[] args)
2323
ShowStartButton: true,
2424
StartButtonLocation: StatusBarLocation.Bottom,
2525
StartButtonPosition: StartButtonPosition.Left,
26-
ShowWindowListInMenu: true
26+
StartMenu: new StartMenuOptions(ShowWindowList: true)
2727
)
2828
);
2929

SharpConsoleUI/Builders/WindowBuilder.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public sealed class WindowBuilder
5353
private readonly List<IWindowControl> _controls = new();
5454
private Window.WindowThreadDelegateAsync? _asyncWindowThread;
5555
private bool _alwaysOnTop = false;
56+
private bool _showInTaskbar = true;
57+
private bool _closeOnDeactivate = false;
5658
private EventHandler? _activatedHandler;
5759
private EventHandler? _deactivatedHandler;
5860
private EventHandler<KeyPressedEventArgs>? _keyPressedHandler;
@@ -543,6 +545,30 @@ public WindowBuilder WithAlwaysOnTop(bool alwaysOnTop = true)
543545
return this;
544546
}
545547

548+
/// <summary>
549+
/// Sets whether this window appears in the taskbar and window lists.
550+
/// Defaults to true. Set to false for transient UI like start menus and popups.
551+
/// </summary>
552+
/// <param name="show">True to show in taskbar; false to hide.</param>
553+
/// <returns>The current builder instance for method chaining.</returns>
554+
public WindowBuilder ShowInTaskbar(bool show = true)
555+
{
556+
_showInTaskbar = show;
557+
return this;
558+
}
559+
560+
/// <summary>
561+
/// Sets whether this window automatically closes when deactivated.
562+
/// Used for transient UI like start menus and popup panels.
563+
/// </summary>
564+
/// <param name="close">True to close on deactivate; false otherwise.</param>
565+
/// <returns>The current builder instance for method chaining.</returns>
566+
public WindowBuilder WithCloseOnDeactivate(bool close = true)
567+
{
568+
_closeOnDeactivate = close;
569+
return this;
570+
}
571+
546572
/// <summary>
547573
/// Subscribes a handler to the window's Activated event, which is raised when the window becomes the active window.
548574
/// </summary>
@@ -694,6 +720,8 @@ public Window Build()
694720
window.IsMinimizable = _isMinimizable;
695721
window.IsMaximizable = _isMaximizable;
696722
window.AlwaysOnTop = _alwaysOnTop;
723+
window.ShowInTaskbar = _showInTaskbar;
724+
window.CloseOnDeactivate = _closeOnDeactivate;
697725
window.BorderStyle = _borderStyle;
698726
window.ShowTitle = _showTitle;
699727
window.ShowCloseButton = _showCloseButton;

SharpConsoleUI/Configuration/ControlDefaults.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,5 +747,49 @@ public static class ControlDefaults
747747
/// </summary>
748748
public const char SliderVerticalBottomCap = '\u2500';
749749

750+
// Start menu defaults
751+
752+
/// <summary>
753+
/// Minimum width of the left column in the Start menu two-column layout (default: 22).
754+
/// </summary>
755+
public const int StartMenuMinLeftColumnWidth = 22;
756+
757+
/// <summary>
758+
/// Maximum width of the left column in the Start menu two-column layout (default: 35).
759+
/// </summary>
760+
public const int StartMenuMaxLeftColumnWidth = 35;
761+
762+
/// <summary>
763+
/// Minimum width of the right column in the Start menu two-column layout (default: 20).
764+
/// </summary>
765+
public const int StartMenuMinRightColumnWidth = 24;
766+
767+
/// <summary>
768+
/// Maximum width of the right column in the Start menu two-column layout (default: 40).
769+
/// </summary>
770+
public const int StartMenuMaxRightColumnWidth = 40;
771+
772+
/// <summary>
773+
/// Maximum number of windows visible in the Start menu window list (default: 15).
774+
/// </summary>
775+
public const int StartMenuMaxVisibleWindows = 15;
776+
777+
/// <summary>
778+
/// Icon prefix for the exit action in the Start menu.
779+
/// U+23FB: Power Symbol — rendered via MarkupParser for correct width handling.
780+
/// </summary>
781+
public const string StartMenuExitIcon = "\u23FB";
782+
783+
/// <summary>
784+
/// Active window indicator in the Start menu window list.
785+
/// U+25C6: Black Diamond — rendered via MarkupParser for correct width handling.
786+
/// </summary>
787+
public const string StartMenuActiveWindowIndicator = "\u25C6";
788+
789+
/// <summary>
790+
/// Minimized window indicator in the Start menu window list.
791+
/// U+25C7: White Diamond — rendered via MarkupParser for correct width handling.
792+
/// </summary>
793+
public const string StartMenuMinimizedWindowIndicator = "\u25C7";
750794
}
751795
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace SharpConsoleUI.Configuration;
2+
3+
/// <summary>
4+
/// Specifies the layout mode for the Start menu panel.
5+
/// </summary>
6+
public enum StartMenuLayout
7+
{
8+
/// <summary>Single-column compact layout with categories as flyout submenus.</summary>
9+
SingleColumn,
10+
/// <summary>Two-column layout with quick actions (left) and window list (right).</summary>
11+
TwoColumn
12+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using SharpConsoleUI.Rendering;
2+
3+
namespace SharpConsoleUI.Configuration;
4+
5+
/// <summary>
6+
/// Configuration options for Start menu appearance and behavior.
7+
/// </summary>
8+
public record StartMenuOptions(
9+
/// <summary>Layout mode: SingleColumn (compact) or TwoColumn (with window list).</summary>
10+
StartMenuLayout Layout = StartMenuLayout.TwoColumn,
11+
12+
/// <summary>Application name shown in the Start menu header. Defaults to "SharpConsoleUI".</summary>
13+
string? AppName = null,
14+
15+
/// <summary>Application version shown in the Start menu header. Defaults to library version.</summary>
16+
string? AppVersion = null,
17+
18+
/// <summary>Whether to show Unicode icons next to categories, headers, and exit.</summary>
19+
bool ShowIcons = true,
20+
21+
/// <summary>Icon displayed next to the app name in the header. Defaults to "☰" (U+2630).</summary>
22+
string HeaderIcon = "\u2630",
23+
24+
/// <summary>Show built-in System category (themes, settings, about, performance).</summary>
25+
bool ShowSystemCategory = true,
26+
27+
/// <summary>Show Windows category with open window list.</summary>
28+
bool ShowWindowList = true,
29+
30+
/// <summary>Optional gradient background for the Start menu window.</summary>
31+
GradientBackground? BackgroundGradient = null
32+
);

SharpConsoleUI/Configuration/StatusBarOptions.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ public record StatusBarOptions(
99
StatusBarLocation StartButtonLocation = StatusBarLocation.Bottom,
1010
StartButtonPosition StartButtonPosition = StartButtonPosition.Left,
1111
string StartButtonText = "☰ Start",
12-
// Note: Ctrl+M doesn't work in terminals (interpreted as Enter/CR)
13-
// Using Ctrl+Spacebar as default which works reliably
1412
ConsoleKey StartMenuShortcutKey = ConsoleKey.Spacebar,
1513
ConsoleModifiers StartMenuShortcutModifiers = ConsoleModifiers.Control,
1614

17-
// Start menu content
18-
bool ShowSystemMenuCategory = true,
19-
bool ShowWindowListInMenu = true,
15+
// Start menu options
16+
StartMenuOptions? StartMenu = null,
2017

21-
// Status bar text (existing functionality)
18+
// Status bar display
2219
bool ShowTopStatus = true,
2320
bool ShowBottomStatus = true,
2421
bool ShowTaskBar = true
2522
)
2623
{
24+
/// <summary>
25+
/// Gets the resolved Start menu configuration (uses defaults if not explicitly set).
26+
/// </summary>
27+
public StartMenuOptions StartMenuConfig => StartMenu ?? new StartMenuOptions();
28+
2729
/// <summary>
2830
/// Gets the default status bar options.
2931
/// </summary>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.Configuration;
10+
11+
namespace SharpConsoleUI.Controls.StartMenu
12+
{
13+
/// <summary>
14+
/// Provides formatting helpers for Start menu elements.
15+
/// Icon display is controlled by <see cref="Configuration.StartMenuOptions.ShowIcons"/>.
16+
/// </summary>
17+
internal static class StartMenuStyleHelper
18+
{
19+
/// <summary>
20+
/// Formats the application header with name and version.
21+
/// Returns multiple markup lines for MarkupControl.
22+
/// </summary>
23+
public static List<string> FormatAppHeaderLines(string appName, string version, bool showIcons, string headerIcon = "\u2630")
24+
{
25+
var icon = showIcons ? $"{headerIcon} " : "";
26+
return new List<string>
27+
{
28+
$"{icon}[bold]{appName}[/]",
29+
$" v{version}",
30+
};
31+
}
32+
33+
/// <summary>
34+
/// Formats a section/category header label.
35+
/// </summary>
36+
public static string FormatCategoryHeader(string text, bool showIcons, string? icon = null)
37+
{
38+
var prefix = showIcons && icon != null ? $"{icon} " : "";
39+
return $"{prefix}{text}";
40+
}
41+
42+
/// <summary>
43+
/// Formats the info strip with theme name, window count, and plugin count.
44+
/// Returns multiple markup lines for MarkupControl.
45+
/// </summary>
46+
public static List<string> FormatInfoStripLines(string themeName, int windowCount, int pluginCount)
47+
{
48+
var windowLabel = windowCount == 1 ? "Window" : "Windows";
49+
var pluginLabel = pluginCount == 1 ? "Plugin" : "Plugins";
50+
51+
return new List<string>
52+
{
53+
$"Theme: {themeName}",
54+
$"{windowLabel}: {windowCount} {pluginLabel}: {pluginCount}",
55+
};
56+
}
57+
58+
/// <summary>
59+
/// Formats a window list item with state indicator and optional keyboard shortcut.
60+
/// </summary>
61+
public static string FormatWindowItem(string title, int index, bool isMinimized, bool isActive)
62+
{
63+
var indicator = isMinimized
64+
? ControlDefaults.StartMenuMinimizedWindowIndicator
65+
: ControlDefaults.StartMenuActiveWindowIndicator;
66+
var shortcut = index < 9 ? $" Alt+{index + 1}" : "";
67+
var dimStart = isMinimized ? "[dim]" : "";
68+
var dimEnd = isMinimized ? "[/]" : "";
69+
var activeStart = isActive ? "[bold]" : "";
70+
var activeEnd = isActive ? "[/]" : "";
71+
72+
return $"{indicator} {dimStart}{activeStart}{title}{activeEnd}{dimEnd}{shortcut}";
73+
}
74+
75+
/// <summary>
76+
/// Formats the exit row text.
77+
/// </summary>
78+
public static string FormatExitRow(bool showIcons)
79+
{
80+
var icon = showIcons ? $"{ControlDefaults.StartMenuExitIcon} " : "";
81+
return $"{icon}Exit Application";
82+
}
83+
84+
}
85+
}

SharpConsoleUI/Controls/Terminal/TerminalControl.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ private void ReadLoop()
9090
{
9191
var windowSystem = window.GetConsoleWindowSystem;
9292
if (windowSystem != null)
93-
windowSystem.EnqueueOnUIThread(() => window.TryClose(force: true));
93+
windowSystem.EnqueueOnUIThread(() => window.Close(force: true));
9494
else
95-
window.TryClose(force: true);
95+
window.Close(force: true);
9696
}
9797
}
9898

SharpConsoleUI/Core/DesktopPortal.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ internal DesktopPortal(DesktopPortalOptions options, int zOrder, ConsoleWindowSy
131131

132132
Container = new DesktopPortalContainer(this);
133133

134-
// Build LayoutNode tree
134+
// Build LayoutNode for the root content control.
135+
// Portal content is always a leaf control (e.g., MenuControl) — composite
136+
// containers like HorizontalGridControl are hosted inside windows, not portals.
135137
Content.Container = Container;
136138
RootNode = new LayoutNode(Content);
137139
RootNode.IsVisible = true;
@@ -218,7 +220,7 @@ public void Invalidate(bool redrawAll, IWindowControl? callerControl = null)
218220
// Connect portal content into the invalidation chain
219221
portalContent.Container = this;
220222

221-
var portalNode = new LayoutNode(portalContent);
223+
var portalNode = LayoutNodeFactory.CreateSubtree(portalContent);
222224
portalNode.IsVisible = true;
223225

224226
// Measure using buffer size (allows submenus to extend beyond portal bounds)

0 commit comments

Comments
 (0)