feat(ui): introduce focus beacon and unified panel styling helpers

Add `focus_beacon_span`, `panel_title_spans`, `panel_hint_style`, and `panel_border_style` utilities to centralize panel header, hint, border, and beacon rendering. Integrate these helpers across all UI panels (files, chat, thinking, agent actions, input, status bar) and update help text. Extend `Theme` with new color fields for beacons, pane headers, and hint text, providing defaults for all built‑in themes. Include comprehensive unit tests for the new styling functions.
This commit is contained in:
2025-10-12 21:37:34 +02:00
parent 33ad3797a1
commit f413a63c5a
2 changed files with 390 additions and 137 deletions

View File

@@ -36,6 +36,42 @@ pub struct Theme {
#[serde(serialize_with = "serialize_color")]
pub unfocused_panel_border: Color,
/// Foreground color for the active pane beacon (`▌`)
#[serde(default = "Theme::default_focus_beacon_fg")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub focus_beacon_fg: Color,
/// Background color for the active pane beacon (`▌`)
#[serde(default = "Theme::default_focus_beacon_bg")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub focus_beacon_bg: Color,
/// Foreground color for the inactive pane beacon (`▌`)
#[serde(default = "Theme::default_unfocused_beacon_fg")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub unfocused_beacon_fg: Color,
/// Title color for active pane headers
#[serde(default = "Theme::default_pane_header_active")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub pane_header_active: Color,
/// Title color for inactive pane headers
#[serde(default = "Theme::default_pane_header_inactive")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub pane_header_inactive: Color,
/// Hint text color used within pane headers
#[serde(default = "Theme::default_pane_hint_text")]
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
pub pane_hint_text: Color,
/// Color for user message role indicator
#[serde(deserialize_with = "deserialize_color")]
#[serde(serialize_with = "serialize_color")]
@@ -313,6 +349,30 @@ impl Theme {
Color::Cyan
}
const fn default_focus_beacon_fg() -> Color {
Color::LightMagenta
}
const fn default_focus_beacon_bg() -> Color {
Color::Black
}
const fn default_unfocused_beacon_fg() -> Color {
Color::DarkGray
}
const fn default_pane_header_active() -> Color {
Color::White
}
const fn default_pane_header_inactive() -> Color {
Color::Gray
}
const fn default_pane_hint_text() -> Color {
Color::DarkGray
}
const fn default_operating_chat_fg() -> Color {
Color::Black
}
@@ -474,6 +534,12 @@ fn default_dark() -> Theme {
background: Color::Black,
focused_panel_border: Color::LightMagenta,
unfocused_panel_border: Color::Rgb(95, 20, 135),
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::LightBlue,
assistant_message_role: Color::Yellow,
tool_output: Color::Gray,
@@ -523,6 +589,12 @@ fn default_light() -> Theme {
background: Color::White,
focused_panel_border: Color::Rgb(74, 144, 226),
unfocused_panel_border: Color::Rgb(221, 221, 221),
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(0, 85, 164),
assistant_message_role: Color::Rgb(142, 68, 173),
tool_output: Color::Gray,
@@ -572,7 +644,13 @@ fn gruvbox() -> Theme {
background: Color::Rgb(40, 40, 40), // #282828
focused_panel_border: Color::Rgb(254, 128, 25), // #fe8019 (orange)
unfocused_panel_border: Color::Rgb(124, 111, 100), // #7c6f64
user_message_role: Color::Rgb(184, 187, 38), // #b8bb26 (green)
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(184, 187, 38), // #b8bb26 (green)
assistant_message_role: Color::Rgb(131, 165, 152), // #83a598 (blue)
tool_output: Color::Rgb(146, 131, 116),
thinking_panel_title: Color::Rgb(211, 134, 155), // #d3869b (purple)
@@ -617,11 +695,17 @@ fn gruvbox() -> Theme {
fn dracula() -> Theme {
Theme {
name: "dracula".to_string(),
text: Color::Rgb(248, 248, 242), // #f8f8f2
background: Color::Rgb(40, 42, 54), // #282a36
focused_panel_border: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
unfocused_panel_border: Color::Rgb(68, 71, 90), // #44475a
user_message_role: Color::Rgb(139, 233, 253), // #8be9fd (cyan)
text: Color::Rgb(248, 248, 242), // #f8f8f2
background: Color::Rgb(40, 42, 54), // #282a36
focused_panel_border: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
unfocused_panel_border: Color::Rgb(68, 71, 90), // #44475a
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(139, 233, 253), // #8be9fd (cyan)
assistant_message_role: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
tool_output: Color::Rgb(98, 114, 164),
thinking_panel_title: Color::Rgb(189, 147, 249), // #bd93f9 (purple)
@@ -670,6 +754,12 @@ fn solarized() -> Theme {
background: Color::Rgb(0, 43, 54), // #002b36 (base03)
focused_panel_border: Color::Rgb(38, 139, 210), // #268bd2 (blue)
unfocused_panel_border: Color::Rgb(7, 54, 66), // #073642 (base02)
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(42, 161, 152), // #2aa198 (cyan)
assistant_message_role: Color::Rgb(203, 75, 22), // #cb4b16 (orange)
tool_output: Color::Rgb(101, 123, 131),
@@ -719,6 +809,12 @@ fn midnight_ocean() -> Theme {
background: Color::Rgb(13, 17, 23),
focused_panel_border: Color::Rgb(88, 166, 255),
unfocused_panel_border: Color::Rgb(48, 54, 61),
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(121, 192, 255),
assistant_message_role: Color::Rgb(137, 221, 255),
tool_output: Color::Rgb(84, 110, 122),
@@ -764,11 +860,17 @@ fn midnight_ocean() -> Theme {
fn rose_pine() -> Theme {
Theme {
name: "rose-pine".to_string(),
text: Color::Rgb(224, 222, 244), // #e0def4
background: Color::Rgb(25, 23, 36), // #191724
focused_panel_border: Color::Rgb(235, 111, 146), // #eb6f92 (love)
unfocused_panel_border: Color::Rgb(38, 35, 58), // #26233a
user_message_role: Color::Rgb(49, 116, 143), // #31748f (foam)
text: Color::Rgb(224, 222, 244), // #e0def4
background: Color::Rgb(25, 23, 36), // #191724
focused_panel_border: Color::Rgb(235, 111, 146), // #eb6f92 (love)
unfocused_panel_border: Color::Rgb(38, 35, 58), // #26233a
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(49, 116, 143), // #31748f (foam)
assistant_message_role: Color::Rgb(156, 207, 216), // #9ccfd8 (foam light)
tool_output: Color::Rgb(110, 106, 134),
thinking_panel_title: Color::Rgb(196, 167, 231), // #c4a7e7 (iris)
@@ -813,11 +915,17 @@ fn rose_pine() -> Theme {
fn monokai() -> Theme {
Theme {
name: "monokai".to_string(),
text: Color::Rgb(248, 248, 242), // #f8f8f2
background: Color::Rgb(39, 40, 34), // #272822
focused_panel_border: Color::Rgb(249, 38, 114), // #f92672 (pink)
unfocused_panel_border: Color::Rgb(117, 113, 94), // #75715e
user_message_role: Color::Rgb(102, 217, 239), // #66d9ef (cyan)
text: Color::Rgb(248, 248, 242), // #f8f8f2
background: Color::Rgb(39, 40, 34), // #272822
focused_panel_border: Color::Rgb(249, 38, 114), // #f92672 (pink)
unfocused_panel_border: Color::Rgb(117, 113, 94), // #75715e
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(102, 217, 239), // #66d9ef (cyan)
assistant_message_role: Color::Rgb(174, 129, 255), // #ae81ff (purple)
tool_output: Color::Rgb(117, 113, 94),
thinking_panel_title: Color::Rgb(230, 219, 116), // #e6db74 (yellow)
@@ -862,11 +970,17 @@ fn monokai() -> Theme {
fn material_dark() -> Theme {
Theme {
name: "material-dark".to_string(),
text: Color::Rgb(238, 255, 255), // #eeffff
background: Color::Rgb(38, 50, 56), // #263238
focused_panel_border: Color::Rgb(128, 203, 196), // #80cbc4 (cyan)
unfocused_panel_border: Color::Rgb(84, 110, 122), // #546e7a
user_message_role: Color::Rgb(130, 170, 255), // #82aaff (blue)
text: Color::Rgb(238, 255, 255), // #eeffff
background: Color::Rgb(38, 50, 56), // #263238
focused_panel_border: Color::Rgb(128, 203, 196), // #80cbc4 (cyan)
unfocused_panel_border: Color::Rgb(84, 110, 122), // #546e7a
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(130, 170, 255), // #82aaff (blue)
assistant_message_role: Color::Rgb(199, 146, 234), // #c792ea (purple)
tool_output: Color::Rgb(84, 110, 122),
thinking_panel_title: Color::Rgb(255, 203, 107), // #ffcb6b (yellow)
@@ -915,6 +1029,12 @@ fn material_light() -> Theme {
background: Color::Rgb(236, 239, 241),
focused_panel_border: Color::Rgb(0, 150, 136),
unfocused_panel_border: Color::Rgb(176, 190, 197),
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(68, 138, 255),
assistant_message_role: Color::Rgb(124, 77, 255),
tool_output: Color::Rgb(144, 164, 174),
@@ -964,6 +1084,12 @@ fn grayscale_high_contrast() -> Theme {
background: Color::Black,
focused_panel_border: Color::White,
unfocused_panel_border: Color::Rgb(76, 76, 76),
focus_beacon_fg: Theme::default_focus_beacon_fg(),
focus_beacon_bg: Theme::default_focus_beacon_bg(),
unfocused_beacon_fg: Theme::default_unfocused_beacon_fg(),
pane_header_active: Theme::default_pane_header_active(),
pane_header_inactive: Theme::default_pane_header_inactive(),
pane_hint_text: Theme::default_pane_hint_text(),
user_message_role: Color::Rgb(240, 240, 240),
assistant_message_role: Color::Rgb(214, 214, 214),
tool_output: Color::Rgb(189, 189, 189),