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
280#[derive(Debug)]
286pub struct Cursor;
287
288impl Cursor {
289 pub const ROW: &'static str = "▶";
291
292 pub const COLUMN: &'static str = "▼";
294
295 pub const PANEL: &'static str = "►";
297
298 pub fn color() -> Color {
300 SELECTION_ACCENT
301 }
302
303 pub fn paint_row(canvas: &mut dyn Canvas, pos: Point) {
305 canvas.draw_text(
306 Self::ROW,
307 pos,
308 &TextStyle {
309 color: Self::color(),
310 ..Default::default()
311 },
312 );
313 }
314}
315
316#[cfg(test)]
317#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
318mod tests {
319 use super::*;
320
321 #[test]
326 #[allow(clippy::assertions_on_constants)]
327 fn test_row_highlight_colors() {
328 assert!(SELECTION_BG.r < 0.25, "Selection bg should be dark");
331 assert!(
332 SELECTION_BG.b > SELECTION_BG.r,
333 "Selection bg should have purple tint"
334 );
335 }
336
337 #[test]
338 #[allow(clippy::assertions_on_constants)]
339 fn test_selection_accent_is_green() {
340 assert!(SELECTION_ACCENT.g > 0.8, "Accent should be bright green");
342 assert!(
343 SELECTION_ACCENT.r > 0.3,
344 "Accent has some red for visibility"
345 );
346 }
347
348 #[test]
349 fn test_selection_gutter_matches_accent() {
350 assert_eq!(SELECTION_GUTTER.r, SELECTION_ACCENT.r);
352 assert_eq!(SELECTION_GUTTER.g, SELECTION_ACCENT.g);
353 assert_eq!(SELECTION_GUTTER.b, SELECTION_ACCENT.b);
354 }
355
356 #[test]
357 #[allow(clippy::assertions_on_constants)]
358 fn test_dimmed_bg_is_dark() {
359 assert!(DIMMED_BG.r < 0.15);
360 assert!(DIMMED_BG.g < 0.15);
361 assert!(DIMMED_BG.b < 0.15);
362 }
363
364 #[test]
369 fn test_row_highlight_new() {
370 let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
371 let highlight = RowHighlight::new(bounds, true);
372
373 assert_eq!(highlight.bounds, bounds);
374 assert!(highlight.selected);
375 assert!(highlight.show_gutter);
376 assert_eq!(highlight.gutter_char, '▐');
377 }
378
379 #[test]
380 fn test_row_highlight_not_selected() {
381 let bounds = Rect::new(0.0, 0.0, 100.0, 1.0);
382 let highlight = RowHighlight::new(bounds, false);
383
384 assert!(!highlight.selected);
385 }
386
387 #[test]
388 fn test_row_highlight_with_gutter() {
389 let highlight = RowHighlight::new(Rect::default(), true).with_gutter(false);
390 assert!(!highlight.show_gutter);
391
392 let highlight2 = highlight.with_gutter(true);
393 assert!(highlight2.show_gutter);
394 }
395
396 #[test]
397 fn test_row_highlight_with_gutter_char() {
398 let highlight = RowHighlight::new(Rect::default(), true).with_gutter_char('│');
399 assert_eq!(highlight.gutter_char, '│');
400 }
401
402 #[test]
403 fn test_row_highlight_text_style_selected() {
404 let highlight = RowHighlight::new(Rect::default(), true);
405 let style = highlight.text_style();
406 assert_eq!(style.color, Color::WHITE);
407 }
408
409 #[test]
410 fn test_row_highlight_text_style_not_selected() {
411 let highlight = RowHighlight::new(Rect::default(), false);
412 let style = highlight.text_style();
413 assert!(style.color.r > 0.8);
415 assert!(style.color.g > 0.8);
416 assert!(style.color.b > 0.8);
417 }
418
419 #[test]
420 fn test_row_highlight_paint_selected() {
421 use crate::direct::{CellBuffer, DirectTerminalCanvas};
422
423 let mut buffer = CellBuffer::new(20, 5);
424 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
425
426 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
427 let highlight = RowHighlight::new(bounds, true);
428 highlight.paint(&mut canvas);
429
430 }
433
434 #[test]
435 fn test_row_highlight_paint_not_selected() {
436 use crate::direct::{CellBuffer, DirectTerminalCanvas};
437
438 let mut buffer = CellBuffer::new(20, 5);
439 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
440
441 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
442 let highlight = RowHighlight::new(bounds, false);
443 highlight.paint(&mut canvas);
444
445 }
447
448 #[test]
449 fn test_row_highlight_paint_selected_no_gutter() {
450 use crate::direct::{CellBuffer, DirectTerminalCanvas};
451
452 let mut buffer = CellBuffer::new(20, 5);
453 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
454
455 let bounds = Rect::new(2.0, 1.0, 10.0, 1.0);
456 let highlight = RowHighlight::new(bounds, true).with_gutter(false);
457 highlight.paint(&mut canvas);
458
459 }
461
462 #[test]
467 fn test_focus_ring_new() {
468 let bounds = Rect::new(0.0, 0.0, 50.0, 20.0);
469 let color = Color::new(0.5, 0.5, 1.0, 1.0);
470 let ring = FocusRing::new(bounds, true, color);
471
472 assert_eq!(ring.bounds, bounds);
473 assert!(ring.focused);
474 assert_eq!(ring.base_color, color);
475 }
476
477 #[test]
478 fn test_focus_ring_color_blend() {
479 let base = Color::new(0.5, 0.5, 1.0, 1.0); let ring = FocusRing::new(Rect::default(), true, base);
481
482 let color = ring.border_color();
483 assert!(color.g > base.g);
485 }
486
487 #[test]
488 fn test_focus_ring_not_focused_is_dimmed() {
489 let base = Color::new(1.0, 0.0, 0.0, 1.0); let ring = FocusRing::new(Rect::default(), false, base);
491
492 let color = ring.border_color();
493 assert!((color.r - 0.4).abs() < 0.01);
495 assert!(color.g < 0.01);
496 assert!(color.b < 0.01);
497 }
498
499 #[test]
500 fn test_focus_ring_title_prefix_focused() {
501 let ring = FocusRing::new(Rect::default(), true, Color::WHITE);
502 assert_eq!(ring.title_prefix(), "► ");
503 }
504
505 #[test]
506 fn test_focus_ring_title_prefix_not_focused() {
507 let ring = FocusRing::new(Rect::default(), false, Color::WHITE);
508 assert_eq!(ring.title_prefix(), "");
509 }
510
511 #[test]
516 fn test_column_highlight_new() {
517 let bounds = Rect::new(10.0, 0.0, 20.0, 1.0);
518 let col = ColumnHighlight::new(bounds);
519
520 assert_eq!(col.bounds, bounds);
521 assert!(!col.selected);
522 assert!(!col.sorted);
523 assert!(col.sort_descending);
524 }
525
526 #[test]
527 fn test_column_highlight_with_selected() {
528 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
529 assert!(col.selected);
530
531 let col2 = col.with_selected(false);
532 assert!(!col2.selected);
533 }
534
535 #[test]
536 fn test_column_highlight_with_sorted() {
537 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
538 assert!(col.sorted);
539 assert!(col.sort_descending);
540
541 let col2 = col.with_sorted(true, false);
542 assert!(col2.sorted);
543 assert!(!col2.sort_descending);
544 }
545
546 #[test]
547 fn test_column_highlight_sort_indicator_descending() {
548 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
549 assert_eq!(col.sort_indicator(), "▼");
550 }
551
552 #[test]
553 fn test_column_highlight_sort_indicator_ascending() {
554 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, false);
555 assert_eq!(col.sort_indicator(), "▲");
556 }
557
558 #[test]
559 fn test_column_highlight_sort_indicator_not_sorted() {
560 let col = ColumnHighlight::new(Rect::default());
561 assert_eq!(col.sort_indicator(), "");
562 }
563
564 #[test]
565 fn test_column_highlight_background_selected() {
566 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
567 let bg = col.background();
568 assert!(bg.is_some());
569 let bg = bg.unwrap();
570 assert!(bg.b > bg.r); }
572
573 #[test]
574 fn test_column_highlight_background_not_selected() {
575 let col = ColumnHighlight::new(Rect::default());
576 assert!(col.background().is_none());
577 }
578
579 #[test]
580 fn test_column_highlight_text_style_sorted() {
581 let col = ColumnHighlight::new(Rect::default()).with_sorted(true, true);
582 let style = col.text_style();
583 assert_eq!(style.color, SELECTION_ACCENT);
584 }
585
586 #[test]
587 fn test_column_highlight_text_style_selected() {
588 let col = ColumnHighlight::new(Rect::default()).with_selected(true);
589 let style = col.text_style();
590 assert_eq!(style.color, Color::WHITE);
591 }
592
593 #[test]
594 fn test_column_highlight_text_style_neither() {
595 let col = ColumnHighlight::new(Rect::default());
596 let style = col.text_style();
597 assert!(style.color.r > 0.5 && style.color.r < 0.7);
599 }
600
601 #[test]
606 fn test_cursor_constants() {
607 assert_eq!(Cursor::ROW, "▶");
608 assert_eq!(Cursor::COLUMN, "▼");
609 assert_eq!(Cursor::PANEL, "►");
610 }
611
612 #[test]
613 fn test_cursor_color() {
614 assert_eq!(Cursor::color(), SELECTION_ACCENT);
615 }
616
617 #[test]
618 fn test_cursor_paint_row() {
619 use crate::direct::{CellBuffer, DirectTerminalCanvas};
620
621 let mut buffer = CellBuffer::new(20, 5);
622 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
623
624 Cursor::paint_row(&mut canvas, Point::new(0.0, 0.0));
625 }
627}