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, Justify, Margin, Modifiers,
8 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 border(mut self, border: Border) -> Self {
728 self.border = Some(border);
729 self
730 }
731
732 pub fn border_top(mut self, show: bool) -> Self {
734 self.border_sides.top = show;
735 self
736 }
737
738 pub fn border_right(mut self, show: bool) -> Self {
740 self.border_sides.right = show;
741 self
742 }
743
744 pub fn border_bottom(mut self, show: bool) -> Self {
746 self.border_sides.bottom = show;
747 self
748 }
749
750 pub fn border_left(mut self, show: bool) -> Self {
752 self.border_sides.left = show;
753 self
754 }
755
756 pub fn border_sides(mut self, sides: BorderSides) -> Self {
758 self.border_sides = sides;
759 self
760 }
761
762 pub fn rounded(self) -> Self {
764 self.border(Border::Rounded)
765 }
766
767 pub fn border_style(mut self, style: Style) -> Self {
769 self.border_style = style;
770 self
771 }
772
773 pub fn dark_border_style(mut self, style: Style) -> Self {
775 self.dark_border_style = Some(style);
776 self
777 }
778
779 pub fn bg(mut self, color: Color) -> Self {
780 self.bg_color = Some(color);
781 self
782 }
783
784 pub fn dark_bg(mut self, color: Color) -> Self {
786 self.dark_bg_color = Some(color);
787 self
788 }
789
790 pub fn group_hover_bg(mut self, color: Color) -> Self {
792 self.group_hover_bg = Some(color);
793 self
794 }
795
796 pub fn group_hover_border_style(mut self, style: Style) -> Self {
798 self.group_hover_border_style = Some(style);
799 self
800 }
801
802 pub fn p(self, value: u32) -> Self {
806 self.pad(value)
807 }
808
809 pub fn pad(mut self, value: u32) -> Self {
811 self.padding = Padding::all(value);
812 self
813 }
814
815 pub fn px(mut self, value: u32) -> Self {
817 self.padding.left = value;
818 self.padding.right = value;
819 self
820 }
821
822 pub fn py(mut self, value: u32) -> Self {
824 self.padding.top = value;
825 self.padding.bottom = value;
826 self
827 }
828
829 pub fn pt(mut self, value: u32) -> Self {
831 self.padding.top = value;
832 self
833 }
834
835 pub fn pr(mut self, value: u32) -> Self {
837 self.padding.right = value;
838 self
839 }
840
841 pub fn pb(mut self, value: u32) -> Self {
843 self.padding.bottom = value;
844 self
845 }
846
847 pub fn pl(mut self, value: u32) -> Self {
849 self.padding.left = value;
850 self
851 }
852
853 pub fn padding(mut self, padding: Padding) -> Self {
855 self.padding = padding;
856 self
857 }
858
859 pub fn m(mut self, value: u32) -> Self {
863 self.margin = Margin::all(value);
864 self
865 }
866
867 pub fn mx(mut self, value: u32) -> Self {
869 self.margin.left = value;
870 self.margin.right = value;
871 self
872 }
873
874 pub fn my(mut self, value: u32) -> Self {
876 self.margin.top = value;
877 self.margin.bottom = value;
878 self
879 }
880
881 pub fn mt(mut self, value: u32) -> Self {
883 self.margin.top = value;
884 self
885 }
886
887 pub fn mr(mut self, value: u32) -> Self {
889 self.margin.right = value;
890 self
891 }
892
893 pub fn mb(mut self, value: u32) -> Self {
895 self.margin.bottom = value;
896 self
897 }
898
899 pub fn ml(mut self, value: u32) -> Self {
901 self.margin.left = value;
902 self
903 }
904
905 pub fn margin(mut self, margin: Margin) -> Self {
907 self.margin = margin;
908 self
909 }
910
911 pub fn w(mut self, value: u32) -> Self {
915 self.constraints.min_width = Some(value);
916 self.constraints.max_width = Some(value);
917 self
918 }
919
920 pub fn xs_w(self, value: u32) -> Self {
927 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
928 if is_xs {
929 self.w(value)
930 } else {
931 self
932 }
933 }
934
935 pub fn sm_w(self, value: u32) -> Self {
937 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
938 if is_sm {
939 self.w(value)
940 } else {
941 self
942 }
943 }
944
945 pub fn md_w(self, value: u32) -> Self {
947 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
948 if is_md {
949 self.w(value)
950 } else {
951 self
952 }
953 }
954
955 pub fn lg_w(self, value: u32) -> Self {
957 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
958 if is_lg {
959 self.w(value)
960 } else {
961 self
962 }
963 }
964
965 pub fn xl_w(self, value: u32) -> Self {
967 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
968 if is_xl {
969 self.w(value)
970 } else {
971 self
972 }
973 }
974
975 pub fn h(mut self, value: u32) -> Self {
977 self.constraints.min_height = Some(value);
978 self.constraints.max_height = Some(value);
979 self
980 }
981
982 pub fn xs_h(self, value: u32) -> Self {
984 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
985 if is_xs {
986 self.h(value)
987 } else {
988 self
989 }
990 }
991
992 pub fn sm_h(self, value: u32) -> Self {
994 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
995 if is_sm {
996 self.h(value)
997 } else {
998 self
999 }
1000 }
1001
1002 pub fn md_h(self, value: u32) -> Self {
1004 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1005 if is_md {
1006 self.h(value)
1007 } else {
1008 self
1009 }
1010 }
1011
1012 pub fn lg_h(self, value: u32) -> Self {
1014 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1015 if is_lg {
1016 self.h(value)
1017 } else {
1018 self
1019 }
1020 }
1021
1022 pub fn xl_h(self, value: u32) -> Self {
1024 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1025 if is_xl {
1026 self.h(value)
1027 } else {
1028 self
1029 }
1030 }
1031
1032 pub fn min_w(mut self, value: u32) -> Self {
1034 self.constraints.min_width = Some(value);
1035 self
1036 }
1037
1038 pub fn xs_min_w(self, value: u32) -> Self {
1040 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1041 if is_xs {
1042 self.min_w(value)
1043 } else {
1044 self
1045 }
1046 }
1047
1048 pub fn sm_min_w(self, value: u32) -> Self {
1050 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1051 if is_sm {
1052 self.min_w(value)
1053 } else {
1054 self
1055 }
1056 }
1057
1058 pub fn md_min_w(self, value: u32) -> Self {
1060 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1061 if is_md {
1062 self.min_w(value)
1063 } else {
1064 self
1065 }
1066 }
1067
1068 pub fn lg_min_w(self, value: u32) -> Self {
1070 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1071 if is_lg {
1072 self.min_w(value)
1073 } else {
1074 self
1075 }
1076 }
1077
1078 pub fn xl_min_w(self, value: u32) -> Self {
1080 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1081 if is_xl {
1082 self.min_w(value)
1083 } else {
1084 self
1085 }
1086 }
1087
1088 pub fn max_w(mut self, value: u32) -> Self {
1090 self.constraints.max_width = Some(value);
1091 self
1092 }
1093
1094 pub fn xs_max_w(self, value: u32) -> Self {
1096 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1097 if is_xs {
1098 self.max_w(value)
1099 } else {
1100 self
1101 }
1102 }
1103
1104 pub fn sm_max_w(self, value: u32) -> Self {
1106 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1107 if is_sm {
1108 self.max_w(value)
1109 } else {
1110 self
1111 }
1112 }
1113
1114 pub fn md_max_w(self, value: u32) -> Self {
1116 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1117 if is_md {
1118 self.max_w(value)
1119 } else {
1120 self
1121 }
1122 }
1123
1124 pub fn lg_max_w(self, value: u32) -> Self {
1126 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1127 if is_lg {
1128 self.max_w(value)
1129 } else {
1130 self
1131 }
1132 }
1133
1134 pub fn xl_max_w(self, value: u32) -> Self {
1136 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1137 if is_xl {
1138 self.max_w(value)
1139 } else {
1140 self
1141 }
1142 }
1143
1144 pub fn min_h(mut self, value: u32) -> Self {
1146 self.constraints.min_height = Some(value);
1147 self
1148 }
1149
1150 pub fn max_h(mut self, value: u32) -> Self {
1152 self.constraints.max_height = Some(value);
1153 self
1154 }
1155
1156 pub fn min_width(mut self, value: u32) -> Self {
1158 self.constraints.min_width = Some(value);
1159 self
1160 }
1161
1162 pub fn max_width(mut self, value: u32) -> Self {
1164 self.constraints.max_width = Some(value);
1165 self
1166 }
1167
1168 pub fn min_height(mut self, value: u32) -> Self {
1170 self.constraints.min_height = Some(value);
1171 self
1172 }
1173
1174 pub fn max_height(mut self, value: u32) -> Self {
1176 self.constraints.max_height = Some(value);
1177 self
1178 }
1179
1180 pub fn w_pct(mut self, pct: u8) -> Self {
1182 self.constraints.width_pct = Some(pct.min(100));
1183 self
1184 }
1185
1186 pub fn h_pct(mut self, pct: u8) -> Self {
1188 self.constraints.height_pct = Some(pct.min(100));
1189 self
1190 }
1191
1192 pub fn constraints(mut self, constraints: Constraints) -> Self {
1194 self.constraints = constraints;
1195 self
1196 }
1197
1198 pub fn gap(mut self, gap: u32) -> Self {
1202 self.gap = gap;
1203 self
1204 }
1205
1206 pub fn xs_gap(self, value: u32) -> Self {
1208 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1209 if is_xs {
1210 self.gap(value)
1211 } else {
1212 self
1213 }
1214 }
1215
1216 pub fn sm_gap(self, value: u32) -> Self {
1218 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1219 if is_sm {
1220 self.gap(value)
1221 } else {
1222 self
1223 }
1224 }
1225
1226 pub fn md_gap(self, value: u32) -> Self {
1233 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1234 if is_md {
1235 self.gap(value)
1236 } else {
1237 self
1238 }
1239 }
1240
1241 pub fn lg_gap(self, value: u32) -> Self {
1243 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1244 if is_lg {
1245 self.gap(value)
1246 } else {
1247 self
1248 }
1249 }
1250
1251 pub fn xl_gap(self, value: u32) -> Self {
1253 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1254 if is_xl {
1255 self.gap(value)
1256 } else {
1257 self
1258 }
1259 }
1260
1261 pub fn grow(mut self, grow: u16) -> Self {
1263 self.grow = grow;
1264 self
1265 }
1266
1267 pub fn xs_grow(self, value: u16) -> Self {
1269 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1270 if is_xs {
1271 self.grow(value)
1272 } else {
1273 self
1274 }
1275 }
1276
1277 pub fn sm_grow(self, value: u16) -> Self {
1279 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1280 if is_sm {
1281 self.grow(value)
1282 } else {
1283 self
1284 }
1285 }
1286
1287 pub fn md_grow(self, value: u16) -> Self {
1289 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1290 if is_md {
1291 self.grow(value)
1292 } else {
1293 self
1294 }
1295 }
1296
1297 pub fn lg_grow(self, value: u16) -> Self {
1299 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1300 if is_lg {
1301 self.grow(value)
1302 } else {
1303 self
1304 }
1305 }
1306
1307 pub fn xl_grow(self, value: u16) -> Self {
1309 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1310 if is_xl {
1311 self.grow(value)
1312 } else {
1313 self
1314 }
1315 }
1316
1317 pub fn xs_p(self, value: u32) -> Self {
1319 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1320 if is_xs {
1321 self.p(value)
1322 } else {
1323 self
1324 }
1325 }
1326
1327 pub fn sm_p(self, value: u32) -> Self {
1329 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1330 if is_sm {
1331 self.p(value)
1332 } else {
1333 self
1334 }
1335 }
1336
1337 pub fn md_p(self, value: u32) -> Self {
1339 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1340 if is_md {
1341 self.p(value)
1342 } else {
1343 self
1344 }
1345 }
1346
1347 pub fn lg_p(self, value: u32) -> Self {
1349 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1350 if is_lg {
1351 self.p(value)
1352 } else {
1353 self
1354 }
1355 }
1356
1357 pub fn xl_p(self, value: u32) -> Self {
1359 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1360 if is_xl {
1361 self.p(value)
1362 } else {
1363 self
1364 }
1365 }
1366
1367 pub fn align(mut self, align: Align) -> Self {
1371 self.align = align;
1372 self
1373 }
1374
1375 pub fn center(self) -> Self {
1377 self.align(Align::Center)
1378 }
1379
1380 pub fn justify(mut self, justify: Justify) -> Self {
1382 self.justify = justify;
1383 self
1384 }
1385
1386 pub fn space_between(self) -> Self {
1388 self.justify(Justify::SpaceBetween)
1389 }
1390
1391 pub fn space_around(self) -> Self {
1393 self.justify(Justify::SpaceAround)
1394 }
1395
1396 pub fn space_evenly(self) -> Self {
1398 self.justify(Justify::SpaceEvenly)
1399 }
1400
1401 pub fn title(self, title: impl Into<String>) -> Self {
1405 self.title_styled(title, Style::new())
1406 }
1407
1408 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1410 self.title = Some((title.into(), style));
1411 self
1412 }
1413
1414 pub fn scroll_offset(mut self, offset: u32) -> Self {
1418 self.scroll_offset = Some(offset);
1419 self
1420 }
1421
1422 fn group_name(mut self, name: String) -> Self {
1423 self.group_name = Some(name);
1424 self
1425 }
1426
1427 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1432 self.finish(Direction::Column, f)
1433 }
1434
1435 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1440 self.finish(Direction::Row, f)
1441 }
1442
1443 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1448 self.gap = 0;
1449 self.finish(Direction::Row, f)
1450 }
1451
1452 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1453 let interaction_id = self.ctx.interaction_count;
1454 self.ctx.interaction_count += 1;
1455
1456 let in_hovered_group = self
1457 .group_name
1458 .as_ref()
1459 .map(|name| self.ctx.is_group_hovered(name))
1460 .unwrap_or(false)
1461 || self
1462 .ctx
1463 .group_stack
1464 .last()
1465 .map(|name| self.ctx.is_group_hovered(name))
1466 .unwrap_or(false);
1467 let in_focused_group = self
1468 .group_name
1469 .as_ref()
1470 .map(|name| self.ctx.is_group_focused(name))
1471 .unwrap_or(false)
1472 || self
1473 .ctx
1474 .group_stack
1475 .last()
1476 .map(|name| self.ctx.is_group_focused(name))
1477 .unwrap_or(false);
1478
1479 let resolved_bg = if self.ctx.dark_mode {
1480 self.dark_bg_color.or(self.bg_color)
1481 } else {
1482 self.bg_color
1483 };
1484 let resolved_border_style = if self.ctx.dark_mode {
1485 self.dark_border_style.unwrap_or(self.border_style)
1486 } else {
1487 self.border_style
1488 };
1489 let bg_color = if in_hovered_group || in_focused_group {
1490 self.group_hover_bg.or(resolved_bg)
1491 } else {
1492 resolved_bg
1493 };
1494 let border_style = if in_hovered_group || in_focused_group {
1495 self.group_hover_border_style
1496 .unwrap_or(resolved_border_style)
1497 } else {
1498 resolved_border_style
1499 };
1500 let group_name = self.group_name.clone();
1501 let is_group_container = group_name.is_some();
1502
1503 if let Some(scroll_offset) = self.scroll_offset {
1504 self.ctx.commands.push(Command::BeginScrollable {
1505 grow: self.grow,
1506 border: self.border,
1507 border_sides: self.border_sides,
1508 border_style,
1509 padding: self.padding,
1510 margin: self.margin,
1511 constraints: self.constraints,
1512 title: self.title,
1513 scroll_offset,
1514 });
1515 } else {
1516 self.ctx.commands.push(Command::BeginContainer {
1517 direction,
1518 gap: self.gap,
1519 align: self.align,
1520 justify: self.justify,
1521 border: self.border,
1522 border_sides: self.border_sides,
1523 border_style,
1524 bg_color,
1525 padding: self.padding,
1526 margin: self.margin,
1527 constraints: self.constraints,
1528 title: self.title,
1529 grow: self.grow,
1530 group_name,
1531 });
1532 }
1533 f(self.ctx);
1534 self.ctx.commands.push(Command::EndContainer);
1535 self.ctx.last_text_idx = None;
1536
1537 if is_group_container {
1538 self.ctx.group_stack.pop();
1539 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1540 }
1541
1542 self.ctx.response_for(interaction_id)
1543 }
1544}
1545
1546impl Context {
1547 #[allow(clippy::too_many_arguments)]
1548 pub(crate) fn new(
1549 events: Vec<Event>,
1550 width: u32,
1551 height: u32,
1552 tick: u64,
1553 mut focus_index: usize,
1554 prev_focus_count: usize,
1555 prev_scroll_infos: Vec<(u32, u32)>,
1556 prev_scroll_rects: Vec<Rect>,
1557 prev_hit_map: Vec<Rect>,
1558 prev_group_rects: Vec<(String, Rect)>,
1559 prev_focus_rects: Vec<(usize, Rect)>,
1560 prev_focus_groups: Vec<Option<String>>,
1561 prev_hook_states: Vec<Box<dyn std::any::Any>>,
1562 debug: bool,
1563 theme: Theme,
1564 last_mouse_pos: Option<(u32, u32)>,
1565 prev_modal_active: bool,
1566 ) -> Self {
1567 let consumed = vec![false; events.len()];
1568
1569 let mut mouse_pos = last_mouse_pos;
1570 let mut click_pos = None;
1571 for event in &events {
1572 if let Event::Mouse(mouse) = event {
1573 mouse_pos = Some((mouse.x, mouse.y));
1574 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1575 click_pos = Some((mouse.x, mouse.y));
1576 }
1577 }
1578 }
1579
1580 if let Some((mx, my)) = click_pos {
1581 let mut best: Option<(usize, u64)> = None;
1582 for &(fid, rect) in &prev_focus_rects {
1583 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1584 let area = rect.width as u64 * rect.height as u64;
1585 if best.map_or(true, |(_, ba)| area < ba) {
1586 best = Some((fid, area));
1587 }
1588 }
1589 }
1590 if let Some((fid, _)) = best {
1591 focus_index = fid;
1592 }
1593 }
1594
1595 Self {
1596 commands: Vec::new(),
1597 events,
1598 consumed,
1599 should_quit: false,
1600 area_width: width,
1601 area_height: height,
1602 tick,
1603 focus_index,
1604 focus_count: 0,
1605 hook_states: prev_hook_states,
1606 hook_cursor: 0,
1607 prev_focus_count,
1608 scroll_count: 0,
1609 prev_scroll_infos,
1610 prev_scroll_rects,
1611 interaction_count: 0,
1612 prev_hit_map,
1613 group_stack: Vec::new(),
1614 prev_group_rects,
1615 group_count: 0,
1616 prev_focus_groups,
1617 _prev_focus_rects: prev_focus_rects,
1618 mouse_pos,
1619 click_pos,
1620 last_text_idx: None,
1621 overlay_depth: 0,
1622 modal_active: false,
1623 prev_modal_active,
1624 clipboard_text: None,
1625 debug,
1626 theme,
1627 dark_mode: true,
1628 }
1629 }
1630
1631 pub(crate) fn process_focus_keys(&mut self) {
1632 for (i, event) in self.events.iter().enumerate() {
1633 if let Event::Key(key) = event {
1634 if key.kind != KeyEventKind::Press {
1635 continue;
1636 }
1637 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1638 if self.prev_focus_count > 0 {
1639 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1640 }
1641 self.consumed[i] = true;
1642 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1643 || key.code == KeyCode::BackTab
1644 {
1645 if self.prev_focus_count > 0 {
1646 self.focus_index = if self.focus_index == 0 {
1647 self.prev_focus_count - 1
1648 } else {
1649 self.focus_index - 1
1650 };
1651 }
1652 self.consumed[i] = true;
1653 }
1654 }
1655 }
1656 }
1657
1658 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1662 w.ui(self)
1663 }
1664
1665 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1680 self.error_boundary_with(f, |ui, msg| {
1681 ui.styled(
1682 format!("⚠ Error: {msg}"),
1683 Style::new().fg(ui.theme.error).bold(),
1684 );
1685 });
1686 }
1687
1688 pub fn error_boundary_with(
1708 &mut self,
1709 f: impl FnOnce(&mut Context),
1710 fallback: impl FnOnce(&mut Context, String),
1711 ) {
1712 let cmd_count = self.commands.len();
1713 let last_text_idx = self.last_text_idx;
1714
1715 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1716 f(self);
1717 }));
1718
1719 match result {
1720 Ok(()) => {}
1721 Err(panic_info) => {
1722 self.commands.truncate(cmd_count);
1723 self.last_text_idx = last_text_idx;
1724
1725 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1726 (*s).to_string()
1727 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1728 s.clone()
1729 } else {
1730 "widget panicked".to_string()
1731 };
1732
1733 fallback(self, msg);
1734 }
1735 }
1736 }
1737
1738 pub fn interaction(&mut self) -> Response {
1744 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1745 return Response::default();
1746 }
1747 let id = self.interaction_count;
1748 self.interaction_count += 1;
1749 self.response_for(id)
1750 }
1751
1752 pub fn register_focusable(&mut self) -> bool {
1757 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1758 return false;
1759 }
1760 let id = self.focus_count;
1761 self.focus_count += 1;
1762 self.commands.push(Command::FocusMarker(id));
1763 if self.prev_focus_count == 0 {
1764 return true;
1765 }
1766 self.focus_index % self.prev_focus_count == id
1767 }
1768
1769 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1787 let idx = self.hook_cursor;
1788 self.hook_cursor += 1;
1789
1790 if idx >= self.hook_states.len() {
1791 self.hook_states.push(Box::new(init()));
1792 }
1793
1794 State {
1795 idx,
1796 _marker: std::marker::PhantomData,
1797 }
1798 }
1799
1800 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1808 &mut self,
1809 deps: &D,
1810 compute: impl FnOnce(&D) -> T,
1811 ) -> &T {
1812 let idx = self.hook_cursor;
1813 self.hook_cursor += 1;
1814
1815 let should_recompute = if idx >= self.hook_states.len() {
1816 true
1817 } else {
1818 let (stored_deps, _) = self.hook_states[idx]
1819 .downcast_ref::<(D, T)>()
1820 .expect("use_memo type mismatch");
1821 stored_deps != deps
1822 };
1823
1824 if should_recompute {
1825 let value = compute(deps);
1826 let slot = Box::new((deps.clone(), value));
1827 if idx < self.hook_states.len() {
1828 self.hook_states[idx] = slot;
1829 } else {
1830 self.hook_states.push(slot);
1831 }
1832 }
1833
1834 let (_, value) = self.hook_states[idx]
1835 .downcast_ref::<(D, T)>()
1836 .expect("use_memo type mismatch");
1837 value
1838 }
1839
1840 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1853 let content = s.into();
1854 self.commands.push(Command::Text {
1855 content,
1856 style: Style::new(),
1857 grow: 0,
1858 align: Align::Start,
1859 wrap: false,
1860 margin: Margin::default(),
1861 constraints: Constraints::default(),
1862 });
1863 self.last_text_idx = Some(self.commands.len() - 1);
1864 self
1865 }
1866
1867 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
1873 let url_str = url.into();
1874 let focused = self.register_focusable();
1875 let interaction_id = self.interaction_count;
1876 self.interaction_count += 1;
1877 let response = self.response_for(interaction_id);
1878
1879 let mut activated = response.clicked;
1880 if focused {
1881 for (i, event) in self.events.iter().enumerate() {
1882 if let Event::Key(key) = event {
1883 if key.kind != KeyEventKind::Press {
1884 continue;
1885 }
1886 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1887 activated = true;
1888 self.consumed[i] = true;
1889 }
1890 }
1891 }
1892 }
1893
1894 if activated {
1895 let _ = open_url(&url_str);
1896 }
1897
1898 let style = if focused {
1899 Style::new()
1900 .fg(self.theme.primary)
1901 .bg(self.theme.surface_hover)
1902 .underline()
1903 .bold()
1904 } else if response.hovered {
1905 Style::new()
1906 .fg(self.theme.accent)
1907 .bg(self.theme.surface_hover)
1908 .underline()
1909 } else {
1910 Style::new().fg(self.theme.primary).underline()
1911 };
1912
1913 self.commands.push(Command::Link {
1914 text: text.into(),
1915 url: url_str,
1916 style,
1917 margin: Margin::default(),
1918 constraints: Constraints::default(),
1919 });
1920 self.last_text_idx = Some(self.commands.len() - 1);
1921 self
1922 }
1923
1924 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1929 let content = s.into();
1930 self.commands.push(Command::Text {
1931 content,
1932 style: Style::new(),
1933 grow: 0,
1934 align: Align::Start,
1935 wrap: true,
1936 margin: Margin::default(),
1937 constraints: Constraints::default(),
1938 });
1939 self.last_text_idx = Some(self.commands.len() - 1);
1940 self
1941 }
1942
1943 pub fn bold(&mut self) -> &mut Self {
1947 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1948 self
1949 }
1950
1951 pub fn dim(&mut self) -> &mut Self {
1956 let text_dim = self.theme.text_dim;
1957 self.modify_last_style(|s| {
1958 s.modifiers |= Modifiers::DIM;
1959 if s.fg.is_none() {
1960 s.fg = Some(text_dim);
1961 }
1962 });
1963 self
1964 }
1965
1966 pub fn italic(&mut self) -> &mut Self {
1968 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1969 self
1970 }
1971
1972 pub fn underline(&mut self) -> &mut Self {
1974 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1975 self
1976 }
1977
1978 pub fn reversed(&mut self) -> &mut Self {
1980 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1981 self
1982 }
1983
1984 pub fn strikethrough(&mut self) -> &mut Self {
1986 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1987 self
1988 }
1989
1990 pub fn fg(&mut self, color: Color) -> &mut Self {
1992 self.modify_last_style(|s| s.fg = Some(color));
1993 self
1994 }
1995
1996 pub fn bg(&mut self, color: Color) -> &mut Self {
1998 self.modify_last_style(|s| s.bg = Some(color));
1999 self
2000 }
2001
2002 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
2003 let apply_group_style = self
2004 .group_stack
2005 .last()
2006 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2007 .unwrap_or(false);
2008 if apply_group_style {
2009 self.modify_last_style(|s| s.fg = Some(color));
2010 }
2011 self
2012 }
2013
2014 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
2015 let apply_group_style = self
2016 .group_stack
2017 .last()
2018 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2019 .unwrap_or(false);
2020 if apply_group_style {
2021 self.modify_last_style(|s| s.bg = Some(color));
2022 }
2023 self
2024 }
2025
2026 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
2031 self.commands.push(Command::Text {
2032 content: s.into(),
2033 style,
2034 grow: 0,
2035 align: Align::Start,
2036 wrap: false,
2037 margin: Margin::default(),
2038 constraints: Constraints::default(),
2039 });
2040 self.last_text_idx = Some(self.commands.len() - 1);
2041 self
2042 }
2043
2044 pub fn image(&mut self, img: &HalfBlockImage) {
2066 let width = img.width;
2067 let height = img.height;
2068
2069 self.container().w(width).h(height).gap(0).col(|ui| {
2070 for row in 0..height {
2071 ui.container().gap(0).row(|ui| {
2072 for col in 0..width {
2073 let idx = (row * width + col) as usize;
2074 if let Some(&(upper, lower)) = img.pixels.get(idx) {
2075 ui.styled("▀", Style::new().fg(upper).bg(lower));
2076 }
2077 }
2078 });
2079 }
2080 });
2081 }
2082
2083 pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
2099 if state.streaming {
2100 state.cursor_tick = state.cursor_tick.wrapping_add(1);
2101 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
2102 }
2103
2104 if state.content.is_empty() && state.streaming {
2105 let cursor = if state.cursor_visible { "▌" } else { " " };
2106 let primary = self.theme.primary;
2107 self.text(cursor).fg(primary);
2108 return;
2109 }
2110
2111 if !state.content.is_empty() {
2112 if state.streaming && state.cursor_visible {
2113 self.text_wrap(format!("{}▌", state.content));
2114 } else {
2115 self.text_wrap(&state.content);
2116 }
2117 }
2118 }
2119
2120 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
2135 let theme = self.theme;
2136 self.bordered(Border::Rounded).col(|ui| {
2137 ui.row(|ui| {
2138 ui.text("⚡").fg(theme.warning);
2139 ui.text(&state.tool_name).bold().fg(theme.primary);
2140 });
2141 ui.text(&state.description).dim();
2142
2143 if state.action == ApprovalAction::Pending {
2144 ui.row(|ui| {
2145 if ui.button("✓ Approve") {
2146 state.action = ApprovalAction::Approved;
2147 }
2148 if ui.button("✗ Reject") {
2149 state.action = ApprovalAction::Rejected;
2150 }
2151 });
2152 } else {
2153 let (label, color) = match state.action {
2154 ApprovalAction::Approved => ("✓ Approved", theme.success),
2155 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
2156 ApprovalAction::Pending => unreachable!(),
2157 };
2158 ui.text(label).fg(color).bold();
2159 }
2160 });
2161 }
2162
2163 pub fn context_bar(&mut self, items: &[ContextItem]) {
2176 if items.is_empty() {
2177 return;
2178 }
2179
2180 let theme = self.theme;
2181 let total: usize = items.iter().map(|item| item.tokens).sum();
2182
2183 self.container().row(|ui| {
2184 ui.text("📎").dim();
2185 for item in items {
2186 ui.text(format!(
2187 "{} ({})",
2188 item.label,
2189 format_token_count(item.tokens)
2190 ))
2191 .fg(theme.secondary);
2192 }
2193 ui.spacer();
2194 ui.text(format!("Σ {}", format_token_count(total))).dim();
2195 });
2196 }
2197
2198 pub fn wrap(&mut self) -> &mut Self {
2200 if let Some(idx) = self.last_text_idx {
2201 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
2202 *wrap = true;
2203 }
2204 }
2205 self
2206 }
2207
2208 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
2209 if let Some(idx) = self.last_text_idx {
2210 match &mut self.commands[idx] {
2211 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
2212 _ => {}
2213 }
2214 }
2215 }
2216
2217 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2235 self.push_container(Direction::Column, 0, f)
2236 }
2237
2238 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2242 self.push_container(Direction::Column, gap, f)
2243 }
2244
2245 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2262 self.push_container(Direction::Row, 0, f)
2263 }
2264
2265 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2269 self.push_container(Direction::Row, gap, f)
2270 }
2271
2272 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2289 let _ = self.push_container(Direction::Row, 0, f);
2290 self
2291 }
2292
2293 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2312 let start = self.commands.len();
2313 f(self);
2314 let mut segments: Vec<(String, Style)> = Vec::new();
2315 for cmd in self.commands.drain(start..) {
2316 if let Command::Text { content, style, .. } = cmd {
2317 segments.push((content, style));
2318 }
2319 }
2320 self.commands.push(Command::RichText {
2321 segments,
2322 wrap: true,
2323 align: Align::Start,
2324 margin: Margin::default(),
2325 constraints: Constraints::default(),
2326 });
2327 self.last_text_idx = None;
2328 self
2329 }
2330
2331 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
2333 self.commands.push(Command::BeginOverlay { modal: true });
2334 self.overlay_depth += 1;
2335 self.modal_active = true;
2336 f(self);
2337 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2338 self.commands.push(Command::EndOverlay);
2339 self.last_text_idx = None;
2340 }
2341
2342 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
2344 self.commands.push(Command::BeginOverlay { modal: false });
2345 self.overlay_depth += 1;
2346 f(self);
2347 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2348 self.commands.push(Command::EndOverlay);
2349 self.last_text_idx = None;
2350 }
2351
2352 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
2354 self.group_count = self.group_count.saturating_add(1);
2355 self.group_stack.push(name.to_string());
2356 self.container().group_name(name.to_string())
2357 }
2358
2359 pub fn container(&mut self) -> ContainerBuilder<'_> {
2380 let border = self.theme.border;
2381 ContainerBuilder {
2382 ctx: self,
2383 gap: 0,
2384 align: Align::Start,
2385 justify: Justify::Start,
2386 border: None,
2387 border_sides: BorderSides::all(),
2388 border_style: Style::new().fg(border),
2389 bg_color: None,
2390 dark_bg_color: None,
2391 dark_border_style: None,
2392 group_hover_bg: None,
2393 group_hover_border_style: None,
2394 group_name: None,
2395 padding: Padding::default(),
2396 margin: Margin::default(),
2397 constraints: Constraints::default(),
2398 title: None,
2399 grow: 0,
2400 scroll_offset: None,
2401 }
2402 }
2403
2404 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
2423 let index = self.scroll_count;
2424 self.scroll_count += 1;
2425 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
2426 state.set_bounds(ch, vh);
2427 let max = ch.saturating_sub(vh) as usize;
2428 state.offset = state.offset.min(max);
2429 }
2430
2431 let next_id = self.interaction_count;
2432 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
2433 let inner_rects: Vec<Rect> = self
2434 .prev_scroll_rects
2435 .iter()
2436 .enumerate()
2437 .filter(|&(j, sr)| {
2438 j != index
2439 && sr.width > 0
2440 && sr.height > 0
2441 && sr.x >= rect.x
2442 && sr.right() <= rect.right()
2443 && sr.y >= rect.y
2444 && sr.bottom() <= rect.bottom()
2445 })
2446 .map(|(_, sr)| *sr)
2447 .collect();
2448 self.auto_scroll_nested(&rect, state, &inner_rects);
2449 }
2450
2451 self.container().scroll_offset(state.offset as u32)
2452 }
2453
2454 pub fn scrollbar(&mut self, state: &ScrollState) {
2474 let vh = state.viewport_height();
2475 let ch = state.content_height();
2476 if vh == 0 || ch <= vh {
2477 return;
2478 }
2479
2480 let track_height = vh;
2481 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
2482 let max_offset = ch.saturating_sub(vh);
2483 let thumb_pos = if max_offset == 0 {
2484 0
2485 } else {
2486 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
2487 .round() as u32
2488 };
2489
2490 let theme = self.theme;
2491 let track_char = '│';
2492 let thumb_char = '█';
2493
2494 self.container().w(1).h(track_height).col(|ui| {
2495 for i in 0..track_height {
2496 if i >= thumb_pos && i < thumb_pos + thumb_height {
2497 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
2498 } else {
2499 ui.styled(
2500 track_char.to_string(),
2501 Style::new().fg(theme.text_dim).dim(),
2502 );
2503 }
2504 }
2505 });
2506 }
2507
2508 fn auto_scroll_nested(
2509 &mut self,
2510 rect: &Rect,
2511 state: &mut ScrollState,
2512 inner_scroll_rects: &[Rect],
2513 ) {
2514 let mut to_consume: Vec<usize> = Vec::new();
2515
2516 for (i, event) in self.events.iter().enumerate() {
2517 if self.consumed[i] {
2518 continue;
2519 }
2520 if let Event::Mouse(mouse) = event {
2521 let in_bounds = mouse.x >= rect.x
2522 && mouse.x < rect.right()
2523 && mouse.y >= rect.y
2524 && mouse.y < rect.bottom();
2525 if !in_bounds {
2526 continue;
2527 }
2528 let in_inner = inner_scroll_rects.iter().any(|sr| {
2529 mouse.x >= sr.x
2530 && mouse.x < sr.right()
2531 && mouse.y >= sr.y
2532 && mouse.y < sr.bottom()
2533 });
2534 if in_inner {
2535 continue;
2536 }
2537 match mouse.kind {
2538 MouseKind::ScrollUp => {
2539 state.scroll_up(1);
2540 to_consume.push(i);
2541 }
2542 MouseKind::ScrollDown => {
2543 state.scroll_down(1);
2544 to_consume.push(i);
2545 }
2546 MouseKind::Drag(MouseButton::Left) => {}
2547 _ => {}
2548 }
2549 }
2550 }
2551
2552 for i in to_consume {
2553 self.consumed[i] = true;
2554 }
2555 }
2556
2557 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
2561 self.container()
2562 .border(border)
2563 .border_sides(BorderSides::all())
2564 }
2565
2566 fn push_container(
2567 &mut self,
2568 direction: Direction,
2569 gap: u32,
2570 f: impl FnOnce(&mut Context),
2571 ) -> Response {
2572 let interaction_id = self.interaction_count;
2573 self.interaction_count += 1;
2574 let border = self.theme.border;
2575
2576 self.commands.push(Command::BeginContainer {
2577 direction,
2578 gap,
2579 align: Align::Start,
2580 justify: Justify::Start,
2581 border: None,
2582 border_sides: BorderSides::all(),
2583 border_style: Style::new().fg(border),
2584 bg_color: None,
2585 padding: Padding::default(),
2586 margin: Margin::default(),
2587 constraints: Constraints::default(),
2588 title: None,
2589 grow: 0,
2590 group_name: None,
2591 });
2592 f(self);
2593 self.commands.push(Command::EndContainer);
2594 self.last_text_idx = None;
2595
2596 self.response_for(interaction_id)
2597 }
2598
2599 fn response_for(&self, interaction_id: usize) -> Response {
2600 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2601 return Response::default();
2602 }
2603 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
2604 let clicked = self
2605 .click_pos
2606 .map(|(mx, my)| {
2607 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2608 })
2609 .unwrap_or(false);
2610 let hovered = self
2611 .mouse_pos
2612 .map(|(mx, my)| {
2613 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2614 })
2615 .unwrap_or(false);
2616 Response { clicked, hovered }
2617 } else {
2618 Response::default()
2619 }
2620 }
2621
2622 pub fn is_group_hovered(&self, name: &str) -> bool {
2624 if let Some(pos) = self.mouse_pos {
2625 self.prev_group_rects.iter().any(|(n, rect)| {
2626 n == name
2627 && pos.0 >= rect.x
2628 && pos.0 < rect.x + rect.width
2629 && pos.1 >= rect.y
2630 && pos.1 < rect.y + rect.height
2631 })
2632 } else {
2633 false
2634 }
2635 }
2636
2637 pub fn is_group_focused(&self, name: &str) -> bool {
2639 if self.prev_focus_count == 0 {
2640 return false;
2641 }
2642 let focused_index = self.focus_index % self.prev_focus_count;
2643 self.prev_focus_groups
2644 .get(focused_index)
2645 .and_then(|group| group.as_deref())
2646 .map(|group| group == name)
2647 .unwrap_or(false)
2648 }
2649
2650 pub fn grow(&mut self, value: u16) -> &mut Self {
2655 if let Some(idx) = self.last_text_idx {
2656 if let Command::Text { grow, .. } = &mut self.commands[idx] {
2657 *grow = value;
2658 }
2659 }
2660 self
2661 }
2662
2663 pub fn align(&mut self, align: Align) -> &mut Self {
2665 if let Some(idx) = self.last_text_idx {
2666 if let Command::Text {
2667 align: text_align, ..
2668 } = &mut self.commands[idx]
2669 {
2670 *text_align = align;
2671 }
2672 }
2673 self
2674 }
2675
2676 pub fn spacer(&mut self) -> &mut Self {
2680 self.commands.push(Command::Spacer { grow: 1 });
2681 self.last_text_idx = None;
2682 self
2683 }
2684
2685 pub fn form(
2689 &mut self,
2690 state: &mut FormState,
2691 f: impl FnOnce(&mut Context, &mut FormState),
2692 ) -> &mut Self {
2693 self.col(|ui| {
2694 f(ui, state);
2695 });
2696 self
2697 }
2698
2699 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2703 self.col(|ui| {
2704 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2705 ui.text_input(&mut field.input);
2706 if let Some(error) = field.error.as_deref() {
2707 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2708 }
2709 });
2710 self
2711 }
2712
2713 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
2717 self.button(label)
2718 }
2719
2720 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
2736 slt_assert(
2737 !state.value.contains('\n'),
2738 "text_input got a newline — use textarea instead",
2739 );
2740 let focused = self.register_focusable();
2741 state.cursor = state.cursor.min(state.value.chars().count());
2742
2743 if focused {
2744 let mut consumed_indices = Vec::new();
2745 for (i, event) in self.events.iter().enumerate() {
2746 if let Event::Key(key) = event {
2747 if key.kind != KeyEventKind::Press {
2748 continue;
2749 }
2750 match key.code {
2751 KeyCode::Char(ch) => {
2752 if let Some(max) = state.max_length {
2753 if state.value.chars().count() >= max {
2754 continue;
2755 }
2756 }
2757 let index = byte_index_for_char(&state.value, state.cursor);
2758 state.value.insert(index, ch);
2759 state.cursor += 1;
2760 consumed_indices.push(i);
2761 }
2762 KeyCode::Backspace => {
2763 if state.cursor > 0 {
2764 let start = byte_index_for_char(&state.value, state.cursor - 1);
2765 let end = byte_index_for_char(&state.value, state.cursor);
2766 state.value.replace_range(start..end, "");
2767 state.cursor -= 1;
2768 }
2769 consumed_indices.push(i);
2770 }
2771 KeyCode::Left => {
2772 state.cursor = state.cursor.saturating_sub(1);
2773 consumed_indices.push(i);
2774 }
2775 KeyCode::Right => {
2776 state.cursor = (state.cursor + 1).min(state.value.chars().count());
2777 consumed_indices.push(i);
2778 }
2779 KeyCode::Home => {
2780 state.cursor = 0;
2781 consumed_indices.push(i);
2782 }
2783 KeyCode::Delete => {
2784 let len = state.value.chars().count();
2785 if state.cursor < len {
2786 let start = byte_index_for_char(&state.value, state.cursor);
2787 let end = byte_index_for_char(&state.value, state.cursor + 1);
2788 state.value.replace_range(start..end, "");
2789 }
2790 consumed_indices.push(i);
2791 }
2792 KeyCode::End => {
2793 state.cursor = state.value.chars().count();
2794 consumed_indices.push(i);
2795 }
2796 _ => {}
2797 }
2798 }
2799 if let Event::Paste(ref text) = event {
2800 for ch in text.chars() {
2801 if let Some(max) = state.max_length {
2802 if state.value.chars().count() >= max {
2803 break;
2804 }
2805 }
2806 let index = byte_index_for_char(&state.value, state.cursor);
2807 state.value.insert(index, ch);
2808 state.cursor += 1;
2809 }
2810 consumed_indices.push(i);
2811 }
2812 }
2813
2814 for index in consumed_indices {
2815 self.consumed[index] = true;
2816 }
2817 }
2818
2819 let visible_width = self.area_width.saturating_sub(4) as usize;
2820 let input_text = if state.value.is_empty() {
2821 if state.placeholder.len() > 100 {
2822 slt_warn(
2823 "text_input placeholder is very long (>100 chars) — consider shortening it",
2824 );
2825 }
2826 let mut ph = state.placeholder.clone();
2827 if focused {
2828 ph.insert(0, '▎');
2829 }
2830 ph
2831 } else {
2832 let chars: Vec<char> = state.value.chars().collect();
2833 let display_chars: Vec<char> = if state.masked {
2834 vec!['•'; chars.len()]
2835 } else {
2836 chars.clone()
2837 };
2838
2839 let cursor_display_pos: usize = display_chars[..state.cursor.min(display_chars.len())]
2840 .iter()
2841 .map(|c| UnicodeWidthChar::width(*c).unwrap_or(1))
2842 .sum();
2843
2844 let scroll_offset = if cursor_display_pos >= visible_width {
2845 cursor_display_pos - visible_width + 1
2846 } else {
2847 0
2848 };
2849
2850 let mut rendered = String::new();
2851 let mut current_width: usize = 0;
2852 for (idx, &ch) in display_chars.iter().enumerate() {
2853 let cw = UnicodeWidthChar::width(ch).unwrap_or(1);
2854 if current_width + cw <= scroll_offset {
2855 current_width += cw;
2856 continue;
2857 }
2858 if current_width - scroll_offset >= visible_width {
2859 break;
2860 }
2861 if focused && idx == state.cursor {
2862 rendered.push('▎');
2863 }
2864 rendered.push(ch);
2865 current_width += cw;
2866 }
2867 if focused && state.cursor >= display_chars.len() {
2868 rendered.push('▎');
2869 }
2870 rendered
2871 };
2872 let input_style = if state.value.is_empty() && !focused {
2873 Style::new().dim().fg(self.theme.text_dim)
2874 } else {
2875 Style::new().fg(self.theme.text)
2876 };
2877
2878 let border_color = if focused {
2879 self.theme.primary
2880 } else if state.validation_error.is_some() {
2881 self.theme.error
2882 } else {
2883 self.theme.border
2884 };
2885
2886 self.bordered(Border::Rounded)
2887 .border_style(Style::new().fg(border_color))
2888 .px(1)
2889 .col(|ui| {
2890 ui.styled(input_text, input_style);
2891 });
2892
2893 if let Some(error) = state.validation_error.clone() {
2894 self.styled(
2895 format!("⚠ {error}"),
2896 Style::new().dim().fg(self.theme.error),
2897 );
2898 }
2899 self
2900 }
2901
2902 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
2908 self.styled(
2909 state.frame(self.tick).to_string(),
2910 Style::new().fg(self.theme.primary),
2911 )
2912 }
2913
2914 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
2919 state.cleanup(self.tick);
2920 if state.messages.is_empty() {
2921 return self;
2922 }
2923
2924 self.interaction_count += 1;
2925 self.commands.push(Command::BeginContainer {
2926 direction: Direction::Column,
2927 gap: 0,
2928 align: Align::Start,
2929 justify: Justify::Start,
2930 border: None,
2931 border_sides: BorderSides::all(),
2932 border_style: Style::new().fg(self.theme.border),
2933 bg_color: None,
2934 padding: Padding::default(),
2935 margin: Margin::default(),
2936 constraints: Constraints::default(),
2937 title: None,
2938 grow: 0,
2939 group_name: None,
2940 });
2941 for message in state.messages.iter().rev() {
2942 let color = match message.level {
2943 ToastLevel::Info => self.theme.primary,
2944 ToastLevel::Success => self.theme.success,
2945 ToastLevel::Warning => self.theme.warning,
2946 ToastLevel::Error => self.theme.error,
2947 };
2948 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
2949 }
2950 self.commands.push(Command::EndContainer);
2951 self.last_text_idx = None;
2952
2953 self
2954 }
2955
2956 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
2964 if state.lines.is_empty() {
2965 state.lines.push(String::new());
2966 }
2967 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
2968 state.cursor_col = state
2969 .cursor_col
2970 .min(state.lines[state.cursor_row].chars().count());
2971
2972 let focused = self.register_focusable();
2973 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
2974 let wrapping = state.wrap_width.is_some();
2975
2976 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
2977
2978 if focused {
2979 let mut consumed_indices = Vec::new();
2980 for (i, event) in self.events.iter().enumerate() {
2981 if let Event::Key(key) = event {
2982 if key.kind != KeyEventKind::Press {
2983 continue;
2984 }
2985 match key.code {
2986 KeyCode::Char(ch) => {
2987 if let Some(max) = state.max_length {
2988 let total: usize =
2989 state.lines.iter().map(|line| line.chars().count()).sum();
2990 if total >= max {
2991 continue;
2992 }
2993 }
2994 let index = byte_index_for_char(
2995 &state.lines[state.cursor_row],
2996 state.cursor_col,
2997 );
2998 state.lines[state.cursor_row].insert(index, ch);
2999 state.cursor_col += 1;
3000 consumed_indices.push(i);
3001 }
3002 KeyCode::Enter => {
3003 let split_index = byte_index_for_char(
3004 &state.lines[state.cursor_row],
3005 state.cursor_col,
3006 );
3007 let remainder = state.lines[state.cursor_row].split_off(split_index);
3008 state.cursor_row += 1;
3009 state.lines.insert(state.cursor_row, remainder);
3010 state.cursor_col = 0;
3011 consumed_indices.push(i);
3012 }
3013 KeyCode::Backspace => {
3014 if state.cursor_col > 0 {
3015 let start = byte_index_for_char(
3016 &state.lines[state.cursor_row],
3017 state.cursor_col - 1,
3018 );
3019 let end = byte_index_for_char(
3020 &state.lines[state.cursor_row],
3021 state.cursor_col,
3022 );
3023 state.lines[state.cursor_row].replace_range(start..end, "");
3024 state.cursor_col -= 1;
3025 } else if state.cursor_row > 0 {
3026 let current = state.lines.remove(state.cursor_row);
3027 state.cursor_row -= 1;
3028 state.cursor_col = state.lines[state.cursor_row].chars().count();
3029 state.lines[state.cursor_row].push_str(¤t);
3030 }
3031 consumed_indices.push(i);
3032 }
3033 KeyCode::Left => {
3034 if state.cursor_col > 0 {
3035 state.cursor_col -= 1;
3036 } else if state.cursor_row > 0 {
3037 state.cursor_row -= 1;
3038 state.cursor_col = state.lines[state.cursor_row].chars().count();
3039 }
3040 consumed_indices.push(i);
3041 }
3042 KeyCode::Right => {
3043 let line_len = state.lines[state.cursor_row].chars().count();
3044 if state.cursor_col < line_len {
3045 state.cursor_col += 1;
3046 } else if state.cursor_row + 1 < state.lines.len() {
3047 state.cursor_row += 1;
3048 state.cursor_col = 0;
3049 }
3050 consumed_indices.push(i);
3051 }
3052 KeyCode::Up => {
3053 if wrapping {
3054 let (vrow, vcol) = textarea_logical_to_visual(
3055 &pre_vlines,
3056 state.cursor_row,
3057 state.cursor_col,
3058 );
3059 if vrow > 0 {
3060 let (lr, lc) =
3061 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
3062 state.cursor_row = lr;
3063 state.cursor_col = lc;
3064 }
3065 } else if state.cursor_row > 0 {
3066 state.cursor_row -= 1;
3067 state.cursor_col = state
3068 .cursor_col
3069 .min(state.lines[state.cursor_row].chars().count());
3070 }
3071 consumed_indices.push(i);
3072 }
3073 KeyCode::Down => {
3074 if wrapping {
3075 let (vrow, vcol) = textarea_logical_to_visual(
3076 &pre_vlines,
3077 state.cursor_row,
3078 state.cursor_col,
3079 );
3080 if vrow + 1 < pre_vlines.len() {
3081 let (lr, lc) =
3082 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
3083 state.cursor_row = lr;
3084 state.cursor_col = lc;
3085 }
3086 } else if state.cursor_row + 1 < state.lines.len() {
3087 state.cursor_row += 1;
3088 state.cursor_col = state
3089 .cursor_col
3090 .min(state.lines[state.cursor_row].chars().count());
3091 }
3092 consumed_indices.push(i);
3093 }
3094 KeyCode::Home => {
3095 state.cursor_col = 0;
3096 consumed_indices.push(i);
3097 }
3098 KeyCode::Delete => {
3099 let line_len = state.lines[state.cursor_row].chars().count();
3100 if state.cursor_col < line_len {
3101 let start = byte_index_for_char(
3102 &state.lines[state.cursor_row],
3103 state.cursor_col,
3104 );
3105 let end = byte_index_for_char(
3106 &state.lines[state.cursor_row],
3107 state.cursor_col + 1,
3108 );
3109 state.lines[state.cursor_row].replace_range(start..end, "");
3110 } else if state.cursor_row + 1 < state.lines.len() {
3111 let next = state.lines.remove(state.cursor_row + 1);
3112 state.lines[state.cursor_row].push_str(&next);
3113 }
3114 consumed_indices.push(i);
3115 }
3116 KeyCode::End => {
3117 state.cursor_col = state.lines[state.cursor_row].chars().count();
3118 consumed_indices.push(i);
3119 }
3120 _ => {}
3121 }
3122 }
3123 if let Event::Paste(ref text) = event {
3124 for ch in text.chars() {
3125 if ch == '\n' || ch == '\r' {
3126 let split_index = byte_index_for_char(
3127 &state.lines[state.cursor_row],
3128 state.cursor_col,
3129 );
3130 let remainder = state.lines[state.cursor_row].split_off(split_index);
3131 state.cursor_row += 1;
3132 state.lines.insert(state.cursor_row, remainder);
3133 state.cursor_col = 0;
3134 } else {
3135 if let Some(max) = state.max_length {
3136 let total: usize =
3137 state.lines.iter().map(|l| l.chars().count()).sum();
3138 if total >= max {
3139 break;
3140 }
3141 }
3142 let index = byte_index_for_char(
3143 &state.lines[state.cursor_row],
3144 state.cursor_col,
3145 );
3146 state.lines[state.cursor_row].insert(index, ch);
3147 state.cursor_col += 1;
3148 }
3149 }
3150 consumed_indices.push(i);
3151 }
3152 }
3153
3154 for index in consumed_indices {
3155 self.consumed[index] = true;
3156 }
3157 }
3158
3159 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3160 let (cursor_vrow, cursor_vcol) =
3161 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
3162
3163 if cursor_vrow < state.scroll_offset {
3164 state.scroll_offset = cursor_vrow;
3165 }
3166 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
3167 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
3168 }
3169
3170 self.interaction_count += 1;
3171 self.commands.push(Command::BeginContainer {
3172 direction: Direction::Column,
3173 gap: 0,
3174 align: Align::Start,
3175 justify: Justify::Start,
3176 border: None,
3177 border_sides: BorderSides::all(),
3178 border_style: Style::new().fg(self.theme.border),
3179 bg_color: None,
3180 padding: Padding::default(),
3181 margin: Margin::default(),
3182 constraints: Constraints::default(),
3183 title: None,
3184 grow: 0,
3185 group_name: None,
3186 });
3187
3188 for vi in 0..visible_rows as usize {
3189 let actual_vi = state.scroll_offset + vi;
3190 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
3191 let line = &state.lines[vl.logical_row];
3192 let text: String = line
3193 .chars()
3194 .skip(vl.char_start)
3195 .take(vl.char_count)
3196 .collect();
3197 (text, actual_vi == cursor_vrow)
3198 } else {
3199 (String::new(), false)
3200 };
3201
3202 let mut rendered = seg_text.clone();
3203 let mut style = if seg_text.is_empty() {
3204 Style::new().fg(self.theme.text_dim)
3205 } else {
3206 Style::new().fg(self.theme.text)
3207 };
3208
3209 if is_cursor_line && focused {
3210 rendered.clear();
3211 for (idx, ch) in seg_text.chars().enumerate() {
3212 if idx == cursor_vcol {
3213 rendered.push('▎');
3214 }
3215 rendered.push(ch);
3216 }
3217 if cursor_vcol >= seg_text.chars().count() {
3218 rendered.push('▎');
3219 }
3220 style = Style::new().fg(self.theme.text);
3221 }
3222
3223 self.styled(rendered, style);
3224 }
3225 self.commands.push(Command::EndContainer);
3226 self.last_text_idx = None;
3227
3228 self
3229 }
3230
3231 pub fn progress(&mut self, ratio: f64) -> &mut Self {
3236 self.progress_bar(ratio, 20)
3237 }
3238
3239 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
3244 let clamped = ratio.clamp(0.0, 1.0);
3245 let filled = (clamped * width as f64).round() as u32;
3246 let empty = width.saturating_sub(filled);
3247 let mut bar = String::new();
3248 for _ in 0..filled {
3249 bar.push('█');
3250 }
3251 for _ in 0..empty {
3252 bar.push('░');
3253 }
3254 self.text(bar)
3255 }
3256
3257 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
3278 if data.is_empty() {
3279 return self;
3280 }
3281
3282 let max_label_width = data
3283 .iter()
3284 .map(|(label, _)| UnicodeWidthStr::width(*label))
3285 .max()
3286 .unwrap_or(0);
3287 let max_value = data
3288 .iter()
3289 .map(|(_, value)| *value)
3290 .fold(f64::NEG_INFINITY, f64::max);
3291 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3292
3293 self.interaction_count += 1;
3294 self.commands.push(Command::BeginContainer {
3295 direction: Direction::Column,
3296 gap: 0,
3297 align: Align::Start,
3298 justify: Justify::Start,
3299 border: None,
3300 border_sides: BorderSides::all(),
3301 border_style: Style::new().fg(self.theme.border),
3302 bg_color: None,
3303 padding: Padding::default(),
3304 margin: Margin::default(),
3305 constraints: Constraints::default(),
3306 title: None,
3307 grow: 0,
3308 group_name: None,
3309 });
3310
3311 for (label, value) in data {
3312 let label_width = UnicodeWidthStr::width(*label);
3313 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3314 let normalized = (*value / denom).clamp(0.0, 1.0);
3315 let bar_len = (normalized * max_width as f64).round() as usize;
3316 let bar = "█".repeat(bar_len);
3317
3318 self.interaction_count += 1;
3319 self.commands.push(Command::BeginContainer {
3320 direction: Direction::Row,
3321 gap: 1,
3322 align: Align::Start,
3323 justify: Justify::Start,
3324 border: None,
3325 border_sides: BorderSides::all(),
3326 border_style: Style::new().fg(self.theme.border),
3327 bg_color: None,
3328 padding: Padding::default(),
3329 margin: Margin::default(),
3330 constraints: Constraints::default(),
3331 title: None,
3332 grow: 0,
3333 group_name: None,
3334 });
3335 self.styled(
3336 format!("{label}{label_padding}"),
3337 Style::new().fg(self.theme.text),
3338 );
3339 self.styled(bar, Style::new().fg(self.theme.primary));
3340 self.styled(
3341 format_compact_number(*value),
3342 Style::new().fg(self.theme.text_dim),
3343 );
3344 self.commands.push(Command::EndContainer);
3345 self.last_text_idx = None;
3346 }
3347
3348 self.commands.push(Command::EndContainer);
3349 self.last_text_idx = None;
3350
3351 self
3352 }
3353
3354 pub fn bar_chart_styled(
3370 &mut self,
3371 bars: &[Bar],
3372 max_width: u32,
3373 direction: BarDirection,
3374 ) -> &mut Self {
3375 if bars.is_empty() {
3376 return self;
3377 }
3378
3379 let max_value = bars
3380 .iter()
3381 .map(|bar| bar.value)
3382 .fold(f64::NEG_INFINITY, f64::max);
3383 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3384
3385 match direction {
3386 BarDirection::Horizontal => {
3387 let max_label_width = bars
3388 .iter()
3389 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3390 .max()
3391 .unwrap_or(0);
3392
3393 self.interaction_count += 1;
3394 self.commands.push(Command::BeginContainer {
3395 direction: Direction::Column,
3396 gap: 0,
3397 align: Align::Start,
3398 justify: Justify::Start,
3399 border: None,
3400 border_sides: BorderSides::all(),
3401 border_style: Style::new().fg(self.theme.border),
3402 bg_color: None,
3403 padding: Padding::default(),
3404 margin: Margin::default(),
3405 constraints: Constraints::default(),
3406 title: None,
3407 grow: 0,
3408 group_name: None,
3409 });
3410
3411 for bar in bars {
3412 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3413 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3414 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3415 let bar_len = (normalized * max_width as f64).round() as usize;
3416 let bar_text = "█".repeat(bar_len);
3417 let color = bar.color.unwrap_or(self.theme.primary);
3418
3419 self.interaction_count += 1;
3420 self.commands.push(Command::BeginContainer {
3421 direction: Direction::Row,
3422 gap: 1,
3423 align: Align::Start,
3424 justify: Justify::Start,
3425 border: None,
3426 border_sides: BorderSides::all(),
3427 border_style: Style::new().fg(self.theme.border),
3428 bg_color: None,
3429 padding: Padding::default(),
3430 margin: Margin::default(),
3431 constraints: Constraints::default(),
3432 title: None,
3433 grow: 0,
3434 group_name: None,
3435 });
3436 self.styled(
3437 format!("{}{label_padding}", bar.label),
3438 Style::new().fg(self.theme.text),
3439 );
3440 self.styled(bar_text, Style::new().fg(color));
3441 self.styled(
3442 format_compact_number(bar.value),
3443 Style::new().fg(self.theme.text_dim),
3444 );
3445 self.commands.push(Command::EndContainer);
3446 self.last_text_idx = None;
3447 }
3448
3449 self.commands.push(Command::EndContainer);
3450 self.last_text_idx = None;
3451 }
3452 BarDirection::Vertical => {
3453 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
3454
3455 let chart_height = max_width.max(1) as usize;
3456 let value_labels: Vec<String> = bars
3457 .iter()
3458 .map(|bar| format_compact_number(bar.value))
3459 .collect();
3460 let col_width = bars
3461 .iter()
3462 .zip(value_labels.iter())
3463 .map(|(bar, value)| {
3464 UnicodeWidthStr::width(bar.label.as_str())
3465 .max(UnicodeWidthStr::width(value.as_str()))
3466 .max(1)
3467 })
3468 .max()
3469 .unwrap_or(1);
3470
3471 let bar_units: Vec<usize> = bars
3472 .iter()
3473 .map(|bar| {
3474 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3475 (normalized * chart_height as f64 * 8.0).round() as usize
3476 })
3477 .collect();
3478
3479 self.interaction_count += 1;
3480 self.commands.push(Command::BeginContainer {
3481 direction: Direction::Column,
3482 gap: 0,
3483 align: Align::Start,
3484 justify: Justify::Start,
3485 border: None,
3486 border_sides: BorderSides::all(),
3487 border_style: Style::new().fg(self.theme.border),
3488 bg_color: None,
3489 padding: Padding::default(),
3490 margin: Margin::default(),
3491 constraints: Constraints::default(),
3492 title: None,
3493 grow: 0,
3494 group_name: None,
3495 });
3496
3497 self.interaction_count += 1;
3498 self.commands.push(Command::BeginContainer {
3499 direction: Direction::Row,
3500 gap: 1,
3501 align: Align::Start,
3502 justify: Justify::Start,
3503 border: None,
3504 border_sides: BorderSides::all(),
3505 border_style: Style::new().fg(self.theme.border),
3506 bg_color: None,
3507 padding: Padding::default(),
3508 margin: Margin::default(),
3509 constraints: Constraints::default(),
3510 title: None,
3511 grow: 0,
3512 group_name: None,
3513 });
3514 for value in &value_labels {
3515 self.styled(
3516 center_text(value, col_width),
3517 Style::new().fg(self.theme.text_dim),
3518 );
3519 }
3520 self.commands.push(Command::EndContainer);
3521 self.last_text_idx = None;
3522
3523 for row in (0..chart_height).rev() {
3524 self.interaction_count += 1;
3525 self.commands.push(Command::BeginContainer {
3526 direction: Direction::Row,
3527 gap: 1,
3528 align: Align::Start,
3529 justify: Justify::Start,
3530 border: None,
3531 border_sides: BorderSides::all(),
3532 border_style: Style::new().fg(self.theme.border),
3533 bg_color: None,
3534 padding: Padding::default(),
3535 margin: Margin::default(),
3536 constraints: Constraints::default(),
3537 title: None,
3538 grow: 0,
3539 group_name: None,
3540 });
3541
3542 let row_base = row * 8;
3543 for (bar, units) in bars.iter().zip(bar_units.iter()) {
3544 let fill = if *units <= row_base {
3545 ' '
3546 } else {
3547 let delta = *units - row_base;
3548 if delta >= 8 {
3549 '█'
3550 } else {
3551 FRACTION_BLOCKS[delta]
3552 }
3553 };
3554
3555 self.styled(
3556 center_text(&fill.to_string(), col_width),
3557 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3558 );
3559 }
3560
3561 self.commands.push(Command::EndContainer);
3562 self.last_text_idx = None;
3563 }
3564
3565 self.interaction_count += 1;
3566 self.commands.push(Command::BeginContainer {
3567 direction: Direction::Row,
3568 gap: 1,
3569 align: Align::Start,
3570 justify: Justify::Start,
3571 border: None,
3572 border_sides: BorderSides::all(),
3573 border_style: Style::new().fg(self.theme.border),
3574 bg_color: None,
3575 padding: Padding::default(),
3576 margin: Margin::default(),
3577 constraints: Constraints::default(),
3578 title: None,
3579 grow: 0,
3580 group_name: None,
3581 });
3582 for bar in bars {
3583 self.styled(
3584 center_text(&bar.label, col_width),
3585 Style::new().fg(self.theme.text),
3586 );
3587 }
3588 self.commands.push(Command::EndContainer);
3589 self.last_text_idx = None;
3590
3591 self.commands.push(Command::EndContainer);
3592 self.last_text_idx = None;
3593 }
3594 }
3595
3596 self
3597 }
3598
3599 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
3616 if groups.is_empty() {
3617 return self;
3618 }
3619
3620 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
3621 if all_bars.is_empty() {
3622 return self;
3623 }
3624
3625 let max_label_width = all_bars
3626 .iter()
3627 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3628 .max()
3629 .unwrap_or(0);
3630 let max_value = all_bars
3631 .iter()
3632 .map(|bar| bar.value)
3633 .fold(f64::NEG_INFINITY, f64::max);
3634 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3635
3636 self.interaction_count += 1;
3637 self.commands.push(Command::BeginContainer {
3638 direction: Direction::Column,
3639 gap: 1,
3640 align: Align::Start,
3641 justify: Justify::Start,
3642 border: None,
3643 border_sides: BorderSides::all(),
3644 border_style: Style::new().fg(self.theme.border),
3645 bg_color: None,
3646 padding: Padding::default(),
3647 margin: Margin::default(),
3648 constraints: Constraints::default(),
3649 title: None,
3650 grow: 0,
3651 group_name: None,
3652 });
3653
3654 for group in groups {
3655 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
3656
3657 for bar in &group.bars {
3658 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3659 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3660 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3661 let bar_len = (normalized * max_width as f64).round() as usize;
3662 let bar_text = "█".repeat(bar_len);
3663
3664 self.interaction_count += 1;
3665 self.commands.push(Command::BeginContainer {
3666 direction: Direction::Row,
3667 gap: 1,
3668 align: Align::Start,
3669 justify: Justify::Start,
3670 border: None,
3671 border_sides: BorderSides::all(),
3672 border_style: Style::new().fg(self.theme.border),
3673 bg_color: None,
3674 padding: Padding::default(),
3675 margin: Margin::default(),
3676 constraints: Constraints::default(),
3677 title: None,
3678 grow: 0,
3679 group_name: None,
3680 });
3681 self.styled(
3682 format!(" {}{label_padding}", bar.label),
3683 Style::new().fg(self.theme.text),
3684 );
3685 self.styled(
3686 bar_text,
3687 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3688 );
3689 self.styled(
3690 format_compact_number(bar.value),
3691 Style::new().fg(self.theme.text_dim),
3692 );
3693 self.commands.push(Command::EndContainer);
3694 self.last_text_idx = None;
3695 }
3696 }
3697
3698 self.commands.push(Command::EndContainer);
3699 self.last_text_idx = None;
3700
3701 self
3702 }
3703
3704 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
3720 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3721
3722 let w = width as usize;
3723 let window = if data.len() > w {
3724 &data[data.len() - w..]
3725 } else {
3726 data
3727 };
3728
3729 if window.is_empty() {
3730 return self;
3731 }
3732
3733 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
3734 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3735 let range = max - min;
3736
3737 let line: String = window
3738 .iter()
3739 .map(|&value| {
3740 let normalized = if range == 0.0 {
3741 0.5
3742 } else {
3743 (value - min) / range
3744 };
3745 let idx = (normalized * 7.0).round() as usize;
3746 BLOCKS[idx.min(7)]
3747 })
3748 .collect();
3749
3750 self.styled(line, Style::new().fg(self.theme.primary))
3751 }
3752
3753 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
3773 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3774
3775 let w = width as usize;
3776 let window = if data.len() > w {
3777 &data[data.len() - w..]
3778 } else {
3779 data
3780 };
3781
3782 if window.is_empty() {
3783 return self;
3784 }
3785
3786 let mut finite_values = window
3787 .iter()
3788 .map(|(value, _)| *value)
3789 .filter(|value| !value.is_nan());
3790 let Some(first) = finite_values.next() else {
3791 return self.styled(
3792 " ".repeat(window.len()),
3793 Style::new().fg(self.theme.text_dim),
3794 );
3795 };
3796
3797 let mut min = first;
3798 let mut max = first;
3799 for value in finite_values {
3800 min = f64::min(min, value);
3801 max = f64::max(max, value);
3802 }
3803 let range = max - min;
3804
3805 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
3806 for (value, color) in window {
3807 if value.is_nan() {
3808 cells.push((' ', self.theme.text_dim));
3809 continue;
3810 }
3811
3812 let normalized = if range == 0.0 {
3813 0.5
3814 } else {
3815 ((*value - min) / range).clamp(0.0, 1.0)
3816 };
3817 let idx = (normalized * 7.0).round() as usize;
3818 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
3819 }
3820
3821 self.interaction_count += 1;
3822 self.commands.push(Command::BeginContainer {
3823 direction: Direction::Row,
3824 gap: 0,
3825 align: Align::Start,
3826 justify: Justify::Start,
3827 border: None,
3828 border_sides: BorderSides::all(),
3829 border_style: Style::new().fg(self.theme.border),
3830 bg_color: None,
3831 padding: Padding::default(),
3832 margin: Margin::default(),
3833 constraints: Constraints::default(),
3834 title: None,
3835 grow: 0,
3836 group_name: None,
3837 });
3838
3839 let mut seg = String::new();
3840 let mut seg_color = cells[0].1;
3841 for (ch, color) in cells {
3842 if color != seg_color {
3843 self.styled(seg, Style::new().fg(seg_color));
3844 seg = String::new();
3845 seg_color = color;
3846 }
3847 seg.push(ch);
3848 }
3849 if !seg.is_empty() {
3850 self.styled(seg, Style::new().fg(seg_color));
3851 }
3852
3853 self.commands.push(Command::EndContainer);
3854 self.last_text_idx = None;
3855
3856 self
3857 }
3858
3859 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
3873 if data.is_empty() || width == 0 || height == 0 {
3874 return self;
3875 }
3876
3877 let cols = width as usize;
3878 let rows = height as usize;
3879 let px_w = cols * 2;
3880 let px_h = rows * 4;
3881
3882 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
3883 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3884 let range = if (max - min).abs() < f64::EPSILON {
3885 1.0
3886 } else {
3887 max - min
3888 };
3889
3890 let points: Vec<usize> = (0..px_w)
3891 .map(|px| {
3892 let data_idx = if px_w <= 1 {
3893 0.0
3894 } else {
3895 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
3896 };
3897 let idx = data_idx.floor() as usize;
3898 let frac = data_idx - idx as f64;
3899 let value = if idx + 1 < data.len() {
3900 data[idx] * (1.0 - frac) + data[idx + 1] * frac
3901 } else {
3902 data[idx.min(data.len() - 1)]
3903 };
3904
3905 let normalized = (value - min) / range;
3906 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
3907 py.min(px_h - 1)
3908 })
3909 .collect();
3910
3911 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
3912 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
3913
3914 let mut grid = vec![vec![0u32; cols]; rows];
3915
3916 for i in 0..points.len() {
3917 let px = i;
3918 let py = points[i];
3919 let char_col = px / 2;
3920 let char_row = py / 4;
3921 let sub_col = px % 2;
3922 let sub_row = py % 4;
3923
3924 if char_col < cols && char_row < rows {
3925 grid[char_row][char_col] |= if sub_col == 0 {
3926 LEFT_BITS[sub_row]
3927 } else {
3928 RIGHT_BITS[sub_row]
3929 };
3930 }
3931
3932 if i + 1 < points.len() {
3933 let py_next = points[i + 1];
3934 let (y_start, y_end) = if py <= py_next {
3935 (py, py_next)
3936 } else {
3937 (py_next, py)
3938 };
3939 for y in y_start..=y_end {
3940 let cell_row = y / 4;
3941 let sub_y = y % 4;
3942 if char_col < cols && cell_row < rows {
3943 grid[cell_row][char_col] |= if sub_col == 0 {
3944 LEFT_BITS[sub_y]
3945 } else {
3946 RIGHT_BITS[sub_y]
3947 };
3948 }
3949 }
3950 }
3951 }
3952
3953 let style = Style::new().fg(self.theme.primary);
3954 for row in grid {
3955 let line: String = row
3956 .iter()
3957 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
3958 .collect();
3959 self.styled(line, style);
3960 }
3961
3962 self
3963 }
3964
3965 pub fn canvas(
3982 &mut self,
3983 width: u32,
3984 height: u32,
3985 draw: impl FnOnce(&mut CanvasContext),
3986 ) -> &mut Self {
3987 if width == 0 || height == 0 {
3988 return self;
3989 }
3990
3991 let mut canvas = CanvasContext::new(width as usize, height as usize);
3992 draw(&mut canvas);
3993
3994 for segments in canvas.render() {
3995 self.interaction_count += 1;
3996 self.commands.push(Command::BeginContainer {
3997 direction: Direction::Row,
3998 gap: 0,
3999 align: Align::Start,
4000 justify: Justify::Start,
4001 border: None,
4002 border_sides: BorderSides::all(),
4003 border_style: Style::new(),
4004 bg_color: None,
4005 padding: Padding::default(),
4006 margin: Margin::default(),
4007 constraints: Constraints::default(),
4008 title: None,
4009 grow: 0,
4010 group_name: None,
4011 });
4012 for (text, color) in segments {
4013 let c = if color == Color::Reset {
4014 self.theme.primary
4015 } else {
4016 color
4017 };
4018 self.styled(text, Style::new().fg(c));
4019 }
4020 self.commands.push(Command::EndContainer);
4021 self.last_text_idx = None;
4022 }
4023
4024 self
4025 }
4026
4027 pub fn chart(
4029 &mut self,
4030 configure: impl FnOnce(&mut ChartBuilder),
4031 width: u32,
4032 height: u32,
4033 ) -> &mut Self {
4034 if width == 0 || height == 0 {
4035 return self;
4036 }
4037
4038 let axis_style = Style::new().fg(self.theme.text_dim);
4039 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
4040 configure(&mut builder);
4041
4042 let config = builder.build();
4043 let rows = render_chart(&config);
4044
4045 for row in rows {
4046 self.interaction_count += 1;
4047 self.commands.push(Command::BeginContainer {
4048 direction: Direction::Row,
4049 gap: 0,
4050 align: Align::Start,
4051 justify: Justify::Start,
4052 border: None,
4053 border_sides: BorderSides::all(),
4054 border_style: Style::new().fg(self.theme.border),
4055 bg_color: None,
4056 padding: Padding::default(),
4057 margin: Margin::default(),
4058 constraints: Constraints::default(),
4059 title: None,
4060 grow: 0,
4061 group_name: None,
4062 });
4063 for (text, style) in row.segments {
4064 self.styled(text, style);
4065 }
4066 self.commands.push(Command::EndContainer);
4067 self.last_text_idx = None;
4068 }
4069
4070 self
4071 }
4072
4073 pub fn scatter(&mut self, data: &[(f64, f64)], width: u32, height: u32) -> &mut Self {
4077 self.chart(
4078 |c| {
4079 c.scatter(data);
4080 c.grid(true);
4081 },
4082 width,
4083 height,
4084 )
4085 }
4086
4087 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
4089 self.histogram_with(data, |_| {}, width, height)
4090 }
4091
4092 pub fn histogram_with(
4094 &mut self,
4095 data: &[f64],
4096 configure: impl FnOnce(&mut HistogramBuilder),
4097 width: u32,
4098 height: u32,
4099 ) -> &mut Self {
4100 if width == 0 || height == 0 {
4101 return self;
4102 }
4103
4104 let mut options = HistogramBuilder::default();
4105 configure(&mut options);
4106 let axis_style = Style::new().fg(self.theme.text_dim);
4107 let config = build_histogram_config(data, &options, width, height, axis_style);
4108 let rows = render_chart(&config);
4109
4110 for row in rows {
4111 self.interaction_count += 1;
4112 self.commands.push(Command::BeginContainer {
4113 direction: Direction::Row,
4114 gap: 0,
4115 align: Align::Start,
4116 justify: Justify::Start,
4117 border: None,
4118 border_sides: BorderSides::all(),
4119 border_style: Style::new().fg(self.theme.border),
4120 bg_color: None,
4121 padding: Padding::default(),
4122 margin: Margin::default(),
4123 constraints: Constraints::default(),
4124 title: None,
4125 grow: 0,
4126 group_name: None,
4127 });
4128 for (text, style) in row.segments {
4129 self.styled(text, style);
4130 }
4131 self.commands.push(Command::EndContainer);
4132 self.last_text_idx = None;
4133 }
4134
4135 self
4136 }
4137
4138 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
4155 slt_assert(cols > 0, "grid() requires at least 1 column");
4156 let interaction_id = self.interaction_count;
4157 self.interaction_count += 1;
4158 let border = self.theme.border;
4159
4160 self.commands.push(Command::BeginContainer {
4161 direction: Direction::Column,
4162 gap: 0,
4163 align: Align::Start,
4164 justify: Justify::Start,
4165 border: None,
4166 border_sides: BorderSides::all(),
4167 border_style: Style::new().fg(border),
4168 bg_color: None,
4169 padding: Padding::default(),
4170 margin: Margin::default(),
4171 constraints: Constraints::default(),
4172 title: None,
4173 grow: 0,
4174 group_name: None,
4175 });
4176
4177 let children_start = self.commands.len();
4178 f(self);
4179 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
4180
4181 let mut elements: Vec<Vec<Command>> = Vec::new();
4182 let mut iter = child_commands.into_iter().peekable();
4183 while let Some(cmd) = iter.next() {
4184 match cmd {
4185 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4186 let mut depth = 1_u32;
4187 let mut element = vec![cmd];
4188 for next in iter.by_ref() {
4189 match next {
4190 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4191 depth += 1;
4192 }
4193 Command::EndContainer => {
4194 depth = depth.saturating_sub(1);
4195 }
4196 _ => {}
4197 }
4198 let at_end = matches!(next, Command::EndContainer) && depth == 0;
4199 element.push(next);
4200 if at_end {
4201 break;
4202 }
4203 }
4204 elements.push(element);
4205 }
4206 Command::EndContainer => {}
4207 _ => elements.push(vec![cmd]),
4208 }
4209 }
4210
4211 let cols = cols.max(1) as usize;
4212 for row in elements.chunks(cols) {
4213 self.interaction_count += 1;
4214 self.commands.push(Command::BeginContainer {
4215 direction: Direction::Row,
4216 gap: 0,
4217 align: Align::Start,
4218 justify: Justify::Start,
4219 border: None,
4220 border_sides: BorderSides::all(),
4221 border_style: Style::new().fg(border),
4222 bg_color: None,
4223 padding: Padding::default(),
4224 margin: Margin::default(),
4225 constraints: Constraints::default(),
4226 title: None,
4227 grow: 0,
4228 group_name: None,
4229 });
4230
4231 for element in row {
4232 self.interaction_count += 1;
4233 self.commands.push(Command::BeginContainer {
4234 direction: Direction::Column,
4235 gap: 0,
4236 align: Align::Start,
4237 justify: Justify::Start,
4238 border: None,
4239 border_sides: BorderSides::all(),
4240 border_style: Style::new().fg(border),
4241 bg_color: None,
4242 padding: Padding::default(),
4243 margin: Margin::default(),
4244 constraints: Constraints::default(),
4245 title: None,
4246 grow: 1,
4247 group_name: None,
4248 });
4249 self.commands.extend(element.iter().cloned());
4250 self.commands.push(Command::EndContainer);
4251 }
4252
4253 self.commands.push(Command::EndContainer);
4254 }
4255
4256 self.commands.push(Command::EndContainer);
4257 self.last_text_idx = None;
4258
4259 self.response_for(interaction_id)
4260 }
4261
4262 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
4267 let visible = state.visible_indices().to_vec();
4268 if visible.is_empty() && state.items.is_empty() {
4269 state.selected = 0;
4270 return self;
4271 }
4272
4273 if !visible.is_empty() {
4274 state.selected = state.selected.min(visible.len().saturating_sub(1));
4275 }
4276
4277 let focused = self.register_focusable();
4278 let interaction_id = self.interaction_count;
4279 self.interaction_count += 1;
4280
4281 if focused {
4282 let mut consumed_indices = Vec::new();
4283 for (i, event) in self.events.iter().enumerate() {
4284 if let Event::Key(key) = event {
4285 if key.kind != KeyEventKind::Press {
4286 continue;
4287 }
4288 match key.code {
4289 KeyCode::Up | KeyCode::Char('k') => {
4290 state.selected = state.selected.saturating_sub(1);
4291 consumed_indices.push(i);
4292 }
4293 KeyCode::Down | KeyCode::Char('j') => {
4294 state.selected =
4295 (state.selected + 1).min(visible.len().saturating_sub(1));
4296 consumed_indices.push(i);
4297 }
4298 _ => {}
4299 }
4300 }
4301 }
4302
4303 for index in consumed_indices {
4304 self.consumed[index] = true;
4305 }
4306 }
4307
4308 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4309 for (i, event) in self.events.iter().enumerate() {
4310 if self.consumed[i] {
4311 continue;
4312 }
4313 if let Event::Mouse(mouse) = event {
4314 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4315 continue;
4316 }
4317 let in_bounds = mouse.x >= rect.x
4318 && mouse.x < rect.right()
4319 && mouse.y >= rect.y
4320 && mouse.y < rect.bottom();
4321 if !in_bounds {
4322 continue;
4323 }
4324 let clicked_idx = (mouse.y - rect.y) as usize;
4325 if clicked_idx < visible.len() {
4326 state.selected = clicked_idx;
4327 self.consumed[i] = true;
4328 }
4329 }
4330 }
4331 }
4332
4333 self.commands.push(Command::BeginContainer {
4334 direction: Direction::Column,
4335 gap: 0,
4336 align: Align::Start,
4337 justify: Justify::Start,
4338 border: None,
4339 border_sides: BorderSides::all(),
4340 border_style: Style::new().fg(self.theme.border),
4341 bg_color: None,
4342 padding: Padding::default(),
4343 margin: Margin::default(),
4344 constraints: Constraints::default(),
4345 title: None,
4346 grow: 0,
4347 group_name: None,
4348 });
4349
4350 for (view_idx, &item_idx) in visible.iter().enumerate() {
4351 let item = &state.items[item_idx];
4352 if view_idx == state.selected {
4353 if focused {
4354 self.styled(
4355 format!("▸ {item}"),
4356 Style::new().bold().fg(self.theme.primary),
4357 );
4358 } else {
4359 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
4360 }
4361 } else {
4362 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
4363 }
4364 }
4365
4366 self.commands.push(Command::EndContainer);
4367 self.last_text_idx = None;
4368
4369 self
4370 }
4371
4372 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
4377 if state.is_dirty() {
4378 state.recompute_widths();
4379 }
4380
4381 let focused = self.register_focusable();
4382 let interaction_id = self.interaction_count;
4383 self.interaction_count += 1;
4384
4385 if focused && !state.visible_indices().is_empty() {
4386 let mut consumed_indices = Vec::new();
4387 for (i, event) in self.events.iter().enumerate() {
4388 if let Event::Key(key) = event {
4389 if key.kind != KeyEventKind::Press {
4390 continue;
4391 }
4392 match key.code {
4393 KeyCode::Up | KeyCode::Char('k') => {
4394 let visible_len = if state.page_size > 0 {
4395 let start = state
4396 .page
4397 .saturating_mul(state.page_size)
4398 .min(state.visible_indices().len());
4399 let end =
4400 (start + state.page_size).min(state.visible_indices().len());
4401 end.saturating_sub(start)
4402 } else {
4403 state.visible_indices().len()
4404 };
4405 state.selected = state.selected.min(visible_len.saturating_sub(1));
4406 state.selected = state.selected.saturating_sub(1);
4407 consumed_indices.push(i);
4408 }
4409 KeyCode::Down | KeyCode::Char('j') => {
4410 let visible_len = if state.page_size > 0 {
4411 let start = state
4412 .page
4413 .saturating_mul(state.page_size)
4414 .min(state.visible_indices().len());
4415 let end =
4416 (start + state.page_size).min(state.visible_indices().len());
4417 end.saturating_sub(start)
4418 } else {
4419 state.visible_indices().len()
4420 };
4421 state.selected =
4422 (state.selected + 1).min(visible_len.saturating_sub(1));
4423 consumed_indices.push(i);
4424 }
4425 KeyCode::PageUp => {
4426 let old_page = state.page;
4427 state.prev_page();
4428 if state.page != old_page {
4429 state.selected = 0;
4430 }
4431 consumed_indices.push(i);
4432 }
4433 KeyCode::PageDown => {
4434 let old_page = state.page;
4435 state.next_page();
4436 if state.page != old_page {
4437 state.selected = 0;
4438 }
4439 consumed_indices.push(i);
4440 }
4441 _ => {}
4442 }
4443 }
4444 }
4445 for index in consumed_indices {
4446 self.consumed[index] = true;
4447 }
4448 }
4449
4450 if !state.visible_indices().is_empty() || !state.headers.is_empty() {
4451 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4452 for (i, event) in self.events.iter().enumerate() {
4453 if self.consumed[i] {
4454 continue;
4455 }
4456 if let Event::Mouse(mouse) = event {
4457 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4458 continue;
4459 }
4460 let in_bounds = mouse.x >= rect.x
4461 && mouse.x < rect.right()
4462 && mouse.y >= rect.y
4463 && mouse.y < rect.bottom();
4464 if !in_bounds {
4465 continue;
4466 }
4467
4468 if mouse.y == rect.y {
4469 let rel_x = mouse.x.saturating_sub(rect.x);
4470 let mut x_offset = 0u32;
4471 for (col_idx, width) in state.column_widths().iter().enumerate() {
4472 if rel_x >= x_offset && rel_x < x_offset + *width {
4473 state.toggle_sort(col_idx);
4474 state.selected = 0;
4475 self.consumed[i] = true;
4476 break;
4477 }
4478 x_offset += *width;
4479 if col_idx + 1 < state.column_widths().len() {
4480 x_offset += 3;
4481 }
4482 }
4483 continue;
4484 }
4485
4486 if mouse.y < rect.y + 2 {
4487 continue;
4488 }
4489
4490 let visible_len = if state.page_size > 0 {
4491 let start = state
4492 .page
4493 .saturating_mul(state.page_size)
4494 .min(state.visible_indices().len());
4495 let end = (start + state.page_size).min(state.visible_indices().len());
4496 end.saturating_sub(start)
4497 } else {
4498 state.visible_indices().len()
4499 };
4500 let clicked_idx = (mouse.y - rect.y - 2) as usize;
4501 if clicked_idx < visible_len {
4502 state.selected = clicked_idx;
4503 self.consumed[i] = true;
4504 }
4505 }
4506 }
4507 }
4508 }
4509
4510 if state.is_dirty() {
4511 state.recompute_widths();
4512 }
4513
4514 let total_visible = state.visible_indices().len();
4515 let page_start = if state.page_size > 0 {
4516 state
4517 .page
4518 .saturating_mul(state.page_size)
4519 .min(total_visible)
4520 } else {
4521 0
4522 };
4523 let page_end = if state.page_size > 0 {
4524 (page_start + state.page_size).min(total_visible)
4525 } else {
4526 total_visible
4527 };
4528 let visible_len = page_end.saturating_sub(page_start);
4529 state.selected = state.selected.min(visible_len.saturating_sub(1));
4530
4531 self.commands.push(Command::BeginContainer {
4532 direction: Direction::Column,
4533 gap: 0,
4534 align: Align::Start,
4535 justify: Justify::Start,
4536 border: None,
4537 border_sides: BorderSides::all(),
4538 border_style: Style::new().fg(self.theme.border),
4539 bg_color: None,
4540 padding: Padding::default(),
4541 margin: Margin::default(),
4542 constraints: Constraints::default(),
4543 title: None,
4544 grow: 0,
4545 group_name: None,
4546 });
4547
4548 let header_cells = state
4549 .headers
4550 .iter()
4551 .enumerate()
4552 .map(|(i, header)| {
4553 if state.sort_column == Some(i) {
4554 if state.sort_ascending {
4555 format!("{header} ▲")
4556 } else {
4557 format!("{header} ▼")
4558 }
4559 } else {
4560 header.clone()
4561 }
4562 })
4563 .collect::<Vec<_>>();
4564 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
4565 self.styled(header_line, Style::new().bold().fg(self.theme.text));
4566
4567 let separator = state
4568 .column_widths()
4569 .iter()
4570 .map(|w| "─".repeat(*w as usize))
4571 .collect::<Vec<_>>()
4572 .join("─┼─");
4573 self.text(separator);
4574
4575 for idx in 0..visible_len {
4576 let data_idx = state.visible_indices()[page_start + idx];
4577 let Some(row) = state.rows.get(data_idx) else {
4578 continue;
4579 };
4580 let line = format_table_row(row, state.column_widths(), " │ ");
4581 if idx == state.selected {
4582 let mut style = Style::new()
4583 .bg(self.theme.selected_bg)
4584 .fg(self.theme.selected_fg);
4585 if focused {
4586 style = style.bold();
4587 }
4588 self.styled(line, style);
4589 } else {
4590 self.styled(line, Style::new().fg(self.theme.text));
4591 }
4592 }
4593
4594 if state.page_size > 0 && state.total_pages() > 1 {
4595 self.styled(
4596 format!("Page {}/{}", state.page + 1, state.total_pages()),
4597 Style::new().dim().fg(self.theme.text_dim),
4598 );
4599 }
4600
4601 self.commands.push(Command::EndContainer);
4602 self.last_text_idx = None;
4603
4604 self
4605 }
4606
4607 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
4612 if state.labels.is_empty() {
4613 state.selected = 0;
4614 return self;
4615 }
4616
4617 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
4618 let focused = self.register_focusable();
4619 let interaction_id = self.interaction_count;
4620
4621 if focused {
4622 let mut consumed_indices = Vec::new();
4623 for (i, event) in self.events.iter().enumerate() {
4624 if let Event::Key(key) = event {
4625 if key.kind != KeyEventKind::Press {
4626 continue;
4627 }
4628 match key.code {
4629 KeyCode::Left => {
4630 state.selected = if state.selected == 0 {
4631 state.labels.len().saturating_sub(1)
4632 } else {
4633 state.selected - 1
4634 };
4635 consumed_indices.push(i);
4636 }
4637 KeyCode::Right => {
4638 state.selected = (state.selected + 1) % state.labels.len();
4639 consumed_indices.push(i);
4640 }
4641 _ => {}
4642 }
4643 }
4644 }
4645
4646 for index in consumed_indices {
4647 self.consumed[index] = true;
4648 }
4649 }
4650
4651 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4652 for (i, event) in self.events.iter().enumerate() {
4653 if self.consumed[i] {
4654 continue;
4655 }
4656 if let Event::Mouse(mouse) = event {
4657 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4658 continue;
4659 }
4660 let in_bounds = mouse.x >= rect.x
4661 && mouse.x < rect.right()
4662 && mouse.y >= rect.y
4663 && mouse.y < rect.bottom();
4664 if !in_bounds {
4665 continue;
4666 }
4667
4668 let mut x_offset = 0u32;
4669 let rel_x = mouse.x - rect.x;
4670 for (idx, label) in state.labels.iter().enumerate() {
4671 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
4672 if rel_x >= x_offset && rel_x < x_offset + tab_width {
4673 state.selected = idx;
4674 self.consumed[i] = true;
4675 break;
4676 }
4677 x_offset += tab_width + 1;
4678 }
4679 }
4680 }
4681 }
4682
4683 self.interaction_count += 1;
4684 self.commands.push(Command::BeginContainer {
4685 direction: Direction::Row,
4686 gap: 1,
4687 align: Align::Start,
4688 justify: Justify::Start,
4689 border: None,
4690 border_sides: BorderSides::all(),
4691 border_style: Style::new().fg(self.theme.border),
4692 bg_color: None,
4693 padding: Padding::default(),
4694 margin: Margin::default(),
4695 constraints: Constraints::default(),
4696 title: None,
4697 grow: 0,
4698 group_name: None,
4699 });
4700 for (idx, label) in state.labels.iter().enumerate() {
4701 let style = if idx == state.selected {
4702 let s = Style::new().fg(self.theme.primary).bold();
4703 if focused {
4704 s.underline()
4705 } else {
4706 s
4707 }
4708 } else {
4709 Style::new().fg(self.theme.text_dim)
4710 };
4711 self.styled(format!("[ {label} ]"), style);
4712 }
4713 self.commands.push(Command::EndContainer);
4714 self.last_text_idx = None;
4715
4716 self
4717 }
4718
4719 pub fn button(&mut self, label: impl Into<String>) -> bool {
4724 let focused = self.register_focusable();
4725 let interaction_id = self.interaction_count;
4726 self.interaction_count += 1;
4727 let response = self.response_for(interaction_id);
4728
4729 let mut activated = response.clicked;
4730 if focused {
4731 let mut consumed_indices = Vec::new();
4732 for (i, event) in self.events.iter().enumerate() {
4733 if let Event::Key(key) = event {
4734 if key.kind != KeyEventKind::Press {
4735 continue;
4736 }
4737 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4738 activated = true;
4739 consumed_indices.push(i);
4740 }
4741 }
4742 }
4743
4744 for index in consumed_indices {
4745 self.consumed[index] = true;
4746 }
4747 }
4748
4749 let hovered = response.hovered;
4750 let style = if focused {
4751 Style::new().fg(self.theme.primary).bold()
4752 } else if hovered {
4753 Style::new().fg(self.theme.accent)
4754 } else {
4755 Style::new().fg(self.theme.text)
4756 };
4757 let hover_bg = if hovered || focused {
4758 Some(self.theme.surface_hover)
4759 } else {
4760 None
4761 };
4762
4763 self.commands.push(Command::BeginContainer {
4764 direction: Direction::Row,
4765 gap: 0,
4766 align: Align::Start,
4767 justify: Justify::Start,
4768 border: None,
4769 border_sides: BorderSides::all(),
4770 border_style: Style::new().fg(self.theme.border),
4771 bg_color: hover_bg,
4772 padding: Padding::default(),
4773 margin: Margin::default(),
4774 constraints: Constraints::default(),
4775 title: None,
4776 grow: 0,
4777 group_name: None,
4778 });
4779 self.styled(format!("[ {} ]", label.into()), style);
4780 self.commands.push(Command::EndContainer);
4781 self.last_text_idx = None;
4782
4783 activated
4784 }
4785
4786 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
4791 let focused = self.register_focusable();
4792 let interaction_id = self.interaction_count;
4793 self.interaction_count += 1;
4794 let response = self.response_for(interaction_id);
4795
4796 let mut activated = response.clicked;
4797 if focused {
4798 let mut consumed_indices = Vec::new();
4799 for (i, event) in self.events.iter().enumerate() {
4800 if let Event::Key(key) = event {
4801 if key.kind != KeyEventKind::Press {
4802 continue;
4803 }
4804 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4805 activated = true;
4806 consumed_indices.push(i);
4807 }
4808 }
4809 }
4810 for index in consumed_indices {
4811 self.consumed[index] = true;
4812 }
4813 }
4814
4815 let label = label.into();
4816 let hover_bg = if response.hovered || focused {
4817 Some(self.theme.surface_hover)
4818 } else {
4819 None
4820 };
4821 let (text, style, bg_color, border) = match variant {
4822 ButtonVariant::Default => {
4823 let style = if focused {
4824 Style::new().fg(self.theme.primary).bold()
4825 } else if response.hovered {
4826 Style::new().fg(self.theme.accent)
4827 } else {
4828 Style::new().fg(self.theme.text)
4829 };
4830 (format!("[ {label} ]"), style, hover_bg, None)
4831 }
4832 ButtonVariant::Primary => {
4833 let style = if focused {
4834 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
4835 } else if response.hovered {
4836 Style::new().fg(self.theme.bg).bg(self.theme.accent)
4837 } else {
4838 Style::new().fg(self.theme.bg).bg(self.theme.primary)
4839 };
4840 (format!(" {label} "), style, hover_bg, None)
4841 }
4842 ButtonVariant::Danger => {
4843 let style = if focused {
4844 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
4845 } else if response.hovered {
4846 Style::new().fg(self.theme.bg).bg(self.theme.warning)
4847 } else {
4848 Style::new().fg(self.theme.bg).bg(self.theme.error)
4849 };
4850 (format!(" {label} "), style, hover_bg, None)
4851 }
4852 ButtonVariant::Outline => {
4853 let border_color = if focused {
4854 self.theme.primary
4855 } else if response.hovered {
4856 self.theme.accent
4857 } else {
4858 self.theme.border
4859 };
4860 let style = if focused {
4861 Style::new().fg(self.theme.primary).bold()
4862 } else if response.hovered {
4863 Style::new().fg(self.theme.accent)
4864 } else {
4865 Style::new().fg(self.theme.text)
4866 };
4867 (
4868 format!(" {label} "),
4869 style,
4870 hover_bg,
4871 Some((Border::Rounded, Style::new().fg(border_color))),
4872 )
4873 }
4874 };
4875
4876 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
4877 self.commands.push(Command::BeginContainer {
4878 direction: Direction::Row,
4879 gap: 0,
4880 align: Align::Center,
4881 justify: Justify::Center,
4882 border: if border.is_some() {
4883 Some(btn_border)
4884 } else {
4885 None
4886 },
4887 border_sides: BorderSides::all(),
4888 border_style: btn_border_style,
4889 bg_color,
4890 padding: Padding::default(),
4891 margin: Margin::default(),
4892 constraints: Constraints::default(),
4893 title: None,
4894 grow: 0,
4895 group_name: None,
4896 });
4897 self.styled(text, style);
4898 self.commands.push(Command::EndContainer);
4899 self.last_text_idx = None;
4900
4901 activated
4902 }
4903
4904 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
4909 let focused = self.register_focusable();
4910 let interaction_id = self.interaction_count;
4911 self.interaction_count += 1;
4912 let response = self.response_for(interaction_id);
4913 let mut should_toggle = response.clicked;
4914
4915 if focused {
4916 let mut consumed_indices = Vec::new();
4917 for (i, event) in self.events.iter().enumerate() {
4918 if let Event::Key(key) = event {
4919 if key.kind != KeyEventKind::Press {
4920 continue;
4921 }
4922 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4923 should_toggle = true;
4924 consumed_indices.push(i);
4925 }
4926 }
4927 }
4928
4929 for index in consumed_indices {
4930 self.consumed[index] = true;
4931 }
4932 }
4933
4934 if should_toggle {
4935 *checked = !*checked;
4936 }
4937
4938 let hover_bg = if response.hovered || focused {
4939 Some(self.theme.surface_hover)
4940 } else {
4941 None
4942 };
4943 self.commands.push(Command::BeginContainer {
4944 direction: Direction::Row,
4945 gap: 1,
4946 align: Align::Start,
4947 justify: Justify::Start,
4948 border: None,
4949 border_sides: BorderSides::all(),
4950 border_style: Style::new().fg(self.theme.border),
4951 bg_color: hover_bg,
4952 padding: Padding::default(),
4953 margin: Margin::default(),
4954 constraints: Constraints::default(),
4955 title: None,
4956 grow: 0,
4957 group_name: None,
4958 });
4959 let marker_style = if *checked {
4960 Style::new().fg(self.theme.success)
4961 } else {
4962 Style::new().fg(self.theme.text_dim)
4963 };
4964 let marker = if *checked { "[x]" } else { "[ ]" };
4965 let label_text = label.into();
4966 if focused {
4967 self.styled(format!("▸ {marker}"), marker_style.bold());
4968 self.styled(label_text, Style::new().fg(self.theme.text).bold());
4969 } else {
4970 self.styled(marker, marker_style);
4971 self.styled(label_text, Style::new().fg(self.theme.text));
4972 }
4973 self.commands.push(Command::EndContainer);
4974 self.last_text_idx = None;
4975
4976 self
4977 }
4978
4979 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
4985 let focused = self.register_focusable();
4986 let interaction_id = self.interaction_count;
4987 self.interaction_count += 1;
4988 let response = self.response_for(interaction_id);
4989 let mut should_toggle = response.clicked;
4990
4991 if focused {
4992 let mut consumed_indices = Vec::new();
4993 for (i, event) in self.events.iter().enumerate() {
4994 if let Event::Key(key) = event {
4995 if key.kind != KeyEventKind::Press {
4996 continue;
4997 }
4998 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4999 should_toggle = true;
5000 consumed_indices.push(i);
5001 }
5002 }
5003 }
5004
5005 for index in consumed_indices {
5006 self.consumed[index] = true;
5007 }
5008 }
5009
5010 if should_toggle {
5011 *on = !*on;
5012 }
5013
5014 let hover_bg = if response.hovered || focused {
5015 Some(self.theme.surface_hover)
5016 } else {
5017 None
5018 };
5019 self.commands.push(Command::BeginContainer {
5020 direction: Direction::Row,
5021 gap: 2,
5022 align: Align::Start,
5023 justify: Justify::Start,
5024 border: None,
5025 border_sides: BorderSides::all(),
5026 border_style: Style::new().fg(self.theme.border),
5027 bg_color: hover_bg,
5028 padding: Padding::default(),
5029 margin: Margin::default(),
5030 constraints: Constraints::default(),
5031 title: None,
5032 grow: 0,
5033 group_name: None,
5034 });
5035 let label_text = label.into();
5036 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
5037 let switch_style = if *on {
5038 Style::new().fg(self.theme.success)
5039 } else {
5040 Style::new().fg(self.theme.text_dim)
5041 };
5042 if focused {
5043 self.styled(
5044 format!("▸ {label_text}"),
5045 Style::new().fg(self.theme.text).bold(),
5046 );
5047 self.styled(switch, switch_style.bold());
5048 } else {
5049 self.styled(label_text, Style::new().fg(self.theme.text));
5050 self.styled(switch, switch_style);
5051 }
5052 self.commands.push(Command::EndContainer);
5053 self.last_text_idx = None;
5054
5055 self
5056 }
5057
5058 pub fn select(&mut self, state: &mut SelectState) -> bool {
5064 if state.items.is_empty() {
5065 return false;
5066 }
5067 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5068
5069 let focused = self.register_focusable();
5070 let interaction_id = self.interaction_count;
5071 self.interaction_count += 1;
5072 let response = self.response_for(interaction_id);
5073 let old_selected = state.selected;
5074
5075 if response.clicked {
5076 state.open = !state.open;
5077 if state.open {
5078 state.set_cursor(state.selected);
5079 }
5080 }
5081
5082 if focused {
5083 let mut consumed_indices = Vec::new();
5084 for (i, event) in self.events.iter().enumerate() {
5085 if self.consumed[i] {
5086 continue;
5087 }
5088 if let Event::Key(key) = event {
5089 if key.kind != KeyEventKind::Press {
5090 continue;
5091 }
5092 if state.open {
5093 match key.code {
5094 KeyCode::Up | KeyCode::Char('k') => {
5095 let c = state.cursor();
5096 state.set_cursor(c.saturating_sub(1));
5097 consumed_indices.push(i);
5098 }
5099 KeyCode::Down | KeyCode::Char('j') => {
5100 let c = state.cursor();
5101 state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
5102 consumed_indices.push(i);
5103 }
5104 KeyCode::Enter | KeyCode::Char(' ') => {
5105 state.selected = state.cursor();
5106 state.open = false;
5107 consumed_indices.push(i);
5108 }
5109 KeyCode::Esc => {
5110 state.open = false;
5111 consumed_indices.push(i);
5112 }
5113 _ => {}
5114 }
5115 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5116 state.open = true;
5117 state.set_cursor(state.selected);
5118 consumed_indices.push(i);
5119 }
5120 }
5121 }
5122 for idx in consumed_indices {
5123 self.consumed[idx] = true;
5124 }
5125 }
5126
5127 let changed = state.selected != old_selected;
5128
5129 let border_color = if focused {
5130 self.theme.primary
5131 } else {
5132 self.theme.border
5133 };
5134 let display_text = state
5135 .items
5136 .get(state.selected)
5137 .cloned()
5138 .unwrap_or_else(|| state.placeholder.clone());
5139 let arrow = if state.open { "▲" } else { "▼" };
5140
5141 self.commands.push(Command::BeginContainer {
5142 direction: Direction::Column,
5143 gap: 0,
5144 align: Align::Start,
5145 justify: Justify::Start,
5146 border: None,
5147 border_sides: BorderSides::all(),
5148 border_style: Style::new().fg(self.theme.border),
5149 bg_color: None,
5150 padding: Padding::default(),
5151 margin: Margin::default(),
5152 constraints: Constraints::default(),
5153 title: None,
5154 grow: 0,
5155 group_name: None,
5156 });
5157
5158 self.commands.push(Command::BeginContainer {
5159 direction: Direction::Row,
5160 gap: 1,
5161 align: Align::Start,
5162 justify: Justify::Start,
5163 border: Some(Border::Rounded),
5164 border_sides: BorderSides::all(),
5165 border_style: Style::new().fg(border_color),
5166 bg_color: None,
5167 padding: Padding {
5168 left: 1,
5169 right: 1,
5170 top: 0,
5171 bottom: 0,
5172 },
5173 margin: Margin::default(),
5174 constraints: Constraints::default(),
5175 title: None,
5176 grow: 0,
5177 group_name: None,
5178 });
5179 self.interaction_count += 1;
5180 self.styled(&display_text, Style::new().fg(self.theme.text));
5181 self.styled(arrow, Style::new().fg(self.theme.text_dim));
5182 self.commands.push(Command::EndContainer);
5183 self.last_text_idx = None;
5184
5185 if state.open {
5186 for (idx, item) in state.items.iter().enumerate() {
5187 let is_cursor = idx == state.cursor();
5188 let style = if is_cursor {
5189 Style::new().bold().fg(self.theme.primary)
5190 } else {
5191 Style::new().fg(self.theme.text)
5192 };
5193 let prefix = if is_cursor { "▸ " } else { " " };
5194 self.styled(format!("{prefix}{item}"), style);
5195 }
5196 }
5197
5198 self.commands.push(Command::EndContainer);
5199 self.last_text_idx = None;
5200 changed
5201 }
5202
5203 pub fn radio(&mut self, state: &mut RadioState) -> bool {
5207 if state.items.is_empty() {
5208 return false;
5209 }
5210 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5211 let focused = self.register_focusable();
5212 let old_selected = state.selected;
5213
5214 if focused {
5215 let mut consumed_indices = Vec::new();
5216 for (i, event) in self.events.iter().enumerate() {
5217 if self.consumed[i] {
5218 continue;
5219 }
5220 if let Event::Key(key) = event {
5221 if key.kind != KeyEventKind::Press {
5222 continue;
5223 }
5224 match key.code {
5225 KeyCode::Up | KeyCode::Char('k') => {
5226 state.selected = state.selected.saturating_sub(1);
5227 consumed_indices.push(i);
5228 }
5229 KeyCode::Down | KeyCode::Char('j') => {
5230 state.selected =
5231 (state.selected + 1).min(state.items.len().saturating_sub(1));
5232 consumed_indices.push(i);
5233 }
5234 KeyCode::Enter | KeyCode::Char(' ') => {
5235 consumed_indices.push(i);
5236 }
5237 _ => {}
5238 }
5239 }
5240 }
5241 for idx in consumed_indices {
5242 self.consumed[idx] = true;
5243 }
5244 }
5245
5246 let interaction_id = self.interaction_count;
5247 self.interaction_count += 1;
5248
5249 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5250 for (i, event) in self.events.iter().enumerate() {
5251 if self.consumed[i] {
5252 continue;
5253 }
5254 if let Event::Mouse(mouse) = event {
5255 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5256 continue;
5257 }
5258 let in_bounds = mouse.x >= rect.x
5259 && mouse.x < rect.right()
5260 && mouse.y >= rect.y
5261 && mouse.y < rect.bottom();
5262 if !in_bounds {
5263 continue;
5264 }
5265 let clicked_idx = (mouse.y - rect.y) as usize;
5266 if clicked_idx < state.items.len() {
5267 state.selected = clicked_idx;
5268 self.consumed[i] = true;
5269 }
5270 }
5271 }
5272 }
5273
5274 self.commands.push(Command::BeginContainer {
5275 direction: Direction::Column,
5276 gap: 0,
5277 align: Align::Start,
5278 justify: Justify::Start,
5279 border: None,
5280 border_sides: BorderSides::all(),
5281 border_style: Style::new().fg(self.theme.border),
5282 bg_color: None,
5283 padding: Padding::default(),
5284 margin: Margin::default(),
5285 constraints: Constraints::default(),
5286 title: None,
5287 grow: 0,
5288 group_name: None,
5289 });
5290
5291 for (idx, item) in state.items.iter().enumerate() {
5292 let is_selected = idx == state.selected;
5293 let marker = if is_selected { "●" } else { "○" };
5294 let style = if is_selected {
5295 if focused {
5296 Style::new().bold().fg(self.theme.primary)
5297 } else {
5298 Style::new().fg(self.theme.primary)
5299 }
5300 } else {
5301 Style::new().fg(self.theme.text)
5302 };
5303 let prefix = if focused && idx == state.selected {
5304 "▸ "
5305 } else {
5306 " "
5307 };
5308 self.styled(format!("{prefix}{marker} {item}"), style);
5309 }
5310
5311 self.commands.push(Command::EndContainer);
5312 self.last_text_idx = None;
5313 state.selected != old_selected
5314 }
5315
5316 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
5320 if state.items.is_empty() {
5321 return self;
5322 }
5323 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
5324 let focused = self.register_focusable();
5325
5326 if focused {
5327 let mut consumed_indices = Vec::new();
5328 for (i, event) in self.events.iter().enumerate() {
5329 if self.consumed[i] {
5330 continue;
5331 }
5332 if let Event::Key(key) = event {
5333 if key.kind != KeyEventKind::Press {
5334 continue;
5335 }
5336 match key.code {
5337 KeyCode::Up | KeyCode::Char('k') => {
5338 state.cursor = state.cursor.saturating_sub(1);
5339 consumed_indices.push(i);
5340 }
5341 KeyCode::Down | KeyCode::Char('j') => {
5342 state.cursor =
5343 (state.cursor + 1).min(state.items.len().saturating_sub(1));
5344 consumed_indices.push(i);
5345 }
5346 KeyCode::Char(' ') | KeyCode::Enter => {
5347 state.toggle(state.cursor);
5348 consumed_indices.push(i);
5349 }
5350 _ => {}
5351 }
5352 }
5353 }
5354 for idx in consumed_indices {
5355 self.consumed[idx] = true;
5356 }
5357 }
5358
5359 let interaction_id = self.interaction_count;
5360 self.interaction_count += 1;
5361
5362 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5363 for (i, event) in self.events.iter().enumerate() {
5364 if self.consumed[i] {
5365 continue;
5366 }
5367 if let Event::Mouse(mouse) = event {
5368 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5369 continue;
5370 }
5371 let in_bounds = mouse.x >= rect.x
5372 && mouse.x < rect.right()
5373 && mouse.y >= rect.y
5374 && mouse.y < rect.bottom();
5375 if !in_bounds {
5376 continue;
5377 }
5378 let clicked_idx = (mouse.y - rect.y) as usize;
5379 if clicked_idx < state.items.len() {
5380 state.toggle(clicked_idx);
5381 state.cursor = clicked_idx;
5382 self.consumed[i] = true;
5383 }
5384 }
5385 }
5386 }
5387
5388 self.commands.push(Command::BeginContainer {
5389 direction: Direction::Column,
5390 gap: 0,
5391 align: Align::Start,
5392 justify: Justify::Start,
5393 border: None,
5394 border_sides: BorderSides::all(),
5395 border_style: Style::new().fg(self.theme.border),
5396 bg_color: None,
5397 padding: Padding::default(),
5398 margin: Margin::default(),
5399 constraints: Constraints::default(),
5400 title: None,
5401 grow: 0,
5402 group_name: None,
5403 });
5404
5405 for (idx, item) in state.items.iter().enumerate() {
5406 let checked = state.selected.contains(&idx);
5407 let marker = if checked { "[x]" } else { "[ ]" };
5408 let is_cursor = idx == state.cursor;
5409 let style = if is_cursor && focused {
5410 Style::new().bold().fg(self.theme.primary)
5411 } else if checked {
5412 Style::new().fg(self.theme.success)
5413 } else {
5414 Style::new().fg(self.theme.text)
5415 };
5416 let prefix = if is_cursor && focused { "▸ " } else { " " };
5417 self.styled(format!("{prefix}{marker} {item}"), style);
5418 }
5419
5420 self.commands.push(Command::EndContainer);
5421 self.last_text_idx = None;
5422 self
5423 }
5424
5425 pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
5429 let entries = state.flatten();
5430 if entries.is_empty() {
5431 return self;
5432 }
5433 state.selected = state.selected.min(entries.len().saturating_sub(1));
5434 let focused = self.register_focusable();
5435
5436 if focused {
5437 let mut consumed_indices = Vec::new();
5438 for (i, event) in self.events.iter().enumerate() {
5439 if self.consumed[i] {
5440 continue;
5441 }
5442 if let Event::Key(key) = event {
5443 if key.kind != KeyEventKind::Press {
5444 continue;
5445 }
5446 match key.code {
5447 KeyCode::Up | KeyCode::Char('k') => {
5448 state.selected = state.selected.saturating_sub(1);
5449 consumed_indices.push(i);
5450 }
5451 KeyCode::Down | KeyCode::Char('j') => {
5452 let max = state.flatten().len().saturating_sub(1);
5453 state.selected = (state.selected + 1).min(max);
5454 consumed_indices.push(i);
5455 }
5456 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
5457 state.toggle_at(state.selected);
5458 consumed_indices.push(i);
5459 }
5460 KeyCode::Left => {
5461 let entry = &entries[state.selected.min(entries.len() - 1)];
5462 if entry.expanded {
5463 state.toggle_at(state.selected);
5464 }
5465 consumed_indices.push(i);
5466 }
5467 _ => {}
5468 }
5469 }
5470 }
5471 for idx in consumed_indices {
5472 self.consumed[idx] = true;
5473 }
5474 }
5475
5476 self.interaction_count += 1;
5477 self.commands.push(Command::BeginContainer {
5478 direction: Direction::Column,
5479 gap: 0,
5480 align: Align::Start,
5481 justify: Justify::Start,
5482 border: None,
5483 border_sides: BorderSides::all(),
5484 border_style: Style::new().fg(self.theme.border),
5485 bg_color: None,
5486 padding: Padding::default(),
5487 margin: Margin::default(),
5488 constraints: Constraints::default(),
5489 title: None,
5490 grow: 0,
5491 group_name: None,
5492 });
5493
5494 let entries = state.flatten();
5495 for (idx, entry) in entries.iter().enumerate() {
5496 let indent = " ".repeat(entry.depth);
5497 let icon = if entry.is_leaf {
5498 " "
5499 } else if entry.expanded {
5500 "▾ "
5501 } else {
5502 "▸ "
5503 };
5504 let is_selected = idx == state.selected;
5505 let style = if is_selected && focused {
5506 Style::new().bold().fg(self.theme.primary)
5507 } else if is_selected {
5508 Style::new().fg(self.theme.primary)
5509 } else {
5510 Style::new().fg(self.theme.text)
5511 };
5512 let cursor = if is_selected && focused { "▸" } else { " " };
5513 self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
5514 }
5515
5516 self.commands.push(Command::EndContainer);
5517 self.last_text_idx = None;
5518 self
5519 }
5520
5521 pub fn virtual_list(
5528 &mut self,
5529 state: &mut ListState,
5530 visible_height: usize,
5531 f: impl Fn(&mut Context, usize),
5532 ) -> &mut Self {
5533 if state.items.is_empty() {
5534 return self;
5535 }
5536 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5537 let focused = self.register_focusable();
5538
5539 if focused {
5540 let mut consumed_indices = Vec::new();
5541 for (i, event) in self.events.iter().enumerate() {
5542 if self.consumed[i] {
5543 continue;
5544 }
5545 if let Event::Key(key) = event {
5546 if key.kind != KeyEventKind::Press {
5547 continue;
5548 }
5549 match key.code {
5550 KeyCode::Up | KeyCode::Char('k') => {
5551 state.selected = state.selected.saturating_sub(1);
5552 consumed_indices.push(i);
5553 }
5554 KeyCode::Down | KeyCode::Char('j') => {
5555 state.selected =
5556 (state.selected + 1).min(state.items.len().saturating_sub(1));
5557 consumed_indices.push(i);
5558 }
5559 KeyCode::PageUp => {
5560 state.selected = state.selected.saturating_sub(visible_height);
5561 consumed_indices.push(i);
5562 }
5563 KeyCode::PageDown => {
5564 state.selected = (state.selected + visible_height)
5565 .min(state.items.len().saturating_sub(1));
5566 consumed_indices.push(i);
5567 }
5568 KeyCode::Home => {
5569 state.selected = 0;
5570 consumed_indices.push(i);
5571 }
5572 KeyCode::End => {
5573 state.selected = state.items.len().saturating_sub(1);
5574 consumed_indices.push(i);
5575 }
5576 _ => {}
5577 }
5578 }
5579 }
5580 for idx in consumed_indices {
5581 self.consumed[idx] = true;
5582 }
5583 }
5584
5585 let start = if state.selected >= visible_height {
5586 state.selected - visible_height + 1
5587 } else {
5588 0
5589 };
5590 let end = (start + visible_height).min(state.items.len());
5591
5592 self.interaction_count += 1;
5593 self.commands.push(Command::BeginContainer {
5594 direction: Direction::Column,
5595 gap: 0,
5596 align: Align::Start,
5597 justify: Justify::Start,
5598 border: None,
5599 border_sides: BorderSides::all(),
5600 border_style: Style::new().fg(self.theme.border),
5601 bg_color: None,
5602 padding: Padding::default(),
5603 margin: Margin::default(),
5604 constraints: Constraints::default(),
5605 title: None,
5606 grow: 0,
5607 group_name: None,
5608 });
5609
5610 if start > 0 {
5611 self.styled(
5612 format!(" ↑ {} more", start),
5613 Style::new().fg(self.theme.text_dim).dim(),
5614 );
5615 }
5616
5617 for idx in start..end {
5618 f(self, idx);
5619 }
5620
5621 let remaining = state.items.len().saturating_sub(end);
5622 if remaining > 0 {
5623 self.styled(
5624 format!(" ↓ {} more", remaining),
5625 Style::new().fg(self.theme.text_dim).dim(),
5626 );
5627 }
5628
5629 self.commands.push(Command::EndContainer);
5630 self.last_text_idx = None;
5631 self
5632 }
5633
5634 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
5638 if !state.open {
5639 return None;
5640 }
5641
5642 let filtered = state.filtered_indices();
5643 let sel = state.selected().min(filtered.len().saturating_sub(1));
5644 state.set_selected(sel);
5645
5646 let mut consumed_indices = Vec::new();
5647 let mut result: Option<usize> = None;
5648
5649 for (i, event) in self.events.iter().enumerate() {
5650 if self.consumed[i] {
5651 continue;
5652 }
5653 if let Event::Key(key) = event {
5654 if key.kind != KeyEventKind::Press {
5655 continue;
5656 }
5657 match key.code {
5658 KeyCode::Esc => {
5659 state.open = false;
5660 consumed_indices.push(i);
5661 }
5662 KeyCode::Up => {
5663 let s = state.selected();
5664 state.set_selected(s.saturating_sub(1));
5665 consumed_indices.push(i);
5666 }
5667 KeyCode::Down => {
5668 let s = state.selected();
5669 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
5670 consumed_indices.push(i);
5671 }
5672 KeyCode::Enter => {
5673 if let Some(&cmd_idx) = filtered.get(state.selected()) {
5674 result = Some(cmd_idx);
5675 state.open = false;
5676 }
5677 consumed_indices.push(i);
5678 }
5679 KeyCode::Backspace => {
5680 if state.cursor > 0 {
5681 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
5682 let end_idx = byte_index_for_char(&state.input, state.cursor);
5683 state.input.replace_range(byte_idx..end_idx, "");
5684 state.cursor -= 1;
5685 state.set_selected(0);
5686 }
5687 consumed_indices.push(i);
5688 }
5689 KeyCode::Char(ch) => {
5690 let byte_idx = byte_index_for_char(&state.input, state.cursor);
5691 state.input.insert(byte_idx, ch);
5692 state.cursor += 1;
5693 state.set_selected(0);
5694 consumed_indices.push(i);
5695 }
5696 _ => {}
5697 }
5698 }
5699 }
5700 for idx in consumed_indices {
5701 self.consumed[idx] = true;
5702 }
5703
5704 let filtered = state.filtered_indices();
5705
5706 self.modal(|ui| {
5707 let primary = ui.theme.primary;
5708 ui.container()
5709 .border(Border::Rounded)
5710 .border_style(Style::new().fg(primary))
5711 .pad(1)
5712 .max_w(60)
5713 .col(|ui| {
5714 let border_color = ui.theme.primary;
5715 ui.bordered(Border::Rounded)
5716 .border_style(Style::new().fg(border_color))
5717 .px(1)
5718 .col(|ui| {
5719 let display = if state.input.is_empty() {
5720 "Type to search...".to_string()
5721 } else {
5722 state.input.clone()
5723 };
5724 let style = if state.input.is_empty() {
5725 Style::new().dim().fg(ui.theme.text_dim)
5726 } else {
5727 Style::new().fg(ui.theme.text)
5728 };
5729 ui.styled(display, style);
5730 });
5731
5732 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
5733 let cmd = &state.commands[cmd_idx];
5734 let is_selected = list_idx == state.selected();
5735 let style = if is_selected {
5736 Style::new().bold().fg(ui.theme.primary)
5737 } else {
5738 Style::new().fg(ui.theme.text)
5739 };
5740 let prefix = if is_selected { "▸ " } else { " " };
5741 let shortcut_text = cmd
5742 .shortcut
5743 .as_deref()
5744 .map(|s| format!(" ({s})"))
5745 .unwrap_or_default();
5746 ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
5747 if is_selected && !cmd.description.is_empty() {
5748 ui.styled(
5749 format!(" {}", cmd.description),
5750 Style::new().dim().fg(ui.theme.text_dim),
5751 );
5752 }
5753 }
5754
5755 if filtered.is_empty() {
5756 ui.styled(
5757 " No matching commands",
5758 Style::new().dim().fg(ui.theme.text_dim),
5759 );
5760 }
5761 });
5762 });
5763
5764 result
5765 }
5766
5767 pub fn markdown(&mut self, text: &str) -> &mut Self {
5774 self.commands.push(Command::BeginContainer {
5775 direction: Direction::Column,
5776 gap: 0,
5777 align: Align::Start,
5778 justify: Justify::Start,
5779 border: None,
5780 border_sides: BorderSides::all(),
5781 border_style: Style::new().fg(self.theme.border),
5782 bg_color: None,
5783 padding: Padding::default(),
5784 margin: Margin::default(),
5785 constraints: Constraints::default(),
5786 title: None,
5787 grow: 0,
5788 group_name: None,
5789 });
5790 self.interaction_count += 1;
5791
5792 let text_style = Style::new().fg(self.theme.text);
5793 let bold_style = Style::new().fg(self.theme.text).bold();
5794 let code_style = Style::new().fg(self.theme.accent);
5795
5796 for line in text.lines() {
5797 let trimmed = line.trim();
5798 if trimmed.is_empty() {
5799 self.text(" ");
5800 continue;
5801 }
5802 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
5803 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
5804 continue;
5805 }
5806 if let Some(heading) = trimmed.strip_prefix("### ") {
5807 self.styled(heading, Style::new().bold().fg(self.theme.accent));
5808 } else if let Some(heading) = trimmed.strip_prefix("## ") {
5809 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
5810 } else if let Some(heading) = trimmed.strip_prefix("# ") {
5811 self.styled(heading, Style::new().bold().fg(self.theme.primary));
5812 } else if let Some(item) = trimmed
5813 .strip_prefix("- ")
5814 .or_else(|| trimmed.strip_prefix("* "))
5815 {
5816 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
5817 if segs.len() <= 1 {
5818 self.styled(format!(" • {item}"), text_style);
5819 } else {
5820 self.line(|ui| {
5821 ui.styled(" • ", text_style);
5822 for (s, st) in segs {
5823 ui.styled(s, st);
5824 }
5825 });
5826 }
5827 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
5828 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
5829 if parts.len() == 2 {
5830 let segs =
5831 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
5832 if segs.len() <= 1 {
5833 self.styled(format!(" {}. {}", parts[0], parts[1]), text_style);
5834 } else {
5835 self.line(|ui| {
5836 ui.styled(format!(" {}. ", parts[0]), text_style);
5837 for (s, st) in segs {
5838 ui.styled(s, st);
5839 }
5840 });
5841 }
5842 } else {
5843 self.text(trimmed);
5844 }
5845 } else if let Some(code) = trimmed.strip_prefix("```") {
5846 let _ = code;
5847 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
5848 } else {
5849 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
5850 if segs.len() <= 1 {
5851 self.styled(trimmed, text_style);
5852 } else {
5853 self.line(|ui| {
5854 for (s, st) in segs {
5855 ui.styled(s, st);
5856 }
5857 });
5858 }
5859 }
5860 }
5861
5862 self.commands.push(Command::EndContainer);
5863 self.last_text_idx = None;
5864 self
5865 }
5866
5867 fn parse_inline_segments(
5868 text: &str,
5869 base: Style,
5870 bold: Style,
5871 code: Style,
5872 ) -> Vec<(String, Style)> {
5873 let mut segments: Vec<(String, Style)> = Vec::new();
5874 let mut current = String::new();
5875 let chars: Vec<char> = text.chars().collect();
5876 let mut i = 0;
5877 while i < chars.len() {
5878 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
5879 if let Some(end) = text[i + 2..].find("**") {
5880 if !current.is_empty() {
5881 segments.push((std::mem::take(&mut current), base));
5882 }
5883 segments.push((text[i + 2..i + 2 + end].to_string(), bold));
5884 i += 4 + end;
5885 continue;
5886 }
5887 }
5888 if chars[i] == '*'
5889 && (i + 1 >= chars.len() || chars[i + 1] != '*')
5890 && (i == 0 || chars[i - 1] != '*')
5891 {
5892 if let Some(end) = text[i + 1..].find('*') {
5893 if !current.is_empty() {
5894 segments.push((std::mem::take(&mut current), base));
5895 }
5896 segments.push((text[i + 1..i + 1 + end].to_string(), base.italic()));
5897 i += 2 + end;
5898 continue;
5899 }
5900 }
5901 if chars[i] == '`' {
5902 if let Some(end) = text[i + 1..].find('`') {
5903 if !current.is_empty() {
5904 segments.push((std::mem::take(&mut current), base));
5905 }
5906 segments.push((text[i + 1..i + 1 + end].to_string(), code));
5907 i += 2 + end;
5908 continue;
5909 }
5910 }
5911 current.push(chars[i]);
5912 i += 1;
5913 }
5914 if !current.is_empty() {
5915 segments.push((current, base));
5916 }
5917 segments
5918 }
5919
5920 pub fn key_seq(&self, seq: &str) -> bool {
5927 if seq.is_empty() {
5928 return false;
5929 }
5930 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
5931 return false;
5932 }
5933 let target: Vec<char> = seq.chars().collect();
5934 let mut matched = 0;
5935 for (i, event) in self.events.iter().enumerate() {
5936 if self.consumed[i] {
5937 continue;
5938 }
5939 if let Event::Key(key) = event {
5940 if key.kind != KeyEventKind::Press {
5941 continue;
5942 }
5943 if let KeyCode::Char(c) = key.code {
5944 if c == target[matched] {
5945 matched += 1;
5946 if matched == target.len() {
5947 return true;
5948 }
5949 } else {
5950 matched = 0;
5951 if c == target[0] {
5952 matched = 1;
5953 }
5954 }
5955 }
5956 }
5957 }
5958 false
5959 }
5960
5961 pub fn separator(&mut self) -> &mut Self {
5966 self.commands.push(Command::Text {
5967 content: "─".repeat(200),
5968 style: Style::new().fg(self.theme.border).dim(),
5969 grow: 0,
5970 align: Align::Start,
5971 wrap: false,
5972 margin: Margin::default(),
5973 constraints: Constraints::default(),
5974 });
5975 self.last_text_idx = Some(self.commands.len() - 1);
5976 self
5977 }
5978
5979 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
5985 if bindings.is_empty() {
5986 return self;
5987 }
5988
5989 self.interaction_count += 1;
5990 self.commands.push(Command::BeginContainer {
5991 direction: Direction::Row,
5992 gap: 2,
5993 align: Align::Start,
5994 justify: Justify::Start,
5995 border: None,
5996 border_sides: BorderSides::all(),
5997 border_style: Style::new().fg(self.theme.border),
5998 bg_color: None,
5999 padding: Padding::default(),
6000 margin: Margin::default(),
6001 constraints: Constraints::default(),
6002 title: None,
6003 grow: 0,
6004 group_name: None,
6005 });
6006 for (idx, (key, action)) in bindings.iter().enumerate() {
6007 if idx > 0 {
6008 self.styled("·", Style::new().fg(self.theme.text_dim));
6009 }
6010 self.styled(*key, Style::new().bold().fg(self.theme.primary));
6011 self.styled(*action, Style::new().fg(self.theme.text_dim));
6012 }
6013 self.commands.push(Command::EndContainer);
6014 self.last_text_idx = None;
6015
6016 self
6017 }
6018
6019 pub fn key(&self, c: char) -> bool {
6025 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6026 return false;
6027 }
6028 self.events.iter().enumerate().any(|(i, e)| {
6029 !self.consumed[i]
6030 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
6031 })
6032 }
6033
6034 pub fn key_code(&self, code: KeyCode) -> bool {
6038 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6039 return false;
6040 }
6041 self.events.iter().enumerate().any(|(i, e)| {
6042 !self.consumed[i]
6043 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
6044 })
6045 }
6046
6047 pub fn key_release(&self, c: char) -> bool {
6051 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6052 return false;
6053 }
6054 self.events.iter().enumerate().any(|(i, e)| {
6055 !self.consumed[i]
6056 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
6057 })
6058 }
6059
6060 pub fn key_code_release(&self, code: KeyCode) -> bool {
6064 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6065 return false;
6066 }
6067 self.events.iter().enumerate().any(|(i, e)| {
6068 !self.consumed[i]
6069 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
6070 })
6071 }
6072
6073 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
6077 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6078 return false;
6079 }
6080 self.events.iter().enumerate().any(|(i, e)| {
6081 !self.consumed[i]
6082 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
6083 })
6084 }
6085
6086 pub fn mouse_down(&self) -> Option<(u32, u32)> {
6090 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6091 return None;
6092 }
6093 self.events.iter().enumerate().find_map(|(i, event)| {
6094 if self.consumed[i] {
6095 return None;
6096 }
6097 if let Event::Mouse(mouse) = event {
6098 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
6099 return Some((mouse.x, mouse.y));
6100 }
6101 }
6102 None
6103 })
6104 }
6105
6106 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
6111 self.mouse_pos
6112 }
6113
6114 pub fn paste(&self) -> Option<&str> {
6116 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6117 return None;
6118 }
6119 self.events.iter().enumerate().find_map(|(i, event)| {
6120 if self.consumed[i] {
6121 return None;
6122 }
6123 if let Event::Paste(ref text) = event {
6124 return Some(text.as_str());
6125 }
6126 None
6127 })
6128 }
6129
6130 pub fn scroll_up(&self) -> bool {
6132 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6133 return false;
6134 }
6135 self.events.iter().enumerate().any(|(i, event)| {
6136 !self.consumed[i]
6137 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
6138 })
6139 }
6140
6141 pub fn scroll_down(&self) -> bool {
6143 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6144 return false;
6145 }
6146 self.events.iter().enumerate().any(|(i, event)| {
6147 !self.consumed[i]
6148 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
6149 })
6150 }
6151
6152 pub fn quit(&mut self) {
6154 self.should_quit = true;
6155 }
6156
6157 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
6165 self.clipboard_text = Some(text.into());
6166 }
6167
6168 pub fn theme(&self) -> &Theme {
6170 &self.theme
6171 }
6172
6173 pub fn set_theme(&mut self, theme: Theme) {
6177 self.theme = theme;
6178 }
6179
6180 pub fn is_dark_mode(&self) -> bool {
6182 self.dark_mode
6183 }
6184
6185 pub fn set_dark_mode(&mut self, dark: bool) {
6187 self.dark_mode = dark;
6188 }
6189
6190 pub fn width(&self) -> u32 {
6194 self.area_width
6195 }
6196
6197 pub fn breakpoint(&self) -> Breakpoint {
6221 let w = self.area_width;
6222 if w < 40 {
6223 Breakpoint::Xs
6224 } else if w < 80 {
6225 Breakpoint::Sm
6226 } else if w < 120 {
6227 Breakpoint::Md
6228 } else if w < 160 {
6229 Breakpoint::Lg
6230 } else {
6231 Breakpoint::Xl
6232 }
6233 }
6234
6235 pub fn height(&self) -> u32 {
6237 self.area_height
6238 }
6239
6240 pub fn tick(&self) -> u64 {
6245 self.tick
6246 }
6247
6248 pub fn debug_enabled(&self) -> bool {
6252 self.debug
6253 }
6254}
6255
6256#[inline]
6257fn byte_index_for_char(value: &str, char_index: usize) -> usize {
6258 if char_index == 0 {
6259 return 0;
6260 }
6261 value
6262 .char_indices()
6263 .nth(char_index)
6264 .map_or(value.len(), |(idx, _)| idx)
6265}
6266
6267fn format_token_count(count: usize) -> String {
6268 if count >= 1_000_000 {
6269 format!("{:.1}M", count as f64 / 1_000_000.0)
6270 } else if count >= 1_000 {
6271 format!("{:.1}k", count as f64 / 1_000.0)
6272 } else {
6273 format!("{count}")
6274 }
6275}
6276
6277fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
6278 let mut parts: Vec<String> = Vec::new();
6279 for (i, width) in widths.iter().enumerate() {
6280 let cell = cells.get(i).map(String::as_str).unwrap_or("");
6281 let cell_width = UnicodeWidthStr::width(cell) as u32;
6282 let padding = (*width).saturating_sub(cell_width) as usize;
6283 parts.push(format!("{cell}{}", " ".repeat(padding)));
6284 }
6285 parts.join(separator)
6286}
6287
6288fn format_compact_number(value: f64) -> String {
6289 if value.fract().abs() < f64::EPSILON {
6290 return format!("{value:.0}");
6291 }
6292
6293 let mut s = format!("{value:.2}");
6294 while s.contains('.') && s.ends_with('0') {
6295 s.pop();
6296 }
6297 if s.ends_with('.') {
6298 s.pop();
6299 }
6300 s
6301}
6302
6303fn center_text(text: &str, width: usize) -> String {
6304 let text_width = UnicodeWidthStr::width(text);
6305 if text_width >= width {
6306 return text.to_string();
6307 }
6308
6309 let total = width - text_width;
6310 let left = total / 2;
6311 let right = total - left;
6312 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
6313}
6314
6315struct TextareaVLine {
6316 logical_row: usize,
6317 char_start: usize,
6318 char_count: usize,
6319}
6320
6321fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
6322 let mut out = Vec::new();
6323 for (row, line) in lines.iter().enumerate() {
6324 if line.is_empty() || wrap_width == u32::MAX {
6325 out.push(TextareaVLine {
6326 logical_row: row,
6327 char_start: 0,
6328 char_count: line.chars().count(),
6329 });
6330 continue;
6331 }
6332 let mut seg_start = 0usize;
6333 let mut seg_chars = 0usize;
6334 let mut seg_width = 0u32;
6335 for (idx, ch) in line.chars().enumerate() {
6336 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
6337 if seg_width + cw > wrap_width && seg_chars > 0 {
6338 out.push(TextareaVLine {
6339 logical_row: row,
6340 char_start: seg_start,
6341 char_count: seg_chars,
6342 });
6343 seg_start = idx;
6344 seg_chars = 0;
6345 seg_width = 0;
6346 }
6347 seg_chars += 1;
6348 seg_width += cw;
6349 }
6350 out.push(TextareaVLine {
6351 logical_row: row,
6352 char_start: seg_start,
6353 char_count: seg_chars,
6354 });
6355 }
6356 out
6357}
6358
6359fn textarea_logical_to_visual(
6360 vlines: &[TextareaVLine],
6361 logical_row: usize,
6362 logical_col: usize,
6363) -> (usize, usize) {
6364 for (i, vl) in vlines.iter().enumerate() {
6365 if vl.logical_row != logical_row {
6366 continue;
6367 }
6368 let seg_end = vl.char_start + vl.char_count;
6369 if logical_col >= vl.char_start && logical_col < seg_end {
6370 return (i, logical_col - vl.char_start);
6371 }
6372 if logical_col == seg_end {
6373 let is_last_seg = vlines
6374 .get(i + 1)
6375 .map_or(true, |next| next.logical_row != logical_row);
6376 if is_last_seg {
6377 return (i, logical_col - vl.char_start);
6378 }
6379 }
6380 }
6381 (vlines.len().saturating_sub(1), 0)
6382}
6383
6384fn textarea_visual_to_logical(
6385 vlines: &[TextareaVLine],
6386 visual_row: usize,
6387 visual_col: usize,
6388) -> (usize, usize) {
6389 if let Some(vl) = vlines.get(visual_row) {
6390 let logical_col = vl.char_start + visual_col.min(vl.char_count);
6391 (vl.logical_row, logical_col)
6392 } else {
6393 (0, 0)
6394 }
6395}
6396
6397fn open_url(url: &str) -> std::io::Result<()> {
6398 #[cfg(target_os = "macos")]
6399 {
6400 std::process::Command::new("open").arg(url).spawn()?;
6401 }
6402 #[cfg(target_os = "linux")]
6403 {
6404 std::process::Command::new("xdg-open").arg(url).spawn()?;
6405 }
6406 #[cfg(target_os = "windows")]
6407 {
6408 std::process::Command::new("cmd")
6409 .args(["/c", "start", "", url])
6410 .spawn()?;
6411 }
6412 Ok(())
6413}