From 2c60a7ed8a7fdeffc02a3eb4dfeb493b569b1ddc Mon Sep 17 00:00:00 2001 From: phranck Date: Wed, 28 Jan 2026 23:02:05 +0100 Subject: [PATCH] feat(Menu): Improve selection bar styling - Selection bar now uses full width (padded to content width) - Selection background uses dimmed theme color (selectionBackground) - Selection text is bold and highlighted with accent color - Add selectionBackground to Theme protocol and all themes - Remove arrow indicator, use pure background highlight for selection --- Sources/TUIKit/Core/Theme.swift | 24 +++++++++++++++------ Sources/TUIKit/Views/Menu.swift | 37 ++++++++++++++------------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Sources/TUIKit/Core/Theme.swift b/Sources/TUIKit/Core/Theme.swift index 196d1b8..24063c7 100644 --- a/Sources/TUIKit/Core/Theme.swift +++ b/Sources/TUIKit/Core/Theme.swift @@ -85,8 +85,11 @@ public protocol Theme: Sendable { /// Separator/divider color. var separator: Color { get } - /// Selection highlight color. + /// Selection highlight color (foreground). var selection: Color { get } + + /// Selection background color (dimmed accent). + var selectionBackground: Color { get } /// Color for disabled elements. var disabled: Color { get } @@ -116,6 +119,7 @@ extension Theme { public var borderFocused: Color { accent } public var separator: Color { border } public var selection: Color { accent } + public var selectionBackground: Color { backgroundSecondary } public var disabled: Color { foregroundTertiary } public var statusBarBackground: Color { backgroundSecondary } public var statusBarForeground: Color { foreground } @@ -233,6 +237,9 @@ public enum ThemeColors { /// Selection color. public static var selection: Color { current.selection } + + /// Selection background color. + public static var selectionBackground: Color { current.selectionBackground } /// Disabled color. public static var disabled: Color { current.disabled } @@ -299,7 +306,8 @@ public struct GreenPhosphorTheme: Theme { // UI elements public let border = Color.hex(0x2D5A2D) // Subtle green border public let borderFocused = Color.hex(0x33FF33) // Bright when focused - public let selection = Color.hex(0x1F4D1F) // Dark green for selection bg + public let selection = Color.hex(0x66FF66) // Bright green for selection text + public let selectionBackground = Color.hex(0x1A4D1A) // Dark green for selection bar bg // Status bar public let statusBarBackground = Color.hex(0x162016) @@ -340,7 +348,8 @@ public struct AmberPhosphorTheme: Theme { // UI elements public let border = Color.hex(0x5A4A2D) // Subtle amber border public let borderFocused = Color.hex(0xFFAA00) // Bright when focused - public let selection = Color.hex(0x4D3A1F) // Dark amber for selection bg + public let selection = Color.hex(0xFFCC33) // Bright amber for selection text + public let selectionBackground = Color.hex(0x4D3A1F) // Dark amber for selection bar bg // Status bar public let statusBarBackground = Color.hex(0x201A14) @@ -381,7 +390,8 @@ public struct WhitePhosphorTheme: Theme { // UI elements public let border = Color.hex(0x484848) // Subtle gray border public let borderFocused = Color.hex(0xE8E8E8) // Bright when focused - public let selection = Color.hex(0x3A3A3A) // Dark gray for selection bg + public let selection = Color.hex(0xFFFFFF) // White for selection text + public let selectionBackground = Color.hex(0x3A3A3A) // Dark gray for selection bar bg // Status bar public let statusBarBackground = Color.hex(0x181C20) @@ -422,7 +432,8 @@ public struct RedPhosphorTheme: Theme { // UI elements public let border = Color.hex(0x5A2D2D) // Subtle red border public let borderFocused = Color.hex(0xFF4444) // Bright when focused - public let selection = Color.hex(0x4D1F1F) // Dark red for selection bg + public let selection = Color.hex(0xFF6666) // Bright red for selection text + public let selectionBackground = Color.hex(0x4D1F1F) // Dark red for selection bar bg // Status bar public let statusBarBackground = Color.hex(0x201414) @@ -455,7 +466,8 @@ public struct NCursesTheme: Theme { public let info = Color.cyan public let border = Color.white public let borderFocused = Color.brightCyan - public let selection = Color.blue + public let selection = Color.brightCyan + public let selectionBackground = Color.blue public let disabled = Color.brightBlack public let statusBarBackground = Color.blue public let statusBarForeground = Color.white diff --git a/Sources/TUIKit/Views/Menu.swift b/Sources/TUIKit/Views/Menu.swift index 7513926..affcf8c 100644 --- a/Sources/TUIKit/Views/Menu.swift +++ b/Sources/TUIKit/Views/Menu.swift @@ -200,8 +200,9 @@ extension Menu: Renderable { // Menu items let currentSelection = selectionBinding?.wrappedValue ?? selectedIndex - let appearance = context.environment.appearance - let isBlockAppearance = appearance.id == .block + + // Calculate the content width for full-width selection bar + let contentWidth = maxItemWidth + 2 // +2 for padding for (index, item) in items.enumerated() { let isSelected = index == currentSelection @@ -214,34 +215,28 @@ extension Menu: Renderable { labelText = " \(item.label)" } - // For block appearance: no indicator, use background highlight - // For other appearances: use indicator prefix - let fullText: String - if isBlockAppearance { - fullText = " " + labelText - } else { - let prefix = isSelected ? selectionIndicator : String(repeating: " ", count: selectionIndicator.count) - fullText = " " + prefix + labelText - } + // Build the full text with padding + let fullText = " " + labelText + + // Pad to full width for selection bar + let visibleLength = fullText.count + let padding = max(0, contentWidth - visibleLength) + let paddedText = fullText + String(repeating: " ", count: padding) // Apply styling var style = TextStyle() if isSelected { + // Selected: bold text with dimmed background, highlighted foreground style.isBold = true - if isBlockAppearance { - // Block appearance: use background highlight - style.foregroundColor = Color.theme.background - style.backgroundColor = selectedColor ?? Color.theme.accent - } else { - // Other appearances: just change foreground color - style.foregroundColor = selectedColor ?? Color.theme.accent - } + style.foregroundColor = selectedColor ?? Color.theme.accent + // Use a dimmed version of the accent color for background + style.backgroundColor = Color.theme.selectionBackground } else { // Use theme foreground color if no custom itemColor is set style.foregroundColor = itemColor ?? Color.theme.foreground } - let styledLine = ANSIRenderer.render(fullText, with: style) + let styledLine = ANSIRenderer.render(paddedText, with: style) lines.append(styledLine) } @@ -310,7 +305,7 @@ extension Menu: Renderable { private var maxItemWidth: Int { items.map { item -> Int in let shortcutPart = item.shortcut != nil ? 4 : 4 // "[x] " or " " - return selectionIndicator.count + shortcutPart + item.label.count + return shortcutPart + item.label.count }.max() ?? 0 }