use crate::primitives::terminal::{TerminalCell, TerminalCursorShape};
use crate::text_util::display_width;
use crate::theme::Theme;
use crate::types::Color;
use crate::Rect;
pub fn resolve_cell_style(cell: &TerminalCell, theme: &Theme) -> (Color, Color) {
if cell.is_cursor
&& cell.cursor_shape == TerminalCursorShape::Block
&& cursor_blink_visible(cell.cursor_blinking)
{
return (cell.fg, cell.bg);
}
let (bg, fg) = if cell.is_find_active {
(theme.find_active_bg(), theme.find_active_fg())
} else if cell.is_find_match {
(theme.find_match_bg(), cell.fg)
} else if cell.selected {
(theme.selection_bg, cell.fg)
} else {
(cell.bg, cell.fg)
};
if cell.dim {
(bg, dim_fg(fg, bg))
} else {
(bg, fg)
}
}
const CURSOR_BLINK_HALF_PERIOD_MS: u128 = 530;
pub fn cursor_blink_visible(blinking: bool) -> bool {
if !blinking {
return true;
}
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
(now_ms / CURSOR_BLINK_HALF_PERIOD_MS).is_multiple_of(2)
}
pub fn cursor_accent_visible(cell: &TerminalCell) -> bool {
cell.is_cursor
&& cell.cursor_shape != TerminalCursorShape::Block
&& cursor_blink_visible(cell.cursor_blinking)
}
pub const CURSOR_ACCENT_THICKNESS: f32 = 2.0;
pub fn cursor_accent_rect(
shape: TerminalCursorShape,
cell_x: f32,
cell_y: f32,
cell_w: f32,
line_height: f32,
) -> Option<Rect> {
match shape {
TerminalCursorShape::Block => None,
TerminalCursorShape::Underline => Some(Rect::new(
cell_x,
cell_y + (line_height - CURSOR_ACCENT_THICKNESS).max(0.0),
cell_w,
CURSOR_ACCENT_THICKNESS.min(line_height),
)),
TerminalCursorShape::Bar => Some(Rect::new(
cell_x,
cell_y,
CURSOR_ACCENT_THICKNESS.min(cell_w),
line_height,
)),
}
}
fn dim_fg(fg: Color, bg: Color) -> Color {
Color::rgba(
((fg.r as u16 + bg.r as u16) / 2) as u8,
((fg.g as u16 + bg.g as u16) / 2) as u8,
((fg.b as u16 + bg.b as u16) / 2) as u8,
fg.a,
)
}
pub fn wide_cell_advance(text: &str, char_width: f64) -> (f64, usize) {
if display_width(text) >= 2 {
(char_width * 2.0, 2)
} else {
(char_width, 1)
}
}
pub fn wide_glyph_x_scale(natural_w: f64, cell_w: f64) -> f64 {
if natural_w <= 0.0 {
1.0
} else {
cell_w / natural_w
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DividerGeometry {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
pub fn divider_geometry(x: f64, y: f64, height: f64) -> DividerGeometry {
DividerGeometry {
x,
y,
width: 1.0,
height,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(ch: char, fg: Color, bg: Color) -> TerminalCell {
TerminalCell {
text: ch.to_string(),
fg,
bg,
bold: false,
italic: false,
underline: false,
dim: false,
selected: false,
is_cursor: false,
is_find_match: false,
is_find_active: false,
cursor_shape: TerminalCursorShape::Block,
cursor_blinking: false,
}
}
#[test]
fn default_cell_uses_own_colors() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let c = cell('a', fg, bg);
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (bg, fg));
}
#[test]
fn cursor_cell_inverts_fg_bg() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_cursor = true;
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (fg, bg));
}
#[test]
fn find_active_cell_uses_theme_highlight() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_find_active = true;
let theme = Theme::default();
assert_eq!(
resolve_cell_style(&c, &theme),
(theme.find_active_bg(), theme.find_active_fg())
);
}
#[test]
fn find_match_cell_keeps_own_fg() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_find_match = true;
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (theme.find_match_bg(), fg));
}
#[test]
fn selected_cell_uses_theme_selection_bg_and_own_fg() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.selected = true;
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (theme.selection_bg, fg));
}
#[test]
fn cursor_takes_precedence_over_every_other_overlay() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_cursor = true;
c.is_find_active = true;
c.is_find_match = true;
c.selected = true;
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (fg, bg));
}
#[test]
fn find_active_takes_precedence_over_find_match_and_selected() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_find_active = true;
c.is_find_match = true;
c.selected = true;
let theme = Theme::default();
assert_eq!(
resolve_cell_style(&c, &theme),
(theme.find_active_bg(), theme.find_active_fg())
);
}
#[test]
fn dim_cell_blends_fg_halfway_to_bg() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(0, 0, 0);
let mut c = cell('a', fg, bg);
c.dim = true;
let theme = Theme::default();
let (resolved_bg, resolved_fg) = resolve_cell_style(&c, &theme);
assert_eq!(resolved_bg, bg, "dim must not change the background");
assert_eq!(resolved_fg, Color::rgb(100, 100, 100));
}
#[test]
fn non_dim_cell_is_unaffected() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let c = cell('a', fg, bg);
assert!(!c.dim);
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (bg, fg));
}
#[test]
fn dim_blends_toward_resolved_overlay_background_not_cells_own_bg() {
let fg = Color::rgb(200, 200, 200);
let cell_bg = Color::rgb(0, 0, 0);
let mut c = cell('a', fg, cell_bg);
c.dim = true;
c.selected = true;
let theme = Theme::default();
let (resolved_bg, resolved_fg) = resolve_cell_style(&c, &theme);
assert_eq!(resolved_bg, theme.selection_bg);
let expected_fg = Color::rgb(
((fg.r as u16 + theme.selection_bg.r as u16) / 2) as u8,
((fg.g as u16 + theme.selection_bg.g as u16) / 2) as u8,
((fg.b as u16 + theme.selection_bg.b as u16) / 2) as u8,
);
assert_eq!(resolved_fg, expected_fg);
}
#[test]
fn dim_does_not_apply_under_cursor_overlay() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.dim = true;
c.is_cursor = true;
let theme = Theme::default();
assert_eq!(resolve_cell_style(&c, &theme), (fg, bg));
}
#[test]
fn underline_and_bar_cursor_do_not_invert() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let theme = Theme::default();
let mut underline = cell('a', fg, bg);
underline.is_cursor = true;
underline.cursor_shape = TerminalCursorShape::Underline;
assert_eq!(resolve_cell_style(&underline, &theme), (bg, fg));
let mut bar = cell('a', fg, bg);
bar.is_cursor = true;
bar.cursor_shape = TerminalCursorShape::Bar;
assert_eq!(resolve_cell_style(&bar, &theme), (bg, fg));
}
#[test]
fn bar_cursor_does_not_block_lower_precedence_overlays() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut c = cell('a', fg, bg);
c.is_cursor = true;
c.cursor_shape = TerminalCursorShape::Bar;
c.is_find_active = true;
let theme = Theme::default();
assert_eq!(
resolve_cell_style(&c, &theme),
(theme.find_active_bg(), theme.find_active_fg())
);
}
#[test]
fn steady_cursor_always_blink_visible() {
assert!(cursor_blink_visible(false));
assert!(cursor_blink_visible(false));
}
#[test]
fn cursor_accent_visible_gates_on_shape_and_is_cursor() {
let fg = Color::rgb(200, 200, 200);
let bg = Color::rgb(10, 10, 10);
let mut not_cursor = cell('a', fg, bg);
not_cursor.cursor_shape = TerminalCursorShape::Bar;
assert!(!cursor_accent_visible(¬_cursor));
let mut block = cell('a', fg, bg);
block.is_cursor = true;
assert!(!cursor_accent_visible(&block));
let mut bar = cell('a', fg, bg);
bar.is_cursor = true;
bar.cursor_shape = TerminalCursorShape::Bar;
assert!(cursor_accent_visible(&bar));
let mut underline = cell('a', fg, bg);
underline.is_cursor = true;
underline.cursor_shape = TerminalCursorShape::Underline;
assert!(cursor_accent_visible(&underline));
}
#[test]
fn cursor_accent_rect_geometry() {
let (cell_x, cell_y, cell_w, line_h) = (10.0, 20.0, 8.0, 16.0);
assert_eq!(
cursor_accent_rect(TerminalCursorShape::Block, cell_x, cell_y, cell_w, line_h),
None
);
let underline = cursor_accent_rect(
TerminalCursorShape::Underline,
cell_x,
cell_y,
cell_w,
line_h,
)
.expect("Underline has an accent rect");
assert_eq!(underline.x, cell_x);
assert_eq!(underline.width, cell_w);
assert!(underline.y >= cell_y && underline.y + underline.height <= cell_y + line_h);
let bar = cursor_accent_rect(TerminalCursorShape::Bar, cell_x, cell_y, cell_w, line_h)
.expect("Bar has an accent rect");
assert_eq!(bar.x, cell_x);
assert_eq!(bar.y, cell_y);
assert_eq!(bar.height, line_h);
assert!(bar.width <= cell_w);
}
#[test]
fn cursor_accent_rect_clamps_to_a_tiny_cell() {
let underline = cursor_accent_rect(TerminalCursorShape::Underline, 0.0, 0.0, 1.0, 1.0)
.expect("Underline has an accent rect");
assert!(underline.height <= 1.0);
assert!(underline.y >= 0.0);
let bar = cursor_accent_rect(TerminalCursorShape::Bar, 0.0, 0.0, 1.0, 1.0)
.expect("Bar has an accent rect");
assert!(bar.width <= 1.0);
}
#[test]
fn narrow_char_advances_one_column() {
assert_eq!(wide_cell_advance("a", 10.0), (10.0, 1));
assert_eq!(wide_cell_advance(" ", 10.0), (10.0, 1));
}
#[test]
fn wide_char_advances_two_columns_at_double_width() {
assert_eq!(wide_cell_advance("日", 10.0), (20.0, 2));
assert_eq!(wide_cell_advance("中", 9.0), (18.0, 2));
}
#[test]
fn combining_mark_grapheme_advances_one_column() {
assert_eq!(wide_cell_advance("e\u{0301}", 10.0), (10.0, 1));
}
#[test]
fn wide_char_with_combining_mark_advances_two_columns() {
assert_eq!(wide_cell_advance("日\u{0301}", 10.0), (20.0, 2));
}
#[test]
fn narrow_wide_glyph_is_stretched_to_fill_two_cells() {
let cell_w = 18.0;
let scale = wide_glyph_x_scale(15.0, cell_w);
assert!(
(scale - 1.2).abs() < 1e-9,
"15px glyph in an 18px box should scale 1.2x, got {scale}"
);
assert!((15.0 * scale - cell_w).abs() < 1e-9);
}
#[test]
fn exact_fit_wide_glyph_is_not_scaled() {
assert_eq!(wide_glyph_x_scale(18.0, 18.0), 1.0);
}
#[test]
fn over_wide_glyph_is_shrunk_into_box() {
let scale = wide_glyph_x_scale(24.0, 18.0);
assert!(
scale < 1.0,
"24px glyph in 18px box should shrink, got {scale}"
);
assert!((24.0 * scale - 18.0).abs() < 1e-9);
}
#[test]
fn degenerate_glyph_width_falls_back_to_no_scale() {
assert_eq!(wide_glyph_x_scale(0.0, 18.0), 1.0);
assert_eq!(wide_glyph_x_scale(-3.0, 18.0), 1.0);
}
#[test]
fn divider_geometry_is_one_unit_wide() {
let g = divider_geometry(50.0, 5.0, 24.0);
assert_eq!(
g,
DividerGeometry {
x: 50.0,
y: 5.0,
width: 1.0,
height: 24.0,
}
);
}
}