1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
3use crate::layout::{Command, Direction};
4use crate::rect::Rect;
5use crate::style::{
6 Align, Border, BorderSides, Color, Constraints, Justify, Margin, Modifiers, Padding, Style,
7 Theme,
8};
9use crate::widgets::{
10 ButtonVariant, CommandPaletteState, FormField, FormState, ListState, MultiSelectState,
11 RadioState, ScrollState, SelectState, SpinnerState, TableState, TabsState, TextInputState,
12 TextareaState, ToastLevel, ToastState, TreeState,
13};
14use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
15
16#[allow(dead_code)]
17fn slt_assert(condition: bool, msg: &str) {
18 if !condition {
19 panic!("[SLT] {}", msg);
20 }
21}
22
23#[cfg(debug_assertions)]
24#[allow(dead_code)]
25fn slt_warn(msg: &str) {
26 eprintln!("\x1b[33m[SLT warning]\x1b[0m {}", msg);
27}
28
29#[cfg(not(debug_assertions))]
30#[allow(dead_code)]
31fn slt_warn(_msg: &str) {}
32
33#[derive(Debug, Clone, Copy, Default)]
39pub struct Response {
40 pub clicked: bool,
42 pub hovered: bool,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum BarDirection {
49 Horizontal,
51 Vertical,
53}
54
55#[derive(Debug, Clone)]
57pub struct Bar {
58 pub label: String,
60 pub value: f64,
62 pub color: Option<Color>,
64}
65
66impl Bar {
67 pub fn new(label: impl Into<String>, value: f64) -> Self {
69 Self {
70 label: label.into(),
71 value,
72 color: None,
73 }
74 }
75
76 pub fn color(mut self, color: Color) -> Self {
78 self.color = Some(color);
79 self
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct BarGroup {
86 pub label: String,
88 pub bars: Vec<Bar>,
90}
91
92impl BarGroup {
93 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
95 Self {
96 label: label.into(),
97 bars,
98 }
99 }
100}
101
102pub trait Widget {
164 type Response;
167
168 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
174}
175
176pub struct Context {
192 pub(crate) commands: Vec<Command>,
193 pub(crate) events: Vec<Event>,
194 pub(crate) consumed: Vec<bool>,
195 pub(crate) should_quit: bool,
196 pub(crate) area_width: u32,
197 pub(crate) area_height: u32,
198 pub(crate) tick: u64,
199 pub(crate) focus_index: usize,
200 pub(crate) focus_count: usize,
201 prev_focus_count: usize,
202 scroll_count: usize,
203 prev_scroll_infos: Vec<(u32, u32)>,
204 interaction_count: usize,
205 pub(crate) prev_hit_map: Vec<Rect>,
206 _prev_focus_rects: Vec<(usize, Rect)>,
207 mouse_pos: Option<(u32, u32)>,
208 click_pos: Option<(u32, u32)>,
209 last_text_idx: Option<usize>,
210 overlay_depth: usize,
211 pub(crate) modal_active: bool,
212 prev_modal_active: bool,
213 debug: bool,
214 theme: Theme,
215}
216
217#[must_use = "configure and finalize with .col() or .row()"]
238pub struct ContainerBuilder<'a> {
239 ctx: &'a mut Context,
240 gap: u32,
241 align: Align,
242 justify: Justify,
243 border: Option<Border>,
244 border_sides: BorderSides,
245 border_style: Style,
246 bg_color: Option<Color>,
247 padding: Padding,
248 margin: Margin,
249 constraints: Constraints,
250 title: Option<(String, Style)>,
251 grow: u16,
252 scroll_offset: Option<u32>,
253}
254
255#[derive(Debug, Clone, Copy)]
262struct CanvasPixel {
263 bits: u32,
264 color: Color,
265}
266
267#[derive(Debug, Clone)]
269struct CanvasLabel {
270 x: usize,
271 y: usize,
272 text: String,
273 color: Color,
274}
275
276#[derive(Debug, Clone)]
278struct CanvasLayer {
279 grid: Vec<Vec<CanvasPixel>>,
280 labels: Vec<CanvasLabel>,
281}
282
283pub struct CanvasContext {
284 layers: Vec<CanvasLayer>,
285 cols: usize,
286 rows: usize,
287 px_w: usize,
288 px_h: usize,
289 current_color: Color,
290}
291
292impl CanvasContext {
293 fn new(cols: usize, rows: usize) -> Self {
294 Self {
295 layers: vec![Self::new_layer(cols, rows)],
296 cols,
297 rows,
298 px_w: cols * 2,
299 px_h: rows * 4,
300 current_color: Color::Reset,
301 }
302 }
303
304 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
305 CanvasLayer {
306 grid: vec![
307 vec![
308 CanvasPixel {
309 bits: 0,
310 color: Color::Reset,
311 };
312 cols
313 ];
314 rows
315 ],
316 labels: Vec::new(),
317 }
318 }
319
320 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
321 self.layers.last_mut()
322 }
323
324 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
325 if x >= self.px_w || y >= self.px_h {
326 return;
327 }
328
329 let char_col = x / 2;
330 let char_row = y / 4;
331 let sub_col = x % 2;
332 let sub_row = y % 4;
333 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
334 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
335
336 let bit = if sub_col == 0 {
337 LEFT_BITS[sub_row]
338 } else {
339 RIGHT_BITS[sub_row]
340 };
341
342 if let Some(layer) = self.current_layer_mut() {
343 let cell = &mut layer.grid[char_row][char_col];
344 let new_bits = cell.bits | bit;
345 if new_bits != cell.bits {
346 cell.bits = new_bits;
347 cell.color = color;
348 }
349 }
350 }
351
352 fn dot_isize(&mut self, x: isize, y: isize) {
353 if x >= 0 && y >= 0 {
354 self.dot(x as usize, y as usize);
355 }
356 }
357
358 pub fn width(&self) -> usize {
360 self.px_w
361 }
362
363 pub fn height(&self) -> usize {
365 self.px_h
366 }
367
368 pub fn dot(&mut self, x: usize, y: usize) {
370 self.dot_with_color(x, y, self.current_color);
371 }
372
373 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
375 let (mut x, mut y) = (x0 as isize, y0 as isize);
376 let (x1, y1) = (x1 as isize, y1 as isize);
377 let dx = (x1 - x).abs();
378 let dy = -(y1 - y).abs();
379 let sx = if x < x1 { 1 } else { -1 };
380 let sy = if y < y1 { 1 } else { -1 };
381 let mut err = dx + dy;
382
383 loop {
384 self.dot_isize(x, y);
385 if x == x1 && y == y1 {
386 break;
387 }
388 let e2 = 2 * err;
389 if e2 >= dy {
390 err += dy;
391 x += sx;
392 }
393 if e2 <= dx {
394 err += dx;
395 y += sy;
396 }
397 }
398 }
399
400 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
402 if w == 0 || h == 0 {
403 return;
404 }
405
406 self.line(x, y, x + w.saturating_sub(1), y);
407 self.line(
408 x + w.saturating_sub(1),
409 y,
410 x + w.saturating_sub(1),
411 y + h.saturating_sub(1),
412 );
413 self.line(
414 x + w.saturating_sub(1),
415 y + h.saturating_sub(1),
416 x,
417 y + h.saturating_sub(1),
418 );
419 self.line(x, y + h.saturating_sub(1), x, y);
420 }
421
422 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
424 let mut x = r as isize;
425 let mut y: isize = 0;
426 let mut err: isize = 1 - x;
427 let (cx, cy) = (cx as isize, cy as isize);
428
429 while x >= y {
430 for &(dx, dy) in &[
431 (x, y),
432 (y, x),
433 (-x, y),
434 (-y, x),
435 (x, -y),
436 (y, -x),
437 (-x, -y),
438 (-y, -x),
439 ] {
440 let px = cx + dx;
441 let py = cy + dy;
442 self.dot_isize(px, py);
443 }
444
445 y += 1;
446 if err < 0 {
447 err += 2 * y + 1;
448 } else {
449 x -= 1;
450 err += 2 * (y - x) + 1;
451 }
452 }
453 }
454
455 pub fn set_color(&mut self, color: Color) {
457 self.current_color = color;
458 }
459
460 pub fn color(&self) -> Color {
462 self.current_color
463 }
464
465 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
467 if w == 0 || h == 0 {
468 return;
469 }
470
471 let x_end = x.saturating_add(w).min(self.px_w);
472 let y_end = y.saturating_add(h).min(self.px_h);
473 if x >= x_end || y >= y_end {
474 return;
475 }
476
477 for yy in y..y_end {
478 self.line(x, yy, x_end.saturating_sub(1), yy);
479 }
480 }
481
482 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
484 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
485 for y in (cy - r)..=(cy + r) {
486 let dy = y - cy;
487 let span_sq = (r * r - dy * dy).max(0);
488 let dx = (span_sq as f64).sqrt() as isize;
489 for x in (cx - dx)..=(cx + dx) {
490 self.dot_isize(x, y);
491 }
492 }
493 }
494
495 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
497 self.line(x0, y0, x1, y1);
498 self.line(x1, y1, x2, y2);
499 self.line(x2, y2, x0, y0);
500 }
501
502 pub fn filled_triangle(
504 &mut self,
505 x0: usize,
506 y0: usize,
507 x1: usize,
508 y1: usize,
509 x2: usize,
510 y2: usize,
511 ) {
512 let vertices = [
513 (x0 as isize, y0 as isize),
514 (x1 as isize, y1 as isize),
515 (x2 as isize, y2 as isize),
516 ];
517 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
518 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
519
520 for y in min_y..=max_y {
521 let mut intersections: Vec<f64> = Vec::new();
522
523 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
524 let (x_a, y_a) = vertices[edge.0];
525 let (x_b, y_b) = vertices[edge.1];
526 if y_a == y_b {
527 continue;
528 }
529
530 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
531 (x_a, y_a, x_b, y_b)
532 } else {
533 (x_b, y_b, x_a, y_a)
534 };
535
536 if y < y_start || y >= y_end {
537 continue;
538 }
539
540 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
541 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
542 }
543
544 intersections.sort_by(|a, b| a.total_cmp(b));
545 let mut i = 0usize;
546 while i + 1 < intersections.len() {
547 let x_start = intersections[i].ceil() as isize;
548 let x_end = intersections[i + 1].floor() as isize;
549 for x in x_start..=x_end {
550 self.dot_isize(x, y);
551 }
552 i += 2;
553 }
554 }
555
556 self.triangle(x0, y0, x1, y1, x2, y2);
557 }
558
559 pub fn points(&mut self, pts: &[(usize, usize)]) {
561 for &(x, y) in pts {
562 self.dot(x, y);
563 }
564 }
565
566 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
568 for window in pts.windows(2) {
569 if let [(x0, y0), (x1, y1)] = window {
570 self.line(*x0, *y0, *x1, *y1);
571 }
572 }
573 }
574
575 pub fn print(&mut self, x: usize, y: usize, text: &str) {
578 if text.is_empty() {
579 return;
580 }
581
582 let color = self.current_color;
583 if let Some(layer) = self.current_layer_mut() {
584 layer.labels.push(CanvasLabel {
585 x,
586 y,
587 text: text.to_string(),
588 color,
589 });
590 }
591 }
592
593 pub fn layer(&mut self) {
595 self.layers.push(Self::new_layer(self.cols, self.rows));
596 }
597
598 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
599 let mut final_grid = vec![
600 vec![
601 CanvasPixel {
602 bits: 0,
603 color: Color::Reset,
604 };
605 self.cols
606 ];
607 self.rows
608 ];
609 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
610 vec![vec![None; self.cols]; self.rows];
611
612 for layer in &self.layers {
613 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
614 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
615 let src = layer.grid[row][col];
616 if src.bits == 0 {
617 continue;
618 }
619
620 let merged = dst.bits | src.bits;
621 if merged != dst.bits {
622 dst.bits = merged;
623 dst.color = src.color;
624 }
625 }
626 }
627
628 for label in &layer.labels {
629 let row = label.y / 4;
630 if row >= self.rows {
631 continue;
632 }
633 let start_col = label.x / 2;
634 for (offset, ch) in label.text.chars().enumerate() {
635 let col = start_col + offset;
636 if col >= self.cols {
637 break;
638 }
639 labels_overlay[row][col] = Some((ch, label.color));
640 }
641 }
642 }
643
644 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
645 for row in 0..self.rows {
646 let mut segments: Vec<(String, Color)> = Vec::new();
647 let mut current_color: Option<Color> = None;
648 let mut current_text = String::new();
649
650 for col in 0..self.cols {
651 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
652 (label_ch, label_color)
653 } else {
654 let bits = final_grid[row][col].bits;
655 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
656 (ch, final_grid[row][col].color)
657 };
658
659 match current_color {
660 Some(c) if c == color => {
661 current_text.push(ch);
662 }
663 Some(c) => {
664 segments.push((std::mem::take(&mut current_text), c));
665 current_text.push(ch);
666 current_color = Some(color);
667 }
668 None => {
669 current_text.push(ch);
670 current_color = Some(color);
671 }
672 }
673 }
674
675 if let Some(color) = current_color {
676 segments.push((current_text, color));
677 }
678 lines.push(segments);
679 }
680
681 lines
682 }
683}
684
685impl<'a> ContainerBuilder<'a> {
686 pub fn border(mut self, border: Border) -> Self {
690 self.border = Some(border);
691 self
692 }
693
694 pub fn border_top(mut self, show: bool) -> Self {
696 self.border_sides.top = show;
697 self
698 }
699
700 pub fn border_right(mut self, show: bool) -> Self {
702 self.border_sides.right = show;
703 self
704 }
705
706 pub fn border_bottom(mut self, show: bool) -> Self {
708 self.border_sides.bottom = show;
709 self
710 }
711
712 pub fn border_left(mut self, show: bool) -> Self {
714 self.border_sides.left = show;
715 self
716 }
717
718 pub fn border_sides(mut self, sides: BorderSides) -> Self {
720 self.border_sides = sides;
721 self
722 }
723
724 pub fn rounded(self) -> Self {
726 self.border(Border::Rounded)
727 }
728
729 pub fn border_style(mut self, style: Style) -> Self {
731 self.border_style = style;
732 self
733 }
734
735 pub fn bg(mut self, color: Color) -> Self {
736 self.bg_color = Some(color);
737 self
738 }
739
740 pub fn p(self, value: u32) -> Self {
744 self.pad(value)
745 }
746
747 pub fn pad(mut self, value: u32) -> Self {
749 self.padding = Padding::all(value);
750 self
751 }
752
753 pub fn px(mut self, value: u32) -> Self {
755 self.padding.left = value;
756 self.padding.right = value;
757 self
758 }
759
760 pub fn py(mut self, value: u32) -> Self {
762 self.padding.top = value;
763 self.padding.bottom = value;
764 self
765 }
766
767 pub fn pt(mut self, value: u32) -> Self {
769 self.padding.top = value;
770 self
771 }
772
773 pub fn pr(mut self, value: u32) -> Self {
775 self.padding.right = value;
776 self
777 }
778
779 pub fn pb(mut self, value: u32) -> Self {
781 self.padding.bottom = value;
782 self
783 }
784
785 pub fn pl(mut self, value: u32) -> Self {
787 self.padding.left = value;
788 self
789 }
790
791 pub fn padding(mut self, padding: Padding) -> Self {
793 self.padding = padding;
794 self
795 }
796
797 pub fn m(mut self, value: u32) -> Self {
801 self.margin = Margin::all(value);
802 self
803 }
804
805 pub fn mx(mut self, value: u32) -> Self {
807 self.margin.left = value;
808 self.margin.right = value;
809 self
810 }
811
812 pub fn my(mut self, value: u32) -> Self {
814 self.margin.top = value;
815 self.margin.bottom = value;
816 self
817 }
818
819 pub fn mt(mut self, value: u32) -> Self {
821 self.margin.top = value;
822 self
823 }
824
825 pub fn mr(mut self, value: u32) -> Self {
827 self.margin.right = value;
828 self
829 }
830
831 pub fn mb(mut self, value: u32) -> Self {
833 self.margin.bottom = value;
834 self
835 }
836
837 pub fn ml(mut self, value: u32) -> Self {
839 self.margin.left = value;
840 self
841 }
842
843 pub fn margin(mut self, margin: Margin) -> Self {
845 self.margin = margin;
846 self
847 }
848
849 pub fn w(mut self, value: u32) -> Self {
853 self.constraints.min_width = Some(value);
854 self.constraints.max_width = Some(value);
855 self
856 }
857
858 pub fn h(mut self, value: u32) -> Self {
860 self.constraints.min_height = Some(value);
861 self.constraints.max_height = Some(value);
862 self
863 }
864
865 pub fn min_w(mut self, value: u32) -> Self {
867 self.constraints.min_width = Some(value);
868 self
869 }
870
871 pub fn max_w(mut self, value: u32) -> Self {
873 self.constraints.max_width = Some(value);
874 self
875 }
876
877 pub fn min_h(mut self, value: u32) -> Self {
879 self.constraints.min_height = Some(value);
880 self
881 }
882
883 pub fn max_h(mut self, value: u32) -> Self {
885 self.constraints.max_height = Some(value);
886 self
887 }
888
889 pub fn min_width(mut self, value: u32) -> Self {
891 self.constraints.min_width = Some(value);
892 self
893 }
894
895 pub fn max_width(mut self, value: u32) -> Self {
897 self.constraints.max_width = Some(value);
898 self
899 }
900
901 pub fn min_height(mut self, value: u32) -> Self {
903 self.constraints.min_height = Some(value);
904 self
905 }
906
907 pub fn max_height(mut self, value: u32) -> Self {
909 self.constraints.max_height = Some(value);
910 self
911 }
912
913 pub fn w_pct(mut self, pct: u8) -> Self {
915 self.constraints.width_pct = Some(pct.min(100));
916 self
917 }
918
919 pub fn h_pct(mut self, pct: u8) -> Self {
921 self.constraints.height_pct = Some(pct.min(100));
922 self
923 }
924
925 pub fn constraints(mut self, constraints: Constraints) -> Self {
927 self.constraints = constraints;
928 self
929 }
930
931 pub fn gap(mut self, gap: u32) -> Self {
935 self.gap = gap;
936 self
937 }
938
939 pub fn grow(mut self, grow: u16) -> Self {
941 self.grow = grow;
942 self
943 }
944
945 pub fn align(mut self, align: Align) -> Self {
949 self.align = align;
950 self
951 }
952
953 pub fn center(self) -> Self {
955 self.align(Align::Center)
956 }
957
958 pub fn justify(mut self, justify: Justify) -> Self {
960 self.justify = justify;
961 self
962 }
963
964 pub fn space_between(self) -> Self {
966 self.justify(Justify::SpaceBetween)
967 }
968
969 pub fn space_around(self) -> Self {
971 self.justify(Justify::SpaceAround)
972 }
973
974 pub fn space_evenly(self) -> Self {
976 self.justify(Justify::SpaceEvenly)
977 }
978
979 pub fn title(self, title: impl Into<String>) -> Self {
983 self.title_styled(title, Style::new())
984 }
985
986 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
988 self.title = Some((title.into(), style));
989 self
990 }
991
992 pub fn scroll_offset(mut self, offset: u32) -> Self {
996 self.scroll_offset = Some(offset);
997 self
998 }
999
1000 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1005 self.finish(Direction::Column, f)
1006 }
1007
1008 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1013 self.finish(Direction::Row, f)
1014 }
1015
1016 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1021 self.gap = 0;
1022 self.finish(Direction::Row, f)
1023 }
1024
1025 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1026 let interaction_id = self.ctx.interaction_count;
1027 self.ctx.interaction_count += 1;
1028
1029 if let Some(scroll_offset) = self.scroll_offset {
1030 self.ctx.commands.push(Command::BeginScrollable {
1031 grow: self.grow,
1032 border: self.border,
1033 border_sides: self.border_sides,
1034 border_style: self.border_style,
1035 padding: self.padding,
1036 margin: self.margin,
1037 constraints: self.constraints,
1038 title: self.title,
1039 scroll_offset,
1040 });
1041 } else {
1042 self.ctx.commands.push(Command::BeginContainer {
1043 direction,
1044 gap: self.gap,
1045 align: self.align,
1046 justify: self.justify,
1047 border: self.border,
1048 border_sides: self.border_sides,
1049 border_style: self.border_style,
1050 bg_color: self.bg_color,
1051 padding: self.padding,
1052 margin: self.margin,
1053 constraints: self.constraints,
1054 title: self.title,
1055 grow: self.grow,
1056 });
1057 }
1058 f(self.ctx);
1059 self.ctx.commands.push(Command::EndContainer);
1060 self.ctx.last_text_idx = None;
1061
1062 self.ctx.response_for(interaction_id)
1063 }
1064}
1065
1066impl Context {
1067 #[allow(clippy::too_many_arguments)]
1068 pub(crate) fn new(
1069 events: Vec<Event>,
1070 width: u32,
1071 height: u32,
1072 tick: u64,
1073 mut focus_index: usize,
1074 prev_focus_count: usize,
1075 prev_scroll_infos: Vec<(u32, u32)>,
1076 prev_hit_map: Vec<Rect>,
1077 prev_focus_rects: Vec<(usize, Rect)>,
1078 debug: bool,
1079 theme: Theme,
1080 last_mouse_pos: Option<(u32, u32)>,
1081 prev_modal_active: bool,
1082 ) -> Self {
1083 let consumed = vec![false; events.len()];
1084
1085 let mut mouse_pos = last_mouse_pos;
1086 let mut click_pos = None;
1087 for event in &events {
1088 if let Event::Mouse(mouse) = event {
1089 mouse_pos = Some((mouse.x, mouse.y));
1090 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1091 click_pos = Some((mouse.x, mouse.y));
1092 }
1093 }
1094 }
1095
1096 if let Some((mx, my)) = click_pos {
1097 let mut best: Option<(usize, u64)> = None;
1098 for &(fid, rect) in &prev_focus_rects {
1099 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1100 let area = rect.width as u64 * rect.height as u64;
1101 if best.map_or(true, |(_, ba)| area < ba) {
1102 best = Some((fid, area));
1103 }
1104 }
1105 }
1106 if let Some((fid, _)) = best {
1107 focus_index = fid;
1108 }
1109 }
1110
1111 Self {
1112 commands: Vec::new(),
1113 events,
1114 consumed,
1115 should_quit: false,
1116 area_width: width,
1117 area_height: height,
1118 tick,
1119 focus_index,
1120 focus_count: 0,
1121 prev_focus_count,
1122 scroll_count: 0,
1123 prev_scroll_infos,
1124 interaction_count: 0,
1125 prev_hit_map,
1126 _prev_focus_rects: prev_focus_rects,
1127 mouse_pos,
1128 click_pos,
1129 last_text_idx: None,
1130 overlay_depth: 0,
1131 modal_active: false,
1132 prev_modal_active,
1133 debug,
1134 theme,
1135 }
1136 }
1137
1138 pub(crate) fn process_focus_keys(&mut self) {
1139 for (i, event) in self.events.iter().enumerate() {
1140 if let Event::Key(key) = event {
1141 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1142 if self.prev_focus_count > 0 {
1143 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1144 }
1145 self.consumed[i] = true;
1146 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1147 || key.code == KeyCode::BackTab
1148 {
1149 if self.prev_focus_count > 0 {
1150 self.focus_index = if self.focus_index == 0 {
1151 self.prev_focus_count - 1
1152 } else {
1153 self.focus_index - 1
1154 };
1155 }
1156 self.consumed[i] = true;
1157 }
1158 }
1159 }
1160 }
1161
1162 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1166 w.ui(self)
1167 }
1168
1169 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1184 self.error_boundary_with(f, |ui, msg| {
1185 ui.styled(
1186 format!("⚠ Error: {msg}"),
1187 Style::new().fg(ui.theme.error).bold(),
1188 );
1189 });
1190 }
1191
1192 pub fn error_boundary_with(
1212 &mut self,
1213 f: impl FnOnce(&mut Context),
1214 fallback: impl FnOnce(&mut Context, String),
1215 ) {
1216 let cmd_count = self.commands.len();
1217 let last_text_idx = self.last_text_idx;
1218
1219 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1220 f(self);
1221 }));
1222
1223 match result {
1224 Ok(()) => {}
1225 Err(panic_info) => {
1226 self.commands.truncate(cmd_count);
1227 self.last_text_idx = last_text_idx;
1228
1229 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1230 (*s).to_string()
1231 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1232 s.clone()
1233 } else {
1234 "widget panicked".to_string()
1235 };
1236
1237 fallback(self, msg);
1238 }
1239 }
1240 }
1241
1242 pub fn interaction(&mut self) -> Response {
1248 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1249 return Response::default();
1250 }
1251 let id = self.interaction_count;
1252 self.interaction_count += 1;
1253 self.response_for(id)
1254 }
1255
1256 pub fn register_focusable(&mut self) -> bool {
1261 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1262 return false;
1263 }
1264 let id = self.focus_count;
1265 self.focus_count += 1;
1266 self.commands.push(Command::FocusMarker(id));
1267 if self.prev_focus_count == 0 {
1268 return true;
1269 }
1270 self.focus_index % self.prev_focus_count == id
1271 }
1272
1273 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1286 let content = s.into();
1287 self.commands.push(Command::Text {
1288 content,
1289 style: Style::new(),
1290 grow: 0,
1291 align: Align::Start,
1292 wrap: false,
1293 margin: Margin::default(),
1294 constraints: Constraints::default(),
1295 });
1296 self.last_text_idx = Some(self.commands.len() - 1);
1297 self
1298 }
1299
1300 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
1306 let url_str = url.into();
1307 let focused = self.register_focusable();
1308 let interaction_id = self.interaction_count;
1309 self.interaction_count += 1;
1310 let response = self.response_for(interaction_id);
1311
1312 let mut activated = response.clicked;
1313 if focused {
1314 for (i, event) in self.events.iter().enumerate() {
1315 if let Event::Key(key) = event {
1316 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1317 activated = true;
1318 self.consumed[i] = true;
1319 }
1320 }
1321 }
1322 }
1323
1324 if activated {
1325 let _ = open_url(&url_str);
1326 }
1327
1328 let style = if focused {
1329 Style::new()
1330 .fg(self.theme.primary)
1331 .bg(self.theme.surface_hover)
1332 .underline()
1333 .bold()
1334 } else if response.hovered {
1335 Style::new()
1336 .fg(self.theme.accent)
1337 .bg(self.theme.surface_hover)
1338 .underline()
1339 } else {
1340 Style::new().fg(self.theme.primary).underline()
1341 };
1342
1343 self.commands.push(Command::Link {
1344 text: text.into(),
1345 url: url_str,
1346 style,
1347 margin: Margin::default(),
1348 constraints: Constraints::default(),
1349 });
1350 self.last_text_idx = Some(self.commands.len() - 1);
1351 self
1352 }
1353
1354 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1359 let content = s.into();
1360 self.commands.push(Command::Text {
1361 content,
1362 style: Style::new(),
1363 grow: 0,
1364 align: Align::Start,
1365 wrap: true,
1366 margin: Margin::default(),
1367 constraints: Constraints::default(),
1368 });
1369 self.last_text_idx = Some(self.commands.len() - 1);
1370 self
1371 }
1372
1373 pub fn bold(&mut self) -> &mut Self {
1377 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1378 self
1379 }
1380
1381 pub fn dim(&mut self) -> &mut Self {
1386 let text_dim = self.theme.text_dim;
1387 self.modify_last_style(|s| {
1388 s.modifiers |= Modifiers::DIM;
1389 if s.fg.is_none() {
1390 s.fg = Some(text_dim);
1391 }
1392 });
1393 self
1394 }
1395
1396 pub fn italic(&mut self) -> &mut Self {
1398 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1399 self
1400 }
1401
1402 pub fn underline(&mut self) -> &mut Self {
1404 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1405 self
1406 }
1407
1408 pub fn reversed(&mut self) -> &mut Self {
1410 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1411 self
1412 }
1413
1414 pub fn strikethrough(&mut self) -> &mut Self {
1416 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1417 self
1418 }
1419
1420 pub fn fg(&mut self, color: Color) -> &mut Self {
1422 self.modify_last_style(|s| s.fg = Some(color));
1423 self
1424 }
1425
1426 pub fn bg(&mut self, color: Color) -> &mut Self {
1428 self.modify_last_style(|s| s.bg = Some(color));
1429 self
1430 }
1431
1432 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
1437 self.commands.push(Command::Text {
1438 content: s.into(),
1439 style,
1440 grow: 0,
1441 align: Align::Start,
1442 wrap: false,
1443 margin: Margin::default(),
1444 constraints: Constraints::default(),
1445 });
1446 self.last_text_idx = Some(self.commands.len() - 1);
1447 self
1448 }
1449
1450 pub fn wrap(&mut self) -> &mut Self {
1452 if let Some(idx) = self.last_text_idx {
1453 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1454 *wrap = true;
1455 }
1456 }
1457 self
1458 }
1459
1460 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1461 if let Some(idx) = self.last_text_idx {
1462 match &mut self.commands[idx] {
1463 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1464 _ => {}
1465 }
1466 }
1467 }
1468
1469 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1487 self.push_container(Direction::Column, 0, f)
1488 }
1489
1490 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1494 self.push_container(Direction::Column, gap, f)
1495 }
1496
1497 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1514 self.push_container(Direction::Row, 0, f)
1515 }
1516
1517 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1521 self.push_container(Direction::Row, gap, f)
1522 }
1523
1524 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1541 let _ = self.push_container(Direction::Row, 0, f);
1542 self
1543 }
1544
1545 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1564 let start = self.commands.len();
1565 f(self);
1566 let mut segments: Vec<(String, Style)> = Vec::new();
1567 for cmd in self.commands.drain(start..) {
1568 if let Command::Text { content, style, .. } = cmd {
1569 segments.push((content, style));
1570 }
1571 }
1572 self.commands.push(Command::RichText {
1573 segments,
1574 wrap: true,
1575 align: Align::Start,
1576 margin: Margin::default(),
1577 constraints: Constraints::default(),
1578 });
1579 self.last_text_idx = None;
1580 self
1581 }
1582
1583 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1584 self.commands.push(Command::BeginOverlay { modal: true });
1585 self.overlay_depth += 1;
1586 self.modal_active = true;
1587 f(self);
1588 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1589 self.commands.push(Command::EndOverlay);
1590 self.last_text_idx = None;
1591 }
1592
1593 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1594 self.commands.push(Command::BeginOverlay { modal: false });
1595 self.overlay_depth += 1;
1596 f(self);
1597 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1598 self.commands.push(Command::EndOverlay);
1599 self.last_text_idx = None;
1600 }
1601
1602 pub fn container(&mut self) -> ContainerBuilder<'_> {
1623 let border = self.theme.border;
1624 ContainerBuilder {
1625 ctx: self,
1626 gap: 0,
1627 align: Align::Start,
1628 justify: Justify::Start,
1629 border: None,
1630 border_sides: BorderSides::all(),
1631 border_style: Style::new().fg(border),
1632 bg_color: None,
1633 padding: Padding::default(),
1634 margin: Margin::default(),
1635 constraints: Constraints::default(),
1636 title: None,
1637 grow: 0,
1638 scroll_offset: None,
1639 }
1640 }
1641
1642 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1661 let index = self.scroll_count;
1662 self.scroll_count += 1;
1663 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1664 state.set_bounds(ch, vh);
1665 let max = ch.saturating_sub(vh) as usize;
1666 state.offset = state.offset.min(max);
1667 }
1668
1669 let next_id = self.interaction_count;
1670 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1671 self.auto_scroll(&rect, state);
1672 }
1673
1674 self.container().scroll_offset(state.offset as u32)
1675 }
1676
1677 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1678 let mut to_consume: Vec<usize> = Vec::new();
1679
1680 for (i, event) in self.events.iter().enumerate() {
1681 if self.consumed[i] {
1682 continue;
1683 }
1684 if let Event::Mouse(mouse) = event {
1685 let in_bounds = mouse.x >= rect.x
1686 && mouse.x < rect.right()
1687 && mouse.y >= rect.y
1688 && mouse.y < rect.bottom();
1689 if !in_bounds {
1690 continue;
1691 }
1692 match mouse.kind {
1693 MouseKind::ScrollUp => {
1694 state.scroll_up(1);
1695 to_consume.push(i);
1696 }
1697 MouseKind::ScrollDown => {
1698 state.scroll_down(1);
1699 to_consume.push(i);
1700 }
1701 MouseKind::Drag(MouseButton::Left) => {
1702 }
1705 _ => {}
1706 }
1707 }
1708 }
1709
1710 for i in to_consume {
1711 self.consumed[i] = true;
1712 }
1713 }
1714
1715 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1719 self.container()
1720 .border(border)
1721 .border_sides(BorderSides::all())
1722 }
1723
1724 fn push_container(
1725 &mut self,
1726 direction: Direction,
1727 gap: u32,
1728 f: impl FnOnce(&mut Context),
1729 ) -> Response {
1730 let interaction_id = self.interaction_count;
1731 self.interaction_count += 1;
1732 let border = self.theme.border;
1733
1734 self.commands.push(Command::BeginContainer {
1735 direction,
1736 gap,
1737 align: Align::Start,
1738 justify: Justify::Start,
1739 border: None,
1740 border_sides: BorderSides::all(),
1741 border_style: Style::new().fg(border),
1742 bg_color: None,
1743 padding: Padding::default(),
1744 margin: Margin::default(),
1745 constraints: Constraints::default(),
1746 title: None,
1747 grow: 0,
1748 });
1749 f(self);
1750 self.commands.push(Command::EndContainer);
1751 self.last_text_idx = None;
1752
1753 self.response_for(interaction_id)
1754 }
1755
1756 fn response_for(&self, interaction_id: usize) -> Response {
1757 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1758 return Response::default();
1759 }
1760 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1761 let clicked = self
1762 .click_pos
1763 .map(|(mx, my)| {
1764 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1765 })
1766 .unwrap_or(false);
1767 let hovered = self
1768 .mouse_pos
1769 .map(|(mx, my)| {
1770 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1771 })
1772 .unwrap_or(false);
1773 Response { clicked, hovered }
1774 } else {
1775 Response::default()
1776 }
1777 }
1778
1779 pub fn grow(&mut self, value: u16) -> &mut Self {
1784 if let Some(idx) = self.last_text_idx {
1785 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1786 *grow = value;
1787 }
1788 }
1789 self
1790 }
1791
1792 pub fn align(&mut self, align: Align) -> &mut Self {
1794 if let Some(idx) = self.last_text_idx {
1795 if let Command::Text {
1796 align: text_align, ..
1797 } = &mut self.commands[idx]
1798 {
1799 *text_align = align;
1800 }
1801 }
1802 self
1803 }
1804
1805 pub fn spacer(&mut self) -> &mut Self {
1809 self.commands.push(Command::Spacer { grow: 1 });
1810 self.last_text_idx = None;
1811 self
1812 }
1813
1814 pub fn form(
1818 &mut self,
1819 state: &mut FormState,
1820 f: impl FnOnce(&mut Context, &mut FormState),
1821 ) -> &mut Self {
1822 self.col(|ui| {
1823 f(ui, state);
1824 });
1825 self
1826 }
1827
1828 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1832 self.col(|ui| {
1833 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1834 ui.text_input(&mut field.input);
1835 if let Some(error) = field.error.as_deref() {
1836 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1837 }
1838 });
1839 self
1840 }
1841
1842 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
1846 self.button(label)
1847 }
1848
1849 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1865 slt_assert(
1866 !state.value.contains('\n'),
1867 "text_input got a newline — use textarea instead",
1868 );
1869 let focused = self.register_focusable();
1870 state.cursor = state.cursor.min(state.value.chars().count());
1871
1872 if focused {
1873 let mut consumed_indices = Vec::new();
1874 for (i, event) in self.events.iter().enumerate() {
1875 if let Event::Key(key) = event {
1876 match key.code {
1877 KeyCode::Char(ch) => {
1878 if let Some(max) = state.max_length {
1879 if state.value.chars().count() >= max {
1880 continue;
1881 }
1882 }
1883 let index = byte_index_for_char(&state.value, state.cursor);
1884 state.value.insert(index, ch);
1885 state.cursor += 1;
1886 consumed_indices.push(i);
1887 }
1888 KeyCode::Backspace => {
1889 if state.cursor > 0 {
1890 let start = byte_index_for_char(&state.value, state.cursor - 1);
1891 let end = byte_index_for_char(&state.value, state.cursor);
1892 state.value.replace_range(start..end, "");
1893 state.cursor -= 1;
1894 }
1895 consumed_indices.push(i);
1896 }
1897 KeyCode::Left => {
1898 state.cursor = state.cursor.saturating_sub(1);
1899 consumed_indices.push(i);
1900 }
1901 KeyCode::Right => {
1902 state.cursor = (state.cursor + 1).min(state.value.chars().count());
1903 consumed_indices.push(i);
1904 }
1905 KeyCode::Home => {
1906 state.cursor = 0;
1907 consumed_indices.push(i);
1908 }
1909 KeyCode::Delete => {
1910 let len = state.value.chars().count();
1911 if state.cursor < len {
1912 let start = byte_index_for_char(&state.value, state.cursor);
1913 let end = byte_index_for_char(&state.value, state.cursor + 1);
1914 state.value.replace_range(start..end, "");
1915 }
1916 consumed_indices.push(i);
1917 }
1918 KeyCode::End => {
1919 state.cursor = state.value.chars().count();
1920 consumed_indices.push(i);
1921 }
1922 _ => {}
1923 }
1924 }
1925 if let Event::Paste(ref text) = event {
1926 for ch in text.chars() {
1927 if let Some(max) = state.max_length {
1928 if state.value.chars().count() >= max {
1929 break;
1930 }
1931 }
1932 let index = byte_index_for_char(&state.value, state.cursor);
1933 state.value.insert(index, ch);
1934 state.cursor += 1;
1935 }
1936 consumed_indices.push(i);
1937 }
1938 }
1939
1940 for index in consumed_indices {
1941 self.consumed[index] = true;
1942 }
1943 }
1944
1945 let show_cursor = focused && (self.tick / 30) % 2 == 0;
1946
1947 let input_text = if state.value.is_empty() {
1948 if state.placeholder.len() > 100 {
1949 slt_warn(
1950 "text_input placeholder is very long (>100 chars) — consider shortening it",
1951 );
1952 }
1953 state.placeholder.clone()
1954 } else {
1955 let mut rendered = String::new();
1956 for (idx, ch) in state.value.chars().enumerate() {
1957 if show_cursor && idx == state.cursor {
1958 rendered.push('▎');
1959 }
1960 rendered.push(if state.masked { '•' } else { ch });
1961 }
1962 if show_cursor && state.cursor >= state.value.chars().count() {
1963 rendered.push('▎');
1964 }
1965 rendered
1966 };
1967 let input_style = if state.value.is_empty() {
1968 Style::new().dim().fg(self.theme.text_dim)
1969 } else {
1970 Style::new().fg(self.theme.text)
1971 };
1972
1973 let border_color = if focused {
1974 self.theme.primary
1975 } else if state.validation_error.is_some() {
1976 self.theme.error
1977 } else {
1978 self.theme.border
1979 };
1980
1981 self.bordered(Border::Rounded)
1982 .border_style(Style::new().fg(border_color))
1983 .px(1)
1984 .col(|ui| {
1985 ui.styled(input_text, input_style);
1986 });
1987
1988 if let Some(error) = state.validation_error.clone() {
1989 self.styled(
1990 format!("⚠ {error}"),
1991 Style::new().dim().fg(self.theme.error),
1992 );
1993 }
1994 self
1995 }
1996
1997 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
2003 self.styled(
2004 state.frame(self.tick).to_string(),
2005 Style::new().fg(self.theme.primary),
2006 )
2007 }
2008
2009 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
2014 state.cleanup(self.tick);
2015 if state.messages.is_empty() {
2016 return self;
2017 }
2018
2019 self.interaction_count += 1;
2020 self.commands.push(Command::BeginContainer {
2021 direction: Direction::Column,
2022 gap: 0,
2023 align: Align::Start,
2024 justify: Justify::Start,
2025 border: None,
2026 border_sides: BorderSides::all(),
2027 border_style: Style::new().fg(self.theme.border),
2028 bg_color: None,
2029 padding: Padding::default(),
2030 margin: Margin::default(),
2031 constraints: Constraints::default(),
2032 title: None,
2033 grow: 0,
2034 });
2035 for message in state.messages.iter().rev() {
2036 let color = match message.level {
2037 ToastLevel::Info => self.theme.primary,
2038 ToastLevel::Success => self.theme.success,
2039 ToastLevel::Warning => self.theme.warning,
2040 ToastLevel::Error => self.theme.error,
2041 };
2042 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
2043 }
2044 self.commands.push(Command::EndContainer);
2045 self.last_text_idx = None;
2046
2047 self
2048 }
2049
2050 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
2058 if state.lines.is_empty() {
2059 state.lines.push(String::new());
2060 }
2061 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
2062 state.cursor_col = state
2063 .cursor_col
2064 .min(state.lines[state.cursor_row].chars().count());
2065
2066 let focused = self.register_focusable();
2067 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
2068 let wrapping = state.wrap_width.is_some();
2069
2070 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
2071
2072 if focused {
2073 let mut consumed_indices = Vec::new();
2074 for (i, event) in self.events.iter().enumerate() {
2075 if let Event::Key(key) = event {
2076 match key.code {
2077 KeyCode::Char(ch) => {
2078 if let Some(max) = state.max_length {
2079 let total: usize =
2080 state.lines.iter().map(|line| line.chars().count()).sum();
2081 if total >= max {
2082 continue;
2083 }
2084 }
2085 let index = byte_index_for_char(
2086 &state.lines[state.cursor_row],
2087 state.cursor_col,
2088 );
2089 state.lines[state.cursor_row].insert(index, ch);
2090 state.cursor_col += 1;
2091 consumed_indices.push(i);
2092 }
2093 KeyCode::Enter => {
2094 let split_index = byte_index_for_char(
2095 &state.lines[state.cursor_row],
2096 state.cursor_col,
2097 );
2098 let remainder = state.lines[state.cursor_row].split_off(split_index);
2099 state.cursor_row += 1;
2100 state.lines.insert(state.cursor_row, remainder);
2101 state.cursor_col = 0;
2102 consumed_indices.push(i);
2103 }
2104 KeyCode::Backspace => {
2105 if state.cursor_col > 0 {
2106 let start = byte_index_for_char(
2107 &state.lines[state.cursor_row],
2108 state.cursor_col - 1,
2109 );
2110 let end = byte_index_for_char(
2111 &state.lines[state.cursor_row],
2112 state.cursor_col,
2113 );
2114 state.lines[state.cursor_row].replace_range(start..end, "");
2115 state.cursor_col -= 1;
2116 } else if state.cursor_row > 0 {
2117 let current = state.lines.remove(state.cursor_row);
2118 state.cursor_row -= 1;
2119 state.cursor_col = state.lines[state.cursor_row].chars().count();
2120 state.lines[state.cursor_row].push_str(¤t);
2121 }
2122 consumed_indices.push(i);
2123 }
2124 KeyCode::Left => {
2125 if state.cursor_col > 0 {
2126 state.cursor_col -= 1;
2127 } else if state.cursor_row > 0 {
2128 state.cursor_row -= 1;
2129 state.cursor_col = state.lines[state.cursor_row].chars().count();
2130 }
2131 consumed_indices.push(i);
2132 }
2133 KeyCode::Right => {
2134 let line_len = state.lines[state.cursor_row].chars().count();
2135 if state.cursor_col < line_len {
2136 state.cursor_col += 1;
2137 } else if state.cursor_row + 1 < state.lines.len() {
2138 state.cursor_row += 1;
2139 state.cursor_col = 0;
2140 }
2141 consumed_indices.push(i);
2142 }
2143 KeyCode::Up => {
2144 if wrapping {
2145 let (vrow, vcol) = textarea_logical_to_visual(
2146 &pre_vlines,
2147 state.cursor_row,
2148 state.cursor_col,
2149 );
2150 if vrow > 0 {
2151 let (lr, lc) =
2152 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
2153 state.cursor_row = lr;
2154 state.cursor_col = lc;
2155 }
2156 } else if state.cursor_row > 0 {
2157 state.cursor_row -= 1;
2158 state.cursor_col = state
2159 .cursor_col
2160 .min(state.lines[state.cursor_row].chars().count());
2161 }
2162 consumed_indices.push(i);
2163 }
2164 KeyCode::Down => {
2165 if wrapping {
2166 let (vrow, vcol) = textarea_logical_to_visual(
2167 &pre_vlines,
2168 state.cursor_row,
2169 state.cursor_col,
2170 );
2171 if vrow + 1 < pre_vlines.len() {
2172 let (lr, lc) =
2173 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
2174 state.cursor_row = lr;
2175 state.cursor_col = lc;
2176 }
2177 } else if state.cursor_row + 1 < state.lines.len() {
2178 state.cursor_row += 1;
2179 state.cursor_col = state
2180 .cursor_col
2181 .min(state.lines[state.cursor_row].chars().count());
2182 }
2183 consumed_indices.push(i);
2184 }
2185 KeyCode::Home => {
2186 state.cursor_col = 0;
2187 consumed_indices.push(i);
2188 }
2189 KeyCode::Delete => {
2190 let line_len = state.lines[state.cursor_row].chars().count();
2191 if state.cursor_col < line_len {
2192 let start = byte_index_for_char(
2193 &state.lines[state.cursor_row],
2194 state.cursor_col,
2195 );
2196 let end = byte_index_for_char(
2197 &state.lines[state.cursor_row],
2198 state.cursor_col + 1,
2199 );
2200 state.lines[state.cursor_row].replace_range(start..end, "");
2201 } else if state.cursor_row + 1 < state.lines.len() {
2202 let next = state.lines.remove(state.cursor_row + 1);
2203 state.lines[state.cursor_row].push_str(&next);
2204 }
2205 consumed_indices.push(i);
2206 }
2207 KeyCode::End => {
2208 state.cursor_col = state.lines[state.cursor_row].chars().count();
2209 consumed_indices.push(i);
2210 }
2211 _ => {}
2212 }
2213 }
2214 if let Event::Paste(ref text) = event {
2215 for ch in text.chars() {
2216 if ch == '\n' || ch == '\r' {
2217 let split_index = byte_index_for_char(
2218 &state.lines[state.cursor_row],
2219 state.cursor_col,
2220 );
2221 let remainder = state.lines[state.cursor_row].split_off(split_index);
2222 state.cursor_row += 1;
2223 state.lines.insert(state.cursor_row, remainder);
2224 state.cursor_col = 0;
2225 } else {
2226 if let Some(max) = state.max_length {
2227 let total: usize =
2228 state.lines.iter().map(|l| l.chars().count()).sum();
2229 if total >= max {
2230 break;
2231 }
2232 }
2233 let index = byte_index_for_char(
2234 &state.lines[state.cursor_row],
2235 state.cursor_col,
2236 );
2237 state.lines[state.cursor_row].insert(index, ch);
2238 state.cursor_col += 1;
2239 }
2240 }
2241 consumed_indices.push(i);
2242 }
2243 }
2244
2245 for index in consumed_indices {
2246 self.consumed[index] = true;
2247 }
2248 }
2249
2250 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
2251 let (cursor_vrow, cursor_vcol) =
2252 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
2253
2254 if cursor_vrow < state.scroll_offset {
2255 state.scroll_offset = cursor_vrow;
2256 }
2257 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
2258 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
2259 }
2260
2261 self.interaction_count += 1;
2262 self.commands.push(Command::BeginContainer {
2263 direction: Direction::Column,
2264 gap: 0,
2265 align: Align::Start,
2266 justify: Justify::Start,
2267 border: None,
2268 border_sides: BorderSides::all(),
2269 border_style: Style::new().fg(self.theme.border),
2270 bg_color: None,
2271 padding: Padding::default(),
2272 margin: Margin::default(),
2273 constraints: Constraints::default(),
2274 title: None,
2275 grow: 0,
2276 });
2277
2278 let show_cursor = focused && (self.tick / 30) % 2 == 0;
2279 for vi in 0..visible_rows as usize {
2280 let actual_vi = state.scroll_offset + vi;
2281 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
2282 let line = &state.lines[vl.logical_row];
2283 let text: String = line
2284 .chars()
2285 .skip(vl.char_start)
2286 .take(vl.char_count)
2287 .collect();
2288 (text, actual_vi == cursor_vrow)
2289 } else {
2290 (String::new(), false)
2291 };
2292
2293 let mut rendered = seg_text.clone();
2294 let mut style = if seg_text.is_empty() {
2295 Style::new().fg(self.theme.text_dim)
2296 } else {
2297 Style::new().fg(self.theme.text)
2298 };
2299
2300 if is_cursor_line {
2301 rendered.clear();
2302 for (idx, ch) in seg_text.chars().enumerate() {
2303 if show_cursor && idx == cursor_vcol {
2304 rendered.push('▎');
2305 }
2306 rendered.push(ch);
2307 }
2308 if show_cursor && cursor_vcol >= seg_text.chars().count() {
2309 rendered.push('▎');
2310 }
2311 style = Style::new().fg(self.theme.text);
2312 }
2313
2314 self.styled(rendered, style);
2315 }
2316 self.commands.push(Command::EndContainer);
2317 self.last_text_idx = None;
2318
2319 self
2320 }
2321
2322 pub fn progress(&mut self, ratio: f64) -> &mut Self {
2327 self.progress_bar(ratio, 20)
2328 }
2329
2330 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
2335 let clamped = ratio.clamp(0.0, 1.0);
2336 let filled = (clamped * width as f64).round() as u32;
2337 let empty = width.saturating_sub(filled);
2338 let mut bar = String::new();
2339 for _ in 0..filled {
2340 bar.push('█');
2341 }
2342 for _ in 0..empty {
2343 bar.push('░');
2344 }
2345 self.text(bar)
2346 }
2347
2348 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
2369 if data.is_empty() {
2370 return self;
2371 }
2372
2373 let max_label_width = data
2374 .iter()
2375 .map(|(label, _)| UnicodeWidthStr::width(*label))
2376 .max()
2377 .unwrap_or(0);
2378 let max_value = data
2379 .iter()
2380 .map(|(_, value)| *value)
2381 .fold(f64::NEG_INFINITY, f64::max);
2382 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2383
2384 self.interaction_count += 1;
2385 self.commands.push(Command::BeginContainer {
2386 direction: Direction::Column,
2387 gap: 0,
2388 align: Align::Start,
2389 justify: Justify::Start,
2390 border: None,
2391 border_sides: BorderSides::all(),
2392 border_style: Style::new().fg(self.theme.border),
2393 bg_color: None,
2394 padding: Padding::default(),
2395 margin: Margin::default(),
2396 constraints: Constraints::default(),
2397 title: None,
2398 grow: 0,
2399 });
2400
2401 for (label, value) in data {
2402 let label_width = UnicodeWidthStr::width(*label);
2403 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2404 let normalized = (*value / denom).clamp(0.0, 1.0);
2405 let bar_len = (normalized * max_width as f64).round() as usize;
2406 let bar = "█".repeat(bar_len);
2407
2408 self.interaction_count += 1;
2409 self.commands.push(Command::BeginContainer {
2410 direction: Direction::Row,
2411 gap: 1,
2412 align: Align::Start,
2413 justify: Justify::Start,
2414 border: None,
2415 border_sides: BorderSides::all(),
2416 border_style: Style::new().fg(self.theme.border),
2417 bg_color: None,
2418 padding: Padding::default(),
2419 margin: Margin::default(),
2420 constraints: Constraints::default(),
2421 title: None,
2422 grow: 0,
2423 });
2424 self.styled(
2425 format!("{label}{label_padding}"),
2426 Style::new().fg(self.theme.text),
2427 );
2428 self.styled(bar, Style::new().fg(self.theme.primary));
2429 self.styled(
2430 format_compact_number(*value),
2431 Style::new().fg(self.theme.text_dim),
2432 );
2433 self.commands.push(Command::EndContainer);
2434 self.last_text_idx = None;
2435 }
2436
2437 self.commands.push(Command::EndContainer);
2438 self.last_text_idx = None;
2439
2440 self
2441 }
2442
2443 pub fn bar_chart_styled(
2459 &mut self,
2460 bars: &[Bar],
2461 max_width: u32,
2462 direction: BarDirection,
2463 ) -> &mut Self {
2464 if bars.is_empty() {
2465 return self;
2466 }
2467
2468 let max_value = bars
2469 .iter()
2470 .map(|bar| bar.value)
2471 .fold(f64::NEG_INFINITY, f64::max);
2472 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2473
2474 match direction {
2475 BarDirection::Horizontal => {
2476 let max_label_width = bars
2477 .iter()
2478 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2479 .max()
2480 .unwrap_or(0);
2481
2482 self.interaction_count += 1;
2483 self.commands.push(Command::BeginContainer {
2484 direction: Direction::Column,
2485 gap: 0,
2486 align: Align::Start,
2487 justify: Justify::Start,
2488 border: None,
2489 border_sides: BorderSides::all(),
2490 border_style: Style::new().fg(self.theme.border),
2491 bg_color: None,
2492 padding: Padding::default(),
2493 margin: Margin::default(),
2494 constraints: Constraints::default(),
2495 title: None,
2496 grow: 0,
2497 });
2498
2499 for bar in bars {
2500 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2501 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2502 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2503 let bar_len = (normalized * max_width as f64).round() as usize;
2504 let bar_text = "█".repeat(bar_len);
2505 let color = bar.color.unwrap_or(self.theme.primary);
2506
2507 self.interaction_count += 1;
2508 self.commands.push(Command::BeginContainer {
2509 direction: Direction::Row,
2510 gap: 1,
2511 align: Align::Start,
2512 justify: Justify::Start,
2513 border: None,
2514 border_sides: BorderSides::all(),
2515 border_style: Style::new().fg(self.theme.border),
2516 bg_color: None,
2517 padding: Padding::default(),
2518 margin: Margin::default(),
2519 constraints: Constraints::default(),
2520 title: None,
2521 grow: 0,
2522 });
2523 self.styled(
2524 format!("{}{label_padding}", bar.label),
2525 Style::new().fg(self.theme.text),
2526 );
2527 self.styled(bar_text, Style::new().fg(color));
2528 self.styled(
2529 format_compact_number(bar.value),
2530 Style::new().fg(self.theme.text_dim),
2531 );
2532 self.commands.push(Command::EndContainer);
2533 self.last_text_idx = None;
2534 }
2535
2536 self.commands.push(Command::EndContainer);
2537 self.last_text_idx = None;
2538 }
2539 BarDirection::Vertical => {
2540 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
2541
2542 let chart_height = max_width.max(1) as usize;
2543 let value_labels: Vec<String> = bars
2544 .iter()
2545 .map(|bar| format_compact_number(bar.value))
2546 .collect();
2547 let col_width = bars
2548 .iter()
2549 .zip(value_labels.iter())
2550 .map(|(bar, value)| {
2551 UnicodeWidthStr::width(bar.label.as_str())
2552 .max(UnicodeWidthStr::width(value.as_str()))
2553 .max(1)
2554 })
2555 .max()
2556 .unwrap_or(1);
2557
2558 let bar_units: Vec<usize> = bars
2559 .iter()
2560 .map(|bar| {
2561 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2562 (normalized * chart_height as f64 * 8.0).round() as usize
2563 })
2564 .collect();
2565
2566 self.interaction_count += 1;
2567 self.commands.push(Command::BeginContainer {
2568 direction: Direction::Column,
2569 gap: 0,
2570 align: Align::Start,
2571 justify: Justify::Start,
2572 border: None,
2573 border_sides: BorderSides::all(),
2574 border_style: Style::new().fg(self.theme.border),
2575 bg_color: None,
2576 padding: Padding::default(),
2577 margin: Margin::default(),
2578 constraints: Constraints::default(),
2579 title: None,
2580 grow: 0,
2581 });
2582
2583 self.interaction_count += 1;
2584 self.commands.push(Command::BeginContainer {
2585 direction: Direction::Row,
2586 gap: 1,
2587 align: Align::Start,
2588 justify: Justify::Start,
2589 border: None,
2590 border_sides: BorderSides::all(),
2591 border_style: Style::new().fg(self.theme.border),
2592 bg_color: None,
2593 padding: Padding::default(),
2594 margin: Margin::default(),
2595 constraints: Constraints::default(),
2596 title: None,
2597 grow: 0,
2598 });
2599 for value in &value_labels {
2600 self.styled(
2601 center_text(value, col_width),
2602 Style::new().fg(self.theme.text_dim),
2603 );
2604 }
2605 self.commands.push(Command::EndContainer);
2606 self.last_text_idx = None;
2607
2608 for row in (0..chart_height).rev() {
2609 self.interaction_count += 1;
2610 self.commands.push(Command::BeginContainer {
2611 direction: Direction::Row,
2612 gap: 1,
2613 align: Align::Start,
2614 justify: Justify::Start,
2615 border: None,
2616 border_sides: BorderSides::all(),
2617 border_style: Style::new().fg(self.theme.border),
2618 bg_color: None,
2619 padding: Padding::default(),
2620 margin: Margin::default(),
2621 constraints: Constraints::default(),
2622 title: None,
2623 grow: 0,
2624 });
2625
2626 let row_base = row * 8;
2627 for (bar, units) in bars.iter().zip(bar_units.iter()) {
2628 let fill = if *units <= row_base {
2629 ' '
2630 } else {
2631 let delta = *units - row_base;
2632 if delta >= 8 {
2633 '█'
2634 } else {
2635 FRACTION_BLOCKS[delta]
2636 }
2637 };
2638
2639 self.styled(
2640 center_text(&fill.to_string(), col_width),
2641 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2642 );
2643 }
2644
2645 self.commands.push(Command::EndContainer);
2646 self.last_text_idx = None;
2647 }
2648
2649 self.interaction_count += 1;
2650 self.commands.push(Command::BeginContainer {
2651 direction: Direction::Row,
2652 gap: 1,
2653 align: Align::Start,
2654 justify: Justify::Start,
2655 border: None,
2656 border_sides: BorderSides::all(),
2657 border_style: Style::new().fg(self.theme.border),
2658 bg_color: None,
2659 padding: Padding::default(),
2660 margin: Margin::default(),
2661 constraints: Constraints::default(),
2662 title: None,
2663 grow: 0,
2664 });
2665 for bar in bars {
2666 self.styled(
2667 center_text(&bar.label, col_width),
2668 Style::new().fg(self.theme.text),
2669 );
2670 }
2671 self.commands.push(Command::EndContainer);
2672 self.last_text_idx = None;
2673
2674 self.commands.push(Command::EndContainer);
2675 self.last_text_idx = None;
2676 }
2677 }
2678
2679 self
2680 }
2681
2682 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
2699 if groups.is_empty() {
2700 return self;
2701 }
2702
2703 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
2704 if all_bars.is_empty() {
2705 return self;
2706 }
2707
2708 let max_label_width = all_bars
2709 .iter()
2710 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2711 .max()
2712 .unwrap_or(0);
2713 let max_value = all_bars
2714 .iter()
2715 .map(|bar| bar.value)
2716 .fold(f64::NEG_INFINITY, f64::max);
2717 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2718
2719 self.interaction_count += 1;
2720 self.commands.push(Command::BeginContainer {
2721 direction: Direction::Column,
2722 gap: 1,
2723 align: Align::Start,
2724 justify: Justify::Start,
2725 border: None,
2726 border_sides: BorderSides::all(),
2727 border_style: Style::new().fg(self.theme.border),
2728 bg_color: None,
2729 padding: Padding::default(),
2730 margin: Margin::default(),
2731 constraints: Constraints::default(),
2732 title: None,
2733 grow: 0,
2734 });
2735
2736 for group in groups {
2737 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
2738
2739 for bar in &group.bars {
2740 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2741 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2742 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2743 let bar_len = (normalized * max_width as f64).round() as usize;
2744 let bar_text = "█".repeat(bar_len);
2745
2746 self.interaction_count += 1;
2747 self.commands.push(Command::BeginContainer {
2748 direction: Direction::Row,
2749 gap: 1,
2750 align: Align::Start,
2751 justify: Justify::Start,
2752 border: None,
2753 border_sides: BorderSides::all(),
2754 border_style: Style::new().fg(self.theme.border),
2755 bg_color: None,
2756 padding: Padding::default(),
2757 margin: Margin::default(),
2758 constraints: Constraints::default(),
2759 title: None,
2760 grow: 0,
2761 });
2762 self.styled(
2763 format!(" {}{label_padding}", bar.label),
2764 Style::new().fg(self.theme.text),
2765 );
2766 self.styled(
2767 bar_text,
2768 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2769 );
2770 self.styled(
2771 format_compact_number(bar.value),
2772 Style::new().fg(self.theme.text_dim),
2773 );
2774 self.commands.push(Command::EndContainer);
2775 self.last_text_idx = None;
2776 }
2777 }
2778
2779 self.commands.push(Command::EndContainer);
2780 self.last_text_idx = None;
2781
2782 self
2783 }
2784
2785 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
2801 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2802
2803 let w = width as usize;
2804 let window = if data.len() > w {
2805 &data[data.len() - w..]
2806 } else {
2807 data
2808 };
2809
2810 if window.is_empty() {
2811 return self;
2812 }
2813
2814 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
2815 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2816 let range = max - min;
2817
2818 let line: String = window
2819 .iter()
2820 .map(|&value| {
2821 let normalized = if range == 0.0 {
2822 0.5
2823 } else {
2824 (value - min) / range
2825 };
2826 let idx = (normalized * 7.0).round() as usize;
2827 BLOCKS[idx.min(7)]
2828 })
2829 .collect();
2830
2831 self.styled(line, Style::new().fg(self.theme.primary))
2832 }
2833
2834 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
2854 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2855
2856 let w = width as usize;
2857 let window = if data.len() > w {
2858 &data[data.len() - w..]
2859 } else {
2860 data
2861 };
2862
2863 if window.is_empty() {
2864 return self;
2865 }
2866
2867 let mut finite_values = window
2868 .iter()
2869 .map(|(value, _)| *value)
2870 .filter(|value| !value.is_nan());
2871 let Some(first) = finite_values.next() else {
2872 return self.styled(
2873 " ".repeat(window.len()),
2874 Style::new().fg(self.theme.text_dim),
2875 );
2876 };
2877
2878 let mut min = first;
2879 let mut max = first;
2880 for value in finite_values {
2881 min = f64::min(min, value);
2882 max = f64::max(max, value);
2883 }
2884 let range = max - min;
2885
2886 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
2887 for (value, color) in window {
2888 if value.is_nan() {
2889 cells.push((' ', self.theme.text_dim));
2890 continue;
2891 }
2892
2893 let normalized = if range == 0.0 {
2894 0.5
2895 } else {
2896 ((*value - min) / range).clamp(0.0, 1.0)
2897 };
2898 let idx = (normalized * 7.0).round() as usize;
2899 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
2900 }
2901
2902 self.interaction_count += 1;
2903 self.commands.push(Command::BeginContainer {
2904 direction: Direction::Row,
2905 gap: 0,
2906 align: Align::Start,
2907 justify: Justify::Start,
2908 border: None,
2909 border_sides: BorderSides::all(),
2910 border_style: Style::new().fg(self.theme.border),
2911 bg_color: None,
2912 padding: Padding::default(),
2913 margin: Margin::default(),
2914 constraints: Constraints::default(),
2915 title: None,
2916 grow: 0,
2917 });
2918
2919 let mut seg = String::new();
2920 let mut seg_color = cells[0].1;
2921 for (ch, color) in cells {
2922 if color != seg_color {
2923 self.styled(seg, Style::new().fg(seg_color));
2924 seg = String::new();
2925 seg_color = color;
2926 }
2927 seg.push(ch);
2928 }
2929 if !seg.is_empty() {
2930 self.styled(seg, Style::new().fg(seg_color));
2931 }
2932
2933 self.commands.push(Command::EndContainer);
2934 self.last_text_idx = None;
2935
2936 self
2937 }
2938
2939 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2953 if data.is_empty() || width == 0 || height == 0 {
2954 return self;
2955 }
2956
2957 let cols = width as usize;
2958 let rows = height as usize;
2959 let px_w = cols * 2;
2960 let px_h = rows * 4;
2961
2962 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
2963 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2964 let range = if (max - min).abs() < f64::EPSILON {
2965 1.0
2966 } else {
2967 max - min
2968 };
2969
2970 let points: Vec<usize> = (0..px_w)
2971 .map(|px| {
2972 let data_idx = if px_w <= 1 {
2973 0.0
2974 } else {
2975 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
2976 };
2977 let idx = data_idx.floor() as usize;
2978 let frac = data_idx - idx as f64;
2979 let value = if idx + 1 < data.len() {
2980 data[idx] * (1.0 - frac) + data[idx + 1] * frac
2981 } else {
2982 data[idx.min(data.len() - 1)]
2983 };
2984
2985 let normalized = (value - min) / range;
2986 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
2987 py.min(px_h - 1)
2988 })
2989 .collect();
2990
2991 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
2992 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
2993
2994 let mut grid = vec![vec![0u32; cols]; rows];
2995
2996 for i in 0..points.len() {
2997 let px = i;
2998 let py = points[i];
2999 let char_col = px / 2;
3000 let char_row = py / 4;
3001 let sub_col = px % 2;
3002 let sub_row = py % 4;
3003
3004 if char_col < cols && char_row < rows {
3005 grid[char_row][char_col] |= if sub_col == 0 {
3006 LEFT_BITS[sub_row]
3007 } else {
3008 RIGHT_BITS[sub_row]
3009 };
3010 }
3011
3012 if i + 1 < points.len() {
3013 let py_next = points[i + 1];
3014 let (y_start, y_end) = if py <= py_next {
3015 (py, py_next)
3016 } else {
3017 (py_next, py)
3018 };
3019 for y in y_start..=y_end {
3020 let cell_row = y / 4;
3021 let sub_y = y % 4;
3022 if char_col < cols && cell_row < rows {
3023 grid[cell_row][char_col] |= if sub_col == 0 {
3024 LEFT_BITS[sub_y]
3025 } else {
3026 RIGHT_BITS[sub_y]
3027 };
3028 }
3029 }
3030 }
3031 }
3032
3033 let style = Style::new().fg(self.theme.primary);
3034 for row in grid {
3035 let line: String = row
3036 .iter()
3037 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
3038 .collect();
3039 self.styled(line, style);
3040 }
3041
3042 self
3043 }
3044
3045 pub fn canvas(
3062 &mut self,
3063 width: u32,
3064 height: u32,
3065 draw: impl FnOnce(&mut CanvasContext),
3066 ) -> &mut Self {
3067 if width == 0 || height == 0 {
3068 return self;
3069 }
3070
3071 let mut canvas = CanvasContext::new(width as usize, height as usize);
3072 draw(&mut canvas);
3073
3074 for segments in canvas.render() {
3075 self.interaction_count += 1;
3076 self.commands.push(Command::BeginContainer {
3077 direction: Direction::Row,
3078 gap: 0,
3079 align: Align::Start,
3080 justify: Justify::Start,
3081 border: None,
3082 border_sides: BorderSides::all(),
3083 border_style: Style::new(),
3084 bg_color: None,
3085 padding: Padding::default(),
3086 margin: Margin::default(),
3087 constraints: Constraints::default(),
3088 title: None,
3089 grow: 0,
3090 });
3091 for (text, color) in segments {
3092 let c = if color == Color::Reset {
3093 self.theme.primary
3094 } else {
3095 color
3096 };
3097 self.styled(text, Style::new().fg(c));
3098 }
3099 self.commands.push(Command::EndContainer);
3100 self.last_text_idx = None;
3101 }
3102
3103 self
3104 }
3105
3106 pub fn chart(
3108 &mut self,
3109 configure: impl FnOnce(&mut ChartBuilder),
3110 width: u32,
3111 height: u32,
3112 ) -> &mut Self {
3113 if width == 0 || height == 0 {
3114 return self;
3115 }
3116
3117 let axis_style = Style::new().fg(self.theme.text_dim);
3118 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
3119 configure(&mut builder);
3120
3121 let config = builder.build();
3122 let rows = render_chart(&config);
3123
3124 for row in rows {
3125 self.interaction_count += 1;
3126 self.commands.push(Command::BeginContainer {
3127 direction: Direction::Row,
3128 gap: 0,
3129 align: Align::Start,
3130 justify: Justify::Start,
3131 border: None,
3132 border_sides: BorderSides::all(),
3133 border_style: Style::new().fg(self.theme.border),
3134 bg_color: None,
3135 padding: Padding::default(),
3136 margin: Margin::default(),
3137 constraints: Constraints::default(),
3138 title: None,
3139 grow: 0,
3140 });
3141 for (text, style) in row.segments {
3142 self.styled(text, style);
3143 }
3144 self.commands.push(Command::EndContainer);
3145 self.last_text_idx = None;
3146 }
3147
3148 self
3149 }
3150
3151 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
3153 self.histogram_with(data, |_| {}, width, height)
3154 }
3155
3156 pub fn histogram_with(
3158 &mut self,
3159 data: &[f64],
3160 configure: impl FnOnce(&mut HistogramBuilder),
3161 width: u32,
3162 height: u32,
3163 ) -> &mut Self {
3164 if width == 0 || height == 0 {
3165 return self;
3166 }
3167
3168 let mut options = HistogramBuilder::default();
3169 configure(&mut options);
3170 let axis_style = Style::new().fg(self.theme.text_dim);
3171 let config = build_histogram_config(data, &options, width, height, axis_style);
3172 let rows = render_chart(&config);
3173
3174 for row in rows {
3175 self.interaction_count += 1;
3176 self.commands.push(Command::BeginContainer {
3177 direction: Direction::Row,
3178 gap: 0,
3179 align: Align::Start,
3180 justify: Justify::Start,
3181 border: None,
3182 border_sides: BorderSides::all(),
3183 border_style: Style::new().fg(self.theme.border),
3184 bg_color: None,
3185 padding: Padding::default(),
3186 margin: Margin::default(),
3187 constraints: Constraints::default(),
3188 title: None,
3189 grow: 0,
3190 });
3191 for (text, style) in row.segments {
3192 self.styled(text, style);
3193 }
3194 self.commands.push(Command::EndContainer);
3195 self.last_text_idx = None;
3196 }
3197
3198 self
3199 }
3200
3201 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
3218 slt_assert(cols > 0, "grid() requires at least 1 column");
3219 let interaction_id = self.interaction_count;
3220 self.interaction_count += 1;
3221 let border = self.theme.border;
3222
3223 self.commands.push(Command::BeginContainer {
3224 direction: Direction::Column,
3225 gap: 0,
3226 align: Align::Start,
3227 justify: Justify::Start,
3228 border: None,
3229 border_sides: BorderSides::all(),
3230 border_style: Style::new().fg(border),
3231 bg_color: None,
3232 padding: Padding::default(),
3233 margin: Margin::default(),
3234 constraints: Constraints::default(),
3235 title: None,
3236 grow: 0,
3237 });
3238
3239 let children_start = self.commands.len();
3240 f(self);
3241 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
3242
3243 let mut elements: Vec<Vec<Command>> = Vec::new();
3244 let mut iter = child_commands.into_iter().peekable();
3245 while let Some(cmd) = iter.next() {
3246 match cmd {
3247 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
3248 let mut depth = 1_u32;
3249 let mut element = vec![cmd];
3250 for next in iter.by_ref() {
3251 match next {
3252 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
3253 depth += 1;
3254 }
3255 Command::EndContainer => {
3256 depth = depth.saturating_sub(1);
3257 }
3258 _ => {}
3259 }
3260 let at_end = matches!(next, Command::EndContainer) && depth == 0;
3261 element.push(next);
3262 if at_end {
3263 break;
3264 }
3265 }
3266 elements.push(element);
3267 }
3268 Command::EndContainer => {}
3269 _ => elements.push(vec![cmd]),
3270 }
3271 }
3272
3273 let cols = cols.max(1) as usize;
3274 for row in elements.chunks(cols) {
3275 self.interaction_count += 1;
3276 self.commands.push(Command::BeginContainer {
3277 direction: Direction::Row,
3278 gap: 0,
3279 align: Align::Start,
3280 justify: Justify::Start,
3281 border: None,
3282 border_sides: BorderSides::all(),
3283 border_style: Style::new().fg(border),
3284 bg_color: None,
3285 padding: Padding::default(),
3286 margin: Margin::default(),
3287 constraints: Constraints::default(),
3288 title: None,
3289 grow: 0,
3290 });
3291
3292 for element in row {
3293 self.interaction_count += 1;
3294 self.commands.push(Command::BeginContainer {
3295 direction: Direction::Column,
3296 gap: 0,
3297 align: Align::Start,
3298 justify: Justify::Start,
3299 border: None,
3300 border_sides: BorderSides::all(),
3301 border_style: Style::new().fg(border),
3302 bg_color: None,
3303 padding: Padding::default(),
3304 margin: Margin::default(),
3305 constraints: Constraints::default(),
3306 title: None,
3307 grow: 1,
3308 });
3309 self.commands.extend(element.iter().cloned());
3310 self.commands.push(Command::EndContainer);
3311 }
3312
3313 self.commands.push(Command::EndContainer);
3314 }
3315
3316 self.commands.push(Command::EndContainer);
3317 self.last_text_idx = None;
3318
3319 self.response_for(interaction_id)
3320 }
3321
3322 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
3327 if state.items.is_empty() {
3328 state.selected = 0;
3329 return self;
3330 }
3331
3332 state.selected = state.selected.min(state.items.len().saturating_sub(1));
3333
3334 let focused = self.register_focusable();
3335 let interaction_id = self.interaction_count;
3336 self.interaction_count += 1;
3337
3338 if focused {
3339 let mut consumed_indices = Vec::new();
3340 for (i, event) in self.events.iter().enumerate() {
3341 if let Event::Key(key) = event {
3342 match key.code {
3343 KeyCode::Up | KeyCode::Char('k') => {
3344 state.selected = state.selected.saturating_sub(1);
3345 consumed_indices.push(i);
3346 }
3347 KeyCode::Down | KeyCode::Char('j') => {
3348 state.selected =
3349 (state.selected + 1).min(state.items.len().saturating_sub(1));
3350 consumed_indices.push(i);
3351 }
3352 _ => {}
3353 }
3354 }
3355 }
3356
3357 for index in consumed_indices {
3358 self.consumed[index] = true;
3359 }
3360 }
3361
3362 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
3363 for (i, event) in self.events.iter().enumerate() {
3364 if self.consumed[i] {
3365 continue;
3366 }
3367 if let Event::Mouse(mouse) = event {
3368 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3369 continue;
3370 }
3371 let in_bounds = mouse.x >= rect.x
3372 && mouse.x < rect.right()
3373 && mouse.y >= rect.y
3374 && mouse.y < rect.bottom();
3375 if !in_bounds {
3376 continue;
3377 }
3378 let clicked_idx = (mouse.y - rect.y) as usize;
3379 if clicked_idx < state.items.len() {
3380 state.selected = clicked_idx;
3381 self.consumed[i] = true;
3382 }
3383 }
3384 }
3385 }
3386
3387 self.commands.push(Command::BeginContainer {
3388 direction: Direction::Column,
3389 gap: 0,
3390 align: Align::Start,
3391 justify: Justify::Start,
3392 border: None,
3393 border_sides: BorderSides::all(),
3394 border_style: Style::new().fg(self.theme.border),
3395 bg_color: None,
3396 padding: Padding::default(),
3397 margin: Margin::default(),
3398 constraints: Constraints::default(),
3399 title: None,
3400 grow: 0,
3401 });
3402
3403 for (idx, item) in state.items.iter().enumerate() {
3404 if idx == state.selected {
3405 if focused {
3406 self.styled(
3407 format!("▸ {item}"),
3408 Style::new().bold().fg(self.theme.primary),
3409 );
3410 } else {
3411 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
3412 }
3413 } else {
3414 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
3415 }
3416 }
3417
3418 self.commands.push(Command::EndContainer);
3419 self.last_text_idx = None;
3420
3421 self
3422 }
3423
3424 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
3429 if state.is_dirty() {
3430 state.recompute_widths();
3431 }
3432
3433 let focused = self.register_focusable();
3434 let interaction_id = self.interaction_count;
3435 self.interaction_count += 1;
3436
3437 if focused && !state.visible_indices().is_empty() {
3438 let mut consumed_indices = Vec::new();
3439 for (i, event) in self.events.iter().enumerate() {
3440 if let Event::Key(key) = event {
3441 match key.code {
3442 KeyCode::Up | KeyCode::Char('k') => {
3443 let visible_len = if state.page_size > 0 {
3444 let start = state
3445 .page
3446 .saturating_mul(state.page_size)
3447 .min(state.visible_indices().len());
3448 let end =
3449 (start + state.page_size).min(state.visible_indices().len());
3450 end.saturating_sub(start)
3451 } else {
3452 state.visible_indices().len()
3453 };
3454 state.selected = state.selected.min(visible_len.saturating_sub(1));
3455 state.selected = state.selected.saturating_sub(1);
3456 consumed_indices.push(i);
3457 }
3458 KeyCode::Down | KeyCode::Char('j') => {
3459 let visible_len = if state.page_size > 0 {
3460 let start = state
3461 .page
3462 .saturating_mul(state.page_size)
3463 .min(state.visible_indices().len());
3464 let end =
3465 (start + state.page_size).min(state.visible_indices().len());
3466 end.saturating_sub(start)
3467 } else {
3468 state.visible_indices().len()
3469 };
3470 state.selected =
3471 (state.selected + 1).min(visible_len.saturating_sub(1));
3472 consumed_indices.push(i);
3473 }
3474 KeyCode::PageUp => {
3475 let old_page = state.page;
3476 state.prev_page();
3477 if state.page != old_page {
3478 state.selected = 0;
3479 }
3480 consumed_indices.push(i);
3481 }
3482 KeyCode::PageDown => {
3483 let old_page = state.page;
3484 state.next_page();
3485 if state.page != old_page {
3486 state.selected = 0;
3487 }
3488 consumed_indices.push(i);
3489 }
3490 _ => {}
3491 }
3492 }
3493 }
3494 for index in consumed_indices {
3495 self.consumed[index] = true;
3496 }
3497 }
3498
3499 if !state.visible_indices().is_empty() || !state.headers.is_empty() {
3500 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
3501 for (i, event) in self.events.iter().enumerate() {
3502 if self.consumed[i] {
3503 continue;
3504 }
3505 if let Event::Mouse(mouse) = event {
3506 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3507 continue;
3508 }
3509 let in_bounds = mouse.x >= rect.x
3510 && mouse.x < rect.right()
3511 && mouse.y >= rect.y
3512 && mouse.y < rect.bottom();
3513 if !in_bounds {
3514 continue;
3515 }
3516
3517 if mouse.y == rect.y {
3518 let rel_x = mouse.x.saturating_sub(rect.x);
3519 let mut x_offset = 0u32;
3520 for (col_idx, width) in state.column_widths().iter().enumerate() {
3521 if rel_x >= x_offset && rel_x < x_offset + *width {
3522 state.toggle_sort(col_idx);
3523 state.selected = 0;
3524 self.consumed[i] = true;
3525 break;
3526 }
3527 x_offset += *width;
3528 if col_idx + 1 < state.column_widths().len() {
3529 x_offset += 3;
3530 }
3531 }
3532 continue;
3533 }
3534
3535 if mouse.y < rect.y + 2 {
3536 continue;
3537 }
3538
3539 let visible_len = if state.page_size > 0 {
3540 let start = state
3541 .page
3542 .saturating_mul(state.page_size)
3543 .min(state.visible_indices().len());
3544 let end = (start + state.page_size).min(state.visible_indices().len());
3545 end.saturating_sub(start)
3546 } else {
3547 state.visible_indices().len()
3548 };
3549 let clicked_idx = (mouse.y - rect.y - 2) as usize;
3550 if clicked_idx < visible_len {
3551 state.selected = clicked_idx;
3552 self.consumed[i] = true;
3553 }
3554 }
3555 }
3556 }
3557 }
3558
3559 if state.is_dirty() {
3560 state.recompute_widths();
3561 }
3562
3563 let total_visible = state.visible_indices().len();
3564 let page_start = if state.page_size > 0 {
3565 state
3566 .page
3567 .saturating_mul(state.page_size)
3568 .min(total_visible)
3569 } else {
3570 0
3571 };
3572 let page_end = if state.page_size > 0 {
3573 (page_start + state.page_size).min(total_visible)
3574 } else {
3575 total_visible
3576 };
3577 let visible_len = page_end.saturating_sub(page_start);
3578 state.selected = state.selected.min(visible_len.saturating_sub(1));
3579
3580 self.commands.push(Command::BeginContainer {
3581 direction: Direction::Column,
3582 gap: 0,
3583 align: Align::Start,
3584 justify: Justify::Start,
3585 border: None,
3586 border_sides: BorderSides::all(),
3587 border_style: Style::new().fg(self.theme.border),
3588 bg_color: None,
3589 padding: Padding::default(),
3590 margin: Margin::default(),
3591 constraints: Constraints::default(),
3592 title: None,
3593 grow: 0,
3594 });
3595
3596 let header_cells = state
3597 .headers
3598 .iter()
3599 .enumerate()
3600 .map(|(i, header)| {
3601 if state.sort_column == Some(i) {
3602 if state.sort_ascending {
3603 format!("{header} ▲")
3604 } else {
3605 format!("{header} ▼")
3606 }
3607 } else {
3608 header.clone()
3609 }
3610 })
3611 .collect::<Vec<_>>();
3612 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
3613 self.styled(header_line, Style::new().bold().fg(self.theme.text));
3614
3615 let separator = state
3616 .column_widths()
3617 .iter()
3618 .map(|w| "─".repeat(*w as usize))
3619 .collect::<Vec<_>>()
3620 .join("─┼─");
3621 self.text(separator);
3622
3623 for idx in 0..visible_len {
3624 let data_idx = state.visible_indices()[page_start + idx];
3625 let Some(row) = state.rows.get(data_idx) else {
3626 continue;
3627 };
3628 let line = format_table_row(row, state.column_widths(), " │ ");
3629 if idx == state.selected {
3630 let mut style = Style::new()
3631 .bg(self.theme.selected_bg)
3632 .fg(self.theme.selected_fg);
3633 if focused {
3634 style = style.bold();
3635 }
3636 self.styled(line, style);
3637 } else {
3638 self.styled(line, Style::new().fg(self.theme.text));
3639 }
3640 }
3641
3642 if state.page_size > 0 && state.total_pages() > 1 {
3643 self.styled(
3644 format!("Page {}/{}", state.page + 1, state.total_pages()),
3645 Style::new().dim().fg(self.theme.text_dim),
3646 );
3647 }
3648
3649 self.commands.push(Command::EndContainer);
3650 self.last_text_idx = None;
3651
3652 self
3653 }
3654
3655 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
3660 if state.labels.is_empty() {
3661 state.selected = 0;
3662 return self;
3663 }
3664
3665 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
3666 let focused = self.register_focusable();
3667 let interaction_id = self.interaction_count;
3668
3669 if focused {
3670 let mut consumed_indices = Vec::new();
3671 for (i, event) in self.events.iter().enumerate() {
3672 if let Event::Key(key) = event {
3673 match key.code {
3674 KeyCode::Left => {
3675 state.selected = if state.selected == 0 {
3676 state.labels.len().saturating_sub(1)
3677 } else {
3678 state.selected - 1
3679 };
3680 consumed_indices.push(i);
3681 }
3682 KeyCode::Right => {
3683 state.selected = (state.selected + 1) % state.labels.len();
3684 consumed_indices.push(i);
3685 }
3686 _ => {}
3687 }
3688 }
3689 }
3690
3691 for index in consumed_indices {
3692 self.consumed[index] = true;
3693 }
3694 }
3695
3696 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
3697 for (i, event) in self.events.iter().enumerate() {
3698 if self.consumed[i] {
3699 continue;
3700 }
3701 if let Event::Mouse(mouse) = event {
3702 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3703 continue;
3704 }
3705 let in_bounds = mouse.x >= rect.x
3706 && mouse.x < rect.right()
3707 && mouse.y >= rect.y
3708 && mouse.y < rect.bottom();
3709 if !in_bounds {
3710 continue;
3711 }
3712
3713 let mut x_offset = 0u32;
3714 let rel_x = mouse.x - rect.x;
3715 for (idx, label) in state.labels.iter().enumerate() {
3716 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
3717 if rel_x >= x_offset && rel_x < x_offset + tab_width {
3718 state.selected = idx;
3719 self.consumed[i] = true;
3720 break;
3721 }
3722 x_offset += tab_width + 1;
3723 }
3724 }
3725 }
3726 }
3727
3728 self.interaction_count += 1;
3729 self.commands.push(Command::BeginContainer {
3730 direction: Direction::Row,
3731 gap: 1,
3732 align: Align::Start,
3733 justify: Justify::Start,
3734 border: None,
3735 border_sides: BorderSides::all(),
3736 border_style: Style::new().fg(self.theme.border),
3737 bg_color: None,
3738 padding: Padding::default(),
3739 margin: Margin::default(),
3740 constraints: Constraints::default(),
3741 title: None,
3742 grow: 0,
3743 });
3744 for (idx, label) in state.labels.iter().enumerate() {
3745 let style = if idx == state.selected {
3746 let s = Style::new().fg(self.theme.primary).bold();
3747 if focused {
3748 s.underline()
3749 } else {
3750 s
3751 }
3752 } else {
3753 Style::new().fg(self.theme.text_dim)
3754 };
3755 self.styled(format!("[ {label} ]"), style);
3756 }
3757 self.commands.push(Command::EndContainer);
3758 self.last_text_idx = None;
3759
3760 self
3761 }
3762
3763 pub fn button(&mut self, label: impl Into<String>) -> bool {
3768 let focused = self.register_focusable();
3769 let interaction_id = self.interaction_count;
3770 self.interaction_count += 1;
3771 let response = self.response_for(interaction_id);
3772
3773 let mut activated = response.clicked;
3774 if focused {
3775 let mut consumed_indices = Vec::new();
3776 for (i, event) in self.events.iter().enumerate() {
3777 if let Event::Key(key) = event {
3778 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3779 activated = true;
3780 consumed_indices.push(i);
3781 }
3782 }
3783 }
3784
3785 for index in consumed_indices {
3786 self.consumed[index] = true;
3787 }
3788 }
3789
3790 let hovered = response.hovered;
3791 let style = if focused {
3792 Style::new().fg(self.theme.primary).bold()
3793 } else if hovered {
3794 Style::new().fg(self.theme.accent)
3795 } else {
3796 Style::new().fg(self.theme.text)
3797 };
3798 let hover_bg = if hovered || focused {
3799 Some(self.theme.surface_hover)
3800 } else {
3801 None
3802 };
3803
3804 self.commands.push(Command::BeginContainer {
3805 direction: Direction::Row,
3806 gap: 0,
3807 align: Align::Start,
3808 justify: Justify::Start,
3809 border: None,
3810 border_sides: BorderSides::all(),
3811 border_style: Style::new().fg(self.theme.border),
3812 bg_color: hover_bg,
3813 padding: Padding::default(),
3814 margin: Margin::default(),
3815 constraints: Constraints::default(),
3816 title: None,
3817 grow: 0,
3818 });
3819 self.styled(format!("[ {} ]", label.into()), style);
3820 self.commands.push(Command::EndContainer);
3821 self.last_text_idx = None;
3822
3823 activated
3824 }
3825
3826 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
3831 let focused = self.register_focusable();
3832 let interaction_id = self.interaction_count;
3833 self.interaction_count += 1;
3834 let response = self.response_for(interaction_id);
3835
3836 let mut activated = response.clicked;
3837 if focused {
3838 let mut consumed_indices = Vec::new();
3839 for (i, event) in self.events.iter().enumerate() {
3840 if let Event::Key(key) = event {
3841 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3842 activated = true;
3843 consumed_indices.push(i);
3844 }
3845 }
3846 }
3847 for index in consumed_indices {
3848 self.consumed[index] = true;
3849 }
3850 }
3851
3852 let label = label.into();
3853 let hover_bg = if response.hovered || focused {
3854 Some(self.theme.surface_hover)
3855 } else {
3856 None
3857 };
3858 let (text, style, bg_color, border) = match variant {
3859 ButtonVariant::Default => {
3860 let style = if focused {
3861 Style::new().fg(self.theme.primary).bold()
3862 } else if response.hovered {
3863 Style::new().fg(self.theme.accent)
3864 } else {
3865 Style::new().fg(self.theme.text)
3866 };
3867 (format!("[ {label} ]"), style, hover_bg, None)
3868 }
3869 ButtonVariant::Primary => {
3870 let style = if focused {
3871 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
3872 } else if response.hovered {
3873 Style::new().fg(self.theme.bg).bg(self.theme.accent)
3874 } else {
3875 Style::new().fg(self.theme.bg).bg(self.theme.primary)
3876 };
3877 (format!(" {label} "), style, hover_bg, None)
3878 }
3879 ButtonVariant::Danger => {
3880 let style = if focused {
3881 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
3882 } else if response.hovered {
3883 Style::new().fg(self.theme.bg).bg(self.theme.warning)
3884 } else {
3885 Style::new().fg(self.theme.bg).bg(self.theme.error)
3886 };
3887 (format!(" {label} "), style, hover_bg, None)
3888 }
3889 ButtonVariant::Outline => {
3890 let border_color = if focused {
3891 self.theme.primary
3892 } else if response.hovered {
3893 self.theme.accent
3894 } else {
3895 self.theme.border
3896 };
3897 let style = if focused {
3898 Style::new().fg(self.theme.primary).bold()
3899 } else if response.hovered {
3900 Style::new().fg(self.theme.accent)
3901 } else {
3902 Style::new().fg(self.theme.text)
3903 };
3904 (
3905 format!(" {label} "),
3906 style,
3907 hover_bg,
3908 Some((Border::Rounded, Style::new().fg(border_color))),
3909 )
3910 }
3911 };
3912
3913 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
3914 self.commands.push(Command::BeginContainer {
3915 direction: Direction::Row,
3916 gap: 0,
3917 align: Align::Center,
3918 justify: Justify::Center,
3919 border: if border.is_some() {
3920 Some(btn_border)
3921 } else {
3922 None
3923 },
3924 border_sides: BorderSides::all(),
3925 border_style: btn_border_style,
3926 bg_color,
3927 padding: Padding::default(),
3928 margin: Margin::default(),
3929 constraints: Constraints::default(),
3930 title: None,
3931 grow: 0,
3932 });
3933 self.styled(text, style);
3934 self.commands.push(Command::EndContainer);
3935 self.last_text_idx = None;
3936
3937 activated
3938 }
3939
3940 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
3945 let focused = self.register_focusable();
3946 let interaction_id = self.interaction_count;
3947 self.interaction_count += 1;
3948 let response = self.response_for(interaction_id);
3949 let mut should_toggle = response.clicked;
3950
3951 if focused {
3952 let mut consumed_indices = Vec::new();
3953 for (i, event) in self.events.iter().enumerate() {
3954 if let Event::Key(key) = event {
3955 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3956 should_toggle = true;
3957 consumed_indices.push(i);
3958 }
3959 }
3960 }
3961
3962 for index in consumed_indices {
3963 self.consumed[index] = true;
3964 }
3965 }
3966
3967 if should_toggle {
3968 *checked = !*checked;
3969 }
3970
3971 let hover_bg = if response.hovered || focused {
3972 Some(self.theme.surface_hover)
3973 } else {
3974 None
3975 };
3976 self.commands.push(Command::BeginContainer {
3977 direction: Direction::Row,
3978 gap: 1,
3979 align: Align::Start,
3980 justify: Justify::Start,
3981 border: None,
3982 border_sides: BorderSides::all(),
3983 border_style: Style::new().fg(self.theme.border),
3984 bg_color: hover_bg,
3985 padding: Padding::default(),
3986 margin: Margin::default(),
3987 constraints: Constraints::default(),
3988 title: None,
3989 grow: 0,
3990 });
3991 let marker_style = if *checked {
3992 Style::new().fg(self.theme.success)
3993 } else {
3994 Style::new().fg(self.theme.text_dim)
3995 };
3996 let marker = if *checked { "[x]" } else { "[ ]" };
3997 let label_text = label.into();
3998 if focused {
3999 self.styled(format!("▸ {marker}"), marker_style.bold());
4000 self.styled(label_text, Style::new().fg(self.theme.text).bold());
4001 } else {
4002 self.styled(marker, marker_style);
4003 self.styled(label_text, Style::new().fg(self.theme.text));
4004 }
4005 self.commands.push(Command::EndContainer);
4006 self.last_text_idx = None;
4007
4008 self
4009 }
4010
4011 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
4017 let focused = self.register_focusable();
4018 let interaction_id = self.interaction_count;
4019 self.interaction_count += 1;
4020 let response = self.response_for(interaction_id);
4021 let mut should_toggle = response.clicked;
4022
4023 if focused {
4024 let mut consumed_indices = Vec::new();
4025 for (i, event) in self.events.iter().enumerate() {
4026 if let Event::Key(key) = event {
4027 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4028 should_toggle = true;
4029 consumed_indices.push(i);
4030 }
4031 }
4032 }
4033
4034 for index in consumed_indices {
4035 self.consumed[index] = true;
4036 }
4037 }
4038
4039 if should_toggle {
4040 *on = !*on;
4041 }
4042
4043 let hover_bg = if response.hovered || focused {
4044 Some(self.theme.surface_hover)
4045 } else {
4046 None
4047 };
4048 self.commands.push(Command::BeginContainer {
4049 direction: Direction::Row,
4050 gap: 2,
4051 align: Align::Start,
4052 justify: Justify::Start,
4053 border: None,
4054 border_sides: BorderSides::all(),
4055 border_style: Style::new().fg(self.theme.border),
4056 bg_color: hover_bg,
4057 padding: Padding::default(),
4058 margin: Margin::default(),
4059 constraints: Constraints::default(),
4060 title: None,
4061 grow: 0,
4062 });
4063 let label_text = label.into();
4064 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
4065 let switch_style = if *on {
4066 Style::new().fg(self.theme.success)
4067 } else {
4068 Style::new().fg(self.theme.text_dim)
4069 };
4070 if focused {
4071 self.styled(
4072 format!("▸ {label_text}"),
4073 Style::new().fg(self.theme.text).bold(),
4074 );
4075 self.styled(switch, switch_style.bold());
4076 } else {
4077 self.styled(label_text, Style::new().fg(self.theme.text));
4078 self.styled(switch, switch_style);
4079 }
4080 self.commands.push(Command::EndContainer);
4081 self.last_text_idx = None;
4082
4083 self
4084 }
4085
4086 pub fn select(&mut self, state: &mut SelectState) -> bool {
4092 if state.items.is_empty() {
4093 return false;
4094 }
4095 state.selected = state.selected.min(state.items.len().saturating_sub(1));
4096
4097 let focused = self.register_focusable();
4098 let interaction_id = self.interaction_count;
4099 self.interaction_count += 1;
4100 let response = self.response_for(interaction_id);
4101 let old_selected = state.selected;
4102
4103 if response.clicked {
4104 state.open = !state.open;
4105 if state.open {
4106 state.set_cursor(state.selected);
4107 }
4108 }
4109
4110 if focused {
4111 let mut consumed_indices = Vec::new();
4112 for (i, event) in self.events.iter().enumerate() {
4113 if self.consumed[i] {
4114 continue;
4115 }
4116 if let Event::Key(key) = event {
4117 if state.open {
4118 match key.code {
4119 KeyCode::Up | KeyCode::Char('k') => {
4120 let c = state.cursor();
4121 state.set_cursor(c.saturating_sub(1));
4122 consumed_indices.push(i);
4123 }
4124 KeyCode::Down | KeyCode::Char('j') => {
4125 let c = state.cursor();
4126 state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
4127 consumed_indices.push(i);
4128 }
4129 KeyCode::Enter | KeyCode::Char(' ') => {
4130 state.selected = state.cursor();
4131 state.open = false;
4132 consumed_indices.push(i);
4133 }
4134 KeyCode::Esc => {
4135 state.open = false;
4136 consumed_indices.push(i);
4137 }
4138 _ => {}
4139 }
4140 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4141 state.open = true;
4142 state.set_cursor(state.selected);
4143 consumed_indices.push(i);
4144 }
4145 }
4146 }
4147 for idx in consumed_indices {
4148 self.consumed[idx] = true;
4149 }
4150 }
4151
4152 let changed = state.selected != old_selected;
4153
4154 let border_color = if focused {
4155 self.theme.primary
4156 } else {
4157 self.theme.border
4158 };
4159 let display_text = state
4160 .items
4161 .get(state.selected)
4162 .cloned()
4163 .unwrap_or_else(|| state.placeholder.clone());
4164 let arrow = if state.open { "▲" } else { "▼" };
4165
4166 self.commands.push(Command::BeginContainer {
4167 direction: Direction::Column,
4168 gap: 0,
4169 align: Align::Start,
4170 justify: Justify::Start,
4171 border: None,
4172 border_sides: BorderSides::all(),
4173 border_style: Style::new().fg(self.theme.border),
4174 bg_color: None,
4175 padding: Padding::default(),
4176 margin: Margin::default(),
4177 constraints: Constraints::default(),
4178 title: None,
4179 grow: 0,
4180 });
4181
4182 self.commands.push(Command::BeginContainer {
4183 direction: Direction::Row,
4184 gap: 1,
4185 align: Align::Start,
4186 justify: Justify::Start,
4187 border: Some(Border::Rounded),
4188 border_sides: BorderSides::all(),
4189 border_style: Style::new().fg(border_color),
4190 bg_color: None,
4191 padding: Padding {
4192 left: 1,
4193 right: 1,
4194 top: 0,
4195 bottom: 0,
4196 },
4197 margin: Margin::default(),
4198 constraints: Constraints::default(),
4199 title: None,
4200 grow: 0,
4201 });
4202 self.interaction_count += 1;
4203 self.styled(&display_text, Style::new().fg(self.theme.text));
4204 self.styled(arrow, Style::new().fg(self.theme.text_dim));
4205 self.commands.push(Command::EndContainer);
4206 self.last_text_idx = None;
4207
4208 if state.open {
4209 for (idx, item) in state.items.iter().enumerate() {
4210 let is_cursor = idx == state.cursor();
4211 let style = if is_cursor {
4212 Style::new().bold().fg(self.theme.primary)
4213 } else {
4214 Style::new().fg(self.theme.text)
4215 };
4216 let prefix = if is_cursor { "▸ " } else { " " };
4217 self.styled(format!("{prefix}{item}"), style);
4218 }
4219 }
4220
4221 self.commands.push(Command::EndContainer);
4222 self.last_text_idx = None;
4223 changed
4224 }
4225
4226 pub fn radio(&mut self, state: &mut RadioState) -> bool {
4230 if state.items.is_empty() {
4231 return false;
4232 }
4233 state.selected = state.selected.min(state.items.len().saturating_sub(1));
4234 let focused = self.register_focusable();
4235 let old_selected = state.selected;
4236
4237 if focused {
4238 let mut consumed_indices = Vec::new();
4239 for (i, event) in self.events.iter().enumerate() {
4240 if self.consumed[i] {
4241 continue;
4242 }
4243 if let Event::Key(key) = event {
4244 match key.code {
4245 KeyCode::Up | KeyCode::Char('k') => {
4246 state.selected = state.selected.saturating_sub(1);
4247 consumed_indices.push(i);
4248 }
4249 KeyCode::Down | KeyCode::Char('j') => {
4250 state.selected =
4251 (state.selected + 1).min(state.items.len().saturating_sub(1));
4252 consumed_indices.push(i);
4253 }
4254 KeyCode::Enter | KeyCode::Char(' ') => {
4255 consumed_indices.push(i);
4256 }
4257 _ => {}
4258 }
4259 }
4260 }
4261 for idx in consumed_indices {
4262 self.consumed[idx] = true;
4263 }
4264 }
4265
4266 let interaction_id = self.interaction_count;
4267 self.interaction_count += 1;
4268
4269 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4270 for (i, event) in self.events.iter().enumerate() {
4271 if self.consumed[i] {
4272 continue;
4273 }
4274 if let Event::Mouse(mouse) = event {
4275 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4276 continue;
4277 }
4278 let in_bounds = mouse.x >= rect.x
4279 && mouse.x < rect.right()
4280 && mouse.y >= rect.y
4281 && mouse.y < rect.bottom();
4282 if !in_bounds {
4283 continue;
4284 }
4285 let clicked_idx = (mouse.y - rect.y) as usize;
4286 if clicked_idx < state.items.len() {
4287 state.selected = clicked_idx;
4288 self.consumed[i] = true;
4289 }
4290 }
4291 }
4292 }
4293
4294 self.commands.push(Command::BeginContainer {
4295 direction: Direction::Column,
4296 gap: 0,
4297 align: Align::Start,
4298 justify: Justify::Start,
4299 border: None,
4300 border_sides: BorderSides::all(),
4301 border_style: Style::new().fg(self.theme.border),
4302 bg_color: None,
4303 padding: Padding::default(),
4304 margin: Margin::default(),
4305 constraints: Constraints::default(),
4306 title: None,
4307 grow: 0,
4308 });
4309
4310 for (idx, item) in state.items.iter().enumerate() {
4311 let is_selected = idx == state.selected;
4312 let marker = if is_selected { "●" } else { "○" };
4313 let style = if is_selected {
4314 if focused {
4315 Style::new().bold().fg(self.theme.primary)
4316 } else {
4317 Style::new().fg(self.theme.primary)
4318 }
4319 } else {
4320 Style::new().fg(self.theme.text)
4321 };
4322 let prefix = if focused && idx == state.selected {
4323 "▸ "
4324 } else {
4325 " "
4326 };
4327 self.styled(format!("{prefix}{marker} {item}"), style);
4328 }
4329
4330 self.commands.push(Command::EndContainer);
4331 self.last_text_idx = None;
4332 state.selected != old_selected
4333 }
4334
4335 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
4339 if state.items.is_empty() {
4340 return self;
4341 }
4342 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
4343 let focused = self.register_focusable();
4344
4345 if focused {
4346 let mut consumed_indices = Vec::new();
4347 for (i, event) in self.events.iter().enumerate() {
4348 if self.consumed[i] {
4349 continue;
4350 }
4351 if let Event::Key(key) = event {
4352 match key.code {
4353 KeyCode::Up | KeyCode::Char('k') => {
4354 state.cursor = state.cursor.saturating_sub(1);
4355 consumed_indices.push(i);
4356 }
4357 KeyCode::Down | KeyCode::Char('j') => {
4358 state.cursor =
4359 (state.cursor + 1).min(state.items.len().saturating_sub(1));
4360 consumed_indices.push(i);
4361 }
4362 KeyCode::Char(' ') | KeyCode::Enter => {
4363 state.toggle(state.cursor);
4364 consumed_indices.push(i);
4365 }
4366 _ => {}
4367 }
4368 }
4369 }
4370 for idx in consumed_indices {
4371 self.consumed[idx] = true;
4372 }
4373 }
4374
4375 let interaction_id = self.interaction_count;
4376 self.interaction_count += 1;
4377
4378 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4379 for (i, event) in self.events.iter().enumerate() {
4380 if self.consumed[i] {
4381 continue;
4382 }
4383 if let Event::Mouse(mouse) = event {
4384 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4385 continue;
4386 }
4387 let in_bounds = mouse.x >= rect.x
4388 && mouse.x < rect.right()
4389 && mouse.y >= rect.y
4390 && mouse.y < rect.bottom();
4391 if !in_bounds {
4392 continue;
4393 }
4394 let clicked_idx = (mouse.y - rect.y) as usize;
4395 if clicked_idx < state.items.len() {
4396 state.toggle(clicked_idx);
4397 state.cursor = clicked_idx;
4398 self.consumed[i] = true;
4399 }
4400 }
4401 }
4402 }
4403
4404 self.commands.push(Command::BeginContainer {
4405 direction: Direction::Column,
4406 gap: 0,
4407 align: Align::Start,
4408 justify: Justify::Start,
4409 border: None,
4410 border_sides: BorderSides::all(),
4411 border_style: Style::new().fg(self.theme.border),
4412 bg_color: None,
4413 padding: Padding::default(),
4414 margin: Margin::default(),
4415 constraints: Constraints::default(),
4416 title: None,
4417 grow: 0,
4418 });
4419
4420 for (idx, item) in state.items.iter().enumerate() {
4421 let checked = state.selected.contains(&idx);
4422 let marker = if checked { "[x]" } else { "[ ]" };
4423 let is_cursor = idx == state.cursor;
4424 let style = if is_cursor && focused {
4425 Style::new().bold().fg(self.theme.primary)
4426 } else if checked {
4427 Style::new().fg(self.theme.success)
4428 } else {
4429 Style::new().fg(self.theme.text)
4430 };
4431 let prefix = if is_cursor && focused { "▸ " } else { " " };
4432 self.styled(format!("{prefix}{marker} {item}"), style);
4433 }
4434
4435 self.commands.push(Command::EndContainer);
4436 self.last_text_idx = None;
4437 self
4438 }
4439
4440 pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
4444 let entries = state.flatten();
4445 if entries.is_empty() {
4446 return self;
4447 }
4448 state.selected = state.selected.min(entries.len().saturating_sub(1));
4449 let focused = self.register_focusable();
4450
4451 if focused {
4452 let mut consumed_indices = Vec::new();
4453 for (i, event) in self.events.iter().enumerate() {
4454 if self.consumed[i] {
4455 continue;
4456 }
4457 if let Event::Key(key) = event {
4458 match key.code {
4459 KeyCode::Up | KeyCode::Char('k') => {
4460 state.selected = state.selected.saturating_sub(1);
4461 consumed_indices.push(i);
4462 }
4463 KeyCode::Down | KeyCode::Char('j') => {
4464 let max = state.flatten().len().saturating_sub(1);
4465 state.selected = (state.selected + 1).min(max);
4466 consumed_indices.push(i);
4467 }
4468 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
4469 state.toggle_at(state.selected);
4470 consumed_indices.push(i);
4471 }
4472 KeyCode::Left => {
4473 let entry = &entries[state.selected.min(entries.len() - 1)];
4474 if entry.expanded {
4475 state.toggle_at(state.selected);
4476 }
4477 consumed_indices.push(i);
4478 }
4479 _ => {}
4480 }
4481 }
4482 }
4483 for idx in consumed_indices {
4484 self.consumed[idx] = true;
4485 }
4486 }
4487
4488 self.interaction_count += 1;
4489 self.commands.push(Command::BeginContainer {
4490 direction: Direction::Column,
4491 gap: 0,
4492 align: Align::Start,
4493 justify: Justify::Start,
4494 border: None,
4495 border_sides: BorderSides::all(),
4496 border_style: Style::new().fg(self.theme.border),
4497 bg_color: None,
4498 padding: Padding::default(),
4499 margin: Margin::default(),
4500 constraints: Constraints::default(),
4501 title: None,
4502 grow: 0,
4503 });
4504
4505 let entries = state.flatten();
4506 for (idx, entry) in entries.iter().enumerate() {
4507 let indent = " ".repeat(entry.depth);
4508 let icon = if entry.is_leaf {
4509 " "
4510 } else if entry.expanded {
4511 "▾ "
4512 } else {
4513 "▸ "
4514 };
4515 let is_selected = idx == state.selected;
4516 let style = if is_selected && focused {
4517 Style::new().bold().fg(self.theme.primary)
4518 } else if is_selected {
4519 Style::new().fg(self.theme.primary)
4520 } else {
4521 Style::new().fg(self.theme.text)
4522 };
4523 let cursor = if is_selected && focused { "▸" } else { " " };
4524 self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
4525 }
4526
4527 self.commands.push(Command::EndContainer);
4528 self.last_text_idx = None;
4529 self
4530 }
4531
4532 pub fn virtual_list(
4539 &mut self,
4540 state: &mut ListState,
4541 visible_height: usize,
4542 f: impl Fn(&mut Context, usize),
4543 ) -> &mut Self {
4544 if state.items.is_empty() {
4545 return self;
4546 }
4547 state.selected = state.selected.min(state.items.len().saturating_sub(1));
4548 let focused = self.register_focusable();
4549
4550 if focused {
4551 let mut consumed_indices = Vec::new();
4552 for (i, event) in self.events.iter().enumerate() {
4553 if self.consumed[i] {
4554 continue;
4555 }
4556 if let Event::Key(key) = event {
4557 match key.code {
4558 KeyCode::Up | KeyCode::Char('k') => {
4559 state.selected = state.selected.saturating_sub(1);
4560 consumed_indices.push(i);
4561 }
4562 KeyCode::Down | KeyCode::Char('j') => {
4563 state.selected =
4564 (state.selected + 1).min(state.items.len().saturating_sub(1));
4565 consumed_indices.push(i);
4566 }
4567 KeyCode::PageUp => {
4568 state.selected = state.selected.saturating_sub(visible_height);
4569 consumed_indices.push(i);
4570 }
4571 KeyCode::PageDown => {
4572 state.selected = (state.selected + visible_height)
4573 .min(state.items.len().saturating_sub(1));
4574 consumed_indices.push(i);
4575 }
4576 KeyCode::Home => {
4577 state.selected = 0;
4578 consumed_indices.push(i);
4579 }
4580 KeyCode::End => {
4581 state.selected = state.items.len().saturating_sub(1);
4582 consumed_indices.push(i);
4583 }
4584 _ => {}
4585 }
4586 }
4587 }
4588 for idx in consumed_indices {
4589 self.consumed[idx] = true;
4590 }
4591 }
4592
4593 let start = if state.selected >= visible_height {
4594 state.selected - visible_height + 1
4595 } else {
4596 0
4597 };
4598 let end = (start + visible_height).min(state.items.len());
4599
4600 self.interaction_count += 1;
4601 self.commands.push(Command::BeginContainer {
4602 direction: Direction::Column,
4603 gap: 0,
4604 align: Align::Start,
4605 justify: Justify::Start,
4606 border: None,
4607 border_sides: BorderSides::all(),
4608 border_style: Style::new().fg(self.theme.border),
4609 bg_color: None,
4610 padding: Padding::default(),
4611 margin: Margin::default(),
4612 constraints: Constraints::default(),
4613 title: None,
4614 grow: 0,
4615 });
4616
4617 if start > 0 {
4618 self.styled(
4619 format!(" ↑ {} more", start),
4620 Style::new().fg(self.theme.text_dim).dim(),
4621 );
4622 }
4623
4624 for idx in start..end {
4625 f(self, idx);
4626 }
4627
4628 let remaining = state.items.len().saturating_sub(end);
4629 if remaining > 0 {
4630 self.styled(
4631 format!(" ↓ {} more", remaining),
4632 Style::new().fg(self.theme.text_dim).dim(),
4633 );
4634 }
4635
4636 self.commands.push(Command::EndContainer);
4637 self.last_text_idx = None;
4638 self
4639 }
4640
4641 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
4645 if !state.open {
4646 return None;
4647 }
4648
4649 let filtered = state.filtered_indices();
4650 let sel = state.selected().min(filtered.len().saturating_sub(1));
4651 state.set_selected(sel);
4652
4653 let mut consumed_indices = Vec::new();
4654 let mut result: Option<usize> = None;
4655
4656 for (i, event) in self.events.iter().enumerate() {
4657 if self.consumed[i] {
4658 continue;
4659 }
4660 if let Event::Key(key) = event {
4661 match key.code {
4662 KeyCode::Esc => {
4663 state.open = false;
4664 consumed_indices.push(i);
4665 }
4666 KeyCode::Up => {
4667 let s = state.selected();
4668 state.set_selected(s.saturating_sub(1));
4669 consumed_indices.push(i);
4670 }
4671 KeyCode::Down => {
4672 let s = state.selected();
4673 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
4674 consumed_indices.push(i);
4675 }
4676 KeyCode::Enter => {
4677 if let Some(&cmd_idx) = filtered.get(state.selected()) {
4678 result = Some(cmd_idx);
4679 state.open = false;
4680 }
4681 consumed_indices.push(i);
4682 }
4683 KeyCode::Backspace => {
4684 if state.cursor > 0 {
4685 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
4686 let end_idx = byte_index_for_char(&state.input, state.cursor);
4687 state.input.replace_range(byte_idx..end_idx, "");
4688 state.cursor -= 1;
4689 state.set_selected(0);
4690 }
4691 consumed_indices.push(i);
4692 }
4693 KeyCode::Char(ch) => {
4694 let byte_idx = byte_index_for_char(&state.input, state.cursor);
4695 state.input.insert(byte_idx, ch);
4696 state.cursor += 1;
4697 state.set_selected(0);
4698 consumed_indices.push(i);
4699 }
4700 _ => {}
4701 }
4702 }
4703 }
4704 for idx in consumed_indices {
4705 self.consumed[idx] = true;
4706 }
4707
4708 let filtered = state.filtered_indices();
4709
4710 self.modal(|ui| {
4711 let primary = ui.theme.primary;
4712 ui.container()
4713 .border(Border::Rounded)
4714 .border_style(Style::new().fg(primary))
4715 .pad(1)
4716 .max_w(60)
4717 .col(|ui| {
4718 let border_color = ui.theme.primary;
4719 ui.bordered(Border::Rounded)
4720 .border_style(Style::new().fg(border_color))
4721 .px(1)
4722 .col(|ui| {
4723 let display = if state.input.is_empty() {
4724 "Type to search...".to_string()
4725 } else {
4726 state.input.clone()
4727 };
4728 let style = if state.input.is_empty() {
4729 Style::new().dim().fg(ui.theme.text_dim)
4730 } else {
4731 Style::new().fg(ui.theme.text)
4732 };
4733 ui.styled(display, style);
4734 });
4735
4736 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
4737 let cmd = &state.commands[cmd_idx];
4738 let is_selected = list_idx == state.selected();
4739 let style = if is_selected {
4740 Style::new().bold().fg(ui.theme.primary)
4741 } else {
4742 Style::new().fg(ui.theme.text)
4743 };
4744 let prefix = if is_selected { "▸ " } else { " " };
4745 let shortcut_text = cmd
4746 .shortcut
4747 .as_deref()
4748 .map(|s| format!(" ({s})"))
4749 .unwrap_or_default();
4750 ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
4751 if is_selected && !cmd.description.is_empty() {
4752 ui.styled(
4753 format!(" {}", cmd.description),
4754 Style::new().dim().fg(ui.theme.text_dim),
4755 );
4756 }
4757 }
4758
4759 if filtered.is_empty() {
4760 ui.styled(
4761 " No matching commands",
4762 Style::new().dim().fg(ui.theme.text_dim),
4763 );
4764 }
4765 });
4766 });
4767
4768 result
4769 }
4770
4771 pub fn markdown(&mut self, text: &str) -> &mut Self {
4778 self.commands.push(Command::BeginContainer {
4779 direction: Direction::Column,
4780 gap: 0,
4781 align: Align::Start,
4782 justify: Justify::Start,
4783 border: None,
4784 border_sides: BorderSides::all(),
4785 border_style: Style::new().fg(self.theme.border),
4786 bg_color: None,
4787 padding: Padding::default(),
4788 margin: Margin::default(),
4789 constraints: Constraints::default(),
4790 title: None,
4791 grow: 0,
4792 });
4793 self.interaction_count += 1;
4794
4795 let text_style = Style::new().fg(self.theme.text);
4796 let bold_style = Style::new().fg(self.theme.text).bold();
4797 let code_style = Style::new().fg(self.theme.accent);
4798
4799 for line in text.lines() {
4800 let trimmed = line.trim();
4801 if trimmed.is_empty() {
4802 self.text(" ");
4803 continue;
4804 }
4805 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
4806 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
4807 continue;
4808 }
4809 if let Some(heading) = trimmed.strip_prefix("### ") {
4810 self.styled(heading, Style::new().bold().fg(self.theme.accent));
4811 } else if let Some(heading) = trimmed.strip_prefix("## ") {
4812 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
4813 } else if let Some(heading) = trimmed.strip_prefix("# ") {
4814 self.styled(heading, Style::new().bold().fg(self.theme.primary));
4815 } else if let Some(item) = trimmed
4816 .strip_prefix("- ")
4817 .or_else(|| trimmed.strip_prefix("* "))
4818 {
4819 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
4820 if segs.len() <= 1 {
4821 self.styled(format!(" • {item}"), text_style);
4822 } else {
4823 self.line(|ui| {
4824 ui.styled(" • ", text_style);
4825 for (s, st) in segs {
4826 ui.styled(s, st);
4827 }
4828 });
4829 }
4830 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
4831 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
4832 if parts.len() == 2 {
4833 let segs =
4834 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
4835 if segs.len() <= 1 {
4836 self.styled(format!(" {}. {}", parts[0], parts[1]), text_style);
4837 } else {
4838 self.line(|ui| {
4839 ui.styled(format!(" {}. ", parts[0]), text_style);
4840 for (s, st) in segs {
4841 ui.styled(s, st);
4842 }
4843 });
4844 }
4845 } else {
4846 self.text(trimmed);
4847 }
4848 } else if let Some(code) = trimmed.strip_prefix("```") {
4849 let _ = code;
4850 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
4851 } else {
4852 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
4853 if segs.len() <= 1 {
4854 self.styled(trimmed, text_style);
4855 } else {
4856 self.line(|ui| {
4857 for (s, st) in segs {
4858 ui.styled(s, st);
4859 }
4860 });
4861 }
4862 }
4863 }
4864
4865 self.commands.push(Command::EndContainer);
4866 self.last_text_idx = None;
4867 self
4868 }
4869
4870 fn parse_inline_segments(
4871 text: &str,
4872 base: Style,
4873 bold: Style,
4874 code: Style,
4875 ) -> Vec<(String, Style)> {
4876 let mut segments: Vec<(String, Style)> = Vec::new();
4877 let mut current = String::new();
4878 let chars: Vec<char> = text.chars().collect();
4879 let mut i = 0;
4880 while i < chars.len() {
4881 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
4882 if let Some(end) = text[i + 2..].find("**") {
4883 if !current.is_empty() {
4884 segments.push((std::mem::take(&mut current), base));
4885 }
4886 segments.push((text[i + 2..i + 2 + end].to_string(), bold));
4887 i += 4 + end;
4888 continue;
4889 }
4890 }
4891 if chars[i] == '*'
4892 && (i + 1 >= chars.len() || chars[i + 1] != '*')
4893 && (i == 0 || chars[i - 1] != '*')
4894 {
4895 if let Some(end) = text[i + 1..].find('*') {
4896 if !current.is_empty() {
4897 segments.push((std::mem::take(&mut current), base));
4898 }
4899 segments.push((text[i + 1..i + 1 + end].to_string(), base.italic()));
4900 i += 2 + end;
4901 continue;
4902 }
4903 }
4904 if chars[i] == '`' {
4905 if let Some(end) = text[i + 1..].find('`') {
4906 if !current.is_empty() {
4907 segments.push((std::mem::take(&mut current), base));
4908 }
4909 segments.push((text[i + 1..i + 1 + end].to_string(), code));
4910 i += 2 + end;
4911 continue;
4912 }
4913 }
4914 current.push(chars[i]);
4915 i += 1;
4916 }
4917 if !current.is_empty() {
4918 segments.push((current, base));
4919 }
4920 segments
4921 }
4922
4923 pub fn key_seq(&self, seq: &str) -> bool {
4930 if seq.is_empty() {
4931 return false;
4932 }
4933 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
4934 return false;
4935 }
4936 let target: Vec<char> = seq.chars().collect();
4937 let mut matched = 0;
4938 for (i, event) in self.events.iter().enumerate() {
4939 if self.consumed[i] {
4940 continue;
4941 }
4942 if let Event::Key(key) = event {
4943 if let KeyCode::Char(c) = key.code {
4944 if c == target[matched] {
4945 matched += 1;
4946 if matched == target.len() {
4947 return true;
4948 }
4949 } else {
4950 matched = 0;
4951 if c == target[0] {
4952 matched = 1;
4953 }
4954 }
4955 }
4956 }
4957 }
4958 false
4959 }
4960
4961 pub fn separator(&mut self) -> &mut Self {
4966 self.commands.push(Command::Text {
4967 content: "─".repeat(200),
4968 style: Style::new().fg(self.theme.border).dim(),
4969 grow: 0,
4970 align: Align::Start,
4971 wrap: false,
4972 margin: Margin::default(),
4973 constraints: Constraints::default(),
4974 });
4975 self.last_text_idx = Some(self.commands.len() - 1);
4976 self
4977 }
4978
4979 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
4985 if bindings.is_empty() {
4986 return self;
4987 }
4988
4989 self.interaction_count += 1;
4990 self.commands.push(Command::BeginContainer {
4991 direction: Direction::Row,
4992 gap: 2,
4993 align: Align::Start,
4994 justify: Justify::Start,
4995 border: None,
4996 border_sides: BorderSides::all(),
4997 border_style: Style::new().fg(self.theme.border),
4998 bg_color: None,
4999 padding: Padding::default(),
5000 margin: Margin::default(),
5001 constraints: Constraints::default(),
5002 title: None,
5003 grow: 0,
5004 });
5005 for (idx, (key, action)) in bindings.iter().enumerate() {
5006 if idx > 0 {
5007 self.styled("·", Style::new().fg(self.theme.text_dim));
5008 }
5009 self.styled(*key, Style::new().bold().fg(self.theme.primary));
5010 self.styled(*action, Style::new().fg(self.theme.text_dim));
5011 }
5012 self.commands.push(Command::EndContainer);
5013 self.last_text_idx = None;
5014
5015 self
5016 }
5017
5018 pub fn key(&self, c: char) -> bool {
5024 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5025 return false;
5026 }
5027 self.events.iter().enumerate().any(|(i, e)| {
5028 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
5029 })
5030 }
5031
5032 pub fn key_code(&self, code: KeyCode) -> bool {
5036 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5037 return false;
5038 }
5039 self.events
5040 .iter()
5041 .enumerate()
5042 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
5043 }
5044
5045 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
5049 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5050 return false;
5051 }
5052 self.events.iter().enumerate().any(|(i, e)| {
5053 !self.consumed[i]
5054 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
5055 })
5056 }
5057
5058 pub fn mouse_down(&self) -> Option<(u32, u32)> {
5062 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5063 return None;
5064 }
5065 self.events.iter().enumerate().find_map(|(i, event)| {
5066 if self.consumed[i] {
5067 return None;
5068 }
5069 if let Event::Mouse(mouse) = event {
5070 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5071 return Some((mouse.x, mouse.y));
5072 }
5073 }
5074 None
5075 })
5076 }
5077
5078 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
5083 self.mouse_pos
5084 }
5085
5086 pub fn paste(&self) -> Option<&str> {
5088 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5089 return None;
5090 }
5091 self.events.iter().enumerate().find_map(|(i, event)| {
5092 if self.consumed[i] {
5093 return None;
5094 }
5095 if let Event::Paste(ref text) = event {
5096 return Some(text.as_str());
5097 }
5098 None
5099 })
5100 }
5101
5102 pub fn scroll_up(&self) -> bool {
5104 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5105 return false;
5106 }
5107 self.events.iter().enumerate().any(|(i, event)| {
5108 !self.consumed[i]
5109 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
5110 })
5111 }
5112
5113 pub fn scroll_down(&self) -> bool {
5115 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5116 return false;
5117 }
5118 self.events.iter().enumerate().any(|(i, event)| {
5119 !self.consumed[i]
5120 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
5121 })
5122 }
5123
5124 pub fn quit(&mut self) {
5126 self.should_quit = true;
5127 }
5128
5129 pub fn theme(&self) -> &Theme {
5131 &self.theme
5132 }
5133
5134 pub fn set_theme(&mut self, theme: Theme) {
5138 self.theme = theme;
5139 }
5140
5141 pub fn width(&self) -> u32 {
5145 self.area_width
5146 }
5147
5148 pub fn height(&self) -> u32 {
5150 self.area_height
5151 }
5152
5153 pub fn tick(&self) -> u64 {
5158 self.tick
5159 }
5160
5161 pub fn debug_enabled(&self) -> bool {
5165 self.debug
5166 }
5167}
5168
5169#[inline]
5170fn byte_index_for_char(value: &str, char_index: usize) -> usize {
5171 if char_index == 0 {
5172 return 0;
5173 }
5174 value
5175 .char_indices()
5176 .nth(char_index)
5177 .map_or(value.len(), |(idx, _)| idx)
5178}
5179
5180fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
5181 let mut parts: Vec<String> = Vec::new();
5182 for (i, width) in widths.iter().enumerate() {
5183 let cell = cells.get(i).map(String::as_str).unwrap_or("");
5184 let cell_width = UnicodeWidthStr::width(cell) as u32;
5185 let padding = (*width).saturating_sub(cell_width) as usize;
5186 parts.push(format!("{cell}{}", " ".repeat(padding)));
5187 }
5188 parts.join(separator)
5189}
5190
5191fn format_compact_number(value: f64) -> String {
5192 if value.fract().abs() < f64::EPSILON {
5193 return format!("{value:.0}");
5194 }
5195
5196 let mut s = format!("{value:.2}");
5197 while s.contains('.') && s.ends_with('0') {
5198 s.pop();
5199 }
5200 if s.ends_with('.') {
5201 s.pop();
5202 }
5203 s
5204}
5205
5206fn center_text(text: &str, width: usize) -> String {
5207 let text_width = UnicodeWidthStr::width(text);
5208 if text_width >= width {
5209 return text.to_string();
5210 }
5211
5212 let total = width - text_width;
5213 let left = total / 2;
5214 let right = total - left;
5215 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
5216}
5217
5218struct TextareaVLine {
5219 logical_row: usize,
5220 char_start: usize,
5221 char_count: usize,
5222}
5223
5224fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
5225 let mut out = Vec::new();
5226 for (row, line) in lines.iter().enumerate() {
5227 if line.is_empty() || wrap_width == u32::MAX {
5228 out.push(TextareaVLine {
5229 logical_row: row,
5230 char_start: 0,
5231 char_count: line.chars().count(),
5232 });
5233 continue;
5234 }
5235 let mut seg_start = 0usize;
5236 let mut seg_chars = 0usize;
5237 let mut seg_width = 0u32;
5238 for (idx, ch) in line.chars().enumerate() {
5239 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
5240 if seg_width + cw > wrap_width && seg_chars > 0 {
5241 out.push(TextareaVLine {
5242 logical_row: row,
5243 char_start: seg_start,
5244 char_count: seg_chars,
5245 });
5246 seg_start = idx;
5247 seg_chars = 0;
5248 seg_width = 0;
5249 }
5250 seg_chars += 1;
5251 seg_width += cw;
5252 }
5253 out.push(TextareaVLine {
5254 logical_row: row,
5255 char_start: seg_start,
5256 char_count: seg_chars,
5257 });
5258 }
5259 out
5260}
5261
5262fn textarea_logical_to_visual(
5263 vlines: &[TextareaVLine],
5264 logical_row: usize,
5265 logical_col: usize,
5266) -> (usize, usize) {
5267 for (i, vl) in vlines.iter().enumerate() {
5268 if vl.logical_row != logical_row {
5269 continue;
5270 }
5271 let seg_end = vl.char_start + vl.char_count;
5272 if logical_col >= vl.char_start && logical_col < seg_end {
5273 return (i, logical_col - vl.char_start);
5274 }
5275 if logical_col == seg_end {
5276 let is_last_seg = vlines
5277 .get(i + 1)
5278 .map_or(true, |next| next.logical_row != logical_row);
5279 if is_last_seg {
5280 return (i, logical_col - vl.char_start);
5281 }
5282 }
5283 }
5284 (vlines.len().saturating_sub(1), 0)
5285}
5286
5287fn textarea_visual_to_logical(
5288 vlines: &[TextareaVLine],
5289 visual_row: usize,
5290 visual_col: usize,
5291) -> (usize, usize) {
5292 if let Some(vl) = vlines.get(visual_row) {
5293 let logical_col = vl.char_start + visual_col.min(vl.char_count);
5294 (vl.logical_row, logical_col)
5295 } else {
5296 (0, 0)
5297 }
5298}
5299
5300fn open_url(url: &str) -> std::io::Result<()> {
5301 #[cfg(target_os = "macos")]
5302 {
5303 std::process::Command::new("open").arg(url).spawn()?;
5304 }
5305 #[cfg(target_os = "linux")]
5306 {
5307 std::process::Command::new("xdg-open").arg(url).spawn()?;
5308 }
5309 #[cfg(target_os = "windows")]
5310 {
5311 std::process::Command::new("cmd")
5312 .args(["/c", "start", "", url])
5313 .spawn()?;
5314 }
5315 Ok(())
5316}