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