fix(tui): use muted selection highlights in dark themes

This commit is contained in:
Hunter B
2026-06-12 06:13:08 -07:00
parent db8039ae46
commit 8f265e204f
8 changed files with 83 additions and 17 deletions
+3
View File
@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Sidebar hover popovers (#3088).** Streaming turns now keep sidebar hover
popovers responsive while continuing to throttle transcript/body mouse
motion.
- **Dark-theme selection contrast (#3074, thanks @drpars).** Session, config,
help, context-menu, and approval selections now use the muted selection
background instead of the bright accent color.
- **Cursor-style activity metadata rows (#3146).** Dense successful tool-run
summaries now render as a single muted `Explored ...` / `Updated metadata`
row, include short command-family labels for successful generic verifier
+3
View File
@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Sidebar hover popovers (#3088).** Streaming turns now keep sidebar hover
popovers responsive while continuing to throttle transcript/body mouse
motion.
- **Dark-theme selection contrast (#3074, thanks @drpars).** Session, config,
help, context-menu, and approval selections now use the muted selection
background instead of the bright accent color.
- **Cursor-style activity metadata rows (#3146).** Dense successful tool-run
summaries now render as a single muted `Explored ...` / `Updated metadata`
row, include short command-family labels for successful generic verifier
+18
View File
@@ -221,6 +221,24 @@ mod tests {
}
}
#[test]
fn selected_row_text_contrasts_on_selection_bg() {
for theme in ALL_THEMES {
let name = theme.name;
let Some(fg) = rgb(theme.text_body) else {
continue;
};
let Some(bg) = rgb(theme.selection_bg) else {
continue;
};
let cr = contrast_ratio(fg, bg);
assert!(
cr >= 4.5,
"{name}: selected-row text contrast {cr:.1}:1 is below 4.5:1 (fg={fg:?}, bg={bg:?})"
);
}
}
#[test]
fn surface_layers_are_distinct() {
for theme in ALL_THEMES {
+2 -2
View File
@@ -188,8 +188,8 @@ impl ModalView for ContextMenuView {
let text = trim_to_width(&format!("{label}{description}"), inner_width);
let style = if idx == self.selected {
Style::default()
.fg(palette::TEXT_PRIMARY)
.bg(palette::DEEPSEEK_BLUE)
.fg(palette::SELECTION_TEXT)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
+4 -3
View File
@@ -668,7 +668,7 @@ fn build_list_lines(
let style = if idx == selected {
Style::default()
.fg(palette::SELECTION_TEXT)
.bg(palette::DEEPSEEK_BLUE)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(palette::TEXT_PRIMARY)
@@ -1086,7 +1086,7 @@ mod tests {
}
#[test]
fn build_list_lines_selected_row_uses_strong_highlight() {
fn build_list_lines_selected_row_uses_muted_selection_highlight() {
let sessions = vec![
test_session(1, "first session"),
test_session(2, "second session"),
@@ -1109,7 +1109,8 @@ mod tests {
.expect("selected row should have a span");
assert_eq!(span.style.fg, Some(palette::SELECTION_TEXT));
assert_eq!(span.style.bg, Some(palette::DEEPSEEK_BLUE));
assert_eq!(span.style.bg, Some(palette::SELECTION_BG));
assert_ne!(span.style.bg, Some(palette::DEEPSEEK_BLUE));
assert!(span.style.add_modifier.contains(Modifier::BOLD));
}
+5 -5
View File
@@ -451,7 +451,7 @@ impl ModalView for HelpView {
let style = if is_selected {
Style::default()
.fg(palette::SELECTION_TEXT)
.bg(palette::DEEPSEEK_BLUE)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(palette::TEXT_PRIMARY)
@@ -699,7 +699,7 @@ mod tests {
let cell = &buf[(x, y)];
row.push_str(cell.symbol());
row_has_highlight |=
cell.bg == palette::DEEPSEEK_BLUE && cell.fg == palette::SELECTION_TEXT;
cell.bg == palette::SELECTION_BG && cell.fg == palette::SELECTION_TEXT;
}
if row_has_highlight && row.contains(&selected_label) {
highlighted_label = true;
@@ -714,7 +714,7 @@ mod tests {
}
#[test]
fn selected_help_row_uses_stronger_highlight() {
fn selected_help_row_uses_selection_highlight() {
let view = HelpView::new();
let area = Rect::new(0, 0, 96, 32);
let mut buf = Buffer::empty(area);
@@ -724,7 +724,7 @@ mod tests {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
let cell = &buf[(x, y)];
if cell.bg == palette::DEEPSEEK_BLUE && cell.fg == palette::SELECTION_TEXT {
if cell.bg == palette::SELECTION_BG && cell.fg == palette::SELECTION_TEXT {
found_highlight = true;
break;
}
@@ -733,7 +733,7 @@ mod tests {
assert!(
found_highlight,
"selected row should use a strong blue highlight"
"selected row should use the semantic selection highlight"
);
}
+43 -2
View File
@@ -1484,8 +1484,8 @@ impl ModalView for ConfigView {
let selected = *idx == self.selected;
let style = if selected {
Style::default()
.fg(ratatui::style::Color::White)
.bg(palette::DEEPSEEK_BLUE)
.fg(palette::SELECTION_TEXT)
.bg(palette::SELECTION_BG)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(palette::TEXT_PRIMARY)
@@ -2088,6 +2088,7 @@ mod tests {
};
use crate::config::Config;
use crate::localization::{Locale, MessageId, tr};
use crate::palette;
use crate::settings::Settings;
use crate::tools::subagent::{
SubAgentAssignment, SubAgentResult, SubAgentStatus, SubAgentType,
@@ -2598,6 +2599,46 @@ base_url = "https://api.xiaomimimo.com/v1"
);
}
#[test]
fn config_view_selected_row_uses_muted_selection_highlight() {
let mut view = create_config_view(Locale::En);
view.selected = view
.rows
.iter()
.position(|row| row.key == "theme")
.expect("theme row");
view.adjust_scroll(8);
let area = Rect::new(0, 0, 100, 24);
let mut buf = Buffer::empty(area);
view.render(area, &mut buf);
let y = view
.last_row_hitboxes
.borrow()
.iter()
.find_map(|(y, idx)| (*idx == view.selected).then_some(*y))
.expect("selected config row should have a hitbox");
let highlighted_cells = (area.x..area.x.saturating_add(area.width))
.filter(|&x| {
let cell = &buf[(x, y)];
!cell.symbol().trim().is_empty()
&& cell.bg == palette::SELECTION_BG
&& cell.fg == palette::SELECTION_TEXT
})
.count();
assert!(
highlighted_cells >= 4,
"selected config row should render readable selection text"
);
assert!(
!(area.x..area.x.saturating_add(area.width))
.any(|x| buf[(x, y)].bg == palette::DEEPSEEK_BLUE),
"selected config row should not use the bright accent background"
);
}
#[test]
fn config_view_keeps_scope_column_aligned_for_long_keys() {
let mut view = create_config_view(Locale::ZhHans);
+5 -5
View File
@@ -1454,7 +1454,7 @@ fn approval_palette(risk: RiskLevel) -> ApprovalColors {
fn approval_selected_style() -> Style {
Style::default()
.fg(palette::SELECTION_TEXT)
.bg(palette::DEEPSEEK_BLUE)
.bg(palette::SELECTION_BG)
.add_modifier(Modifier::BOLD)
}
@@ -4072,21 +4072,21 @@ mod tests {
let selected_row = (area.y..area.y.saturating_add(area.height))
.find(|&y| {
(area.x..area.x.saturating_add(area.width))
.any(|x| buf[(x, y)].bg == palette::DEEPSEEK_BLUE)
.any(|x| buf[(x, y)].bg == palette::SELECTION_BG)
})
.expect("selected approval row should use blue background");
.expect("selected approval row should use selection background");
let highlighted_cells = (area.x..area.x.saturating_add(area.width))
.filter(|&x| {
let cell = &buf[(x, selected_row)];
!cell.symbol().trim().is_empty()
&& cell.bg == palette::DEEPSEEK_BLUE
&& cell.bg == palette::SELECTION_BG
&& cell.fg == palette::SELECTION_TEXT
})
.count();
assert!(
highlighted_cells >= 4,
"selected destructive option should render visible blue/white text"
"selected destructive option should render visible selection text"
);
}