1use presentar_core::{Canvas, Color, Point, Rect, TextStyle};
17
18pub const SELECTION_BG: Color = Color {
27 r: 0.15,
28 g: 0.12,
29 b: 0.22,
30 a: 1.0,
31}; pub const SELECTION_ACCENT: Color = Color {
36 r: 0.4,
37 g: 0.9,
38 b: 0.4,
39 a: 1.0,
40}; pub const SELECTION_GUTTER: Color = Color {
44 r: 0.4,
45 g: 0.9,
46 b: 0.4,
47 a: 1.0,
48}; pub const DIMMED_BG: Color = Color {
52 r: 0.08,
53 g: 0.08,
54 b: 0.1,
55 a: 1.0,
56};
57
58#[derive(Debug, Clone)]
70pub struct RowHighlight {
71 pub bounds: Rect,
73 pub selected: bool,
75 pub show_gutter: bool,
77 pub gutter_char: char,
79}
80
81impl RowHighlight {
82 pub fn new(bounds: Rect, selected: bool) -> Self {
83 Self {
84 bounds,
85 selected,
86 show_gutter: true,
87 gutter_char: '▐',
88 }
89 }
90
91 pub fn with_gutter(mut self, show: bool) -> Self {
92 self.show_gutter = show;
93 self
94 }
95
96 pub fn with_gutter_char(mut self, ch: char) -> Self {
97 self.gutter_char = ch;
98 self
99 }
100
101 pub fn paint(&self, canvas: &mut dyn Canvas) {
106 if self.selected {
107 canvas.fill_rect(self.bounds, SELECTION_BG);
109
110 if self.show_gutter {
112 canvas.draw_text(
113 &self.gutter_char.to_string(),
114 Point::new(self.bounds.x - 1.0, self.bounds.y),
115 &TextStyle {
116 color: SELECTION_GUTTER,
117 ..Default::default()
118 },
119 );
120 }
121 } else {
122 canvas.fill_rect(self.bounds, DIMMED_BG);
124 }
125 }
126
127 pub fn text_style(&self) -> TextStyle {
129 if self.selected {
130 TextStyle {
131 color: Color::WHITE,
132 ..Default::default()
133 }
134 } else {
135 TextStyle {
136 color: Color::new(0.85, 0.85, 0.85, 1.0),
137 ..Default::default()
138 }
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
154pub struct FocusRing {
155 pub bounds: Rect,
157 pub focused: bool,
159 pub base_color: Color,
161}
162
163impl FocusRing {
164 pub fn new(bounds: Rect, focused: bool, base_color: Color) -> Self {
165 Self {
166 bounds,
167 focused,
168 base_color,
169 }
170 }
171
172 pub fn border_color(&self) -> Color {
174 if self.focused {
175 Color {
177 r: (self.base_color.r * 0.4 + SELECTION_ACCENT.r * 0.6).min(1.0),
178 g: (self.base_color.g * 0.4 + SELECTION_ACCENT.g * 0.6).min(1.0),
179 b: (self.base_color.b * 0.4 + SELECTION_ACCENT.b * 0.6).min(1.0),
180 a: 1.0,
181 }
182 } else {
183 Color {
185 r: self.base_color.r * 0.4,
186 g: self.base_color.g * 0.4,
187 b: self.base_color.b * 0.4,
188 a: 1.0,
189 }
190 }
191 }
192
193 pub fn title_prefix(&self) -> &'static str {
195 if self.focused {
196 "► "
197 } else {
198 ""
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
209pub struct ColumnHighlight {
210 pub bounds: Rect,
212 pub selected: bool,
214 pub sorted: bool,
216 pub sort_descending: bool,
218}
219
220impl ColumnHighlight {
221 pub fn new(bounds: Rect) -> Self {
222 Self {
223 bounds,
224 selected: false,
225 sorted: false,
226 sort_descending: true,
227 }
228 }
229
230 pub fn with_selected(mut self, selected: bool) -> Self {
231 self.selected = selected;
232 self
233 }
234
235 pub fn with_sorted(mut self, sorted: bool, descending: bool) -> Self {
236 self.sorted = sorted;
237 self.sort_descending = descending;
238 self
239 }
240
241 pub fn background(&self) -> Option<Color> {
243 if self.selected {
244 Some(Color::new(0.15, 0.35, 0.55, 1.0))
245 } else {
246 None
247 }
248 }
249
250 pub fn sort_indicator(&self) -> &'static str {
252 if self.sorted {
253 if self.sort_descending {
254 "▼"
255 } else {
256 "▲"
257 }
258 } else {
259 ""
260 }
261 }
262
263 pub fn text_style(&self) -> TextStyle {
265 let color = if self.sorted {
266 SELECTION_ACCENT
267 } else if self.selected {
268 Color::WHITE
269 } else {
270 Color::new(0.6, 0.6, 0.6, 1.0)
271 };
272
273 TextStyle {
274 color,
275 ..Default::default()
276 }
277 }
278}
279
280pub struct Cursor;
286
287impl Cursor {
288 pub const ROW: &'static str = "▶";
290
291 pub const COLUMN: &'static str = "▼";
293
294 pub const PANEL: &'static str = "►";
296
297 pub fn color() -> Color {
299 SELECTION_ACCENT
300 }
301
302 pub fn paint_row(canvas: &mut dyn Canvas, pos: Point) {
304 canvas.draw_text(
305 Self::ROW,
306 pos,
307 &TextStyle {
308 color: Self::color(),
309 ..Default::default()
310 },
311 );
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
324 fn test_row_highlight_colors() {
325 assert!(SELECTION_BG.r < 0.25, "Selection bg should be dark");
328 assert!(
329 SELECTION_BG.b > SELECTION_BG.r,
330 "Selection bg should have purple tint"
331 );
332 }
333
334 #[test]
335 fn test_selection_accent_is_green() {
336 assert!(SELECTION_ACCENT.g > 0.8, "Accent should be bright green");
338 assert!(
339 SELECTION_ACCENT.r > 0.3,
340 "Accent has some red for visibility"
341 );
342 }
343
344 #[test]
345 fn test_selection_gutter_matches_accent() {
346 assert_eq!(SELECTION_GUTTER.r, SELECTION_ACCENT.r);
348 assert_eq!(SELECTION_GUTTER.g, SELECTION_ACCENT.g);
349 assert_eq!(SELECTION_GUTTER.b, SELECTION_ACCENT.b);
350 }
351
352 #[test]
353 fn test_dimmed_bg_is_dark() {
354 assert!(DIMMED_BG.r < 0.15);
355 assert!(DIMMED_BG.g < 0.15);
356 assert!(DIMMED_BG.b < 0.15);
357 }
358
359 #[test]
364 fn test_row_highlight_new() {
365 let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
366 let highlight = RowHighlight::new(bounds, true);
367
368 assert_eq!(highlight.bounds, bounds);
369 assert!(highlight.selected);
370 assert!(highlight.show_gutter);
371 assert_eq!(highlight.gutter_char, '▐');
372 }
373
374 #[test]
375 fn test_row_highlight_not_selected() {
376 let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
377 let highlight = RowHighlight::new(bounds, false);
378
379 assert!(!highlight.selected);
380 }
381
382 #[test]
383 fn test_row_highlight_with_gutter() {
384 let highlight = RowHighlight::new(Rect::default(), true).with_gutter(false);
385 assert!(!highlight.show_gutter);
386
387 let highlight2 = highlight.with_gutter(true);
388 assert!(highlight2.show_gutter);
389 }
390
391 #[test]
392 fn test_row_highlight_with_gutter_char() {
393 let highlight = RowHighlight::new(Rect::default(), true).with_gutter_char('│');
394 assert_eq!(highlight.gutter_char, '│');
395 }
396
397 #[test]
398 fn test_row_highlight_text_style_selected() {
399 let highlight = RowHighlight::new(Rect::default(), true);
400 let style = highlight.text_style();
401 assert_eq!(style.color, Color::WHITE);
402 }
403
404 #[test]
405 fn test_row_highlight_text_style_not_selected() {
406 let highlight = RowHighlight::new(Rect::default(), false);
407 let style = highlight.text_style();
408 assert!(style.color.r > 0.8);
410 assert!(style.color.g > 0.8);
411 assert!(style.color.b > 0.8);
412 }
413
414 #[test]
415 fn test_row_highlight_paint_selected() {
416 use crate::direct::{CellBuffer, DirectTerminalCanvas};
417
418 let mut buffer = CellBuffer::new(20, 5);
419 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
420
421 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
422 let highlight = RowHighlight::new(bounds, true);
423 highlight.paint(&mut canvas);
424
425 }
428
429 #[test]
430 fn test_row_highlight_paint_not_selected() {
431 use crate::direct::{CellBuffer, DirectTerminalCanvas};
432
433 let mut buffer = CellBuffer::new(20, 5);
434 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
435
436 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
437 let highlight = RowHighlight::new(bounds, false);
438 highlight.paint(&mut canvas);
439
440 }
442
443 #[test]
444 fn test_row_highlight_paint_selected_no_gutter() {
445 use crate::direct::{CellBuffer, DirectTerminalCanvas};
446
447 let mut buffer = CellBuffer::new(20, 5);
448 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
449
450 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
451 let highlight = RowHighlight::new(bounds, true).with_gutter(false);
452 highlight.paint(&mut canvas);
453
454 }
456
457 #[test]
462 fn test_focus_ring_new() {
463 let bounds = Rect::new(0.0, 0.0, 50.0, 20.0);
464 let color = Color::new(0.5, 0.5, 1.0, 1.0);
465 let ring = FocusRing::new(bounds, true, color);
466
467 assert_eq!(ring.bounds, bounds);
468 assert!(ring.focused);
469 assert_eq!(ring.base_color, color);
470 }
471
472 #[test]
473 fn test_focus_ring_color_blend() {
474 let base = Color::new(0.5, 0.5, 1.0, 1.0); let ring = FocusRing::new(Rect::default(), true, base);
476
477 let color = ring.border_color();
478 assert!(color.g > base.g);
480 }
481
482 #[test]
483 fn test_focus_ring_not_focused_is_dimmed() {
484 let base = Color::new(1.0, 0.0, 0.0, 1.0); let ring = FocusRing::new(Rect::default(), false, base);
486
487 let color = ring.border_color();
488 assert!((color.r - 0.4).abs() < 0.01);
490 assert!(color.g < 0.01);
491 assert!(color.b < 0.01);
492 }
493
494 #[test]
495 fn test_focus_ring_title_prefix_focused() {
496 let ring = FocusRing::new(Rect::default(), true, Color::WHITE);
497 assert_eq!(ring.title_prefix(), "► ");
498 }
499
500 #[test]
501 fn test_focus_ring_title_prefix_not_focused() {
502 let ring = FocusRing::new(Rect::default(), false, Color::WHITE);
503 assert_eq!(ring.title_prefix(), "");
504 }
505
506 #[test]
511 fn test_column_highlight_new() {
512 let bounds = Rect::new(10.0, 0.0, 20.0, 1.0);
513 let col = ColumnHighlight::new(bounds);
514
515 assert_eq!(col.bounds, bounds);
516 assert!(!col.selected);
517 assert!(!col.sorted);
518 assert!(col.sort_descending);
519 }
520
521 #[test]
522 fn test_column_highlight_with_selected() {
523 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
524 assert!(col.selected);
525
526 let col2 = col.with_selected(false);
527 assert!(!col2.selected);
528 }
529
530 #[test]
531 fn test_column_highlight_with_sorted() {
532 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
533 assert!(col.sorted);
534 assert!(col.sort_descending);
535
536 let col2 = col.with_sorted(true, false);
537 assert!(col2.sorted);
538 assert!(!col2.sort_descending);
539 }
540
541 #[test]
542 fn test_column_highlight_sort_indicator_descending() {
543 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
544 assert_eq!(col.sort_indicator(), "▼");
545 }
546
547 #[test]
548 fn test_column_highlight_sort_indicator_ascending() {
549 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, false);
550 assert_eq!(col.sort_indicator(), "▲");
551 }
552
553 #[test]
554 fn test_column_highlight_sort_indicator_not_sorted() {
555 let col = ColumnHighlight::new(Rect::default());
556 assert_eq!(col.sort_indicator(), "");
557 }
558
559 #[test]
560 fn test_column_highlight_background_selected() {
561 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
562 let bg = col.background();
563 assert!(bg.is_some());
564 let bg = bg.unwrap();
565 assert!(bg.b > bg.r); }
567
568 #[test]
569 fn test_column_highlight_background_not_selected() {
570 let col = ColumnHighlight::new(Rect::default());
571 assert!(col.background().is_none());
572 }
573
574 #[test]
575 fn test_column_highlight_text_style_sorted() {
576 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
577 let style = col.text_style();
578 assert_eq!(style.color, SELECTION_ACCENT);
579 }
580
581 #[test]
582 fn test_column_highlight_text_style_selected() {
583 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
584 let style = col.text_style();
585 assert_eq!(style.color, Color::WHITE);
586 }
587
588 #[test]
589 fn test_column_highlight_text_style_neither() {
590 let col = ColumnHighlight::new(Rect::default());
591 let style = col.text_style();
592 assert!(style.color.r > 0.5 && style.color.r < 0.7);
594 }
595
596 #[test]
601 fn test_cursor_constants() {
602 assert_eq!(Cursor::ROW, "▶");
603 assert_eq!(Cursor::COLUMN, "▼");
604 assert_eq!(Cursor::PANEL, "►");
605 }
606
607 #[test]
608 fn test_cursor_color() {
609 assert_eq!(Cursor::color(), SELECTION_ACCENT);
610 }
611
612 #[test]
613 fn test_cursor_paint_row() {
614 use crate::direct::{CellBuffer, DirectTerminalCanvas};
615
616 let mut buffer = CellBuffer::new(20, 5);
617 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
618
619 Cursor::paint_row(&mut canvas, Point::new(0.0, 0.0));
620 }
622}