use presentar_core::{Canvas, Color, Point, Rect, TextStyle};
pub const SELECTION_BG: Color = Color {
r: 0.15,
g: 0.12,
b: 0.22,
a: 1.0,
};
pub const SELECTION_ACCENT: Color = Color {
r: 0.4,
g: 0.9,
b: 0.4,
a: 1.0,
};
pub const SELECTION_GUTTER: Color = Color {
r: 0.4,
g: 0.9,
b: 0.4,
a: 1.0,
};
pub const DIMMED_BG: Color = Color {
r: 0.08,
g: 0.08,
b: 0.1,
a: 1.0,
};
#[derive(Debug, Clone)]
pub struct RowHighlight {
pub bounds: Rect,
pub selected: bool,
pub show_gutter: bool,
pub gutter_char: char,
}
impl RowHighlight {
pub fn new(bounds: Rect, selected: bool) -> Self {
Self {
bounds,
selected,
show_gutter: true,
gutter_char: '▐',
}
}
pub fn with_gutter(mut self, show: bool) -> Self {
self.show_gutter = show;
self
}
pub fn with_gutter_char(mut self, ch: char) -> Self {
self.gutter_char = ch;
self
}
pub fn paint(&self, canvas: &mut dyn Canvas) {
if self.selected {
canvas.fill_rect(self.bounds, SELECTION_BG);
if self.show_gutter {
canvas.draw_text(
&self.gutter_char.to_string(),
Point::new(self.bounds.x - 1.0, self.bounds.y),
&TextStyle {
color: SELECTION_GUTTER,
..Default::default()
},
);
}
} else {
canvas.fill_rect(self.bounds, DIMMED_BG);
}
}
pub fn text_style(&self) -> TextStyle {
if self.selected {
TextStyle {
color: Color::WHITE,
..Default::default()
}
} else {
TextStyle {
color: Color::new(0.85, 0.85, 0.85, 1.0),
..Default::default()
}
}
}
}
#[derive(Debug, Clone)]
pub struct FocusRing {
pub bounds: Rect,
pub focused: bool,
pub base_color: Color,
}
impl FocusRing {
pub fn new(bounds: Rect, focused: bool, base_color: Color) -> Self {
Self {
bounds,
focused,
base_color,
}
}
pub fn border_color(&self) -> Color {
if self.focused {
Color {
r: (self.base_color.r * 0.4 + SELECTION_ACCENT.r * 0.6).min(1.0),
g: (self.base_color.g * 0.4 + SELECTION_ACCENT.g * 0.6).min(1.0),
b: (self.base_color.b * 0.4 + SELECTION_ACCENT.b * 0.6).min(1.0),
a: 1.0,
}
} else {
Color {
r: self.base_color.r * 0.4,
g: self.base_color.g * 0.4,
b: self.base_color.b * 0.4,
a: 1.0,
}
}
}
pub fn title_prefix(&self) -> &'static str {
if self.focused {
"► "
} else {
""
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnHighlight {
pub bounds: Rect,
pub selected: bool,
pub sorted: bool,
pub sort_descending: bool,
}
impl ColumnHighlight {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
selected: false,
sorted: false,
sort_descending: true,
}
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn with_sorted(mut self, sorted: bool, descending: bool) -> Self {
self.sorted = sorted;
self.sort_descending = descending;
self
}
pub fn background(&self) -> Option<Color> {
if self.selected {
Some(Color::new(0.15, 0.35, 0.55, 1.0))
} else {
None
}
}
pub fn sort_indicator(&self) -> &'static str {
if self.sorted {
if self.sort_descending {
"▼"
} else {
"▲"
}
} else {
""
}
}
pub fn text_style(&self) -> TextStyle {
let color = if self.sorted {
SELECTION_ACCENT
} else if self.selected {
Color::WHITE
} else {
Color::new(0.6, 0.6, 0.6, 1.0)
};
TextStyle {
color,
..Default::default()
}
}
}
pub struct Cursor;
impl Cursor {
pub const ROW: &'static str = "▶";
pub const COLUMN: &'static str = "▼";
pub const PANEL: &'static str = "►";
pub fn color() -> Color {
SELECTION_ACCENT
}
pub fn paint_row(canvas: &mut dyn Canvas, pos: Point) {
canvas.draw_text(
Self::ROW,
pos,
&TextStyle {
color: Self::color(),
..Default::default()
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_row_highlight_colors() {
assert!(SELECTION_BG.r < 0.25, "Selection bg should be dark");
assert!(
SELECTION_BG.b > SELECTION_BG.r,
"Selection bg should have purple tint"
);
}
#[test]
fn test_selection_accent_is_green() {
assert!(SELECTION_ACCENT.g > 0.8, "Accent should be bright green");
assert!(
SELECTION_ACCENT.r > 0.3,
"Accent has some red for visibility"
);
}
#[test]
fn test_selection_gutter_matches_accent() {
assert_eq!(SELECTION_GUTTER.r, SELECTION_ACCENT.r);
assert_eq!(SELECTION_GUTTER.g, SELECTION_ACCENT.g);
assert_eq!(SELECTION_GUTTER.b, SELECTION_ACCENT.b);
}
#[test]
fn test_dimmed_bg_is_dark() {
assert!(DIMMED_BG.r < 0.15);
assert!(DIMMED_BG.g < 0.15);
assert!(DIMMED_BG.b < 0.15);
}
#[test]
fn test_row_highlight_new() {
let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
let highlight = RowHighlight::new(bounds, true);
assert_eq!(highlight.bounds, bounds);
assert!(highlight.selected);
assert!(highlight.show_gutter);
assert_eq!(highlight.gutter_char, '▐');
}
#[test]
fn test_row_highlight_not_selected() {
let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
let highlight = RowHighlight::new(bounds, false);
assert!(!highlight.selected);
}
#[test]
fn test_row_highlight_with_gutter() {
let highlight = RowHighlight::new(Rect::default(), true).with_gutter(false);
assert!(!highlight.show_gutter);
let highlight2 = highlight.with_gutter(true);
assert!(highlight2.show_gutter);
}
#[test]
fn test_row_highlight_with_gutter_char() {
let highlight = RowHighlight::new(Rect::default(), true).with_gutter_char('│');
assert_eq!(highlight.gutter_char, '│');
}
#[test]
fn test_row_highlight_text_style_selected() {
let highlight = RowHighlight::new(Rect::default(), true);
let style = highlight.text_style();
assert_eq!(style.color, Color::WHITE);
}
#[test]
fn test_row_highlight_text_style_not_selected() {
let highlight = RowHighlight::new(Rect::default(), false);
let style = highlight.text_style();
assert!(style.color.r > 0.8);
assert!(style.color.g > 0.8);
assert!(style.color.b > 0.8);
}
#[test]
fn test_row_highlight_paint_selected() {
use crate::direct::{CellBuffer, DirectTerminalCanvas};
let mut buffer = CellBuffer::new(20, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
let highlight = RowHighlight::new(bounds, true);
highlight.paint(&mut canvas);
}
#[test]
fn test_row_highlight_paint_not_selected() {
use crate::direct::{CellBuffer, DirectTerminalCanvas};
let mut buffer = CellBuffer::new(20, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
let highlight = RowHighlight::new(bounds, false);
highlight.paint(&mut canvas);
}
#[test]
fn test_row_highlight_paint_selected_no_gutter() {
use crate::direct::{CellBuffer, DirectTerminalCanvas};
let mut buffer = CellBuffer::new(20, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
let highlight = RowHighlight::new(bounds, true).with_gutter(false);
highlight.paint(&mut canvas);
}
#[test]
fn test_focus_ring_new() {
let bounds = Rect::new(0.0, 0.0, 50.0, 20.0);
let color = Color::new(0.5, 0.5, 1.0, 1.0);
let ring = FocusRing::new(bounds, true, color);
assert_eq!(ring.bounds, bounds);
assert!(ring.focused);
assert_eq!(ring.base_color, color);
}
#[test]
fn test_focus_ring_color_blend() {
let base = Color::new(0.5, 0.5, 1.0, 1.0); let ring = FocusRing::new(Rect::default(), true, base);
let color = ring.border_color();
assert!(color.g > base.g);
}
#[test]
fn test_focus_ring_not_focused_is_dimmed() {
let base = Color::new(1.0, 0.0, 0.0, 1.0); let ring = FocusRing::new(Rect::default(), false, base);
let color = ring.border_color();
assert!((color.r - 0.4).abs() < 0.01);
assert!(color.g < 0.01);
assert!(color.b < 0.01);
}
#[test]
fn test_focus_ring_title_prefix_focused() {
let ring = FocusRing::new(Rect::default(), true, Color::WHITE);
assert_eq!(ring.title_prefix(), "► ");
}
#[test]
fn test_focus_ring_title_prefix_not_focused() {
let ring = FocusRing::new(Rect::default(), false, Color::WHITE);
assert_eq!(ring.title_prefix(), "");
}
#[test]
fn test_column_highlight_new() {
let bounds = Rect::new(10.0, 0.0, 20.0, 1.0);
let col = ColumnHighlight::new(bounds);
assert_eq!(col.bounds, bounds);
assert!(!col.selected);
assert!(!col.sorted);
assert!(col.sort_descending);
}
#[test]
fn test_column_highlight_with_selected() {
let col = ColumnHighlight::new(Rect::default()).with_selected(true);
assert!(col.selected);
let col2 = col.with_selected(false);
assert!(!col2.selected);
}
#[test]
fn test_column_highlight_with_sorted() {
let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
assert!(col.sorted);
assert!(col.sort_descending);
let col2 = col.with_sorted(true, false);
assert!(col2.sorted);
assert!(!col2.sort_descending);
}
#[test]
fn test_column_highlight_sort_indicator_descending() {
let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
assert_eq!(col.sort_indicator(), "▼");
}
#[test]
fn test_column_highlight_sort_indicator_ascending() {
let col = ColumnHighlight::new(Rect::default()).with_sorted(true, false);
assert_eq!(col.sort_indicator(), "▲");
}
#[test]
fn test_column_highlight_sort_indicator_not_sorted() {
let col = ColumnHighlight::new(Rect::default());
assert_eq!(col.sort_indicator(), "");
}
#[test]
fn test_column_highlight_background_selected() {
let col = ColumnHighlight::new(Rect::default()).with_selected(true);
let bg = col.background();
assert!(bg.is_some());
let bg = bg.unwrap();
assert!(bg.b > bg.r); }
#[test]
fn test_column_highlight_background_not_selected() {
let col = ColumnHighlight::new(Rect::default());
assert!(col.background().is_none());
}
#[test]
fn test_column_highlight_text_style_sorted() {
let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
let style = col.text_style();
assert_eq!(style.color, SELECTION_ACCENT);
}
#[test]
fn test_column_highlight_text_style_selected() {
let col = ColumnHighlight::new(Rect::default()).with_selected(true);
let style = col.text_style();
assert_eq!(style.color, Color::WHITE);
}
#[test]
fn test_column_highlight_text_style_neither() {
let col = ColumnHighlight::new(Rect::default());
let style = col.text_style();
assert!(style.color.r > 0.5 && style.color.r < 0.7);
}
#[test]
fn test_cursor_constants() {
assert_eq!(Cursor::ROW, "▶");
assert_eq!(Cursor::COLUMN, "▼");
assert_eq!(Cursor::PANEL, "►");
}
#[test]
fn test_cursor_color() {
assert_eq!(Cursor::color(), SELECTION_ACCENT);
}
#[test]
fn test_cursor_paint_row() {
use crate::direct::{CellBuffer, DirectTerminalCanvas};
let mut buffer = CellBuffer::new(20, 5);
let mut canvas = DirectTerminalCanvas::new(&mut buffer);
Cursor::paint_row(&mut canvas, Point::new(0.0, 0.0));
}
}