1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
3use crate::halfblock::HalfBlockImage;
4use crate::layout::{Command, Direction};
5use crate::rect::Rect;
6use crate::style::{
7 Align, Border, BorderSides, Breakpoint, Color, Constraints, ContainerStyle, Justify, Margin,
8 Modifiers, Padding, Style, Theme,
9};
10use crate::widgets::{
11 ApprovalAction, ButtonVariant, CommandPaletteState, ContextItem, FormField, FormState,
12 ListState, MultiSelectState, RadioState, ScrollState, SelectState, SpinnerState,
13 StreamingTextState, TableState, TabsState, TextInputState, TextareaState, ToastLevel,
14 ToastState, ToolApprovalState, TreeState,
15};
16use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
17
18#[allow(dead_code)]
19fn slt_assert(condition: bool, msg: &str) {
20 if !condition {
21 panic!("[SLT] {}", msg);
22 }
23}
24
25#[cfg(debug_assertions)]
26#[allow(dead_code)]
27fn slt_warn(msg: &str) {
28 eprintln!("\x1b[33m[SLT warning]\x1b[0m {}", msg);
29}
30
31#[cfg(not(debug_assertions))]
32#[allow(dead_code)]
33fn slt_warn(_msg: &str) {}
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37pub struct State<T> {
38 idx: usize,
39 _marker: std::marker::PhantomData<T>,
40}
41
42impl<T: 'static> State<T> {
43 pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
45 ui.hook_states[self.idx]
46 .downcast_ref::<T>()
47 .unwrap_or_else(|| {
48 panic!(
49 "use_state type mismatch at hook index {} — expected {}",
50 self.idx,
51 std::any::type_name::<T>()
52 )
53 })
54 }
55
56 pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
58 ui.hook_states[self.idx]
59 .downcast_mut::<T>()
60 .unwrap_or_else(|| {
61 panic!(
62 "use_state type mismatch at hook index {} — expected {}",
63 self.idx,
64 std::any::type_name::<T>()
65 )
66 })
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default)]
76pub struct Response {
77 pub clicked: bool,
79 pub hovered: bool,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum BarDirection {
86 Horizontal,
88 Vertical,
90}
91
92#[derive(Debug, Clone)]
94pub struct Bar {
95 pub label: String,
97 pub value: f64,
99 pub color: Option<Color>,
101}
102
103impl Bar {
104 pub fn new(label: impl Into<String>, value: f64) -> Self {
106 Self {
107 label: label.into(),
108 value,
109 color: None,
110 }
111 }
112
113 pub fn color(mut self, color: Color) -> Self {
115 self.color = Some(color);
116 self
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct BarGroup {
123 pub label: String,
125 pub bars: Vec<Bar>,
127}
128
129impl BarGroup {
130 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
132 Self {
133 label: label.into(),
134 bars,
135 }
136 }
137}
138
139pub trait Widget {
201 type Response;
204
205 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
211}
212
213pub struct Context {
229 pub(crate) commands: Vec<Command>,
230 pub(crate) events: Vec<Event>,
231 pub(crate) consumed: Vec<bool>,
232 pub(crate) should_quit: bool,
233 pub(crate) area_width: u32,
234 pub(crate) area_height: u32,
235 pub(crate) tick: u64,
236 pub(crate) focus_index: usize,
237 pub(crate) focus_count: usize,
238 pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
239 pub(crate) hook_cursor: usize,
240 prev_focus_count: usize,
241 scroll_count: usize,
242 prev_scroll_infos: Vec<(u32, u32)>,
243 prev_scroll_rects: Vec<Rect>,
244 interaction_count: usize,
245 pub(crate) prev_hit_map: Vec<Rect>,
246 pub(crate) group_stack: Vec<String>,
247 pub(crate) prev_group_rects: Vec<(String, Rect)>,
248 group_count: usize,
249 prev_focus_groups: Vec<Option<String>>,
250 _prev_focus_rects: Vec<(usize, Rect)>,
251 mouse_pos: Option<(u32, u32)>,
252 click_pos: Option<(u32, u32)>,
253 last_text_idx: Option<usize>,
254 overlay_depth: usize,
255 pub(crate) modal_active: bool,
256 prev_modal_active: bool,
257 pub(crate) clipboard_text: Option<String>,
258 debug: bool,
259 theme: Theme,
260 pub(crate) dark_mode: bool,
261 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
262}
263
264type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
265
266#[must_use = "configure and finalize with .col() or .row()"]
287pub struct ContainerBuilder<'a> {
288 ctx: &'a mut Context,
289 gap: u32,
290 align: Align,
291 justify: Justify,
292 border: Option<Border>,
293 border_sides: BorderSides,
294 border_style: Style,
295 bg_color: Option<Color>,
296 dark_bg_color: Option<Color>,
297 dark_border_style: Option<Style>,
298 group_hover_bg: Option<Color>,
299 group_hover_border_style: Option<Style>,
300 group_name: Option<String>,
301 padding: Padding,
302 margin: Margin,
303 constraints: Constraints,
304 title: Option<(String, Style)>,
305 grow: u16,
306 scroll_offset: Option<u32>,
307}
308
309#[derive(Debug, Clone, Copy)]
316struct CanvasPixel {
317 bits: u32,
318 color: Color,
319}
320
321#[derive(Debug, Clone)]
323struct CanvasLabel {
324 x: usize,
325 y: usize,
326 text: String,
327 color: Color,
328}
329
330#[derive(Debug, Clone)]
332struct CanvasLayer {
333 grid: Vec<Vec<CanvasPixel>>,
334 labels: Vec<CanvasLabel>,
335}
336
337pub struct CanvasContext {
338 layers: Vec<CanvasLayer>,
339 cols: usize,
340 rows: usize,
341 px_w: usize,
342 px_h: usize,
343 current_color: Color,
344}
345
346impl CanvasContext {
347 fn new(cols: usize, rows: usize) -> Self {
348 Self {
349 layers: vec![Self::new_layer(cols, rows)],
350 cols,
351 rows,
352 px_w: cols * 2,
353 px_h: rows * 4,
354 current_color: Color::Reset,
355 }
356 }
357
358 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
359 CanvasLayer {
360 grid: vec![
361 vec![
362 CanvasPixel {
363 bits: 0,
364 color: Color::Reset,
365 };
366 cols
367 ];
368 rows
369 ],
370 labels: Vec::new(),
371 }
372 }
373
374 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
375 self.layers.last_mut()
376 }
377
378 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
379 if x >= self.px_w || y >= self.px_h {
380 return;
381 }
382
383 let char_col = x / 2;
384 let char_row = y / 4;
385 let sub_col = x % 2;
386 let sub_row = y % 4;
387 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
388 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
389
390 let bit = if sub_col == 0 {
391 LEFT_BITS[sub_row]
392 } else {
393 RIGHT_BITS[sub_row]
394 };
395
396 if let Some(layer) = self.current_layer_mut() {
397 let cell = &mut layer.grid[char_row][char_col];
398 let new_bits = cell.bits | bit;
399 if new_bits != cell.bits {
400 cell.bits = new_bits;
401 cell.color = color;
402 }
403 }
404 }
405
406 fn dot_isize(&mut self, x: isize, y: isize) {
407 if x >= 0 && y >= 0 {
408 self.dot(x as usize, y as usize);
409 }
410 }
411
412 pub fn width(&self) -> usize {
414 self.px_w
415 }
416
417 pub fn height(&self) -> usize {
419 self.px_h
420 }
421
422 pub fn dot(&mut self, x: usize, y: usize) {
424 self.dot_with_color(x, y, self.current_color);
425 }
426
427 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
429 let (mut x, mut y) = (x0 as isize, y0 as isize);
430 let (x1, y1) = (x1 as isize, y1 as isize);
431 let dx = (x1 - x).abs();
432 let dy = -(y1 - y).abs();
433 let sx = if x < x1 { 1 } else { -1 };
434 let sy = if y < y1 { 1 } else { -1 };
435 let mut err = dx + dy;
436
437 loop {
438 self.dot_isize(x, y);
439 if x == x1 && y == y1 {
440 break;
441 }
442 let e2 = 2 * err;
443 if e2 >= dy {
444 err += dy;
445 x += sx;
446 }
447 if e2 <= dx {
448 err += dx;
449 y += sy;
450 }
451 }
452 }
453
454 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
456 if w == 0 || h == 0 {
457 return;
458 }
459
460 self.line(x, y, x + w.saturating_sub(1), y);
461 self.line(
462 x + w.saturating_sub(1),
463 y,
464 x + w.saturating_sub(1),
465 y + h.saturating_sub(1),
466 );
467 self.line(
468 x + w.saturating_sub(1),
469 y + h.saturating_sub(1),
470 x,
471 y + h.saturating_sub(1),
472 );
473 self.line(x, y + h.saturating_sub(1), x, y);
474 }
475
476 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
478 let mut x = r as isize;
479 let mut y: isize = 0;
480 let mut err: isize = 1 - x;
481 let (cx, cy) = (cx as isize, cy as isize);
482
483 while x >= y {
484 for &(dx, dy) in &[
485 (x, y),
486 (y, x),
487 (-x, y),
488 (-y, x),
489 (x, -y),
490 (y, -x),
491 (-x, -y),
492 (-y, -x),
493 ] {
494 let px = cx + dx;
495 let py = cy + dy;
496 self.dot_isize(px, py);
497 }
498
499 y += 1;
500 if err < 0 {
501 err += 2 * y + 1;
502 } else {
503 x -= 1;
504 err += 2 * (y - x) + 1;
505 }
506 }
507 }
508
509 pub fn set_color(&mut self, color: Color) {
511 self.current_color = color;
512 }
513
514 pub fn color(&self) -> Color {
516 self.current_color
517 }
518
519 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
521 if w == 0 || h == 0 {
522 return;
523 }
524
525 let x_end = x.saturating_add(w).min(self.px_w);
526 let y_end = y.saturating_add(h).min(self.px_h);
527 if x >= x_end || y >= y_end {
528 return;
529 }
530
531 for yy in y..y_end {
532 self.line(x, yy, x_end.saturating_sub(1), yy);
533 }
534 }
535
536 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
538 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
539 for y in (cy - r)..=(cy + r) {
540 let dy = y - cy;
541 let span_sq = (r * r - dy * dy).max(0);
542 let dx = (span_sq as f64).sqrt() as isize;
543 for x in (cx - dx)..=(cx + dx) {
544 self.dot_isize(x, y);
545 }
546 }
547 }
548
549 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
551 self.line(x0, y0, x1, y1);
552 self.line(x1, y1, x2, y2);
553 self.line(x2, y2, x0, y0);
554 }
555
556 pub fn filled_triangle(
558 &mut self,
559 x0: usize,
560 y0: usize,
561 x1: usize,
562 y1: usize,
563 x2: usize,
564 y2: usize,
565 ) {
566 let vertices = [
567 (x0 as isize, y0 as isize),
568 (x1 as isize, y1 as isize),
569 (x2 as isize, y2 as isize),
570 ];
571 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
572 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
573
574 for y in min_y..=max_y {
575 let mut intersections: Vec<f64> = Vec::new();
576
577 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
578 let (x_a, y_a) = vertices[edge.0];
579 let (x_b, y_b) = vertices[edge.1];
580 if y_a == y_b {
581 continue;
582 }
583
584 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
585 (x_a, y_a, x_b, y_b)
586 } else {
587 (x_b, y_b, x_a, y_a)
588 };
589
590 if y < y_start || y >= y_end {
591 continue;
592 }
593
594 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
595 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
596 }
597
598 intersections.sort_by(|a, b| a.total_cmp(b));
599 let mut i = 0usize;
600 while i + 1 < intersections.len() {
601 let x_start = intersections[i].ceil() as isize;
602 let x_end = intersections[i + 1].floor() as isize;
603 for x in x_start..=x_end {
604 self.dot_isize(x, y);
605 }
606 i += 2;
607 }
608 }
609
610 self.triangle(x0, y0, x1, y1, x2, y2);
611 }
612
613 pub fn points(&mut self, pts: &[(usize, usize)]) {
615 for &(x, y) in pts {
616 self.dot(x, y);
617 }
618 }
619
620 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
622 for window in pts.windows(2) {
623 if let [(x0, y0), (x1, y1)] = window {
624 self.line(*x0, *y0, *x1, *y1);
625 }
626 }
627 }
628
629 pub fn print(&mut self, x: usize, y: usize, text: &str) {
632 if text.is_empty() {
633 return;
634 }
635
636 let color = self.current_color;
637 if let Some(layer) = self.current_layer_mut() {
638 layer.labels.push(CanvasLabel {
639 x,
640 y,
641 text: text.to_string(),
642 color,
643 });
644 }
645 }
646
647 pub fn layer(&mut self) {
649 self.layers.push(Self::new_layer(self.cols, self.rows));
650 }
651
652 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
653 let mut final_grid = vec![
654 vec![
655 CanvasPixel {
656 bits: 0,
657 color: Color::Reset,
658 };
659 self.cols
660 ];
661 self.rows
662 ];
663 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
664 vec![vec![None; self.cols]; self.rows];
665
666 for layer in &self.layers {
667 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
668 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
669 let src = layer.grid[row][col];
670 if src.bits == 0 {
671 continue;
672 }
673
674 let merged = dst.bits | src.bits;
675 if merged != dst.bits {
676 dst.bits = merged;
677 dst.color = src.color;
678 }
679 }
680 }
681
682 for label in &layer.labels {
683 let row = label.y / 4;
684 if row >= self.rows {
685 continue;
686 }
687 let start_col = label.x / 2;
688 for (offset, ch) in label.text.chars().enumerate() {
689 let col = start_col + offset;
690 if col >= self.cols {
691 break;
692 }
693 labels_overlay[row][col] = Some((ch, label.color));
694 }
695 }
696 }
697
698 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
699 for row in 0..self.rows {
700 let mut segments: Vec<(String, Color)> = Vec::new();
701 let mut current_color: Option<Color> = None;
702 let mut current_text = String::new();
703
704 for col in 0..self.cols {
705 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
706 (label_ch, label_color)
707 } else {
708 let bits = final_grid[row][col].bits;
709 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
710 (ch, final_grid[row][col].color)
711 };
712
713 match current_color {
714 Some(c) if c == color => {
715 current_text.push(ch);
716 }
717 Some(c) => {
718 segments.push((std::mem::take(&mut current_text), c));
719 current_text.push(ch);
720 current_color = Some(color);
721 }
722 None => {
723 current_text.push(ch);
724 current_color = Some(color);
725 }
726 }
727 }
728
729 if let Some(color) = current_color {
730 segments.push((current_text, color));
731 }
732 lines.push(segments);
733 }
734
735 lines
736 }
737}
738
739impl<'a> ContainerBuilder<'a> {
740 pub fn apply(mut self, style: &ContainerStyle) -> Self {
745 if let Some(v) = style.border {
746 self.border = Some(v);
747 }
748 if let Some(v) = style.border_sides {
749 self.border_sides = v;
750 }
751 if let Some(v) = style.border_style {
752 self.border_style = v;
753 }
754 if let Some(v) = style.bg {
755 self.bg_color = Some(v);
756 }
757 if let Some(v) = style.dark_bg {
758 self.dark_bg_color = Some(v);
759 }
760 if let Some(v) = style.dark_border_style {
761 self.dark_border_style = Some(v);
762 }
763 if let Some(v) = style.padding {
764 self.padding = v;
765 }
766 if let Some(v) = style.margin {
767 self.margin = v;
768 }
769 if let Some(v) = style.gap {
770 self.gap = v;
771 }
772 if let Some(v) = style.grow {
773 self.grow = v;
774 }
775 if let Some(v) = style.align {
776 self.align = v;
777 }
778 if let Some(v) = style.justify {
779 self.justify = v;
780 }
781 if let Some(w) = style.w {
782 self.constraints.min_width = Some(w);
783 self.constraints.max_width = Some(w);
784 }
785 if let Some(h) = style.h {
786 self.constraints.min_height = Some(h);
787 self.constraints.max_height = Some(h);
788 }
789 if let Some(v) = style.min_w {
790 self.constraints.min_width = Some(v);
791 }
792 if let Some(v) = style.max_w {
793 self.constraints.max_width = Some(v);
794 }
795 if let Some(v) = style.min_h {
796 self.constraints.min_height = Some(v);
797 }
798 if let Some(v) = style.max_h {
799 self.constraints.max_height = Some(v);
800 }
801 if let Some(v) = style.w_pct {
802 self.constraints.width_pct = Some(v);
803 }
804 if let Some(v) = style.h_pct {
805 self.constraints.height_pct = Some(v);
806 }
807 self
808 }
809
810 pub fn border(mut self, border: Border) -> Self {
812 self.border = Some(border);
813 self
814 }
815
816 pub fn border_top(mut self, show: bool) -> Self {
818 self.border_sides.top = show;
819 self
820 }
821
822 pub fn border_right(mut self, show: bool) -> Self {
824 self.border_sides.right = show;
825 self
826 }
827
828 pub fn border_bottom(mut self, show: bool) -> Self {
830 self.border_sides.bottom = show;
831 self
832 }
833
834 pub fn border_left(mut self, show: bool) -> Self {
836 self.border_sides.left = show;
837 self
838 }
839
840 pub fn border_sides(mut self, sides: BorderSides) -> Self {
842 self.border_sides = sides;
843 self
844 }
845
846 pub fn rounded(self) -> Self {
848 self.border(Border::Rounded)
849 }
850
851 pub fn border_style(mut self, style: Style) -> Self {
853 self.border_style = style;
854 self
855 }
856
857 pub fn dark_border_style(mut self, style: Style) -> Self {
859 self.dark_border_style = Some(style);
860 self
861 }
862
863 pub fn bg(mut self, color: Color) -> Self {
864 self.bg_color = Some(color);
865 self
866 }
867
868 pub fn dark_bg(mut self, color: Color) -> Self {
870 self.dark_bg_color = Some(color);
871 self
872 }
873
874 pub fn group_hover_bg(mut self, color: Color) -> Self {
876 self.group_hover_bg = Some(color);
877 self
878 }
879
880 pub fn group_hover_border_style(mut self, style: Style) -> Self {
882 self.group_hover_border_style = Some(style);
883 self
884 }
885
886 pub fn p(self, value: u32) -> Self {
890 self.pad(value)
891 }
892
893 pub fn pad(mut self, value: u32) -> Self {
895 self.padding = Padding::all(value);
896 self
897 }
898
899 pub fn px(mut self, value: u32) -> Self {
901 self.padding.left = value;
902 self.padding.right = value;
903 self
904 }
905
906 pub fn py(mut self, value: u32) -> Self {
908 self.padding.top = value;
909 self.padding.bottom = value;
910 self
911 }
912
913 pub fn pt(mut self, value: u32) -> Self {
915 self.padding.top = value;
916 self
917 }
918
919 pub fn pr(mut self, value: u32) -> Self {
921 self.padding.right = value;
922 self
923 }
924
925 pub fn pb(mut self, value: u32) -> Self {
927 self.padding.bottom = value;
928 self
929 }
930
931 pub fn pl(mut self, value: u32) -> Self {
933 self.padding.left = value;
934 self
935 }
936
937 pub fn padding(mut self, padding: Padding) -> Self {
939 self.padding = padding;
940 self
941 }
942
943 pub fn m(mut self, value: u32) -> Self {
947 self.margin = Margin::all(value);
948 self
949 }
950
951 pub fn mx(mut self, value: u32) -> Self {
953 self.margin.left = value;
954 self.margin.right = value;
955 self
956 }
957
958 pub fn my(mut self, value: u32) -> Self {
960 self.margin.top = value;
961 self.margin.bottom = value;
962 self
963 }
964
965 pub fn mt(mut self, value: u32) -> Self {
967 self.margin.top = value;
968 self
969 }
970
971 pub fn mr(mut self, value: u32) -> Self {
973 self.margin.right = value;
974 self
975 }
976
977 pub fn mb(mut self, value: u32) -> Self {
979 self.margin.bottom = value;
980 self
981 }
982
983 pub fn ml(mut self, value: u32) -> Self {
985 self.margin.left = value;
986 self
987 }
988
989 pub fn margin(mut self, margin: Margin) -> Self {
991 self.margin = margin;
992 self
993 }
994
995 pub fn w(mut self, value: u32) -> Self {
999 self.constraints.min_width = Some(value);
1000 self.constraints.max_width = Some(value);
1001 self
1002 }
1003
1004 pub fn xs_w(self, value: u32) -> Self {
1011 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1012 if is_xs {
1013 self.w(value)
1014 } else {
1015 self
1016 }
1017 }
1018
1019 pub fn sm_w(self, value: u32) -> Self {
1021 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1022 if is_sm {
1023 self.w(value)
1024 } else {
1025 self
1026 }
1027 }
1028
1029 pub fn md_w(self, value: u32) -> Self {
1031 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1032 if is_md {
1033 self.w(value)
1034 } else {
1035 self
1036 }
1037 }
1038
1039 pub fn lg_w(self, value: u32) -> Self {
1041 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1042 if is_lg {
1043 self.w(value)
1044 } else {
1045 self
1046 }
1047 }
1048
1049 pub fn xl_w(self, value: u32) -> Self {
1051 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1052 if is_xl {
1053 self.w(value)
1054 } else {
1055 self
1056 }
1057 }
1058
1059 pub fn h(mut self, value: u32) -> Self {
1061 self.constraints.min_height = Some(value);
1062 self.constraints.max_height = Some(value);
1063 self
1064 }
1065
1066 pub fn xs_h(self, value: u32) -> Self {
1068 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1069 if is_xs {
1070 self.h(value)
1071 } else {
1072 self
1073 }
1074 }
1075
1076 pub fn sm_h(self, value: u32) -> Self {
1078 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1079 if is_sm {
1080 self.h(value)
1081 } else {
1082 self
1083 }
1084 }
1085
1086 pub fn md_h(self, value: u32) -> Self {
1088 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1089 if is_md {
1090 self.h(value)
1091 } else {
1092 self
1093 }
1094 }
1095
1096 pub fn lg_h(self, value: u32) -> Self {
1098 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1099 if is_lg {
1100 self.h(value)
1101 } else {
1102 self
1103 }
1104 }
1105
1106 pub fn xl_h(self, value: u32) -> Self {
1108 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1109 if is_xl {
1110 self.h(value)
1111 } else {
1112 self
1113 }
1114 }
1115
1116 pub fn min_w(mut self, value: u32) -> Self {
1118 self.constraints.min_width = Some(value);
1119 self
1120 }
1121
1122 pub fn xs_min_w(self, value: u32) -> Self {
1124 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1125 if is_xs {
1126 self.min_w(value)
1127 } else {
1128 self
1129 }
1130 }
1131
1132 pub fn sm_min_w(self, value: u32) -> Self {
1134 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1135 if is_sm {
1136 self.min_w(value)
1137 } else {
1138 self
1139 }
1140 }
1141
1142 pub fn md_min_w(self, value: u32) -> Self {
1144 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1145 if is_md {
1146 self.min_w(value)
1147 } else {
1148 self
1149 }
1150 }
1151
1152 pub fn lg_min_w(self, value: u32) -> Self {
1154 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1155 if is_lg {
1156 self.min_w(value)
1157 } else {
1158 self
1159 }
1160 }
1161
1162 pub fn xl_min_w(self, value: u32) -> Self {
1164 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1165 if is_xl {
1166 self.min_w(value)
1167 } else {
1168 self
1169 }
1170 }
1171
1172 pub fn max_w(mut self, value: u32) -> Self {
1174 self.constraints.max_width = Some(value);
1175 self
1176 }
1177
1178 pub fn xs_max_w(self, value: u32) -> Self {
1180 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1181 if is_xs {
1182 self.max_w(value)
1183 } else {
1184 self
1185 }
1186 }
1187
1188 pub fn sm_max_w(self, value: u32) -> Self {
1190 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1191 if is_sm {
1192 self.max_w(value)
1193 } else {
1194 self
1195 }
1196 }
1197
1198 pub fn md_max_w(self, value: u32) -> Self {
1200 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1201 if is_md {
1202 self.max_w(value)
1203 } else {
1204 self
1205 }
1206 }
1207
1208 pub fn lg_max_w(self, value: u32) -> Self {
1210 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1211 if is_lg {
1212 self.max_w(value)
1213 } else {
1214 self
1215 }
1216 }
1217
1218 pub fn xl_max_w(self, value: u32) -> Self {
1220 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1221 if is_xl {
1222 self.max_w(value)
1223 } else {
1224 self
1225 }
1226 }
1227
1228 pub fn min_h(mut self, value: u32) -> Self {
1230 self.constraints.min_height = Some(value);
1231 self
1232 }
1233
1234 pub fn max_h(mut self, value: u32) -> Self {
1236 self.constraints.max_height = Some(value);
1237 self
1238 }
1239
1240 pub fn min_width(mut self, value: u32) -> Self {
1242 self.constraints.min_width = Some(value);
1243 self
1244 }
1245
1246 pub fn max_width(mut self, value: u32) -> Self {
1248 self.constraints.max_width = Some(value);
1249 self
1250 }
1251
1252 pub fn min_height(mut self, value: u32) -> Self {
1254 self.constraints.min_height = Some(value);
1255 self
1256 }
1257
1258 pub fn max_height(mut self, value: u32) -> Self {
1260 self.constraints.max_height = Some(value);
1261 self
1262 }
1263
1264 pub fn w_pct(mut self, pct: u8) -> Self {
1266 self.constraints.width_pct = Some(pct.min(100));
1267 self
1268 }
1269
1270 pub fn h_pct(mut self, pct: u8) -> Self {
1272 self.constraints.height_pct = Some(pct.min(100));
1273 self
1274 }
1275
1276 pub fn constraints(mut self, constraints: Constraints) -> Self {
1278 self.constraints = constraints;
1279 self
1280 }
1281
1282 pub fn gap(mut self, gap: u32) -> Self {
1286 self.gap = gap;
1287 self
1288 }
1289
1290 pub fn xs_gap(self, value: u32) -> Self {
1292 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1293 if is_xs {
1294 self.gap(value)
1295 } else {
1296 self
1297 }
1298 }
1299
1300 pub fn sm_gap(self, value: u32) -> Self {
1302 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1303 if is_sm {
1304 self.gap(value)
1305 } else {
1306 self
1307 }
1308 }
1309
1310 pub fn md_gap(self, value: u32) -> Self {
1317 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1318 if is_md {
1319 self.gap(value)
1320 } else {
1321 self
1322 }
1323 }
1324
1325 pub fn lg_gap(self, value: u32) -> Self {
1327 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1328 if is_lg {
1329 self.gap(value)
1330 } else {
1331 self
1332 }
1333 }
1334
1335 pub fn xl_gap(self, value: u32) -> Self {
1337 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1338 if is_xl {
1339 self.gap(value)
1340 } else {
1341 self
1342 }
1343 }
1344
1345 pub fn grow(mut self, grow: u16) -> Self {
1347 self.grow = grow;
1348 self
1349 }
1350
1351 pub fn xs_grow(self, value: u16) -> Self {
1353 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1354 if is_xs {
1355 self.grow(value)
1356 } else {
1357 self
1358 }
1359 }
1360
1361 pub fn sm_grow(self, value: u16) -> Self {
1363 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1364 if is_sm {
1365 self.grow(value)
1366 } else {
1367 self
1368 }
1369 }
1370
1371 pub fn md_grow(self, value: u16) -> Self {
1373 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1374 if is_md {
1375 self.grow(value)
1376 } else {
1377 self
1378 }
1379 }
1380
1381 pub fn lg_grow(self, value: u16) -> Self {
1383 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1384 if is_lg {
1385 self.grow(value)
1386 } else {
1387 self
1388 }
1389 }
1390
1391 pub fn xl_grow(self, value: u16) -> Self {
1393 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1394 if is_xl {
1395 self.grow(value)
1396 } else {
1397 self
1398 }
1399 }
1400
1401 pub fn xs_p(self, value: u32) -> Self {
1403 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1404 if is_xs {
1405 self.p(value)
1406 } else {
1407 self
1408 }
1409 }
1410
1411 pub fn sm_p(self, value: u32) -> Self {
1413 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1414 if is_sm {
1415 self.p(value)
1416 } else {
1417 self
1418 }
1419 }
1420
1421 pub fn md_p(self, value: u32) -> Self {
1423 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1424 if is_md {
1425 self.p(value)
1426 } else {
1427 self
1428 }
1429 }
1430
1431 pub fn lg_p(self, value: u32) -> Self {
1433 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1434 if is_lg {
1435 self.p(value)
1436 } else {
1437 self
1438 }
1439 }
1440
1441 pub fn xl_p(self, value: u32) -> Self {
1443 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1444 if is_xl {
1445 self.p(value)
1446 } else {
1447 self
1448 }
1449 }
1450
1451 pub fn align(mut self, align: Align) -> Self {
1455 self.align = align;
1456 self
1457 }
1458
1459 pub fn center(self) -> Self {
1461 self.align(Align::Center)
1462 }
1463
1464 pub fn justify(mut self, justify: Justify) -> Self {
1466 self.justify = justify;
1467 self
1468 }
1469
1470 pub fn space_between(self) -> Self {
1472 self.justify(Justify::SpaceBetween)
1473 }
1474
1475 pub fn space_around(self) -> Self {
1477 self.justify(Justify::SpaceAround)
1478 }
1479
1480 pub fn space_evenly(self) -> Self {
1482 self.justify(Justify::SpaceEvenly)
1483 }
1484
1485 pub fn title(self, title: impl Into<String>) -> Self {
1489 self.title_styled(title, Style::new())
1490 }
1491
1492 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1494 self.title = Some((title.into(), style));
1495 self
1496 }
1497
1498 pub fn scroll_offset(mut self, offset: u32) -> Self {
1502 self.scroll_offset = Some(offset);
1503 self
1504 }
1505
1506 fn group_name(mut self, name: String) -> Self {
1507 self.group_name = Some(name);
1508 self
1509 }
1510
1511 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1516 self.finish(Direction::Column, f)
1517 }
1518
1519 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1524 self.finish(Direction::Row, f)
1525 }
1526
1527 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1532 self.gap = 0;
1533 self.finish(Direction::Row, f)
1534 }
1535
1536 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1551 let draw_id = self.ctx.deferred_draws.len();
1552 self.ctx.deferred_draws.push(Some(Box::new(f)));
1553 self.ctx.interaction_count += 1;
1554 self.ctx.commands.push(Command::RawDraw {
1555 draw_id,
1556 constraints: self.constraints,
1557 grow: self.grow,
1558 margin: self.margin,
1559 });
1560 }
1561
1562 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1563 let interaction_id = self.ctx.interaction_count;
1564 self.ctx.interaction_count += 1;
1565
1566 let in_hovered_group = self
1567 .group_name
1568 .as_ref()
1569 .map(|name| self.ctx.is_group_hovered(name))
1570 .unwrap_or(false)
1571 || self
1572 .ctx
1573 .group_stack
1574 .last()
1575 .map(|name| self.ctx.is_group_hovered(name))
1576 .unwrap_or(false);
1577 let in_focused_group = self
1578 .group_name
1579 .as_ref()
1580 .map(|name| self.ctx.is_group_focused(name))
1581 .unwrap_or(false)
1582 || self
1583 .ctx
1584 .group_stack
1585 .last()
1586 .map(|name| self.ctx.is_group_focused(name))
1587 .unwrap_or(false);
1588
1589 let resolved_bg = if self.ctx.dark_mode {
1590 self.dark_bg_color.or(self.bg_color)
1591 } else {
1592 self.bg_color
1593 };
1594 let resolved_border_style = if self.ctx.dark_mode {
1595 self.dark_border_style.unwrap_or(self.border_style)
1596 } else {
1597 self.border_style
1598 };
1599 let bg_color = if in_hovered_group || in_focused_group {
1600 self.group_hover_bg.or(resolved_bg)
1601 } else {
1602 resolved_bg
1603 };
1604 let border_style = if in_hovered_group || in_focused_group {
1605 self.group_hover_border_style
1606 .unwrap_or(resolved_border_style)
1607 } else {
1608 resolved_border_style
1609 };
1610 let group_name = self.group_name.clone();
1611 let is_group_container = group_name.is_some();
1612
1613 if let Some(scroll_offset) = self.scroll_offset {
1614 self.ctx.commands.push(Command::BeginScrollable {
1615 grow: self.grow,
1616 border: self.border,
1617 border_sides: self.border_sides,
1618 border_style,
1619 padding: self.padding,
1620 margin: self.margin,
1621 constraints: self.constraints,
1622 title: self.title,
1623 scroll_offset,
1624 });
1625 } else {
1626 self.ctx.commands.push(Command::BeginContainer {
1627 direction,
1628 gap: self.gap,
1629 align: self.align,
1630 justify: self.justify,
1631 border: self.border,
1632 border_sides: self.border_sides,
1633 border_style,
1634 bg_color,
1635 padding: self.padding,
1636 margin: self.margin,
1637 constraints: self.constraints,
1638 title: self.title,
1639 grow: self.grow,
1640 group_name,
1641 });
1642 }
1643 f(self.ctx);
1644 self.ctx.commands.push(Command::EndContainer);
1645 self.ctx.last_text_idx = None;
1646
1647 if is_group_container {
1648 self.ctx.group_stack.pop();
1649 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1650 }
1651
1652 self.ctx.response_for(interaction_id)
1653 }
1654}
1655
1656impl Context {
1657 #[allow(clippy::too_many_arguments)]
1658 pub(crate) fn new(
1659 events: Vec<Event>,
1660 width: u32,
1661 height: u32,
1662 tick: u64,
1663 mut focus_index: usize,
1664 prev_focus_count: usize,
1665 prev_scroll_infos: Vec<(u32, u32)>,
1666 prev_scroll_rects: Vec<Rect>,
1667 prev_hit_map: Vec<Rect>,
1668 prev_group_rects: Vec<(String, Rect)>,
1669 prev_focus_rects: Vec<(usize, Rect)>,
1670 prev_focus_groups: Vec<Option<String>>,
1671 prev_hook_states: Vec<Box<dyn std::any::Any>>,
1672 debug: bool,
1673 theme: Theme,
1674 last_mouse_pos: Option<(u32, u32)>,
1675 prev_modal_active: bool,
1676 ) -> Self {
1677 let consumed = vec![false; events.len()];
1678
1679 let mut mouse_pos = last_mouse_pos;
1680 let mut click_pos = None;
1681 for event in &events {
1682 if let Event::Mouse(mouse) = event {
1683 mouse_pos = Some((mouse.x, mouse.y));
1684 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1685 click_pos = Some((mouse.x, mouse.y));
1686 }
1687 }
1688 }
1689
1690 if let Some((mx, my)) = click_pos {
1691 let mut best: Option<(usize, u64)> = None;
1692 for &(fid, rect) in &prev_focus_rects {
1693 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1694 let area = rect.width as u64 * rect.height as u64;
1695 if best.map_or(true, |(_, ba)| area < ba) {
1696 best = Some((fid, area));
1697 }
1698 }
1699 }
1700 if let Some((fid, _)) = best {
1701 focus_index = fid;
1702 }
1703 }
1704
1705 Self {
1706 commands: Vec::new(),
1707 events,
1708 consumed,
1709 should_quit: false,
1710 area_width: width,
1711 area_height: height,
1712 tick,
1713 focus_index,
1714 focus_count: 0,
1715 hook_states: prev_hook_states,
1716 hook_cursor: 0,
1717 prev_focus_count,
1718 scroll_count: 0,
1719 prev_scroll_infos,
1720 prev_scroll_rects,
1721 interaction_count: 0,
1722 prev_hit_map,
1723 group_stack: Vec::new(),
1724 prev_group_rects,
1725 group_count: 0,
1726 prev_focus_groups,
1727 _prev_focus_rects: prev_focus_rects,
1728 mouse_pos,
1729 click_pos,
1730 last_text_idx: None,
1731 overlay_depth: 0,
1732 modal_active: false,
1733 prev_modal_active,
1734 clipboard_text: None,
1735 debug,
1736 theme,
1737 dark_mode: true,
1738 deferred_draws: Vec::new(),
1739 }
1740 }
1741
1742 pub(crate) fn process_focus_keys(&mut self) {
1743 for (i, event) in self.events.iter().enumerate() {
1744 if let Event::Key(key) = event {
1745 if key.kind != KeyEventKind::Press {
1746 continue;
1747 }
1748 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1749 if self.prev_focus_count > 0 {
1750 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1751 }
1752 self.consumed[i] = true;
1753 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1754 || key.code == KeyCode::BackTab
1755 {
1756 if self.prev_focus_count > 0 {
1757 self.focus_index = if self.focus_index == 0 {
1758 self.prev_focus_count - 1
1759 } else {
1760 self.focus_index - 1
1761 };
1762 }
1763 self.consumed[i] = true;
1764 }
1765 }
1766 }
1767 }
1768
1769 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1773 w.ui(self)
1774 }
1775
1776 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1791 self.error_boundary_with(f, |ui, msg| {
1792 ui.styled(
1793 format!("⚠ Error: {msg}"),
1794 Style::new().fg(ui.theme.error).bold(),
1795 );
1796 });
1797 }
1798
1799 pub fn error_boundary_with(
1819 &mut self,
1820 f: impl FnOnce(&mut Context),
1821 fallback: impl FnOnce(&mut Context, String),
1822 ) {
1823 let cmd_count = self.commands.len();
1824 let last_text_idx = self.last_text_idx;
1825
1826 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1827 f(self);
1828 }));
1829
1830 match result {
1831 Ok(()) => {}
1832 Err(panic_info) => {
1833 self.commands.truncate(cmd_count);
1834 self.last_text_idx = last_text_idx;
1835
1836 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1837 (*s).to_string()
1838 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1839 s.clone()
1840 } else {
1841 "widget panicked".to_string()
1842 };
1843
1844 fallback(self, msg);
1845 }
1846 }
1847 }
1848
1849 pub fn interaction(&mut self) -> Response {
1855 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1856 return Response::default();
1857 }
1858 let id = self.interaction_count;
1859 self.interaction_count += 1;
1860 self.response_for(id)
1861 }
1862
1863 pub fn register_focusable(&mut self) -> bool {
1868 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1869 return false;
1870 }
1871 let id = self.focus_count;
1872 self.focus_count += 1;
1873 self.commands.push(Command::FocusMarker(id));
1874 if self.prev_focus_count == 0 {
1875 return true;
1876 }
1877 self.focus_index % self.prev_focus_count == id
1878 }
1879
1880 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1898 let idx = self.hook_cursor;
1899 self.hook_cursor += 1;
1900
1901 if idx >= self.hook_states.len() {
1902 self.hook_states.push(Box::new(init()));
1903 }
1904
1905 State {
1906 idx,
1907 _marker: std::marker::PhantomData,
1908 }
1909 }
1910
1911 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1919 &mut self,
1920 deps: &D,
1921 compute: impl FnOnce(&D) -> T,
1922 ) -> &T {
1923 let idx = self.hook_cursor;
1924 self.hook_cursor += 1;
1925
1926 let should_recompute = if idx >= self.hook_states.len() {
1927 true
1928 } else {
1929 let (stored_deps, _) = self.hook_states[idx]
1930 .downcast_ref::<(D, T)>()
1931 .expect("use_memo type mismatch");
1932 stored_deps != deps
1933 };
1934
1935 if should_recompute {
1936 let value = compute(deps);
1937 let slot = Box::new((deps.clone(), value));
1938 if idx < self.hook_states.len() {
1939 self.hook_states[idx] = slot;
1940 } else {
1941 self.hook_states.push(slot);
1942 }
1943 }
1944
1945 let (_, value) = self.hook_states[idx]
1946 .downcast_ref::<(D, T)>()
1947 .expect("use_memo type mismatch");
1948 value
1949 }
1950
1951 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1964 let content = s.into();
1965 self.commands.push(Command::Text {
1966 content,
1967 style: Style::new(),
1968 grow: 0,
1969 align: Align::Start,
1970 wrap: false,
1971 margin: Margin::default(),
1972 constraints: Constraints::default(),
1973 });
1974 self.last_text_idx = Some(self.commands.len() - 1);
1975 self
1976 }
1977
1978 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
1984 let url_str = url.into();
1985 let focused = self.register_focusable();
1986 let interaction_id = self.interaction_count;
1987 self.interaction_count += 1;
1988 let response = self.response_for(interaction_id);
1989
1990 let mut activated = response.clicked;
1991 if focused {
1992 for (i, event) in self.events.iter().enumerate() {
1993 if let Event::Key(key) = event {
1994 if key.kind != KeyEventKind::Press {
1995 continue;
1996 }
1997 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1998 activated = true;
1999 self.consumed[i] = true;
2000 }
2001 }
2002 }
2003 }
2004
2005 if activated {
2006 let _ = open_url(&url_str);
2007 }
2008
2009 let style = if focused {
2010 Style::new()
2011 .fg(self.theme.primary)
2012 .bg(self.theme.surface_hover)
2013 .underline()
2014 .bold()
2015 } else if response.hovered {
2016 Style::new()
2017 .fg(self.theme.accent)
2018 .bg(self.theme.surface_hover)
2019 .underline()
2020 } else {
2021 Style::new().fg(self.theme.primary).underline()
2022 };
2023
2024 self.commands.push(Command::Link {
2025 text: text.into(),
2026 url: url_str,
2027 style,
2028 margin: Margin::default(),
2029 constraints: Constraints::default(),
2030 });
2031 self.last_text_idx = Some(self.commands.len() - 1);
2032 self
2033 }
2034
2035 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
2040 let content = s.into();
2041 self.commands.push(Command::Text {
2042 content,
2043 style: Style::new(),
2044 grow: 0,
2045 align: Align::Start,
2046 wrap: true,
2047 margin: Margin::default(),
2048 constraints: Constraints::default(),
2049 });
2050 self.last_text_idx = Some(self.commands.len() - 1);
2051 self
2052 }
2053
2054 pub fn bold(&mut self) -> &mut Self {
2058 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
2059 self
2060 }
2061
2062 pub fn dim(&mut self) -> &mut Self {
2067 let text_dim = self.theme.text_dim;
2068 self.modify_last_style(|s| {
2069 s.modifiers |= Modifiers::DIM;
2070 if s.fg.is_none() {
2071 s.fg = Some(text_dim);
2072 }
2073 });
2074 self
2075 }
2076
2077 pub fn italic(&mut self) -> &mut Self {
2079 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
2080 self
2081 }
2082
2083 pub fn underline(&mut self) -> &mut Self {
2085 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
2086 self
2087 }
2088
2089 pub fn reversed(&mut self) -> &mut Self {
2091 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
2092 self
2093 }
2094
2095 pub fn strikethrough(&mut self) -> &mut Self {
2097 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
2098 self
2099 }
2100
2101 pub fn fg(&mut self, color: Color) -> &mut Self {
2103 self.modify_last_style(|s| s.fg = Some(color));
2104 self
2105 }
2106
2107 pub fn bg(&mut self, color: Color) -> &mut Self {
2109 self.modify_last_style(|s| s.bg = Some(color));
2110 self
2111 }
2112
2113 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
2114 let apply_group_style = self
2115 .group_stack
2116 .last()
2117 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2118 .unwrap_or(false);
2119 if apply_group_style {
2120 self.modify_last_style(|s| s.fg = Some(color));
2121 }
2122 self
2123 }
2124
2125 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
2126 let apply_group_style = self
2127 .group_stack
2128 .last()
2129 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2130 .unwrap_or(false);
2131 if apply_group_style {
2132 self.modify_last_style(|s| s.bg = Some(color));
2133 }
2134 self
2135 }
2136
2137 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
2142 self.commands.push(Command::Text {
2143 content: s.into(),
2144 style,
2145 grow: 0,
2146 align: Align::Start,
2147 wrap: false,
2148 margin: Margin::default(),
2149 constraints: Constraints::default(),
2150 });
2151 self.last_text_idx = Some(self.commands.len() - 1);
2152 self
2153 }
2154
2155 pub fn image(&mut self, img: &HalfBlockImage) {
2177 let width = img.width;
2178 let height = img.height;
2179
2180 self.container().w(width).h(height).gap(0).col(|ui| {
2181 for row in 0..height {
2182 ui.container().gap(0).row(|ui| {
2183 for col in 0..width {
2184 let idx = (row * width + col) as usize;
2185 if let Some(&(upper, lower)) = img.pixels.get(idx) {
2186 ui.styled("▀", Style::new().fg(upper).bg(lower));
2187 }
2188 }
2189 });
2190 }
2191 });
2192 }
2193
2194 pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
2210 if state.streaming {
2211 state.cursor_tick = state.cursor_tick.wrapping_add(1);
2212 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
2213 }
2214
2215 if state.content.is_empty() && state.streaming {
2216 let cursor = if state.cursor_visible { "▌" } else { " " };
2217 let primary = self.theme.primary;
2218 self.text(cursor).fg(primary);
2219 return;
2220 }
2221
2222 if !state.content.is_empty() {
2223 if state.streaming && state.cursor_visible {
2224 self.text_wrap(format!("{}▌", state.content));
2225 } else {
2226 self.text_wrap(&state.content);
2227 }
2228 }
2229 }
2230
2231 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
2246 let theme = self.theme;
2247 self.bordered(Border::Rounded).col(|ui| {
2248 ui.row(|ui| {
2249 ui.text("⚡").fg(theme.warning);
2250 ui.text(&state.tool_name).bold().fg(theme.primary);
2251 });
2252 ui.text(&state.description).dim();
2253
2254 if state.action == ApprovalAction::Pending {
2255 ui.row(|ui| {
2256 if ui.button("✓ Approve") {
2257 state.action = ApprovalAction::Approved;
2258 }
2259 if ui.button("✗ Reject") {
2260 state.action = ApprovalAction::Rejected;
2261 }
2262 });
2263 } else {
2264 let (label, color) = match state.action {
2265 ApprovalAction::Approved => ("✓ Approved", theme.success),
2266 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
2267 ApprovalAction::Pending => unreachable!(),
2268 };
2269 ui.text(label).fg(color).bold();
2270 }
2271 });
2272 }
2273
2274 pub fn context_bar(&mut self, items: &[ContextItem]) {
2287 if items.is_empty() {
2288 return;
2289 }
2290
2291 let theme = self.theme;
2292 let total: usize = items.iter().map(|item| item.tokens).sum();
2293
2294 self.container().row(|ui| {
2295 ui.text("📎").dim();
2296 for item in items {
2297 ui.text(format!(
2298 "{} ({})",
2299 item.label,
2300 format_token_count(item.tokens)
2301 ))
2302 .fg(theme.secondary);
2303 }
2304 ui.spacer();
2305 ui.text(format!("Σ {}", format_token_count(total))).dim();
2306 });
2307 }
2308
2309 pub fn wrap(&mut self) -> &mut Self {
2311 if let Some(idx) = self.last_text_idx {
2312 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
2313 *wrap = true;
2314 }
2315 }
2316 self
2317 }
2318
2319 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
2320 if let Some(idx) = self.last_text_idx {
2321 match &mut self.commands[idx] {
2322 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
2323 _ => {}
2324 }
2325 }
2326 }
2327
2328 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2346 self.push_container(Direction::Column, 0, f)
2347 }
2348
2349 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2353 self.push_container(Direction::Column, gap, f)
2354 }
2355
2356 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2373 self.push_container(Direction::Row, 0, f)
2374 }
2375
2376 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2380 self.push_container(Direction::Row, gap, f)
2381 }
2382
2383 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2400 let _ = self.push_container(Direction::Row, 0, f);
2401 self
2402 }
2403
2404 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2423 let start = self.commands.len();
2424 f(self);
2425 let mut segments: Vec<(String, Style)> = Vec::new();
2426 for cmd in self.commands.drain(start..) {
2427 if let Command::Text { content, style, .. } = cmd {
2428 segments.push((content, style));
2429 }
2430 }
2431 self.commands.push(Command::RichText {
2432 segments,
2433 wrap: true,
2434 align: Align::Start,
2435 margin: Margin::default(),
2436 constraints: Constraints::default(),
2437 });
2438 self.last_text_idx = None;
2439 self
2440 }
2441
2442 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
2451 self.commands.push(Command::BeginOverlay { modal: true });
2452 self.overlay_depth += 1;
2453 self.modal_active = true;
2454 f(self);
2455 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2456 self.commands.push(Command::EndOverlay);
2457 self.last_text_idx = None;
2458 }
2459
2460 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
2462 self.commands.push(Command::BeginOverlay { modal: false });
2463 self.overlay_depth += 1;
2464 f(self);
2465 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2466 self.commands.push(Command::EndOverlay);
2467 self.last_text_idx = None;
2468 }
2469
2470 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
2478 self.group_count = self.group_count.saturating_add(1);
2479 self.group_stack.push(name.to_string());
2480 self.container().group_name(name.to_string())
2481 }
2482
2483 pub fn container(&mut self) -> ContainerBuilder<'_> {
2504 let border = self.theme.border;
2505 ContainerBuilder {
2506 ctx: self,
2507 gap: 0,
2508 align: Align::Start,
2509 justify: Justify::Start,
2510 border: None,
2511 border_sides: BorderSides::all(),
2512 border_style: Style::new().fg(border),
2513 bg_color: None,
2514 dark_bg_color: None,
2515 dark_border_style: None,
2516 group_hover_bg: None,
2517 group_hover_border_style: None,
2518 group_name: None,
2519 padding: Padding::default(),
2520 margin: Margin::default(),
2521 constraints: Constraints::default(),
2522 title: None,
2523 grow: 0,
2524 scroll_offset: None,
2525 }
2526 }
2527
2528 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
2547 let index = self.scroll_count;
2548 self.scroll_count += 1;
2549 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
2550 state.set_bounds(ch, vh);
2551 let max = ch.saturating_sub(vh) as usize;
2552 state.offset = state.offset.min(max);
2553 }
2554
2555 let next_id = self.interaction_count;
2556 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
2557 let inner_rects: Vec<Rect> = self
2558 .prev_scroll_rects
2559 .iter()
2560 .enumerate()
2561 .filter(|&(j, sr)| {
2562 j != index
2563 && sr.width > 0
2564 && sr.height > 0
2565 && sr.x >= rect.x
2566 && sr.right() <= rect.right()
2567 && sr.y >= rect.y
2568 && sr.bottom() <= rect.bottom()
2569 })
2570 .map(|(_, sr)| *sr)
2571 .collect();
2572 self.auto_scroll_nested(&rect, state, &inner_rects);
2573 }
2574
2575 self.container().scroll_offset(state.offset as u32)
2576 }
2577
2578 pub fn scrollbar(&mut self, state: &ScrollState) {
2598 let vh = state.viewport_height();
2599 let ch = state.content_height();
2600 if vh == 0 || ch <= vh {
2601 return;
2602 }
2603
2604 let track_height = vh;
2605 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
2606 let max_offset = ch.saturating_sub(vh);
2607 let thumb_pos = if max_offset == 0 {
2608 0
2609 } else {
2610 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
2611 .round() as u32
2612 };
2613
2614 let theme = self.theme;
2615 let track_char = '│';
2616 let thumb_char = '█';
2617
2618 self.container().w(1).h(track_height).col(|ui| {
2619 for i in 0..track_height {
2620 if i >= thumb_pos && i < thumb_pos + thumb_height {
2621 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
2622 } else {
2623 ui.styled(
2624 track_char.to_string(),
2625 Style::new().fg(theme.text_dim).dim(),
2626 );
2627 }
2628 }
2629 });
2630 }
2631
2632 fn auto_scroll_nested(
2633 &mut self,
2634 rect: &Rect,
2635 state: &mut ScrollState,
2636 inner_scroll_rects: &[Rect],
2637 ) {
2638 let mut to_consume: Vec<usize> = Vec::new();
2639
2640 for (i, event) in self.events.iter().enumerate() {
2641 if self.consumed[i] {
2642 continue;
2643 }
2644 if let Event::Mouse(mouse) = event {
2645 let in_bounds = mouse.x >= rect.x
2646 && mouse.x < rect.right()
2647 && mouse.y >= rect.y
2648 && mouse.y < rect.bottom();
2649 if !in_bounds {
2650 continue;
2651 }
2652 let in_inner = inner_scroll_rects.iter().any(|sr| {
2653 mouse.x >= sr.x
2654 && mouse.x < sr.right()
2655 && mouse.y >= sr.y
2656 && mouse.y < sr.bottom()
2657 });
2658 if in_inner {
2659 continue;
2660 }
2661 match mouse.kind {
2662 MouseKind::ScrollUp => {
2663 state.scroll_up(1);
2664 to_consume.push(i);
2665 }
2666 MouseKind::ScrollDown => {
2667 state.scroll_down(1);
2668 to_consume.push(i);
2669 }
2670 MouseKind::Drag(MouseButton::Left) => {}
2671 _ => {}
2672 }
2673 }
2674 }
2675
2676 for i in to_consume {
2677 self.consumed[i] = true;
2678 }
2679 }
2680
2681 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
2685 self.container()
2686 .border(border)
2687 .border_sides(BorderSides::all())
2688 }
2689
2690 fn push_container(
2691 &mut self,
2692 direction: Direction,
2693 gap: u32,
2694 f: impl FnOnce(&mut Context),
2695 ) -> Response {
2696 let interaction_id = self.interaction_count;
2697 self.interaction_count += 1;
2698 let border = self.theme.border;
2699
2700 self.commands.push(Command::BeginContainer {
2701 direction,
2702 gap,
2703 align: Align::Start,
2704 justify: Justify::Start,
2705 border: None,
2706 border_sides: BorderSides::all(),
2707 border_style: Style::new().fg(border),
2708 bg_color: None,
2709 padding: Padding::default(),
2710 margin: Margin::default(),
2711 constraints: Constraints::default(),
2712 title: None,
2713 grow: 0,
2714 group_name: None,
2715 });
2716 f(self);
2717 self.commands.push(Command::EndContainer);
2718 self.last_text_idx = None;
2719
2720 self.response_for(interaction_id)
2721 }
2722
2723 fn response_for(&self, interaction_id: usize) -> Response {
2724 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2725 return Response::default();
2726 }
2727 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
2728 let clicked = self
2729 .click_pos
2730 .map(|(mx, my)| {
2731 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2732 })
2733 .unwrap_or(false);
2734 let hovered = self
2735 .mouse_pos
2736 .map(|(mx, my)| {
2737 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2738 })
2739 .unwrap_or(false);
2740 Response { clicked, hovered }
2741 } else {
2742 Response::default()
2743 }
2744 }
2745
2746 pub fn is_group_hovered(&self, name: &str) -> bool {
2748 if let Some(pos) = self.mouse_pos {
2749 self.prev_group_rects.iter().any(|(n, rect)| {
2750 n == name
2751 && pos.0 >= rect.x
2752 && pos.0 < rect.x + rect.width
2753 && pos.1 >= rect.y
2754 && pos.1 < rect.y + rect.height
2755 })
2756 } else {
2757 false
2758 }
2759 }
2760
2761 pub fn is_group_focused(&self, name: &str) -> bool {
2763 if self.prev_focus_count == 0 {
2764 return false;
2765 }
2766 let focused_index = self.focus_index % self.prev_focus_count;
2767 self.prev_focus_groups
2768 .get(focused_index)
2769 .and_then(|group| group.as_deref())
2770 .map(|group| group == name)
2771 .unwrap_or(false)
2772 }
2773
2774 pub fn grow(&mut self, value: u16) -> &mut Self {
2779 if let Some(idx) = self.last_text_idx {
2780 if let Command::Text { grow, .. } = &mut self.commands[idx] {
2781 *grow = value;
2782 }
2783 }
2784 self
2785 }
2786
2787 pub fn align(&mut self, align: Align) -> &mut Self {
2789 if let Some(idx) = self.last_text_idx {
2790 if let Command::Text {
2791 align: text_align, ..
2792 } = &mut self.commands[idx]
2793 {
2794 *text_align = align;
2795 }
2796 }
2797 self
2798 }
2799
2800 pub fn spacer(&mut self) -> &mut Self {
2804 self.commands.push(Command::Spacer { grow: 1 });
2805 self.last_text_idx = None;
2806 self
2807 }
2808
2809 pub fn form(
2813 &mut self,
2814 state: &mut FormState,
2815 f: impl FnOnce(&mut Context, &mut FormState),
2816 ) -> &mut Self {
2817 self.col(|ui| {
2818 f(ui, state);
2819 });
2820 self
2821 }
2822
2823 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2827 self.col(|ui| {
2828 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2829 ui.text_input(&mut field.input);
2830 if let Some(error) = field.error.as_deref() {
2831 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2832 }
2833 });
2834 self
2835 }
2836
2837 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
2841 self.button(label)
2842 }
2843
2844 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
2860 slt_assert(
2861 !state.value.contains('\n'),
2862 "text_input got a newline — use textarea instead",
2863 );
2864 let focused = self.register_focusable();
2865 state.cursor = state.cursor.min(state.value.chars().count());
2866
2867 if focused {
2868 let mut consumed_indices = Vec::new();
2869 for (i, event) in self.events.iter().enumerate() {
2870 if let Event::Key(key) = event {
2871 if key.kind != KeyEventKind::Press {
2872 continue;
2873 }
2874 match key.code {
2875 KeyCode::Char(ch) => {
2876 if let Some(max) = state.max_length {
2877 if state.value.chars().count() >= max {
2878 continue;
2879 }
2880 }
2881 let index = byte_index_for_char(&state.value, state.cursor);
2882 state.value.insert(index, ch);
2883 state.cursor += 1;
2884 consumed_indices.push(i);
2885 }
2886 KeyCode::Backspace => {
2887 if state.cursor > 0 {
2888 let start = byte_index_for_char(&state.value, state.cursor - 1);
2889 let end = byte_index_for_char(&state.value, state.cursor);
2890 state.value.replace_range(start..end, "");
2891 state.cursor -= 1;
2892 }
2893 consumed_indices.push(i);
2894 }
2895 KeyCode::Left => {
2896 state.cursor = state.cursor.saturating_sub(1);
2897 consumed_indices.push(i);
2898 }
2899 KeyCode::Right => {
2900 state.cursor = (state.cursor + 1).min(state.value.chars().count());
2901 consumed_indices.push(i);
2902 }
2903 KeyCode::Home => {
2904 state.cursor = 0;
2905 consumed_indices.push(i);
2906 }
2907 KeyCode::Delete => {
2908 let len = state.value.chars().count();
2909 if state.cursor < len {
2910 let start = byte_index_for_char(&state.value, state.cursor);
2911 let end = byte_index_for_char(&state.value, state.cursor + 1);
2912 state.value.replace_range(start..end, "");
2913 }
2914 consumed_indices.push(i);
2915 }
2916 KeyCode::End => {
2917 state.cursor = state.value.chars().count();
2918 consumed_indices.push(i);
2919 }
2920 _ => {}
2921 }
2922 }
2923 if let Event::Paste(ref text) = event {
2924 for ch in text.chars() {
2925 if let Some(max) = state.max_length {
2926 if state.value.chars().count() >= max {
2927 break;
2928 }
2929 }
2930 let index = byte_index_for_char(&state.value, state.cursor);
2931 state.value.insert(index, ch);
2932 state.cursor += 1;
2933 }
2934 consumed_indices.push(i);
2935 }
2936 }
2937
2938 for index in consumed_indices {
2939 self.consumed[index] = true;
2940 }
2941 }
2942
2943 let visible_width = self.area_width.saturating_sub(4) as usize;
2944 let input_text = if state.value.is_empty() {
2945 if state.placeholder.len() > 100 {
2946 slt_warn(
2947 "text_input placeholder is very long (>100 chars) — consider shortening it",
2948 );
2949 }
2950 let mut ph = state.placeholder.clone();
2951 if focused {
2952 ph.insert(0, '▎');
2953 }
2954 ph
2955 } else {
2956 let chars: Vec<char> = state.value.chars().collect();
2957 let display_chars: Vec<char> = if state.masked {
2958 vec!['•'; chars.len()]
2959 } else {
2960 chars.clone()
2961 };
2962
2963 let cursor_display_pos: usize = display_chars[..state.cursor.min(display_chars.len())]
2964 .iter()
2965 .map(|c| UnicodeWidthChar::width(*c).unwrap_or(1))
2966 .sum();
2967
2968 let scroll_offset = if cursor_display_pos >= visible_width {
2969 cursor_display_pos - visible_width + 1
2970 } else {
2971 0
2972 };
2973
2974 let mut rendered = String::new();
2975 let mut current_width: usize = 0;
2976 for (idx, &ch) in display_chars.iter().enumerate() {
2977 let cw = UnicodeWidthChar::width(ch).unwrap_or(1);
2978 if current_width + cw <= scroll_offset {
2979 current_width += cw;
2980 continue;
2981 }
2982 if current_width - scroll_offset >= visible_width {
2983 break;
2984 }
2985 if focused && idx == state.cursor {
2986 rendered.push('▎');
2987 }
2988 rendered.push(ch);
2989 current_width += cw;
2990 }
2991 if focused && state.cursor >= display_chars.len() {
2992 rendered.push('▎');
2993 }
2994 rendered
2995 };
2996 let input_style = if state.value.is_empty() && !focused {
2997 Style::new().dim().fg(self.theme.text_dim)
2998 } else {
2999 Style::new().fg(self.theme.text)
3000 };
3001
3002 let border_color = if focused {
3003 self.theme.primary
3004 } else if state.validation_error.is_some() {
3005 self.theme.error
3006 } else {
3007 self.theme.border
3008 };
3009
3010 self.bordered(Border::Rounded)
3011 .border_style(Style::new().fg(border_color))
3012 .px(1)
3013 .col(|ui| {
3014 ui.styled(input_text, input_style);
3015 });
3016
3017 if let Some(error) = state.validation_error.clone() {
3018 self.styled(
3019 format!("⚠ {error}"),
3020 Style::new().dim().fg(self.theme.error),
3021 );
3022 }
3023 self
3024 }
3025
3026 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
3032 self.styled(
3033 state.frame(self.tick).to_string(),
3034 Style::new().fg(self.theme.primary),
3035 )
3036 }
3037
3038 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
3043 state.cleanup(self.tick);
3044 if state.messages.is_empty() {
3045 return self;
3046 }
3047
3048 self.interaction_count += 1;
3049 self.commands.push(Command::BeginContainer {
3050 direction: Direction::Column,
3051 gap: 0,
3052 align: Align::Start,
3053 justify: Justify::Start,
3054 border: None,
3055 border_sides: BorderSides::all(),
3056 border_style: Style::new().fg(self.theme.border),
3057 bg_color: None,
3058 padding: Padding::default(),
3059 margin: Margin::default(),
3060 constraints: Constraints::default(),
3061 title: None,
3062 grow: 0,
3063 group_name: None,
3064 });
3065 for message in state.messages.iter().rev() {
3066 let color = match message.level {
3067 ToastLevel::Info => self.theme.primary,
3068 ToastLevel::Success => self.theme.success,
3069 ToastLevel::Warning => self.theme.warning,
3070 ToastLevel::Error => self.theme.error,
3071 };
3072 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
3073 }
3074 self.commands.push(Command::EndContainer);
3075 self.last_text_idx = None;
3076
3077 self
3078 }
3079
3080 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
3088 if state.lines.is_empty() {
3089 state.lines.push(String::new());
3090 }
3091 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
3092 state.cursor_col = state
3093 .cursor_col
3094 .min(state.lines[state.cursor_row].chars().count());
3095
3096 let focused = self.register_focusable();
3097 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
3098 let wrapping = state.wrap_width.is_some();
3099
3100 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3101
3102 if focused {
3103 let mut consumed_indices = Vec::new();
3104 for (i, event) in self.events.iter().enumerate() {
3105 if let Event::Key(key) = event {
3106 if key.kind != KeyEventKind::Press {
3107 continue;
3108 }
3109 match key.code {
3110 KeyCode::Char(ch) => {
3111 if let Some(max) = state.max_length {
3112 let total: usize =
3113 state.lines.iter().map(|line| line.chars().count()).sum();
3114 if total >= max {
3115 continue;
3116 }
3117 }
3118 let index = byte_index_for_char(
3119 &state.lines[state.cursor_row],
3120 state.cursor_col,
3121 );
3122 state.lines[state.cursor_row].insert(index, ch);
3123 state.cursor_col += 1;
3124 consumed_indices.push(i);
3125 }
3126 KeyCode::Enter => {
3127 let split_index = byte_index_for_char(
3128 &state.lines[state.cursor_row],
3129 state.cursor_col,
3130 );
3131 let remainder = state.lines[state.cursor_row].split_off(split_index);
3132 state.cursor_row += 1;
3133 state.lines.insert(state.cursor_row, remainder);
3134 state.cursor_col = 0;
3135 consumed_indices.push(i);
3136 }
3137 KeyCode::Backspace => {
3138 if state.cursor_col > 0 {
3139 let start = byte_index_for_char(
3140 &state.lines[state.cursor_row],
3141 state.cursor_col - 1,
3142 );
3143 let end = byte_index_for_char(
3144 &state.lines[state.cursor_row],
3145 state.cursor_col,
3146 );
3147 state.lines[state.cursor_row].replace_range(start..end, "");
3148 state.cursor_col -= 1;
3149 } else if state.cursor_row > 0 {
3150 let current = state.lines.remove(state.cursor_row);
3151 state.cursor_row -= 1;
3152 state.cursor_col = state.lines[state.cursor_row].chars().count();
3153 state.lines[state.cursor_row].push_str(¤t);
3154 }
3155 consumed_indices.push(i);
3156 }
3157 KeyCode::Left => {
3158 if state.cursor_col > 0 {
3159 state.cursor_col -= 1;
3160 } else if state.cursor_row > 0 {
3161 state.cursor_row -= 1;
3162 state.cursor_col = state.lines[state.cursor_row].chars().count();
3163 }
3164 consumed_indices.push(i);
3165 }
3166 KeyCode::Right => {
3167 let line_len = state.lines[state.cursor_row].chars().count();
3168 if state.cursor_col < line_len {
3169 state.cursor_col += 1;
3170 } else if state.cursor_row + 1 < state.lines.len() {
3171 state.cursor_row += 1;
3172 state.cursor_col = 0;
3173 }
3174 consumed_indices.push(i);
3175 }
3176 KeyCode::Up => {
3177 if wrapping {
3178 let (vrow, vcol) = textarea_logical_to_visual(
3179 &pre_vlines,
3180 state.cursor_row,
3181 state.cursor_col,
3182 );
3183 if vrow > 0 {
3184 let (lr, lc) =
3185 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
3186 state.cursor_row = lr;
3187 state.cursor_col = lc;
3188 }
3189 } else if state.cursor_row > 0 {
3190 state.cursor_row -= 1;
3191 state.cursor_col = state
3192 .cursor_col
3193 .min(state.lines[state.cursor_row].chars().count());
3194 }
3195 consumed_indices.push(i);
3196 }
3197 KeyCode::Down => {
3198 if wrapping {
3199 let (vrow, vcol) = textarea_logical_to_visual(
3200 &pre_vlines,
3201 state.cursor_row,
3202 state.cursor_col,
3203 );
3204 if vrow + 1 < pre_vlines.len() {
3205 let (lr, lc) =
3206 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
3207 state.cursor_row = lr;
3208 state.cursor_col = lc;
3209 }
3210 } else if state.cursor_row + 1 < state.lines.len() {
3211 state.cursor_row += 1;
3212 state.cursor_col = state
3213 .cursor_col
3214 .min(state.lines[state.cursor_row].chars().count());
3215 }
3216 consumed_indices.push(i);
3217 }
3218 KeyCode::Home => {
3219 state.cursor_col = 0;
3220 consumed_indices.push(i);
3221 }
3222 KeyCode::Delete => {
3223 let line_len = state.lines[state.cursor_row].chars().count();
3224 if state.cursor_col < line_len {
3225 let start = byte_index_for_char(
3226 &state.lines[state.cursor_row],
3227 state.cursor_col,
3228 );
3229 let end = byte_index_for_char(
3230 &state.lines[state.cursor_row],
3231 state.cursor_col + 1,
3232 );
3233 state.lines[state.cursor_row].replace_range(start..end, "");
3234 } else if state.cursor_row + 1 < state.lines.len() {
3235 let next = state.lines.remove(state.cursor_row + 1);
3236 state.lines[state.cursor_row].push_str(&next);
3237 }
3238 consumed_indices.push(i);
3239 }
3240 KeyCode::End => {
3241 state.cursor_col = state.lines[state.cursor_row].chars().count();
3242 consumed_indices.push(i);
3243 }
3244 _ => {}
3245 }
3246 }
3247 if let Event::Paste(ref text) = event {
3248 for ch in text.chars() {
3249 if ch == '\n' || ch == '\r' {
3250 let split_index = byte_index_for_char(
3251 &state.lines[state.cursor_row],
3252 state.cursor_col,
3253 );
3254 let remainder = state.lines[state.cursor_row].split_off(split_index);
3255 state.cursor_row += 1;
3256 state.lines.insert(state.cursor_row, remainder);
3257 state.cursor_col = 0;
3258 } else {
3259 if let Some(max) = state.max_length {
3260 let total: usize =
3261 state.lines.iter().map(|l| l.chars().count()).sum();
3262 if total >= max {
3263 break;
3264 }
3265 }
3266 let index = byte_index_for_char(
3267 &state.lines[state.cursor_row],
3268 state.cursor_col,
3269 );
3270 state.lines[state.cursor_row].insert(index, ch);
3271 state.cursor_col += 1;
3272 }
3273 }
3274 consumed_indices.push(i);
3275 }
3276 }
3277
3278 for index in consumed_indices {
3279 self.consumed[index] = true;
3280 }
3281 }
3282
3283 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3284 let (cursor_vrow, cursor_vcol) =
3285 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
3286
3287 if cursor_vrow < state.scroll_offset {
3288 state.scroll_offset = cursor_vrow;
3289 }
3290 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
3291 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
3292 }
3293
3294 self.interaction_count += 1;
3295 self.commands.push(Command::BeginContainer {
3296 direction: Direction::Column,
3297 gap: 0,
3298 align: Align::Start,
3299 justify: Justify::Start,
3300 border: None,
3301 border_sides: BorderSides::all(),
3302 border_style: Style::new().fg(self.theme.border),
3303 bg_color: None,
3304 padding: Padding::default(),
3305 margin: Margin::default(),
3306 constraints: Constraints::default(),
3307 title: None,
3308 grow: 0,
3309 group_name: None,
3310 });
3311
3312 for vi in 0..visible_rows as usize {
3313 let actual_vi = state.scroll_offset + vi;
3314 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
3315 let line = &state.lines[vl.logical_row];
3316 let text: String = line
3317 .chars()
3318 .skip(vl.char_start)
3319 .take(vl.char_count)
3320 .collect();
3321 (text, actual_vi == cursor_vrow)
3322 } else {
3323 (String::new(), false)
3324 };
3325
3326 let mut rendered = seg_text.clone();
3327 let mut style = if seg_text.is_empty() {
3328 Style::new().fg(self.theme.text_dim)
3329 } else {
3330 Style::new().fg(self.theme.text)
3331 };
3332
3333 if is_cursor_line && focused {
3334 rendered.clear();
3335 for (idx, ch) in seg_text.chars().enumerate() {
3336 if idx == cursor_vcol {
3337 rendered.push('▎');
3338 }
3339 rendered.push(ch);
3340 }
3341 if cursor_vcol >= seg_text.chars().count() {
3342 rendered.push('▎');
3343 }
3344 style = Style::new().fg(self.theme.text);
3345 }
3346
3347 self.styled(rendered, style);
3348 }
3349 self.commands.push(Command::EndContainer);
3350 self.last_text_idx = None;
3351
3352 self
3353 }
3354
3355 pub fn progress(&mut self, ratio: f64) -> &mut Self {
3360 self.progress_bar(ratio, 20)
3361 }
3362
3363 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
3368 let clamped = ratio.clamp(0.0, 1.0);
3369 let filled = (clamped * width as f64).round() as u32;
3370 let empty = width.saturating_sub(filled);
3371 let mut bar = String::new();
3372 for _ in 0..filled {
3373 bar.push('█');
3374 }
3375 for _ in 0..empty {
3376 bar.push('░');
3377 }
3378 self.text(bar)
3379 }
3380
3381 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
3402 if data.is_empty() {
3403 return self;
3404 }
3405
3406 let max_label_width = data
3407 .iter()
3408 .map(|(label, _)| UnicodeWidthStr::width(*label))
3409 .max()
3410 .unwrap_or(0);
3411 let max_value = data
3412 .iter()
3413 .map(|(_, value)| *value)
3414 .fold(f64::NEG_INFINITY, f64::max);
3415 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3416
3417 self.interaction_count += 1;
3418 self.commands.push(Command::BeginContainer {
3419 direction: Direction::Column,
3420 gap: 0,
3421 align: Align::Start,
3422 justify: Justify::Start,
3423 border: None,
3424 border_sides: BorderSides::all(),
3425 border_style: Style::new().fg(self.theme.border),
3426 bg_color: None,
3427 padding: Padding::default(),
3428 margin: Margin::default(),
3429 constraints: Constraints::default(),
3430 title: None,
3431 grow: 0,
3432 group_name: None,
3433 });
3434
3435 for (label, value) in data {
3436 let label_width = UnicodeWidthStr::width(*label);
3437 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3438 let normalized = (*value / denom).clamp(0.0, 1.0);
3439 let bar_len = (normalized * max_width as f64).round() as usize;
3440 let bar = "█".repeat(bar_len);
3441
3442 self.interaction_count += 1;
3443 self.commands.push(Command::BeginContainer {
3444 direction: Direction::Row,
3445 gap: 1,
3446 align: Align::Start,
3447 justify: Justify::Start,
3448 border: None,
3449 border_sides: BorderSides::all(),
3450 border_style: Style::new().fg(self.theme.border),
3451 bg_color: None,
3452 padding: Padding::default(),
3453 margin: Margin::default(),
3454 constraints: Constraints::default(),
3455 title: None,
3456 grow: 0,
3457 group_name: None,
3458 });
3459 self.styled(
3460 format!("{label}{label_padding}"),
3461 Style::new().fg(self.theme.text),
3462 );
3463 self.styled(bar, Style::new().fg(self.theme.primary));
3464 self.styled(
3465 format_compact_number(*value),
3466 Style::new().fg(self.theme.text_dim),
3467 );
3468 self.commands.push(Command::EndContainer);
3469 self.last_text_idx = None;
3470 }
3471
3472 self.commands.push(Command::EndContainer);
3473 self.last_text_idx = None;
3474
3475 self
3476 }
3477
3478 pub fn bar_chart_styled(
3494 &mut self,
3495 bars: &[Bar],
3496 max_width: u32,
3497 direction: BarDirection,
3498 ) -> &mut Self {
3499 if bars.is_empty() {
3500 return self;
3501 }
3502
3503 let max_value = bars
3504 .iter()
3505 .map(|bar| bar.value)
3506 .fold(f64::NEG_INFINITY, f64::max);
3507 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3508
3509 match direction {
3510 BarDirection::Horizontal => {
3511 let max_label_width = bars
3512 .iter()
3513 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3514 .max()
3515 .unwrap_or(0);
3516
3517 self.interaction_count += 1;
3518 self.commands.push(Command::BeginContainer {
3519 direction: Direction::Column,
3520 gap: 0,
3521 align: Align::Start,
3522 justify: Justify::Start,
3523 border: None,
3524 border_sides: BorderSides::all(),
3525 border_style: Style::new().fg(self.theme.border),
3526 bg_color: None,
3527 padding: Padding::default(),
3528 margin: Margin::default(),
3529 constraints: Constraints::default(),
3530 title: None,
3531 grow: 0,
3532 group_name: None,
3533 });
3534
3535 for bar in bars {
3536 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3537 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3538 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3539 let bar_len = (normalized * max_width as f64).round() as usize;
3540 let bar_text = "█".repeat(bar_len);
3541 let color = bar.color.unwrap_or(self.theme.primary);
3542
3543 self.interaction_count += 1;
3544 self.commands.push(Command::BeginContainer {
3545 direction: Direction::Row,
3546 gap: 1,
3547 align: Align::Start,
3548 justify: Justify::Start,
3549 border: None,
3550 border_sides: BorderSides::all(),
3551 border_style: Style::new().fg(self.theme.border),
3552 bg_color: None,
3553 padding: Padding::default(),
3554 margin: Margin::default(),
3555 constraints: Constraints::default(),
3556 title: None,
3557 grow: 0,
3558 group_name: None,
3559 });
3560 self.styled(
3561 format!("{}{label_padding}", bar.label),
3562 Style::new().fg(self.theme.text),
3563 );
3564 self.styled(bar_text, Style::new().fg(color));
3565 self.styled(
3566 format_compact_number(bar.value),
3567 Style::new().fg(self.theme.text_dim),
3568 );
3569 self.commands.push(Command::EndContainer);
3570 self.last_text_idx = None;
3571 }
3572
3573 self.commands.push(Command::EndContainer);
3574 self.last_text_idx = None;
3575 }
3576 BarDirection::Vertical => {
3577 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
3578
3579 let chart_height = max_width.max(1) as usize;
3580 let value_labels: Vec<String> = bars
3581 .iter()
3582 .map(|bar| format_compact_number(bar.value))
3583 .collect();
3584 let col_width = bars
3585 .iter()
3586 .zip(value_labels.iter())
3587 .map(|(bar, value)| {
3588 UnicodeWidthStr::width(bar.label.as_str())
3589 .max(UnicodeWidthStr::width(value.as_str()))
3590 .max(1)
3591 })
3592 .max()
3593 .unwrap_or(1);
3594
3595 let bar_units: Vec<usize> = bars
3596 .iter()
3597 .map(|bar| {
3598 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3599 (normalized * chart_height as f64 * 8.0).round() as usize
3600 })
3601 .collect();
3602
3603 self.interaction_count += 1;
3604 self.commands.push(Command::BeginContainer {
3605 direction: Direction::Column,
3606 gap: 0,
3607 align: Align::Start,
3608 justify: Justify::Start,
3609 border: None,
3610 border_sides: BorderSides::all(),
3611 border_style: Style::new().fg(self.theme.border),
3612 bg_color: None,
3613 padding: Padding::default(),
3614 margin: Margin::default(),
3615 constraints: Constraints::default(),
3616 title: None,
3617 grow: 0,
3618 group_name: None,
3619 });
3620
3621 self.interaction_count += 1;
3622 self.commands.push(Command::BeginContainer {
3623 direction: Direction::Row,
3624 gap: 1,
3625 align: Align::Start,
3626 justify: Justify::Start,
3627 border: None,
3628 border_sides: BorderSides::all(),
3629 border_style: Style::new().fg(self.theme.border),
3630 bg_color: None,
3631 padding: Padding::default(),
3632 margin: Margin::default(),
3633 constraints: Constraints::default(),
3634 title: None,
3635 grow: 0,
3636 group_name: None,
3637 });
3638 for value in &value_labels {
3639 self.styled(
3640 center_text(value, col_width),
3641 Style::new().fg(self.theme.text_dim),
3642 );
3643 }
3644 self.commands.push(Command::EndContainer);
3645 self.last_text_idx = None;
3646
3647 for row in (0..chart_height).rev() {
3648 self.interaction_count += 1;
3649 self.commands.push(Command::BeginContainer {
3650 direction: Direction::Row,
3651 gap: 1,
3652 align: Align::Start,
3653 justify: Justify::Start,
3654 border: None,
3655 border_sides: BorderSides::all(),
3656 border_style: Style::new().fg(self.theme.border),
3657 bg_color: None,
3658 padding: Padding::default(),
3659 margin: Margin::default(),
3660 constraints: Constraints::default(),
3661 title: None,
3662 grow: 0,
3663 group_name: None,
3664 });
3665
3666 let row_base = row * 8;
3667 for (bar, units) in bars.iter().zip(bar_units.iter()) {
3668 let fill = if *units <= row_base {
3669 ' '
3670 } else {
3671 let delta = *units - row_base;
3672 if delta >= 8 {
3673 '█'
3674 } else {
3675 FRACTION_BLOCKS[delta]
3676 }
3677 };
3678
3679 self.styled(
3680 center_text(&fill.to_string(), col_width),
3681 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3682 );
3683 }
3684
3685 self.commands.push(Command::EndContainer);
3686 self.last_text_idx = None;
3687 }
3688
3689 self.interaction_count += 1;
3690 self.commands.push(Command::BeginContainer {
3691 direction: Direction::Row,
3692 gap: 1,
3693 align: Align::Start,
3694 justify: Justify::Start,
3695 border: None,
3696 border_sides: BorderSides::all(),
3697 border_style: Style::new().fg(self.theme.border),
3698 bg_color: None,
3699 padding: Padding::default(),
3700 margin: Margin::default(),
3701 constraints: Constraints::default(),
3702 title: None,
3703 grow: 0,
3704 group_name: None,
3705 });
3706 for bar in bars {
3707 self.styled(
3708 center_text(&bar.label, col_width),
3709 Style::new().fg(self.theme.text),
3710 );
3711 }
3712 self.commands.push(Command::EndContainer);
3713 self.last_text_idx = None;
3714
3715 self.commands.push(Command::EndContainer);
3716 self.last_text_idx = None;
3717 }
3718 }
3719
3720 self
3721 }
3722
3723 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
3740 if groups.is_empty() {
3741 return self;
3742 }
3743
3744 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
3745 if all_bars.is_empty() {
3746 return self;
3747 }
3748
3749 let max_label_width = all_bars
3750 .iter()
3751 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3752 .max()
3753 .unwrap_or(0);
3754 let max_value = all_bars
3755 .iter()
3756 .map(|bar| bar.value)
3757 .fold(f64::NEG_INFINITY, f64::max);
3758 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3759
3760 self.interaction_count += 1;
3761 self.commands.push(Command::BeginContainer {
3762 direction: Direction::Column,
3763 gap: 1,
3764 align: Align::Start,
3765 justify: Justify::Start,
3766 border: None,
3767 border_sides: BorderSides::all(),
3768 border_style: Style::new().fg(self.theme.border),
3769 bg_color: None,
3770 padding: Padding::default(),
3771 margin: Margin::default(),
3772 constraints: Constraints::default(),
3773 title: None,
3774 grow: 0,
3775 group_name: None,
3776 });
3777
3778 for group in groups {
3779 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
3780
3781 for bar in &group.bars {
3782 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3783 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3784 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3785 let bar_len = (normalized * max_width as f64).round() as usize;
3786 let bar_text = "█".repeat(bar_len);
3787
3788 self.interaction_count += 1;
3789 self.commands.push(Command::BeginContainer {
3790 direction: Direction::Row,
3791 gap: 1,
3792 align: Align::Start,
3793 justify: Justify::Start,
3794 border: None,
3795 border_sides: BorderSides::all(),
3796 border_style: Style::new().fg(self.theme.border),
3797 bg_color: None,
3798 padding: Padding::default(),
3799 margin: Margin::default(),
3800 constraints: Constraints::default(),
3801 title: None,
3802 grow: 0,
3803 group_name: None,
3804 });
3805 self.styled(
3806 format!(" {}{label_padding}", bar.label),
3807 Style::new().fg(self.theme.text),
3808 );
3809 self.styled(
3810 bar_text,
3811 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3812 );
3813 self.styled(
3814 format_compact_number(bar.value),
3815 Style::new().fg(self.theme.text_dim),
3816 );
3817 self.commands.push(Command::EndContainer);
3818 self.last_text_idx = None;
3819 }
3820 }
3821
3822 self.commands.push(Command::EndContainer);
3823 self.last_text_idx = None;
3824
3825 self
3826 }
3827
3828 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
3844 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3845
3846 let w = width as usize;
3847 let window = if data.len() > w {
3848 &data[data.len() - w..]
3849 } else {
3850 data
3851 };
3852
3853 if window.is_empty() {
3854 return self;
3855 }
3856
3857 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
3858 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3859 let range = max - min;
3860
3861 let line: String = window
3862 .iter()
3863 .map(|&value| {
3864 let normalized = if range == 0.0 {
3865 0.5
3866 } else {
3867 (value - min) / range
3868 };
3869 let idx = (normalized * 7.0).round() as usize;
3870 BLOCKS[idx.min(7)]
3871 })
3872 .collect();
3873
3874 self.styled(line, Style::new().fg(self.theme.primary))
3875 }
3876
3877 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
3897 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3898
3899 let w = width as usize;
3900 let window = if data.len() > w {
3901 &data[data.len() - w..]
3902 } else {
3903 data
3904 };
3905
3906 if window.is_empty() {
3907 return self;
3908 }
3909
3910 let mut finite_values = window
3911 .iter()
3912 .map(|(value, _)| *value)
3913 .filter(|value| !value.is_nan());
3914 let Some(first) = finite_values.next() else {
3915 return self.styled(
3916 " ".repeat(window.len()),
3917 Style::new().fg(self.theme.text_dim),
3918 );
3919 };
3920
3921 let mut min = first;
3922 let mut max = first;
3923 for value in finite_values {
3924 min = f64::min(min, value);
3925 max = f64::max(max, value);
3926 }
3927 let range = max - min;
3928
3929 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
3930 for (value, color) in window {
3931 if value.is_nan() {
3932 cells.push((' ', self.theme.text_dim));
3933 continue;
3934 }
3935
3936 let normalized = if range == 0.0 {
3937 0.5
3938 } else {
3939 ((*value - min) / range).clamp(0.0, 1.0)
3940 };
3941 let idx = (normalized * 7.0).round() as usize;
3942 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
3943 }
3944
3945 self.interaction_count += 1;
3946 self.commands.push(Command::BeginContainer {
3947 direction: Direction::Row,
3948 gap: 0,
3949 align: Align::Start,
3950 justify: Justify::Start,
3951 border: None,
3952 border_sides: BorderSides::all(),
3953 border_style: Style::new().fg(self.theme.border),
3954 bg_color: None,
3955 padding: Padding::default(),
3956 margin: Margin::default(),
3957 constraints: Constraints::default(),
3958 title: None,
3959 grow: 0,
3960 group_name: None,
3961 });
3962
3963 let mut seg = String::new();
3964 let mut seg_color = cells[0].1;
3965 for (ch, color) in cells {
3966 if color != seg_color {
3967 self.styled(seg, Style::new().fg(seg_color));
3968 seg = String::new();
3969 seg_color = color;
3970 }
3971 seg.push(ch);
3972 }
3973 if !seg.is_empty() {
3974 self.styled(seg, Style::new().fg(seg_color));
3975 }
3976
3977 self.commands.push(Command::EndContainer);
3978 self.last_text_idx = None;
3979
3980 self
3981 }
3982
3983 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
3997 if data.is_empty() || width == 0 || height == 0 {
3998 return self;
3999 }
4000
4001 let cols = width as usize;
4002 let rows = height as usize;
4003 let px_w = cols * 2;
4004 let px_h = rows * 4;
4005
4006 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
4007 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
4008 let range = if (max - min).abs() < f64::EPSILON {
4009 1.0
4010 } else {
4011 max - min
4012 };
4013
4014 let points: Vec<usize> = (0..px_w)
4015 .map(|px| {
4016 let data_idx = if px_w <= 1 {
4017 0.0
4018 } else {
4019 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
4020 };
4021 let idx = data_idx.floor() as usize;
4022 let frac = data_idx - idx as f64;
4023 let value = if idx + 1 < data.len() {
4024 data[idx] * (1.0 - frac) + data[idx + 1] * frac
4025 } else {
4026 data[idx.min(data.len() - 1)]
4027 };
4028
4029 let normalized = (value - min) / range;
4030 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
4031 py.min(px_h - 1)
4032 })
4033 .collect();
4034
4035 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
4036 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
4037
4038 let mut grid = vec![vec![0u32; cols]; rows];
4039
4040 for i in 0..points.len() {
4041 let px = i;
4042 let py = points[i];
4043 let char_col = px / 2;
4044 let char_row = py / 4;
4045 let sub_col = px % 2;
4046 let sub_row = py % 4;
4047
4048 if char_col < cols && char_row < rows {
4049 grid[char_row][char_col] |= if sub_col == 0 {
4050 LEFT_BITS[sub_row]
4051 } else {
4052 RIGHT_BITS[sub_row]
4053 };
4054 }
4055
4056 if i + 1 < points.len() {
4057 let py_next = points[i + 1];
4058 let (y_start, y_end) = if py <= py_next {
4059 (py, py_next)
4060 } else {
4061 (py_next, py)
4062 };
4063 for y in y_start..=y_end {
4064 let cell_row = y / 4;
4065 let sub_y = y % 4;
4066 if char_col < cols && cell_row < rows {
4067 grid[cell_row][char_col] |= if sub_col == 0 {
4068 LEFT_BITS[sub_y]
4069 } else {
4070 RIGHT_BITS[sub_y]
4071 };
4072 }
4073 }
4074 }
4075 }
4076
4077 let style = Style::new().fg(self.theme.primary);
4078 for row in grid {
4079 let line: String = row
4080 .iter()
4081 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
4082 .collect();
4083 self.styled(line, style);
4084 }
4085
4086 self
4087 }
4088
4089 pub fn canvas(
4106 &mut self,
4107 width: u32,
4108 height: u32,
4109 draw: impl FnOnce(&mut CanvasContext),
4110 ) -> &mut Self {
4111 if width == 0 || height == 0 {
4112 return self;
4113 }
4114
4115 let mut canvas = CanvasContext::new(width as usize, height as usize);
4116 draw(&mut canvas);
4117
4118 for segments in canvas.render() {
4119 self.interaction_count += 1;
4120 self.commands.push(Command::BeginContainer {
4121 direction: Direction::Row,
4122 gap: 0,
4123 align: Align::Start,
4124 justify: Justify::Start,
4125 border: None,
4126 border_sides: BorderSides::all(),
4127 border_style: Style::new(),
4128 bg_color: None,
4129 padding: Padding::default(),
4130 margin: Margin::default(),
4131 constraints: Constraints::default(),
4132 title: None,
4133 grow: 0,
4134 group_name: None,
4135 });
4136 for (text, color) in segments {
4137 let c = if color == Color::Reset {
4138 self.theme.primary
4139 } else {
4140 color
4141 };
4142 self.styled(text, Style::new().fg(c));
4143 }
4144 self.commands.push(Command::EndContainer);
4145 self.last_text_idx = None;
4146 }
4147
4148 self
4149 }
4150
4151 pub fn chart(
4153 &mut self,
4154 configure: impl FnOnce(&mut ChartBuilder),
4155 width: u32,
4156 height: u32,
4157 ) -> &mut Self {
4158 if width == 0 || height == 0 {
4159 return self;
4160 }
4161
4162 let axis_style = Style::new().fg(self.theme.text_dim);
4163 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
4164 configure(&mut builder);
4165
4166 let config = builder.build();
4167 let rows = render_chart(&config);
4168
4169 for row in rows {
4170 self.interaction_count += 1;
4171 self.commands.push(Command::BeginContainer {
4172 direction: Direction::Row,
4173 gap: 0,
4174 align: Align::Start,
4175 justify: Justify::Start,
4176 border: None,
4177 border_sides: BorderSides::all(),
4178 border_style: Style::new().fg(self.theme.border),
4179 bg_color: None,
4180 padding: Padding::default(),
4181 margin: Margin::default(),
4182 constraints: Constraints::default(),
4183 title: None,
4184 grow: 0,
4185 group_name: None,
4186 });
4187 for (text, style) in row.segments {
4188 self.styled(text, style);
4189 }
4190 self.commands.push(Command::EndContainer);
4191 self.last_text_idx = None;
4192 }
4193
4194 self
4195 }
4196
4197 pub fn scatter(&mut self, data: &[(f64, f64)], width: u32, height: u32) -> &mut Self {
4201 self.chart(
4202 |c| {
4203 c.scatter(data);
4204 c.grid(true);
4205 },
4206 width,
4207 height,
4208 )
4209 }
4210
4211 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
4213 self.histogram_with(data, |_| {}, width, height)
4214 }
4215
4216 pub fn histogram_with(
4218 &mut self,
4219 data: &[f64],
4220 configure: impl FnOnce(&mut HistogramBuilder),
4221 width: u32,
4222 height: u32,
4223 ) -> &mut Self {
4224 if width == 0 || height == 0 {
4225 return self;
4226 }
4227
4228 let mut options = HistogramBuilder::default();
4229 configure(&mut options);
4230 let axis_style = Style::new().fg(self.theme.text_dim);
4231 let config = build_histogram_config(data, &options, width, height, axis_style);
4232 let rows = render_chart(&config);
4233
4234 for row in rows {
4235 self.interaction_count += 1;
4236 self.commands.push(Command::BeginContainer {
4237 direction: Direction::Row,
4238 gap: 0,
4239 align: Align::Start,
4240 justify: Justify::Start,
4241 border: None,
4242 border_sides: BorderSides::all(),
4243 border_style: Style::new().fg(self.theme.border),
4244 bg_color: None,
4245 padding: Padding::default(),
4246 margin: Margin::default(),
4247 constraints: Constraints::default(),
4248 title: None,
4249 grow: 0,
4250 group_name: None,
4251 });
4252 for (text, style) in row.segments {
4253 self.styled(text, style);
4254 }
4255 self.commands.push(Command::EndContainer);
4256 self.last_text_idx = None;
4257 }
4258
4259 self
4260 }
4261
4262 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
4279 slt_assert(cols > 0, "grid() requires at least 1 column");
4280 let interaction_id = self.interaction_count;
4281 self.interaction_count += 1;
4282 let border = self.theme.border;
4283
4284 self.commands.push(Command::BeginContainer {
4285 direction: Direction::Column,
4286 gap: 0,
4287 align: Align::Start,
4288 justify: Justify::Start,
4289 border: None,
4290 border_sides: BorderSides::all(),
4291 border_style: Style::new().fg(border),
4292 bg_color: None,
4293 padding: Padding::default(),
4294 margin: Margin::default(),
4295 constraints: Constraints::default(),
4296 title: None,
4297 grow: 0,
4298 group_name: None,
4299 });
4300
4301 let children_start = self.commands.len();
4302 f(self);
4303 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
4304
4305 let mut elements: Vec<Vec<Command>> = Vec::new();
4306 let mut iter = child_commands.into_iter().peekable();
4307 while let Some(cmd) = iter.next() {
4308 match cmd {
4309 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4310 let mut depth = 1_u32;
4311 let mut element = vec![cmd];
4312 for next in iter.by_ref() {
4313 match next {
4314 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4315 depth += 1;
4316 }
4317 Command::EndContainer => {
4318 depth = depth.saturating_sub(1);
4319 }
4320 _ => {}
4321 }
4322 let at_end = matches!(next, Command::EndContainer) && depth == 0;
4323 element.push(next);
4324 if at_end {
4325 break;
4326 }
4327 }
4328 elements.push(element);
4329 }
4330 Command::EndContainer => {}
4331 _ => elements.push(vec![cmd]),
4332 }
4333 }
4334
4335 let cols = cols.max(1) as usize;
4336 for row in elements.chunks(cols) {
4337 self.interaction_count += 1;
4338 self.commands.push(Command::BeginContainer {
4339 direction: Direction::Row,
4340 gap: 0,
4341 align: Align::Start,
4342 justify: Justify::Start,
4343 border: None,
4344 border_sides: BorderSides::all(),
4345 border_style: Style::new().fg(border),
4346 bg_color: None,
4347 padding: Padding::default(),
4348 margin: Margin::default(),
4349 constraints: Constraints::default(),
4350 title: None,
4351 grow: 0,
4352 group_name: None,
4353 });
4354
4355 for element in row {
4356 self.interaction_count += 1;
4357 self.commands.push(Command::BeginContainer {
4358 direction: Direction::Column,
4359 gap: 0,
4360 align: Align::Start,
4361 justify: Justify::Start,
4362 border: None,
4363 border_sides: BorderSides::all(),
4364 border_style: Style::new().fg(border),
4365 bg_color: None,
4366 padding: Padding::default(),
4367 margin: Margin::default(),
4368 constraints: Constraints::default(),
4369 title: None,
4370 grow: 1,
4371 group_name: None,
4372 });
4373 self.commands.extend(element.iter().cloned());
4374 self.commands.push(Command::EndContainer);
4375 }
4376
4377 self.commands.push(Command::EndContainer);
4378 }
4379
4380 self.commands.push(Command::EndContainer);
4381 self.last_text_idx = None;
4382
4383 self.response_for(interaction_id)
4384 }
4385
4386 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
4391 let visible = state.visible_indices().to_vec();
4392 if visible.is_empty() && state.items.is_empty() {
4393 state.selected = 0;
4394 return self;
4395 }
4396
4397 if !visible.is_empty() {
4398 state.selected = state.selected.min(visible.len().saturating_sub(1));
4399 }
4400
4401 let focused = self.register_focusable();
4402 let interaction_id = self.interaction_count;
4403 self.interaction_count += 1;
4404
4405 if focused {
4406 let mut consumed_indices = Vec::new();
4407 for (i, event) in self.events.iter().enumerate() {
4408 if let Event::Key(key) = event {
4409 if key.kind != KeyEventKind::Press {
4410 continue;
4411 }
4412 match key.code {
4413 KeyCode::Up | KeyCode::Char('k') => {
4414 state.selected = state.selected.saturating_sub(1);
4415 consumed_indices.push(i);
4416 }
4417 KeyCode::Down | KeyCode::Char('j') => {
4418 state.selected =
4419 (state.selected + 1).min(visible.len().saturating_sub(1));
4420 consumed_indices.push(i);
4421 }
4422 _ => {}
4423 }
4424 }
4425 }
4426
4427 for index in consumed_indices {
4428 self.consumed[index] = true;
4429 }
4430 }
4431
4432 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4433 for (i, event) in self.events.iter().enumerate() {
4434 if self.consumed[i] {
4435 continue;
4436 }
4437 if let Event::Mouse(mouse) = event {
4438 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4439 continue;
4440 }
4441 let in_bounds = mouse.x >= rect.x
4442 && mouse.x < rect.right()
4443 && mouse.y >= rect.y
4444 && mouse.y < rect.bottom();
4445 if !in_bounds {
4446 continue;
4447 }
4448 let clicked_idx = (mouse.y - rect.y) as usize;
4449 if clicked_idx < visible.len() {
4450 state.selected = clicked_idx;
4451 self.consumed[i] = true;
4452 }
4453 }
4454 }
4455 }
4456
4457 self.commands.push(Command::BeginContainer {
4458 direction: Direction::Column,
4459 gap: 0,
4460 align: Align::Start,
4461 justify: Justify::Start,
4462 border: None,
4463 border_sides: BorderSides::all(),
4464 border_style: Style::new().fg(self.theme.border),
4465 bg_color: None,
4466 padding: Padding::default(),
4467 margin: Margin::default(),
4468 constraints: Constraints::default(),
4469 title: None,
4470 grow: 0,
4471 group_name: None,
4472 });
4473
4474 for (view_idx, &item_idx) in visible.iter().enumerate() {
4475 let item = &state.items[item_idx];
4476 if view_idx == state.selected {
4477 if focused {
4478 self.styled(
4479 format!("▸ {item}"),
4480 Style::new().bold().fg(self.theme.primary),
4481 );
4482 } else {
4483 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
4484 }
4485 } else {
4486 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
4487 }
4488 }
4489
4490 self.commands.push(Command::EndContainer);
4491 self.last_text_idx = None;
4492
4493 self
4494 }
4495
4496 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
4501 if state.is_dirty() {
4502 state.recompute_widths();
4503 }
4504
4505 let focused = self.register_focusable();
4506 let interaction_id = self.interaction_count;
4507 self.interaction_count += 1;
4508
4509 if focused && !state.visible_indices().is_empty() {
4510 let mut consumed_indices = Vec::new();
4511 for (i, event) in self.events.iter().enumerate() {
4512 if let Event::Key(key) = event {
4513 if key.kind != KeyEventKind::Press {
4514 continue;
4515 }
4516 match key.code {
4517 KeyCode::Up | KeyCode::Char('k') => {
4518 let visible_len = if state.page_size > 0 {
4519 let start = state
4520 .page
4521 .saturating_mul(state.page_size)
4522 .min(state.visible_indices().len());
4523 let end =
4524 (start + state.page_size).min(state.visible_indices().len());
4525 end.saturating_sub(start)
4526 } else {
4527 state.visible_indices().len()
4528 };
4529 state.selected = state.selected.min(visible_len.saturating_sub(1));
4530 state.selected = state.selected.saturating_sub(1);
4531 consumed_indices.push(i);
4532 }
4533 KeyCode::Down | KeyCode::Char('j') => {
4534 let visible_len = if state.page_size > 0 {
4535 let start = state
4536 .page
4537 .saturating_mul(state.page_size)
4538 .min(state.visible_indices().len());
4539 let end =
4540 (start + state.page_size).min(state.visible_indices().len());
4541 end.saturating_sub(start)
4542 } else {
4543 state.visible_indices().len()
4544 };
4545 state.selected =
4546 (state.selected + 1).min(visible_len.saturating_sub(1));
4547 consumed_indices.push(i);
4548 }
4549 KeyCode::PageUp => {
4550 let old_page = state.page;
4551 state.prev_page();
4552 if state.page != old_page {
4553 state.selected = 0;
4554 }
4555 consumed_indices.push(i);
4556 }
4557 KeyCode::PageDown => {
4558 let old_page = state.page;
4559 state.next_page();
4560 if state.page != old_page {
4561 state.selected = 0;
4562 }
4563 consumed_indices.push(i);
4564 }
4565 _ => {}
4566 }
4567 }
4568 }
4569 for index in consumed_indices {
4570 self.consumed[index] = true;
4571 }
4572 }
4573
4574 if !state.visible_indices().is_empty() || !state.headers.is_empty() {
4575 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4576 for (i, event) in self.events.iter().enumerate() {
4577 if self.consumed[i] {
4578 continue;
4579 }
4580 if let Event::Mouse(mouse) = event {
4581 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4582 continue;
4583 }
4584 let in_bounds = mouse.x >= rect.x
4585 && mouse.x < rect.right()
4586 && mouse.y >= rect.y
4587 && mouse.y < rect.bottom();
4588 if !in_bounds {
4589 continue;
4590 }
4591
4592 if mouse.y == rect.y {
4593 let rel_x = mouse.x.saturating_sub(rect.x);
4594 let mut x_offset = 0u32;
4595 for (col_idx, width) in state.column_widths().iter().enumerate() {
4596 if rel_x >= x_offset && rel_x < x_offset + *width {
4597 state.toggle_sort(col_idx);
4598 state.selected = 0;
4599 self.consumed[i] = true;
4600 break;
4601 }
4602 x_offset += *width;
4603 if col_idx + 1 < state.column_widths().len() {
4604 x_offset += 3;
4605 }
4606 }
4607 continue;
4608 }
4609
4610 if mouse.y < rect.y + 2 {
4611 continue;
4612 }
4613
4614 let visible_len = if state.page_size > 0 {
4615 let start = state
4616 .page
4617 .saturating_mul(state.page_size)
4618 .min(state.visible_indices().len());
4619 let end = (start + state.page_size).min(state.visible_indices().len());
4620 end.saturating_sub(start)
4621 } else {
4622 state.visible_indices().len()
4623 };
4624 let clicked_idx = (mouse.y - rect.y - 2) as usize;
4625 if clicked_idx < visible_len {
4626 state.selected = clicked_idx;
4627 self.consumed[i] = true;
4628 }
4629 }
4630 }
4631 }
4632 }
4633
4634 if state.is_dirty() {
4635 state.recompute_widths();
4636 }
4637
4638 let total_visible = state.visible_indices().len();
4639 let page_start = if state.page_size > 0 {
4640 state
4641 .page
4642 .saturating_mul(state.page_size)
4643 .min(total_visible)
4644 } else {
4645 0
4646 };
4647 let page_end = if state.page_size > 0 {
4648 (page_start + state.page_size).min(total_visible)
4649 } else {
4650 total_visible
4651 };
4652 let visible_len = page_end.saturating_sub(page_start);
4653 state.selected = state.selected.min(visible_len.saturating_sub(1));
4654
4655 self.commands.push(Command::BeginContainer {
4656 direction: Direction::Column,
4657 gap: 0,
4658 align: Align::Start,
4659 justify: Justify::Start,
4660 border: None,
4661 border_sides: BorderSides::all(),
4662 border_style: Style::new().fg(self.theme.border),
4663 bg_color: None,
4664 padding: Padding::default(),
4665 margin: Margin::default(),
4666 constraints: Constraints::default(),
4667 title: None,
4668 grow: 0,
4669 group_name: None,
4670 });
4671
4672 let header_cells = state
4673 .headers
4674 .iter()
4675 .enumerate()
4676 .map(|(i, header)| {
4677 if state.sort_column == Some(i) {
4678 if state.sort_ascending {
4679 format!("{header} ▲")
4680 } else {
4681 format!("{header} ▼")
4682 }
4683 } else {
4684 header.clone()
4685 }
4686 })
4687 .collect::<Vec<_>>();
4688 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
4689 self.styled(header_line, Style::new().bold().fg(self.theme.text));
4690
4691 let separator = state
4692 .column_widths()
4693 .iter()
4694 .map(|w| "─".repeat(*w as usize))
4695 .collect::<Vec<_>>()
4696 .join("─┼─");
4697 self.text(separator);
4698
4699 for idx in 0..visible_len {
4700 let data_idx = state.visible_indices()[page_start + idx];
4701 let Some(row) = state.rows.get(data_idx) else {
4702 continue;
4703 };
4704 let line = format_table_row(row, state.column_widths(), " │ ");
4705 if idx == state.selected {
4706 let mut style = Style::new()
4707 .bg(self.theme.selected_bg)
4708 .fg(self.theme.selected_fg);
4709 if focused {
4710 style = style.bold();
4711 }
4712 self.styled(line, style);
4713 } else {
4714 self.styled(line, Style::new().fg(self.theme.text));
4715 }
4716 }
4717
4718 if state.page_size > 0 && state.total_pages() > 1 {
4719 self.styled(
4720 format!("Page {}/{}", state.page + 1, state.total_pages()),
4721 Style::new().dim().fg(self.theme.text_dim),
4722 );
4723 }
4724
4725 self.commands.push(Command::EndContainer);
4726 self.last_text_idx = None;
4727
4728 self
4729 }
4730
4731 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
4736 if state.labels.is_empty() {
4737 state.selected = 0;
4738 return self;
4739 }
4740
4741 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
4742 let focused = self.register_focusable();
4743 let interaction_id = self.interaction_count;
4744
4745 if focused {
4746 let mut consumed_indices = Vec::new();
4747 for (i, event) in self.events.iter().enumerate() {
4748 if let Event::Key(key) = event {
4749 if key.kind != KeyEventKind::Press {
4750 continue;
4751 }
4752 match key.code {
4753 KeyCode::Left => {
4754 state.selected = if state.selected == 0 {
4755 state.labels.len().saturating_sub(1)
4756 } else {
4757 state.selected - 1
4758 };
4759 consumed_indices.push(i);
4760 }
4761 KeyCode::Right => {
4762 if !state.labels.is_empty() {
4763 state.selected = (state.selected + 1) % state.labels.len();
4764 }
4765 consumed_indices.push(i);
4766 }
4767 _ => {}
4768 }
4769 }
4770 }
4771
4772 for index in consumed_indices {
4773 self.consumed[index] = true;
4774 }
4775 }
4776
4777 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4778 for (i, event) in self.events.iter().enumerate() {
4779 if self.consumed[i] {
4780 continue;
4781 }
4782 if let Event::Mouse(mouse) = event {
4783 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4784 continue;
4785 }
4786 let in_bounds = mouse.x >= rect.x
4787 && mouse.x < rect.right()
4788 && mouse.y >= rect.y
4789 && mouse.y < rect.bottom();
4790 if !in_bounds {
4791 continue;
4792 }
4793
4794 let mut x_offset = 0u32;
4795 let rel_x = mouse.x - rect.x;
4796 for (idx, label) in state.labels.iter().enumerate() {
4797 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
4798 if rel_x >= x_offset && rel_x < x_offset + tab_width {
4799 state.selected = idx;
4800 self.consumed[i] = true;
4801 break;
4802 }
4803 x_offset += tab_width + 1;
4804 }
4805 }
4806 }
4807 }
4808
4809 self.interaction_count += 1;
4810 self.commands.push(Command::BeginContainer {
4811 direction: Direction::Row,
4812 gap: 1,
4813 align: Align::Start,
4814 justify: Justify::Start,
4815 border: None,
4816 border_sides: BorderSides::all(),
4817 border_style: Style::new().fg(self.theme.border),
4818 bg_color: None,
4819 padding: Padding::default(),
4820 margin: Margin::default(),
4821 constraints: Constraints::default(),
4822 title: None,
4823 grow: 0,
4824 group_name: None,
4825 });
4826 for (idx, label) in state.labels.iter().enumerate() {
4827 let style = if idx == state.selected {
4828 let s = Style::new().fg(self.theme.primary).bold();
4829 if focused {
4830 s.underline()
4831 } else {
4832 s
4833 }
4834 } else {
4835 Style::new().fg(self.theme.text_dim)
4836 };
4837 self.styled(format!("[ {label} ]"), style);
4838 }
4839 self.commands.push(Command::EndContainer);
4840 self.last_text_idx = None;
4841
4842 self
4843 }
4844
4845 pub fn button(&mut self, label: impl Into<String>) -> bool {
4850 let focused = self.register_focusable();
4851 let interaction_id = self.interaction_count;
4852 self.interaction_count += 1;
4853 let response = self.response_for(interaction_id);
4854
4855 let mut activated = response.clicked;
4856 if focused {
4857 let mut consumed_indices = Vec::new();
4858 for (i, event) in self.events.iter().enumerate() {
4859 if let Event::Key(key) = event {
4860 if key.kind != KeyEventKind::Press {
4861 continue;
4862 }
4863 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4864 activated = true;
4865 consumed_indices.push(i);
4866 }
4867 }
4868 }
4869
4870 for index in consumed_indices {
4871 self.consumed[index] = true;
4872 }
4873 }
4874
4875 let hovered = response.hovered;
4876 let style = if focused {
4877 Style::new().fg(self.theme.primary).bold()
4878 } else if hovered {
4879 Style::new().fg(self.theme.accent)
4880 } else {
4881 Style::new().fg(self.theme.text)
4882 };
4883 let hover_bg = if hovered || focused {
4884 Some(self.theme.surface_hover)
4885 } else {
4886 None
4887 };
4888
4889 self.commands.push(Command::BeginContainer {
4890 direction: Direction::Row,
4891 gap: 0,
4892 align: Align::Start,
4893 justify: Justify::Start,
4894 border: None,
4895 border_sides: BorderSides::all(),
4896 border_style: Style::new().fg(self.theme.border),
4897 bg_color: hover_bg,
4898 padding: Padding::default(),
4899 margin: Margin::default(),
4900 constraints: Constraints::default(),
4901 title: None,
4902 grow: 0,
4903 group_name: None,
4904 });
4905 self.styled(format!("[ {} ]", label.into()), style);
4906 self.commands.push(Command::EndContainer);
4907 self.last_text_idx = None;
4908
4909 activated
4910 }
4911
4912 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
4917 let focused = self.register_focusable();
4918 let interaction_id = self.interaction_count;
4919 self.interaction_count += 1;
4920 let response = self.response_for(interaction_id);
4921
4922 let mut activated = response.clicked;
4923 if focused {
4924 let mut consumed_indices = Vec::new();
4925 for (i, event) in self.events.iter().enumerate() {
4926 if let Event::Key(key) = event {
4927 if key.kind != KeyEventKind::Press {
4928 continue;
4929 }
4930 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4931 activated = true;
4932 consumed_indices.push(i);
4933 }
4934 }
4935 }
4936 for index in consumed_indices {
4937 self.consumed[index] = true;
4938 }
4939 }
4940
4941 let label = label.into();
4942 let hover_bg = if response.hovered || focused {
4943 Some(self.theme.surface_hover)
4944 } else {
4945 None
4946 };
4947 let (text, style, bg_color, border) = match variant {
4948 ButtonVariant::Default => {
4949 let style = if focused {
4950 Style::new().fg(self.theme.primary).bold()
4951 } else if response.hovered {
4952 Style::new().fg(self.theme.accent)
4953 } else {
4954 Style::new().fg(self.theme.text)
4955 };
4956 (format!("[ {label} ]"), style, hover_bg, None)
4957 }
4958 ButtonVariant::Primary => {
4959 let style = if focused {
4960 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
4961 } else if response.hovered {
4962 Style::new().fg(self.theme.bg).bg(self.theme.accent)
4963 } else {
4964 Style::new().fg(self.theme.bg).bg(self.theme.primary)
4965 };
4966 (format!(" {label} "), style, hover_bg, None)
4967 }
4968 ButtonVariant::Danger => {
4969 let style = if focused {
4970 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
4971 } else if response.hovered {
4972 Style::new().fg(self.theme.bg).bg(self.theme.warning)
4973 } else {
4974 Style::new().fg(self.theme.bg).bg(self.theme.error)
4975 };
4976 (format!(" {label} "), style, hover_bg, None)
4977 }
4978 ButtonVariant::Outline => {
4979 let border_color = if focused {
4980 self.theme.primary
4981 } else if response.hovered {
4982 self.theme.accent
4983 } else {
4984 self.theme.border
4985 };
4986 let style = if focused {
4987 Style::new().fg(self.theme.primary).bold()
4988 } else if response.hovered {
4989 Style::new().fg(self.theme.accent)
4990 } else {
4991 Style::new().fg(self.theme.text)
4992 };
4993 (
4994 format!(" {label} "),
4995 style,
4996 hover_bg,
4997 Some((Border::Rounded, Style::new().fg(border_color))),
4998 )
4999 }
5000 };
5001
5002 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
5003 self.commands.push(Command::BeginContainer {
5004 direction: Direction::Row,
5005 gap: 0,
5006 align: Align::Center,
5007 justify: Justify::Center,
5008 border: if border.is_some() {
5009 Some(btn_border)
5010 } else {
5011 None
5012 },
5013 border_sides: BorderSides::all(),
5014 border_style: btn_border_style,
5015 bg_color,
5016 padding: Padding::default(),
5017 margin: Margin::default(),
5018 constraints: Constraints::default(),
5019 title: None,
5020 grow: 0,
5021 group_name: None,
5022 });
5023 self.styled(text, style);
5024 self.commands.push(Command::EndContainer);
5025 self.last_text_idx = None;
5026
5027 activated
5028 }
5029
5030 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
5035 let focused = self.register_focusable();
5036 let interaction_id = self.interaction_count;
5037 self.interaction_count += 1;
5038 let response = self.response_for(interaction_id);
5039 let mut should_toggle = response.clicked;
5040
5041 if focused {
5042 let mut consumed_indices = Vec::new();
5043 for (i, event) in self.events.iter().enumerate() {
5044 if let Event::Key(key) = event {
5045 if key.kind != KeyEventKind::Press {
5046 continue;
5047 }
5048 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5049 should_toggle = true;
5050 consumed_indices.push(i);
5051 }
5052 }
5053 }
5054
5055 for index in consumed_indices {
5056 self.consumed[index] = true;
5057 }
5058 }
5059
5060 if should_toggle {
5061 *checked = !*checked;
5062 }
5063
5064 let hover_bg = if response.hovered || focused {
5065 Some(self.theme.surface_hover)
5066 } else {
5067 None
5068 };
5069 self.commands.push(Command::BeginContainer {
5070 direction: Direction::Row,
5071 gap: 1,
5072 align: Align::Start,
5073 justify: Justify::Start,
5074 border: None,
5075 border_sides: BorderSides::all(),
5076 border_style: Style::new().fg(self.theme.border),
5077 bg_color: hover_bg,
5078 padding: Padding::default(),
5079 margin: Margin::default(),
5080 constraints: Constraints::default(),
5081 title: None,
5082 grow: 0,
5083 group_name: None,
5084 });
5085 let marker_style = if *checked {
5086 Style::new().fg(self.theme.success)
5087 } else {
5088 Style::new().fg(self.theme.text_dim)
5089 };
5090 let marker = if *checked { "[x]" } else { "[ ]" };
5091 let label_text = label.into();
5092 if focused {
5093 self.styled(format!("▸ {marker}"), marker_style.bold());
5094 self.styled(label_text, Style::new().fg(self.theme.text).bold());
5095 } else {
5096 self.styled(marker, marker_style);
5097 self.styled(label_text, Style::new().fg(self.theme.text));
5098 }
5099 self.commands.push(Command::EndContainer);
5100 self.last_text_idx = None;
5101
5102 self
5103 }
5104
5105 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
5111 let focused = self.register_focusable();
5112 let interaction_id = self.interaction_count;
5113 self.interaction_count += 1;
5114 let response = self.response_for(interaction_id);
5115 let mut should_toggle = response.clicked;
5116
5117 if focused {
5118 let mut consumed_indices = Vec::new();
5119 for (i, event) in self.events.iter().enumerate() {
5120 if let Event::Key(key) = event {
5121 if key.kind != KeyEventKind::Press {
5122 continue;
5123 }
5124 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5125 should_toggle = true;
5126 consumed_indices.push(i);
5127 }
5128 }
5129 }
5130
5131 for index in consumed_indices {
5132 self.consumed[index] = true;
5133 }
5134 }
5135
5136 if should_toggle {
5137 *on = !*on;
5138 }
5139
5140 let hover_bg = if response.hovered || focused {
5141 Some(self.theme.surface_hover)
5142 } else {
5143 None
5144 };
5145 self.commands.push(Command::BeginContainer {
5146 direction: Direction::Row,
5147 gap: 2,
5148 align: Align::Start,
5149 justify: Justify::Start,
5150 border: None,
5151 border_sides: BorderSides::all(),
5152 border_style: Style::new().fg(self.theme.border),
5153 bg_color: hover_bg,
5154 padding: Padding::default(),
5155 margin: Margin::default(),
5156 constraints: Constraints::default(),
5157 title: None,
5158 grow: 0,
5159 group_name: None,
5160 });
5161 let label_text = label.into();
5162 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
5163 let switch_style = if *on {
5164 Style::new().fg(self.theme.success)
5165 } else {
5166 Style::new().fg(self.theme.text_dim)
5167 };
5168 if focused {
5169 self.styled(
5170 format!("▸ {label_text}"),
5171 Style::new().fg(self.theme.text).bold(),
5172 );
5173 self.styled(switch, switch_style.bold());
5174 } else {
5175 self.styled(label_text, Style::new().fg(self.theme.text));
5176 self.styled(switch, switch_style);
5177 }
5178 self.commands.push(Command::EndContainer);
5179 self.last_text_idx = None;
5180
5181 self
5182 }
5183
5184 pub fn select(&mut self, state: &mut SelectState) -> bool {
5190 if state.items.is_empty() {
5191 return false;
5192 }
5193 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5194
5195 let focused = self.register_focusable();
5196 let interaction_id = self.interaction_count;
5197 self.interaction_count += 1;
5198 let response = self.response_for(interaction_id);
5199 let old_selected = state.selected;
5200
5201 if response.clicked {
5202 state.open = !state.open;
5203 if state.open {
5204 state.set_cursor(state.selected);
5205 }
5206 }
5207
5208 if focused {
5209 let mut consumed_indices = Vec::new();
5210 for (i, event) in self.events.iter().enumerate() {
5211 if self.consumed[i] {
5212 continue;
5213 }
5214 if let Event::Key(key) = event {
5215 if key.kind != KeyEventKind::Press {
5216 continue;
5217 }
5218 if state.open {
5219 match key.code {
5220 KeyCode::Up | KeyCode::Char('k') => {
5221 let c = state.cursor();
5222 state.set_cursor(c.saturating_sub(1));
5223 consumed_indices.push(i);
5224 }
5225 KeyCode::Down | KeyCode::Char('j') => {
5226 let c = state.cursor();
5227 state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
5228 consumed_indices.push(i);
5229 }
5230 KeyCode::Enter | KeyCode::Char(' ') => {
5231 state.selected = state.cursor();
5232 state.open = false;
5233 consumed_indices.push(i);
5234 }
5235 KeyCode::Esc => {
5236 state.open = false;
5237 consumed_indices.push(i);
5238 }
5239 _ => {}
5240 }
5241 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5242 state.open = true;
5243 state.set_cursor(state.selected);
5244 consumed_indices.push(i);
5245 }
5246 }
5247 }
5248 for idx in consumed_indices {
5249 self.consumed[idx] = true;
5250 }
5251 }
5252
5253 let changed = state.selected != old_selected;
5254
5255 let border_color = if focused {
5256 self.theme.primary
5257 } else {
5258 self.theme.border
5259 };
5260 let display_text = state
5261 .items
5262 .get(state.selected)
5263 .cloned()
5264 .unwrap_or_else(|| state.placeholder.clone());
5265 let arrow = if state.open { "▲" } else { "▼" };
5266
5267 self.commands.push(Command::BeginContainer {
5268 direction: Direction::Column,
5269 gap: 0,
5270 align: Align::Start,
5271 justify: Justify::Start,
5272 border: None,
5273 border_sides: BorderSides::all(),
5274 border_style: Style::new().fg(self.theme.border),
5275 bg_color: None,
5276 padding: Padding::default(),
5277 margin: Margin::default(),
5278 constraints: Constraints::default(),
5279 title: None,
5280 grow: 0,
5281 group_name: None,
5282 });
5283
5284 self.commands.push(Command::BeginContainer {
5285 direction: Direction::Row,
5286 gap: 1,
5287 align: Align::Start,
5288 justify: Justify::Start,
5289 border: Some(Border::Rounded),
5290 border_sides: BorderSides::all(),
5291 border_style: Style::new().fg(border_color),
5292 bg_color: None,
5293 padding: Padding {
5294 left: 1,
5295 right: 1,
5296 top: 0,
5297 bottom: 0,
5298 },
5299 margin: Margin::default(),
5300 constraints: Constraints::default(),
5301 title: None,
5302 grow: 0,
5303 group_name: None,
5304 });
5305 self.interaction_count += 1;
5306 self.styled(&display_text, Style::new().fg(self.theme.text));
5307 self.styled(arrow, Style::new().fg(self.theme.text_dim));
5308 self.commands.push(Command::EndContainer);
5309 self.last_text_idx = None;
5310
5311 if state.open {
5312 for (idx, item) in state.items.iter().enumerate() {
5313 let is_cursor = idx == state.cursor();
5314 let style = if is_cursor {
5315 Style::new().bold().fg(self.theme.primary)
5316 } else {
5317 Style::new().fg(self.theme.text)
5318 };
5319 let prefix = if is_cursor { "▸ " } else { " " };
5320 self.styled(format!("{prefix}{item}"), style);
5321 }
5322 }
5323
5324 self.commands.push(Command::EndContainer);
5325 self.last_text_idx = None;
5326 changed
5327 }
5328
5329 pub fn radio(&mut self, state: &mut RadioState) -> bool {
5333 if state.items.is_empty() {
5334 return false;
5335 }
5336 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5337 let focused = self.register_focusable();
5338 let old_selected = state.selected;
5339
5340 if focused {
5341 let mut consumed_indices = Vec::new();
5342 for (i, event) in self.events.iter().enumerate() {
5343 if self.consumed[i] {
5344 continue;
5345 }
5346 if let Event::Key(key) = event {
5347 if key.kind != KeyEventKind::Press {
5348 continue;
5349 }
5350 match key.code {
5351 KeyCode::Up | KeyCode::Char('k') => {
5352 state.selected = state.selected.saturating_sub(1);
5353 consumed_indices.push(i);
5354 }
5355 KeyCode::Down | KeyCode::Char('j') => {
5356 state.selected =
5357 (state.selected + 1).min(state.items.len().saturating_sub(1));
5358 consumed_indices.push(i);
5359 }
5360 KeyCode::Enter | KeyCode::Char(' ') => {
5361 consumed_indices.push(i);
5362 }
5363 _ => {}
5364 }
5365 }
5366 }
5367 for idx in consumed_indices {
5368 self.consumed[idx] = true;
5369 }
5370 }
5371
5372 let interaction_id = self.interaction_count;
5373 self.interaction_count += 1;
5374
5375 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5376 for (i, event) in self.events.iter().enumerate() {
5377 if self.consumed[i] {
5378 continue;
5379 }
5380 if let Event::Mouse(mouse) = event {
5381 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5382 continue;
5383 }
5384 let in_bounds = mouse.x >= rect.x
5385 && mouse.x < rect.right()
5386 && mouse.y >= rect.y
5387 && mouse.y < rect.bottom();
5388 if !in_bounds {
5389 continue;
5390 }
5391 let clicked_idx = (mouse.y - rect.y) as usize;
5392 if clicked_idx < state.items.len() {
5393 state.selected = clicked_idx;
5394 self.consumed[i] = true;
5395 }
5396 }
5397 }
5398 }
5399
5400 self.commands.push(Command::BeginContainer {
5401 direction: Direction::Column,
5402 gap: 0,
5403 align: Align::Start,
5404 justify: Justify::Start,
5405 border: None,
5406 border_sides: BorderSides::all(),
5407 border_style: Style::new().fg(self.theme.border),
5408 bg_color: None,
5409 padding: Padding::default(),
5410 margin: Margin::default(),
5411 constraints: Constraints::default(),
5412 title: None,
5413 grow: 0,
5414 group_name: None,
5415 });
5416
5417 for (idx, item) in state.items.iter().enumerate() {
5418 let is_selected = idx == state.selected;
5419 let marker = if is_selected { "●" } else { "○" };
5420 let style = if is_selected {
5421 if focused {
5422 Style::new().bold().fg(self.theme.primary)
5423 } else {
5424 Style::new().fg(self.theme.primary)
5425 }
5426 } else {
5427 Style::new().fg(self.theme.text)
5428 };
5429 let prefix = if focused && idx == state.selected {
5430 "▸ "
5431 } else {
5432 " "
5433 };
5434 self.styled(format!("{prefix}{marker} {item}"), style);
5435 }
5436
5437 self.commands.push(Command::EndContainer);
5438 self.last_text_idx = None;
5439 state.selected != old_selected
5440 }
5441
5442 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
5446 if state.items.is_empty() {
5447 return self;
5448 }
5449 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
5450 let focused = self.register_focusable();
5451
5452 if focused {
5453 let mut consumed_indices = Vec::new();
5454 for (i, event) in self.events.iter().enumerate() {
5455 if self.consumed[i] {
5456 continue;
5457 }
5458 if let Event::Key(key) = event {
5459 if key.kind != KeyEventKind::Press {
5460 continue;
5461 }
5462 match key.code {
5463 KeyCode::Up | KeyCode::Char('k') => {
5464 state.cursor = state.cursor.saturating_sub(1);
5465 consumed_indices.push(i);
5466 }
5467 KeyCode::Down | KeyCode::Char('j') => {
5468 state.cursor =
5469 (state.cursor + 1).min(state.items.len().saturating_sub(1));
5470 consumed_indices.push(i);
5471 }
5472 KeyCode::Char(' ') | KeyCode::Enter => {
5473 state.toggle(state.cursor);
5474 consumed_indices.push(i);
5475 }
5476 _ => {}
5477 }
5478 }
5479 }
5480 for idx in consumed_indices {
5481 self.consumed[idx] = true;
5482 }
5483 }
5484
5485 let interaction_id = self.interaction_count;
5486 self.interaction_count += 1;
5487
5488 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5489 for (i, event) in self.events.iter().enumerate() {
5490 if self.consumed[i] {
5491 continue;
5492 }
5493 if let Event::Mouse(mouse) = event {
5494 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5495 continue;
5496 }
5497 let in_bounds = mouse.x >= rect.x
5498 && mouse.x < rect.right()
5499 && mouse.y >= rect.y
5500 && mouse.y < rect.bottom();
5501 if !in_bounds {
5502 continue;
5503 }
5504 let clicked_idx = (mouse.y - rect.y) as usize;
5505 if clicked_idx < state.items.len() {
5506 state.toggle(clicked_idx);
5507 state.cursor = clicked_idx;
5508 self.consumed[i] = true;
5509 }
5510 }
5511 }
5512 }
5513
5514 self.commands.push(Command::BeginContainer {
5515 direction: Direction::Column,
5516 gap: 0,
5517 align: Align::Start,
5518 justify: Justify::Start,
5519 border: None,
5520 border_sides: BorderSides::all(),
5521 border_style: Style::new().fg(self.theme.border),
5522 bg_color: None,
5523 padding: Padding::default(),
5524 margin: Margin::default(),
5525 constraints: Constraints::default(),
5526 title: None,
5527 grow: 0,
5528 group_name: None,
5529 });
5530
5531 for (idx, item) in state.items.iter().enumerate() {
5532 let checked = state.selected.contains(&idx);
5533 let marker = if checked { "[x]" } else { "[ ]" };
5534 let is_cursor = idx == state.cursor;
5535 let style = if is_cursor && focused {
5536 Style::new().bold().fg(self.theme.primary)
5537 } else if checked {
5538 Style::new().fg(self.theme.success)
5539 } else {
5540 Style::new().fg(self.theme.text)
5541 };
5542 let prefix = if is_cursor && focused { "▸ " } else { " " };
5543 self.styled(format!("{prefix}{marker} {item}"), style);
5544 }
5545
5546 self.commands.push(Command::EndContainer);
5547 self.last_text_idx = None;
5548 self
5549 }
5550
5551 pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
5555 let entries = state.flatten();
5556 if entries.is_empty() {
5557 return self;
5558 }
5559 state.selected = state.selected.min(entries.len().saturating_sub(1));
5560 let focused = self.register_focusable();
5561
5562 if focused {
5563 let mut consumed_indices = Vec::new();
5564 for (i, event) in self.events.iter().enumerate() {
5565 if self.consumed[i] {
5566 continue;
5567 }
5568 if let Event::Key(key) = event {
5569 if key.kind != KeyEventKind::Press {
5570 continue;
5571 }
5572 match key.code {
5573 KeyCode::Up | KeyCode::Char('k') => {
5574 state.selected = state.selected.saturating_sub(1);
5575 consumed_indices.push(i);
5576 }
5577 KeyCode::Down | KeyCode::Char('j') => {
5578 let max = state.flatten().len().saturating_sub(1);
5579 state.selected = (state.selected + 1).min(max);
5580 consumed_indices.push(i);
5581 }
5582 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
5583 state.toggle_at(state.selected);
5584 consumed_indices.push(i);
5585 }
5586 KeyCode::Left => {
5587 let entry = &entries[state.selected.min(entries.len() - 1)];
5588 if entry.expanded {
5589 state.toggle_at(state.selected);
5590 }
5591 consumed_indices.push(i);
5592 }
5593 _ => {}
5594 }
5595 }
5596 }
5597 for idx in consumed_indices {
5598 self.consumed[idx] = true;
5599 }
5600 }
5601
5602 self.interaction_count += 1;
5603 self.commands.push(Command::BeginContainer {
5604 direction: Direction::Column,
5605 gap: 0,
5606 align: Align::Start,
5607 justify: Justify::Start,
5608 border: None,
5609 border_sides: BorderSides::all(),
5610 border_style: Style::new().fg(self.theme.border),
5611 bg_color: None,
5612 padding: Padding::default(),
5613 margin: Margin::default(),
5614 constraints: Constraints::default(),
5615 title: None,
5616 grow: 0,
5617 group_name: None,
5618 });
5619
5620 let entries = state.flatten();
5621 for (idx, entry) in entries.iter().enumerate() {
5622 let indent = " ".repeat(entry.depth);
5623 let icon = if entry.is_leaf {
5624 " "
5625 } else if entry.expanded {
5626 "▾ "
5627 } else {
5628 "▸ "
5629 };
5630 let is_selected = idx == state.selected;
5631 let style = if is_selected && focused {
5632 Style::new().bold().fg(self.theme.primary)
5633 } else if is_selected {
5634 Style::new().fg(self.theme.primary)
5635 } else {
5636 Style::new().fg(self.theme.text)
5637 };
5638 let cursor = if is_selected && focused { "▸" } else { " " };
5639 self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
5640 }
5641
5642 self.commands.push(Command::EndContainer);
5643 self.last_text_idx = None;
5644 self
5645 }
5646
5647 pub fn virtual_list(
5654 &mut self,
5655 state: &mut ListState,
5656 visible_height: usize,
5657 f: impl Fn(&mut Context, usize),
5658 ) -> &mut Self {
5659 if state.items.is_empty() {
5660 return self;
5661 }
5662 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5663 let focused = self.register_focusable();
5664
5665 if focused {
5666 let mut consumed_indices = Vec::new();
5667 for (i, event) in self.events.iter().enumerate() {
5668 if self.consumed[i] {
5669 continue;
5670 }
5671 if let Event::Key(key) = event {
5672 if key.kind != KeyEventKind::Press {
5673 continue;
5674 }
5675 match key.code {
5676 KeyCode::Up | KeyCode::Char('k') => {
5677 state.selected = state.selected.saturating_sub(1);
5678 consumed_indices.push(i);
5679 }
5680 KeyCode::Down | KeyCode::Char('j') => {
5681 state.selected =
5682 (state.selected + 1).min(state.items.len().saturating_sub(1));
5683 consumed_indices.push(i);
5684 }
5685 KeyCode::PageUp => {
5686 state.selected = state.selected.saturating_sub(visible_height);
5687 consumed_indices.push(i);
5688 }
5689 KeyCode::PageDown => {
5690 state.selected = (state.selected + visible_height)
5691 .min(state.items.len().saturating_sub(1));
5692 consumed_indices.push(i);
5693 }
5694 KeyCode::Home => {
5695 state.selected = 0;
5696 consumed_indices.push(i);
5697 }
5698 KeyCode::End => {
5699 state.selected = state.items.len().saturating_sub(1);
5700 consumed_indices.push(i);
5701 }
5702 _ => {}
5703 }
5704 }
5705 }
5706 for idx in consumed_indices {
5707 self.consumed[idx] = true;
5708 }
5709 }
5710
5711 let start = if state.selected >= visible_height {
5712 state.selected - visible_height + 1
5713 } else {
5714 0
5715 };
5716 let end = (start + visible_height).min(state.items.len());
5717
5718 self.interaction_count += 1;
5719 self.commands.push(Command::BeginContainer {
5720 direction: Direction::Column,
5721 gap: 0,
5722 align: Align::Start,
5723 justify: Justify::Start,
5724 border: None,
5725 border_sides: BorderSides::all(),
5726 border_style: Style::new().fg(self.theme.border),
5727 bg_color: None,
5728 padding: Padding::default(),
5729 margin: Margin::default(),
5730 constraints: Constraints::default(),
5731 title: None,
5732 grow: 0,
5733 group_name: None,
5734 });
5735
5736 if start > 0 {
5737 self.styled(
5738 format!(" ↑ {} more", start),
5739 Style::new().fg(self.theme.text_dim).dim(),
5740 );
5741 }
5742
5743 for idx in start..end {
5744 f(self, idx);
5745 }
5746
5747 let remaining = state.items.len().saturating_sub(end);
5748 if remaining > 0 {
5749 self.styled(
5750 format!(" ↓ {} more", remaining),
5751 Style::new().fg(self.theme.text_dim).dim(),
5752 );
5753 }
5754
5755 self.commands.push(Command::EndContainer);
5756 self.last_text_idx = None;
5757 self
5758 }
5759
5760 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
5764 if !state.open {
5765 return None;
5766 }
5767
5768 let filtered = state.filtered_indices();
5769 let sel = state.selected().min(filtered.len().saturating_sub(1));
5770 state.set_selected(sel);
5771
5772 let mut consumed_indices = Vec::new();
5773 let mut result: Option<usize> = None;
5774
5775 for (i, event) in self.events.iter().enumerate() {
5776 if self.consumed[i] {
5777 continue;
5778 }
5779 if let Event::Key(key) = event {
5780 if key.kind != KeyEventKind::Press {
5781 continue;
5782 }
5783 match key.code {
5784 KeyCode::Esc => {
5785 state.open = false;
5786 consumed_indices.push(i);
5787 }
5788 KeyCode::Up => {
5789 let s = state.selected();
5790 state.set_selected(s.saturating_sub(1));
5791 consumed_indices.push(i);
5792 }
5793 KeyCode::Down => {
5794 let s = state.selected();
5795 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
5796 consumed_indices.push(i);
5797 }
5798 KeyCode::Enter => {
5799 if let Some(&cmd_idx) = filtered.get(state.selected()) {
5800 result = Some(cmd_idx);
5801 state.open = false;
5802 }
5803 consumed_indices.push(i);
5804 }
5805 KeyCode::Backspace => {
5806 if state.cursor > 0 {
5807 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
5808 let end_idx = byte_index_for_char(&state.input, state.cursor);
5809 state.input.replace_range(byte_idx..end_idx, "");
5810 state.cursor -= 1;
5811 state.set_selected(0);
5812 }
5813 consumed_indices.push(i);
5814 }
5815 KeyCode::Char(ch) => {
5816 let byte_idx = byte_index_for_char(&state.input, state.cursor);
5817 state.input.insert(byte_idx, ch);
5818 state.cursor += 1;
5819 state.set_selected(0);
5820 consumed_indices.push(i);
5821 }
5822 _ => {}
5823 }
5824 }
5825 }
5826 for idx in consumed_indices {
5827 self.consumed[idx] = true;
5828 }
5829
5830 let filtered = state.filtered_indices();
5831
5832 self.modal(|ui| {
5833 let primary = ui.theme.primary;
5834 ui.container()
5835 .border(Border::Rounded)
5836 .border_style(Style::new().fg(primary))
5837 .pad(1)
5838 .max_w(60)
5839 .col(|ui| {
5840 let border_color = ui.theme.primary;
5841 ui.bordered(Border::Rounded)
5842 .border_style(Style::new().fg(border_color))
5843 .px(1)
5844 .col(|ui| {
5845 let display = if state.input.is_empty() {
5846 "Type to search...".to_string()
5847 } else {
5848 state.input.clone()
5849 };
5850 let style = if state.input.is_empty() {
5851 Style::new().dim().fg(ui.theme.text_dim)
5852 } else {
5853 Style::new().fg(ui.theme.text)
5854 };
5855 ui.styled(display, style);
5856 });
5857
5858 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
5859 let cmd = &state.commands[cmd_idx];
5860 let is_selected = list_idx == state.selected();
5861 let style = if is_selected {
5862 Style::new().bold().fg(ui.theme.primary)
5863 } else {
5864 Style::new().fg(ui.theme.text)
5865 };
5866 let prefix = if is_selected { "▸ " } else { " " };
5867 let shortcut_text = cmd
5868 .shortcut
5869 .as_deref()
5870 .map(|s| format!(" ({s})"))
5871 .unwrap_or_default();
5872 ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
5873 if is_selected && !cmd.description.is_empty() {
5874 ui.styled(
5875 format!(" {}", cmd.description),
5876 Style::new().dim().fg(ui.theme.text_dim),
5877 );
5878 }
5879 }
5880
5881 if filtered.is_empty() {
5882 ui.styled(
5883 " No matching commands",
5884 Style::new().dim().fg(ui.theme.text_dim),
5885 );
5886 }
5887 });
5888 });
5889
5890 result
5891 }
5892
5893 pub fn markdown(&mut self, text: &str) -> &mut Self {
5900 self.commands.push(Command::BeginContainer {
5901 direction: Direction::Column,
5902 gap: 0,
5903 align: Align::Start,
5904 justify: Justify::Start,
5905 border: None,
5906 border_sides: BorderSides::all(),
5907 border_style: Style::new().fg(self.theme.border),
5908 bg_color: None,
5909 padding: Padding::default(),
5910 margin: Margin::default(),
5911 constraints: Constraints::default(),
5912 title: None,
5913 grow: 0,
5914 group_name: None,
5915 });
5916 self.interaction_count += 1;
5917
5918 let text_style = Style::new().fg(self.theme.text);
5919 let bold_style = Style::new().fg(self.theme.text).bold();
5920 let code_style = Style::new().fg(self.theme.accent);
5921
5922 for line in text.lines() {
5923 let trimmed = line.trim();
5924 if trimmed.is_empty() {
5925 self.text(" ");
5926 continue;
5927 }
5928 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
5929 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
5930 continue;
5931 }
5932 if let Some(heading) = trimmed.strip_prefix("### ") {
5933 self.styled(heading, Style::new().bold().fg(self.theme.accent));
5934 } else if let Some(heading) = trimmed.strip_prefix("## ") {
5935 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
5936 } else if let Some(heading) = trimmed.strip_prefix("# ") {
5937 self.styled(heading, Style::new().bold().fg(self.theme.primary));
5938 } else if let Some(item) = trimmed
5939 .strip_prefix("- ")
5940 .or_else(|| trimmed.strip_prefix("* "))
5941 {
5942 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
5943 if segs.len() <= 1 {
5944 self.styled(format!(" • {item}"), text_style);
5945 } else {
5946 self.line(|ui| {
5947 ui.styled(" • ", text_style);
5948 for (s, st) in segs {
5949 ui.styled(s, st);
5950 }
5951 });
5952 }
5953 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
5954 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
5955 if parts.len() == 2 {
5956 let segs =
5957 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
5958 if segs.len() <= 1 {
5959 self.styled(format!(" {}. {}", parts[0], parts[1]), text_style);
5960 } else {
5961 self.line(|ui| {
5962 ui.styled(format!(" {}. ", parts[0]), text_style);
5963 for (s, st) in segs {
5964 ui.styled(s, st);
5965 }
5966 });
5967 }
5968 } else {
5969 self.text(trimmed);
5970 }
5971 } else if let Some(code) = trimmed.strip_prefix("```") {
5972 let _ = code;
5973 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
5974 } else {
5975 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
5976 if segs.len() <= 1 {
5977 self.styled(trimmed, text_style);
5978 } else {
5979 self.line(|ui| {
5980 for (s, st) in segs {
5981 ui.styled(s, st);
5982 }
5983 });
5984 }
5985 }
5986 }
5987
5988 self.commands.push(Command::EndContainer);
5989 self.last_text_idx = None;
5990 self
5991 }
5992
5993 fn parse_inline_segments(
5994 text: &str,
5995 base: Style,
5996 bold: Style,
5997 code: Style,
5998 ) -> Vec<(String, Style)> {
5999 let mut segments: Vec<(String, Style)> = Vec::new();
6000 let mut current = String::new();
6001 let chars: Vec<char> = text.chars().collect();
6002 let mut i = 0;
6003 while i < chars.len() {
6004 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
6005 let rest: String = chars[i + 2..].iter().collect();
6006 if let Some(end) = rest.find("**") {
6007 if !current.is_empty() {
6008 segments.push((std::mem::take(&mut current), base));
6009 }
6010 let inner: String = rest[..end].to_string();
6011 let char_count = inner.chars().count();
6012 segments.push((inner, bold));
6013 i += 2 + char_count + 2;
6014 continue;
6015 }
6016 }
6017 if chars[i] == '*'
6018 && (i + 1 >= chars.len() || chars[i + 1] != '*')
6019 && (i == 0 || chars[i - 1] != '*')
6020 {
6021 let rest: String = chars[i + 1..].iter().collect();
6022 if let Some(end) = rest.find('*') {
6023 if !current.is_empty() {
6024 segments.push((std::mem::take(&mut current), base));
6025 }
6026 let inner: String = rest[..end].to_string();
6027 let char_count = inner.chars().count();
6028 segments.push((inner, base.italic()));
6029 i += 1 + char_count + 1;
6030 continue;
6031 }
6032 }
6033 if chars[i] == '`' {
6034 let rest: String = chars[i + 1..].iter().collect();
6035 if let Some(end) = rest.find('`') {
6036 if !current.is_empty() {
6037 segments.push((std::mem::take(&mut current), base));
6038 }
6039 let inner: String = rest[..end].to_string();
6040 let char_count = inner.chars().count();
6041 segments.push((inner, code));
6042 i += 1 + char_count + 1;
6043 continue;
6044 }
6045 }
6046 current.push(chars[i]);
6047 i += 1;
6048 }
6049 if !current.is_empty() {
6050 segments.push((current, base));
6051 }
6052 segments
6053 }
6054
6055 pub fn key_seq(&self, seq: &str) -> bool {
6062 if seq.is_empty() {
6063 return false;
6064 }
6065 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6066 return false;
6067 }
6068 let target: Vec<char> = seq.chars().collect();
6069 let mut matched = 0;
6070 for (i, event) in self.events.iter().enumerate() {
6071 if self.consumed[i] {
6072 continue;
6073 }
6074 if let Event::Key(key) = event {
6075 if key.kind != KeyEventKind::Press {
6076 continue;
6077 }
6078 if let KeyCode::Char(c) = key.code {
6079 if c == target[matched] {
6080 matched += 1;
6081 if matched == target.len() {
6082 return true;
6083 }
6084 } else {
6085 matched = 0;
6086 if c == target[0] {
6087 matched = 1;
6088 }
6089 }
6090 }
6091 }
6092 }
6093 false
6094 }
6095
6096 pub fn separator(&mut self) -> &mut Self {
6101 self.commands.push(Command::Text {
6102 content: "─".repeat(200),
6103 style: Style::new().fg(self.theme.border).dim(),
6104 grow: 0,
6105 align: Align::Start,
6106 wrap: false,
6107 margin: Margin::default(),
6108 constraints: Constraints::default(),
6109 });
6110 self.last_text_idx = Some(self.commands.len() - 1);
6111 self
6112 }
6113
6114 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
6120 if bindings.is_empty() {
6121 return self;
6122 }
6123
6124 self.interaction_count += 1;
6125 self.commands.push(Command::BeginContainer {
6126 direction: Direction::Row,
6127 gap: 2,
6128 align: Align::Start,
6129 justify: Justify::Start,
6130 border: None,
6131 border_sides: BorderSides::all(),
6132 border_style: Style::new().fg(self.theme.border),
6133 bg_color: None,
6134 padding: Padding::default(),
6135 margin: Margin::default(),
6136 constraints: Constraints::default(),
6137 title: None,
6138 grow: 0,
6139 group_name: None,
6140 });
6141 for (idx, (key, action)) in bindings.iter().enumerate() {
6142 if idx > 0 {
6143 self.styled("·", Style::new().fg(self.theme.text_dim));
6144 }
6145 self.styled(*key, Style::new().bold().fg(self.theme.primary));
6146 self.styled(*action, Style::new().fg(self.theme.text_dim));
6147 }
6148 self.commands.push(Command::EndContainer);
6149 self.last_text_idx = None;
6150
6151 self
6152 }
6153
6154 pub fn key(&self, c: char) -> bool {
6160 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6161 return false;
6162 }
6163 self.events.iter().enumerate().any(|(i, e)| {
6164 !self.consumed[i]
6165 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
6166 })
6167 }
6168
6169 pub fn key_code(&self, code: KeyCode) -> bool {
6173 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6174 return false;
6175 }
6176 self.events.iter().enumerate().any(|(i, e)| {
6177 !self.consumed[i]
6178 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
6179 })
6180 }
6181
6182 pub fn key_release(&self, c: char) -> bool {
6186 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6187 return false;
6188 }
6189 self.events.iter().enumerate().any(|(i, e)| {
6190 !self.consumed[i]
6191 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
6192 })
6193 }
6194
6195 pub fn key_code_release(&self, code: KeyCode) -> bool {
6199 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6200 return false;
6201 }
6202 self.events.iter().enumerate().any(|(i, e)| {
6203 !self.consumed[i]
6204 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
6205 })
6206 }
6207
6208 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
6212 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6213 return false;
6214 }
6215 self.events.iter().enumerate().any(|(i, e)| {
6216 !self.consumed[i]
6217 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
6218 })
6219 }
6220
6221 pub fn mouse_down(&self) -> Option<(u32, u32)> {
6225 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6226 return None;
6227 }
6228 self.events.iter().enumerate().find_map(|(i, event)| {
6229 if self.consumed[i] {
6230 return None;
6231 }
6232 if let Event::Mouse(mouse) = event {
6233 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
6234 return Some((mouse.x, mouse.y));
6235 }
6236 }
6237 None
6238 })
6239 }
6240
6241 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
6246 self.mouse_pos
6247 }
6248
6249 pub fn paste(&self) -> Option<&str> {
6251 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6252 return None;
6253 }
6254 self.events.iter().enumerate().find_map(|(i, event)| {
6255 if self.consumed[i] {
6256 return None;
6257 }
6258 if let Event::Paste(ref text) = event {
6259 return Some(text.as_str());
6260 }
6261 None
6262 })
6263 }
6264
6265 pub fn scroll_up(&self) -> bool {
6267 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6268 return false;
6269 }
6270 self.events.iter().enumerate().any(|(i, event)| {
6271 !self.consumed[i]
6272 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
6273 })
6274 }
6275
6276 pub fn scroll_down(&self) -> bool {
6278 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6279 return false;
6280 }
6281 self.events.iter().enumerate().any(|(i, event)| {
6282 !self.consumed[i]
6283 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
6284 })
6285 }
6286
6287 pub fn quit(&mut self) {
6289 self.should_quit = true;
6290 }
6291
6292 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
6300 self.clipboard_text = Some(text.into());
6301 }
6302
6303 pub fn theme(&self) -> &Theme {
6305 &self.theme
6306 }
6307
6308 pub fn set_theme(&mut self, theme: Theme) {
6312 self.theme = theme;
6313 }
6314
6315 pub fn is_dark_mode(&self) -> bool {
6317 self.dark_mode
6318 }
6319
6320 pub fn set_dark_mode(&mut self, dark: bool) {
6322 self.dark_mode = dark;
6323 }
6324
6325 pub fn width(&self) -> u32 {
6329 self.area_width
6330 }
6331
6332 pub fn breakpoint(&self) -> Breakpoint {
6356 let w = self.area_width;
6357 if w < 40 {
6358 Breakpoint::Xs
6359 } else if w < 80 {
6360 Breakpoint::Sm
6361 } else if w < 120 {
6362 Breakpoint::Md
6363 } else if w < 160 {
6364 Breakpoint::Lg
6365 } else {
6366 Breakpoint::Xl
6367 }
6368 }
6369
6370 pub fn height(&self) -> u32 {
6372 self.area_height
6373 }
6374
6375 pub fn tick(&self) -> u64 {
6380 self.tick
6381 }
6382
6383 pub fn debug_enabled(&self) -> bool {
6387 self.debug
6388 }
6389}
6390
6391#[inline]
6392fn byte_index_for_char(value: &str, char_index: usize) -> usize {
6393 if char_index == 0 {
6394 return 0;
6395 }
6396 value
6397 .char_indices()
6398 .nth(char_index)
6399 .map_or(value.len(), |(idx, _)| idx)
6400}
6401
6402fn format_token_count(count: usize) -> String {
6403 if count >= 1_000_000 {
6404 format!("{:.1}M", count as f64 / 1_000_000.0)
6405 } else if count >= 1_000 {
6406 format!("{:.1}k", count as f64 / 1_000.0)
6407 } else {
6408 format!("{count}")
6409 }
6410}
6411
6412fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
6413 let mut parts: Vec<String> = Vec::new();
6414 for (i, width) in widths.iter().enumerate() {
6415 let cell = cells.get(i).map(String::as_str).unwrap_or("");
6416 let cell_width = UnicodeWidthStr::width(cell) as u32;
6417 let padding = (*width).saturating_sub(cell_width) as usize;
6418 parts.push(format!("{cell}{}", " ".repeat(padding)));
6419 }
6420 parts.join(separator)
6421}
6422
6423fn format_compact_number(value: f64) -> String {
6424 if value.fract().abs() < f64::EPSILON {
6425 return format!("{value:.0}");
6426 }
6427
6428 let mut s = format!("{value:.2}");
6429 while s.contains('.') && s.ends_with('0') {
6430 s.pop();
6431 }
6432 if s.ends_with('.') {
6433 s.pop();
6434 }
6435 s
6436}
6437
6438fn center_text(text: &str, width: usize) -> String {
6439 let text_width = UnicodeWidthStr::width(text);
6440 if text_width >= width {
6441 return text.to_string();
6442 }
6443
6444 let total = width - text_width;
6445 let left = total / 2;
6446 let right = total - left;
6447 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
6448}
6449
6450struct TextareaVLine {
6451 logical_row: usize,
6452 char_start: usize,
6453 char_count: usize,
6454}
6455
6456fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
6457 let mut out = Vec::new();
6458 for (row, line) in lines.iter().enumerate() {
6459 if line.is_empty() || wrap_width == u32::MAX {
6460 out.push(TextareaVLine {
6461 logical_row: row,
6462 char_start: 0,
6463 char_count: line.chars().count(),
6464 });
6465 continue;
6466 }
6467 let mut seg_start = 0usize;
6468 let mut seg_chars = 0usize;
6469 let mut seg_width = 0u32;
6470 for (idx, ch) in line.chars().enumerate() {
6471 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
6472 if seg_width + cw > wrap_width && seg_chars > 0 {
6473 out.push(TextareaVLine {
6474 logical_row: row,
6475 char_start: seg_start,
6476 char_count: seg_chars,
6477 });
6478 seg_start = idx;
6479 seg_chars = 0;
6480 seg_width = 0;
6481 }
6482 seg_chars += 1;
6483 seg_width += cw;
6484 }
6485 out.push(TextareaVLine {
6486 logical_row: row,
6487 char_start: seg_start,
6488 char_count: seg_chars,
6489 });
6490 }
6491 out
6492}
6493
6494fn textarea_logical_to_visual(
6495 vlines: &[TextareaVLine],
6496 logical_row: usize,
6497 logical_col: usize,
6498) -> (usize, usize) {
6499 for (i, vl) in vlines.iter().enumerate() {
6500 if vl.logical_row != logical_row {
6501 continue;
6502 }
6503 let seg_end = vl.char_start + vl.char_count;
6504 if logical_col >= vl.char_start && logical_col < seg_end {
6505 return (i, logical_col - vl.char_start);
6506 }
6507 if logical_col == seg_end {
6508 let is_last_seg = vlines
6509 .get(i + 1)
6510 .map_or(true, |next| next.logical_row != logical_row);
6511 if is_last_seg {
6512 return (i, logical_col - vl.char_start);
6513 }
6514 }
6515 }
6516 (vlines.len().saturating_sub(1), 0)
6517}
6518
6519fn textarea_visual_to_logical(
6520 vlines: &[TextareaVLine],
6521 visual_row: usize,
6522 visual_col: usize,
6523) -> (usize, usize) {
6524 if let Some(vl) = vlines.get(visual_row) {
6525 let logical_col = vl.char_start + visual_col.min(vl.char_count);
6526 (vl.logical_row, logical_col)
6527 } else {
6528 (0, 0)
6529 }
6530}
6531
6532fn open_url(url: &str) -> std::io::Result<()> {
6533 #[cfg(target_os = "macos")]
6534 {
6535 std::process::Command::new("open").arg(url).spawn()?;
6536 }
6537 #[cfg(target_os = "linux")]
6538 {
6539 std::process::Command::new("xdg-open").arg(url).spawn()?;
6540 }
6541 #[cfg(target_os = "windows")]
6542 {
6543 std::process::Command::new("cmd")
6544 .args(["/c", "start", "", url])
6545 .spawn()?;
6546 }
6547 Ok(())
6548}