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