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 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
250}
251
252type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
253
254#[must_use = "configure and finalize with .col() or .row()"]
275pub struct ContainerBuilder<'a> {
276 ctx: &'a mut Context,
277 gap: u32,
278 align: Align,
279 justify: Justify,
280 border: Option<Border>,
281 border_sides: BorderSides,
282 border_style: Style,
283 bg_color: Option<Color>,
284 dark_bg_color: Option<Color>,
285 dark_border_style: Option<Style>,
286 group_hover_bg: Option<Color>,
287 group_hover_border_style: Option<Style>,
288 group_name: Option<String>,
289 padding: Padding,
290 margin: Margin,
291 constraints: Constraints,
292 title: Option<(String, Style)>,
293 grow: u16,
294 scroll_offset: Option<u32>,
295}
296
297#[derive(Debug, Clone, Copy)]
304struct CanvasPixel {
305 bits: u32,
306 color: Color,
307}
308
309#[derive(Debug, Clone)]
311struct CanvasLabel {
312 x: usize,
313 y: usize,
314 text: String,
315 color: Color,
316}
317
318#[derive(Debug, Clone)]
320struct CanvasLayer {
321 grid: Vec<Vec<CanvasPixel>>,
322 labels: Vec<CanvasLabel>,
323}
324
325pub struct CanvasContext {
326 layers: Vec<CanvasLayer>,
327 cols: usize,
328 rows: usize,
329 px_w: usize,
330 px_h: usize,
331 current_color: Color,
332}
333
334impl CanvasContext {
335 fn new(cols: usize, rows: usize) -> Self {
336 Self {
337 layers: vec![Self::new_layer(cols, rows)],
338 cols,
339 rows,
340 px_w: cols * 2,
341 px_h: rows * 4,
342 current_color: Color::Reset,
343 }
344 }
345
346 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
347 CanvasLayer {
348 grid: vec![
349 vec![
350 CanvasPixel {
351 bits: 0,
352 color: Color::Reset,
353 };
354 cols
355 ];
356 rows
357 ],
358 labels: Vec::new(),
359 }
360 }
361
362 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
363 self.layers.last_mut()
364 }
365
366 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
367 if x >= self.px_w || y >= self.px_h {
368 return;
369 }
370
371 let char_col = x / 2;
372 let char_row = y / 4;
373 let sub_col = x % 2;
374 let sub_row = y % 4;
375 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
376 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
377
378 let bit = if sub_col == 0 {
379 LEFT_BITS[sub_row]
380 } else {
381 RIGHT_BITS[sub_row]
382 };
383
384 if let Some(layer) = self.current_layer_mut() {
385 let cell = &mut layer.grid[char_row][char_col];
386 let new_bits = cell.bits | bit;
387 if new_bits != cell.bits {
388 cell.bits = new_bits;
389 cell.color = color;
390 }
391 }
392 }
393
394 fn dot_isize(&mut self, x: isize, y: isize) {
395 if x >= 0 && y >= 0 {
396 self.dot(x as usize, y as usize);
397 }
398 }
399
400 pub fn width(&self) -> usize {
402 self.px_w
403 }
404
405 pub fn height(&self) -> usize {
407 self.px_h
408 }
409
410 pub fn dot(&mut self, x: usize, y: usize) {
412 self.dot_with_color(x, y, self.current_color);
413 }
414
415 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
417 let (mut x, mut y) = (x0 as isize, y0 as isize);
418 let (x1, y1) = (x1 as isize, y1 as isize);
419 let dx = (x1 - x).abs();
420 let dy = -(y1 - y).abs();
421 let sx = if x < x1 { 1 } else { -1 };
422 let sy = if y < y1 { 1 } else { -1 };
423 let mut err = dx + dy;
424
425 loop {
426 self.dot_isize(x, y);
427 if x == x1 && y == y1 {
428 break;
429 }
430 let e2 = 2 * err;
431 if e2 >= dy {
432 err += dy;
433 x += sx;
434 }
435 if e2 <= dx {
436 err += dx;
437 y += sy;
438 }
439 }
440 }
441
442 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
444 if w == 0 || h == 0 {
445 return;
446 }
447
448 self.line(x, y, x + w.saturating_sub(1), y);
449 self.line(
450 x + w.saturating_sub(1),
451 y,
452 x + w.saturating_sub(1),
453 y + h.saturating_sub(1),
454 );
455 self.line(
456 x + w.saturating_sub(1),
457 y + h.saturating_sub(1),
458 x,
459 y + h.saturating_sub(1),
460 );
461 self.line(x, y + h.saturating_sub(1), x, y);
462 }
463
464 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
466 let mut x = r as isize;
467 let mut y: isize = 0;
468 let mut err: isize = 1 - x;
469 let (cx, cy) = (cx as isize, cy as isize);
470
471 while x >= y {
472 for &(dx, dy) in &[
473 (x, y),
474 (y, x),
475 (-x, y),
476 (-y, x),
477 (x, -y),
478 (y, -x),
479 (-x, -y),
480 (-y, -x),
481 ] {
482 let px = cx + dx;
483 let py = cy + dy;
484 self.dot_isize(px, py);
485 }
486
487 y += 1;
488 if err < 0 {
489 err += 2 * y + 1;
490 } else {
491 x -= 1;
492 err += 2 * (y - x) + 1;
493 }
494 }
495 }
496
497 pub fn set_color(&mut self, color: Color) {
499 self.current_color = color;
500 }
501
502 pub fn color(&self) -> Color {
504 self.current_color
505 }
506
507 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
509 if w == 0 || h == 0 {
510 return;
511 }
512
513 let x_end = x.saturating_add(w).min(self.px_w);
514 let y_end = y.saturating_add(h).min(self.px_h);
515 if x >= x_end || y >= y_end {
516 return;
517 }
518
519 for yy in y..y_end {
520 self.line(x, yy, x_end.saturating_sub(1), yy);
521 }
522 }
523
524 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
526 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
527 for y in (cy - r)..=(cy + r) {
528 let dy = y - cy;
529 let span_sq = (r * r - dy * dy).max(0);
530 let dx = (span_sq as f64).sqrt() as isize;
531 for x in (cx - dx)..=(cx + dx) {
532 self.dot_isize(x, y);
533 }
534 }
535 }
536
537 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
539 self.line(x0, y0, x1, y1);
540 self.line(x1, y1, x2, y2);
541 self.line(x2, y2, x0, y0);
542 }
543
544 pub fn filled_triangle(
546 &mut self,
547 x0: usize,
548 y0: usize,
549 x1: usize,
550 y1: usize,
551 x2: usize,
552 y2: usize,
553 ) {
554 let vertices = [
555 (x0 as isize, y0 as isize),
556 (x1 as isize, y1 as isize),
557 (x2 as isize, y2 as isize),
558 ];
559 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
560 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
561
562 for y in min_y..=max_y {
563 let mut intersections: Vec<f64> = Vec::new();
564
565 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
566 let (x_a, y_a) = vertices[edge.0];
567 let (x_b, y_b) = vertices[edge.1];
568 if y_a == y_b {
569 continue;
570 }
571
572 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
573 (x_a, y_a, x_b, y_b)
574 } else {
575 (x_b, y_b, x_a, y_a)
576 };
577
578 if y < y_start || y >= y_end {
579 continue;
580 }
581
582 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
583 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
584 }
585
586 intersections.sort_by(|a, b| a.total_cmp(b));
587 let mut i = 0usize;
588 while i + 1 < intersections.len() {
589 let x_start = intersections[i].ceil() as isize;
590 let x_end = intersections[i + 1].floor() as isize;
591 for x in x_start..=x_end {
592 self.dot_isize(x, y);
593 }
594 i += 2;
595 }
596 }
597
598 self.triangle(x0, y0, x1, y1, x2, y2);
599 }
600
601 pub fn points(&mut self, pts: &[(usize, usize)]) {
603 for &(x, y) in pts {
604 self.dot(x, y);
605 }
606 }
607
608 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
610 for window in pts.windows(2) {
611 if let [(x0, y0), (x1, y1)] = window {
612 self.line(*x0, *y0, *x1, *y1);
613 }
614 }
615 }
616
617 pub fn print(&mut self, x: usize, y: usize, text: &str) {
620 if text.is_empty() {
621 return;
622 }
623
624 let color = self.current_color;
625 if let Some(layer) = self.current_layer_mut() {
626 layer.labels.push(CanvasLabel {
627 x,
628 y,
629 text: text.to_string(),
630 color,
631 });
632 }
633 }
634
635 pub fn layer(&mut self) {
637 self.layers.push(Self::new_layer(self.cols, self.rows));
638 }
639
640 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
641 let mut final_grid = vec![
642 vec![
643 CanvasPixel {
644 bits: 0,
645 color: Color::Reset,
646 };
647 self.cols
648 ];
649 self.rows
650 ];
651 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
652 vec![vec![None; self.cols]; self.rows];
653
654 for layer in &self.layers {
655 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
656 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
657 let src = layer.grid[row][col];
658 if src.bits == 0 {
659 continue;
660 }
661
662 let merged = dst.bits | src.bits;
663 if merged != dst.bits {
664 dst.bits = merged;
665 dst.color = src.color;
666 }
667 }
668 }
669
670 for label in &layer.labels {
671 let row = label.y / 4;
672 if row >= self.rows {
673 continue;
674 }
675 let start_col = label.x / 2;
676 for (offset, ch) in label.text.chars().enumerate() {
677 let col = start_col + offset;
678 if col >= self.cols {
679 break;
680 }
681 labels_overlay[row][col] = Some((ch, label.color));
682 }
683 }
684 }
685
686 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
687 for row in 0..self.rows {
688 let mut segments: Vec<(String, Color)> = Vec::new();
689 let mut current_color: Option<Color> = None;
690 let mut current_text = String::new();
691
692 for col in 0..self.cols {
693 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
694 (label_ch, label_color)
695 } else {
696 let bits = final_grid[row][col].bits;
697 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
698 (ch, final_grid[row][col].color)
699 };
700
701 match current_color {
702 Some(c) if c == color => {
703 current_text.push(ch);
704 }
705 Some(c) => {
706 segments.push((std::mem::take(&mut current_text), c));
707 current_text.push(ch);
708 current_color = Some(color);
709 }
710 None => {
711 current_text.push(ch);
712 current_color = Some(color);
713 }
714 }
715 }
716
717 if let Some(color) = current_color {
718 segments.push((current_text, color));
719 }
720 lines.push(segments);
721 }
722
723 lines
724 }
725}
726
727impl<'a> ContainerBuilder<'a> {
728 pub fn apply(mut self, style: &ContainerStyle) -> Self {
733 if let Some(v) = style.border {
734 self.border = Some(v);
735 }
736 if let Some(v) = style.border_sides {
737 self.border_sides = v;
738 }
739 if let Some(v) = style.border_style {
740 self.border_style = v;
741 }
742 if let Some(v) = style.bg {
743 self.bg_color = Some(v);
744 }
745 if let Some(v) = style.dark_bg {
746 self.dark_bg_color = Some(v);
747 }
748 if let Some(v) = style.dark_border_style {
749 self.dark_border_style = Some(v);
750 }
751 if let Some(v) = style.padding {
752 self.padding = v;
753 }
754 if let Some(v) = style.margin {
755 self.margin = v;
756 }
757 if let Some(v) = style.gap {
758 self.gap = v;
759 }
760 if let Some(v) = style.grow {
761 self.grow = v;
762 }
763 if let Some(v) = style.align {
764 self.align = v;
765 }
766 if let Some(v) = style.justify {
767 self.justify = v;
768 }
769 if let Some(w) = style.w {
770 self.constraints.min_width = Some(w);
771 self.constraints.max_width = Some(w);
772 }
773 if let Some(h) = style.h {
774 self.constraints.min_height = Some(h);
775 self.constraints.max_height = Some(h);
776 }
777 if let Some(v) = style.min_w {
778 self.constraints.min_width = Some(v);
779 }
780 if let Some(v) = style.max_w {
781 self.constraints.max_width = Some(v);
782 }
783 if let Some(v) = style.min_h {
784 self.constraints.min_height = Some(v);
785 }
786 if let Some(v) = style.max_h {
787 self.constraints.max_height = Some(v);
788 }
789 if let Some(v) = style.w_pct {
790 self.constraints.width_pct = Some(v);
791 }
792 if let Some(v) = style.h_pct {
793 self.constraints.height_pct = Some(v);
794 }
795 self
796 }
797
798 pub fn border(mut self, border: Border) -> Self {
800 self.border = Some(border);
801 self
802 }
803
804 pub fn border_top(mut self, show: bool) -> Self {
806 self.border_sides.top = show;
807 self
808 }
809
810 pub fn border_right(mut self, show: bool) -> Self {
812 self.border_sides.right = show;
813 self
814 }
815
816 pub fn border_bottom(mut self, show: bool) -> Self {
818 self.border_sides.bottom = show;
819 self
820 }
821
822 pub fn border_left(mut self, show: bool) -> Self {
824 self.border_sides.left = show;
825 self
826 }
827
828 pub fn border_sides(mut self, sides: BorderSides) -> Self {
830 self.border_sides = sides;
831 self
832 }
833
834 pub fn rounded(self) -> Self {
836 self.border(Border::Rounded)
837 }
838
839 pub fn border_style(mut self, style: Style) -> Self {
841 self.border_style = style;
842 self
843 }
844
845 pub fn dark_border_style(mut self, style: Style) -> Self {
847 self.dark_border_style = Some(style);
848 self
849 }
850
851 pub fn bg(mut self, color: Color) -> Self {
852 self.bg_color = Some(color);
853 self
854 }
855
856 pub fn dark_bg(mut self, color: Color) -> Self {
858 self.dark_bg_color = Some(color);
859 self
860 }
861
862 pub fn group_hover_bg(mut self, color: Color) -> Self {
864 self.group_hover_bg = Some(color);
865 self
866 }
867
868 pub fn group_hover_border_style(mut self, style: Style) -> Self {
870 self.group_hover_border_style = Some(style);
871 self
872 }
873
874 pub fn p(self, value: u32) -> Self {
878 self.pad(value)
879 }
880
881 pub fn pad(mut self, value: u32) -> Self {
883 self.padding = Padding::all(value);
884 self
885 }
886
887 pub fn px(mut self, value: u32) -> Self {
889 self.padding.left = value;
890 self.padding.right = value;
891 self
892 }
893
894 pub fn py(mut self, value: u32) -> Self {
896 self.padding.top = value;
897 self.padding.bottom = value;
898 self
899 }
900
901 pub fn pt(mut self, value: u32) -> Self {
903 self.padding.top = value;
904 self
905 }
906
907 pub fn pr(mut self, value: u32) -> Self {
909 self.padding.right = value;
910 self
911 }
912
913 pub fn pb(mut self, value: u32) -> Self {
915 self.padding.bottom = value;
916 self
917 }
918
919 pub fn pl(mut self, value: u32) -> Self {
921 self.padding.left = value;
922 self
923 }
924
925 pub fn padding(mut self, padding: Padding) -> Self {
927 self.padding = padding;
928 self
929 }
930
931 pub fn m(mut self, value: u32) -> Self {
935 self.margin = Margin::all(value);
936 self
937 }
938
939 pub fn mx(mut self, value: u32) -> Self {
941 self.margin.left = value;
942 self.margin.right = value;
943 self
944 }
945
946 pub fn my(mut self, value: u32) -> Self {
948 self.margin.top = value;
949 self.margin.bottom = value;
950 self
951 }
952
953 pub fn mt(mut self, value: u32) -> Self {
955 self.margin.top = value;
956 self
957 }
958
959 pub fn mr(mut self, value: u32) -> Self {
961 self.margin.right = value;
962 self
963 }
964
965 pub fn mb(mut self, value: u32) -> Self {
967 self.margin.bottom = value;
968 self
969 }
970
971 pub fn ml(mut self, value: u32) -> Self {
973 self.margin.left = value;
974 self
975 }
976
977 pub fn margin(mut self, margin: Margin) -> Self {
979 self.margin = margin;
980 self
981 }
982
983 pub fn w(mut self, value: u32) -> Self {
987 self.constraints.min_width = Some(value);
988 self.constraints.max_width = Some(value);
989 self
990 }
991
992 pub fn xs_w(self, value: u32) -> Self {
999 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1000 if is_xs {
1001 self.w(value)
1002 } else {
1003 self
1004 }
1005 }
1006
1007 pub fn sm_w(self, value: u32) -> Self {
1009 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1010 if is_sm {
1011 self.w(value)
1012 } else {
1013 self
1014 }
1015 }
1016
1017 pub fn md_w(self, value: u32) -> Self {
1019 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1020 if is_md {
1021 self.w(value)
1022 } else {
1023 self
1024 }
1025 }
1026
1027 pub fn lg_w(self, value: u32) -> Self {
1029 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1030 if is_lg {
1031 self.w(value)
1032 } else {
1033 self
1034 }
1035 }
1036
1037 pub fn xl_w(self, value: u32) -> Self {
1039 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1040 if is_xl {
1041 self.w(value)
1042 } else {
1043 self
1044 }
1045 }
1046
1047 pub fn h(mut self, value: u32) -> Self {
1049 self.constraints.min_height = Some(value);
1050 self.constraints.max_height = Some(value);
1051 self
1052 }
1053
1054 pub fn xs_h(self, value: u32) -> Self {
1056 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1057 if is_xs {
1058 self.h(value)
1059 } else {
1060 self
1061 }
1062 }
1063
1064 pub fn sm_h(self, value: u32) -> Self {
1066 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1067 if is_sm {
1068 self.h(value)
1069 } else {
1070 self
1071 }
1072 }
1073
1074 pub fn md_h(self, value: u32) -> Self {
1076 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1077 if is_md {
1078 self.h(value)
1079 } else {
1080 self
1081 }
1082 }
1083
1084 pub fn lg_h(self, value: u32) -> Self {
1086 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1087 if is_lg {
1088 self.h(value)
1089 } else {
1090 self
1091 }
1092 }
1093
1094 pub fn xl_h(self, value: u32) -> Self {
1096 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1097 if is_xl {
1098 self.h(value)
1099 } else {
1100 self
1101 }
1102 }
1103
1104 pub fn min_w(mut self, value: u32) -> Self {
1106 self.constraints.min_width = Some(value);
1107 self
1108 }
1109
1110 pub fn xs_min_w(self, value: u32) -> Self {
1112 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1113 if is_xs {
1114 self.min_w(value)
1115 } else {
1116 self
1117 }
1118 }
1119
1120 pub fn sm_min_w(self, value: u32) -> Self {
1122 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1123 if is_sm {
1124 self.min_w(value)
1125 } else {
1126 self
1127 }
1128 }
1129
1130 pub fn md_min_w(self, value: u32) -> Self {
1132 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1133 if is_md {
1134 self.min_w(value)
1135 } else {
1136 self
1137 }
1138 }
1139
1140 pub fn lg_min_w(self, value: u32) -> Self {
1142 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1143 if is_lg {
1144 self.min_w(value)
1145 } else {
1146 self
1147 }
1148 }
1149
1150 pub fn xl_min_w(self, value: u32) -> Self {
1152 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1153 if is_xl {
1154 self.min_w(value)
1155 } else {
1156 self
1157 }
1158 }
1159
1160 pub fn max_w(mut self, value: u32) -> Self {
1162 self.constraints.max_width = Some(value);
1163 self
1164 }
1165
1166 pub fn xs_max_w(self, value: u32) -> Self {
1168 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1169 if is_xs {
1170 self.max_w(value)
1171 } else {
1172 self
1173 }
1174 }
1175
1176 pub fn sm_max_w(self, value: u32) -> Self {
1178 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1179 if is_sm {
1180 self.max_w(value)
1181 } else {
1182 self
1183 }
1184 }
1185
1186 pub fn md_max_w(self, value: u32) -> Self {
1188 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1189 if is_md {
1190 self.max_w(value)
1191 } else {
1192 self
1193 }
1194 }
1195
1196 pub fn lg_max_w(self, value: u32) -> Self {
1198 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1199 if is_lg {
1200 self.max_w(value)
1201 } else {
1202 self
1203 }
1204 }
1205
1206 pub fn xl_max_w(self, value: u32) -> Self {
1208 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1209 if is_xl {
1210 self.max_w(value)
1211 } else {
1212 self
1213 }
1214 }
1215
1216 pub fn min_h(mut self, value: u32) -> Self {
1218 self.constraints.min_height = Some(value);
1219 self
1220 }
1221
1222 pub fn max_h(mut self, value: u32) -> Self {
1224 self.constraints.max_height = Some(value);
1225 self
1226 }
1227
1228 pub fn min_width(mut self, value: u32) -> Self {
1230 self.constraints.min_width = Some(value);
1231 self
1232 }
1233
1234 pub fn max_width(mut self, value: u32) -> Self {
1236 self.constraints.max_width = Some(value);
1237 self
1238 }
1239
1240 pub fn min_height(mut self, value: u32) -> Self {
1242 self.constraints.min_height = Some(value);
1243 self
1244 }
1245
1246 pub fn max_height(mut self, value: u32) -> Self {
1248 self.constraints.max_height = Some(value);
1249 self
1250 }
1251
1252 pub fn w_pct(mut self, pct: u8) -> Self {
1254 self.constraints.width_pct = Some(pct.min(100));
1255 self
1256 }
1257
1258 pub fn h_pct(mut self, pct: u8) -> Self {
1260 self.constraints.height_pct = Some(pct.min(100));
1261 self
1262 }
1263
1264 pub fn constraints(mut self, constraints: Constraints) -> Self {
1266 self.constraints = constraints;
1267 self
1268 }
1269
1270 pub fn gap(mut self, gap: u32) -> Self {
1274 self.gap = gap;
1275 self
1276 }
1277
1278 pub fn xs_gap(self, value: u32) -> Self {
1280 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1281 if is_xs {
1282 self.gap(value)
1283 } else {
1284 self
1285 }
1286 }
1287
1288 pub fn sm_gap(self, value: u32) -> Self {
1290 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1291 if is_sm {
1292 self.gap(value)
1293 } else {
1294 self
1295 }
1296 }
1297
1298 pub fn md_gap(self, value: u32) -> Self {
1305 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1306 if is_md {
1307 self.gap(value)
1308 } else {
1309 self
1310 }
1311 }
1312
1313 pub fn lg_gap(self, value: u32) -> Self {
1315 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1316 if is_lg {
1317 self.gap(value)
1318 } else {
1319 self
1320 }
1321 }
1322
1323 pub fn xl_gap(self, value: u32) -> Self {
1325 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1326 if is_xl {
1327 self.gap(value)
1328 } else {
1329 self
1330 }
1331 }
1332
1333 pub fn grow(mut self, grow: u16) -> Self {
1335 self.grow = grow;
1336 self
1337 }
1338
1339 pub fn xs_grow(self, value: u16) -> Self {
1341 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1342 if is_xs {
1343 self.grow(value)
1344 } else {
1345 self
1346 }
1347 }
1348
1349 pub fn sm_grow(self, value: u16) -> Self {
1351 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1352 if is_sm {
1353 self.grow(value)
1354 } else {
1355 self
1356 }
1357 }
1358
1359 pub fn md_grow(self, value: u16) -> Self {
1361 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1362 if is_md {
1363 self.grow(value)
1364 } else {
1365 self
1366 }
1367 }
1368
1369 pub fn lg_grow(self, value: u16) -> Self {
1371 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1372 if is_lg {
1373 self.grow(value)
1374 } else {
1375 self
1376 }
1377 }
1378
1379 pub fn xl_grow(self, value: u16) -> Self {
1381 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1382 if is_xl {
1383 self.grow(value)
1384 } else {
1385 self
1386 }
1387 }
1388
1389 pub fn xs_p(self, value: u32) -> Self {
1391 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1392 if is_xs {
1393 self.p(value)
1394 } else {
1395 self
1396 }
1397 }
1398
1399 pub fn sm_p(self, value: u32) -> Self {
1401 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1402 if is_sm {
1403 self.p(value)
1404 } else {
1405 self
1406 }
1407 }
1408
1409 pub fn md_p(self, value: u32) -> Self {
1411 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1412 if is_md {
1413 self.p(value)
1414 } else {
1415 self
1416 }
1417 }
1418
1419 pub fn lg_p(self, value: u32) -> Self {
1421 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1422 if is_lg {
1423 self.p(value)
1424 } else {
1425 self
1426 }
1427 }
1428
1429 pub fn xl_p(self, value: u32) -> Self {
1431 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1432 if is_xl {
1433 self.p(value)
1434 } else {
1435 self
1436 }
1437 }
1438
1439 pub fn align(mut self, align: Align) -> Self {
1443 self.align = align;
1444 self
1445 }
1446
1447 pub fn center(self) -> Self {
1449 self.align(Align::Center)
1450 }
1451
1452 pub fn justify(mut self, justify: Justify) -> Self {
1454 self.justify = justify;
1455 self
1456 }
1457
1458 pub fn space_between(self) -> Self {
1460 self.justify(Justify::SpaceBetween)
1461 }
1462
1463 pub fn space_around(self) -> Self {
1465 self.justify(Justify::SpaceAround)
1466 }
1467
1468 pub fn space_evenly(self) -> Self {
1470 self.justify(Justify::SpaceEvenly)
1471 }
1472
1473 pub fn title(self, title: impl Into<String>) -> Self {
1477 self.title_styled(title, Style::new())
1478 }
1479
1480 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1482 self.title = Some((title.into(), style));
1483 self
1484 }
1485
1486 pub fn scroll_offset(mut self, offset: u32) -> Self {
1490 self.scroll_offset = Some(offset);
1491 self
1492 }
1493
1494 fn group_name(mut self, name: String) -> Self {
1495 self.group_name = Some(name);
1496 self
1497 }
1498
1499 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1504 self.finish(Direction::Column, f)
1505 }
1506
1507 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1512 self.finish(Direction::Row, f)
1513 }
1514
1515 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1520 self.gap = 0;
1521 self.finish(Direction::Row, f)
1522 }
1523
1524 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1530 let draw_id = self.ctx.deferred_draws.len();
1531 self.ctx.deferred_draws.push(Some(Box::new(f)));
1532 self.ctx.interaction_count += 1;
1533 self.ctx.commands.push(Command::RawDraw {
1534 draw_id,
1535 constraints: self.constraints,
1536 grow: self.grow,
1537 margin: self.margin,
1538 });
1539 }
1540
1541 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1542 let interaction_id = self.ctx.interaction_count;
1543 self.ctx.interaction_count += 1;
1544
1545 let in_hovered_group = self
1546 .group_name
1547 .as_ref()
1548 .map(|name| self.ctx.is_group_hovered(name))
1549 .unwrap_or(false)
1550 || self
1551 .ctx
1552 .group_stack
1553 .last()
1554 .map(|name| self.ctx.is_group_hovered(name))
1555 .unwrap_or(false);
1556 let in_focused_group = self
1557 .group_name
1558 .as_ref()
1559 .map(|name| self.ctx.is_group_focused(name))
1560 .unwrap_or(false)
1561 || self
1562 .ctx
1563 .group_stack
1564 .last()
1565 .map(|name| self.ctx.is_group_focused(name))
1566 .unwrap_or(false);
1567
1568 let resolved_bg = if self.ctx.dark_mode {
1569 self.dark_bg_color.or(self.bg_color)
1570 } else {
1571 self.bg_color
1572 };
1573 let resolved_border_style = if self.ctx.dark_mode {
1574 self.dark_border_style.unwrap_or(self.border_style)
1575 } else {
1576 self.border_style
1577 };
1578 let bg_color = if in_hovered_group || in_focused_group {
1579 self.group_hover_bg.or(resolved_bg)
1580 } else {
1581 resolved_bg
1582 };
1583 let border_style = if in_hovered_group || in_focused_group {
1584 self.group_hover_border_style
1585 .unwrap_or(resolved_border_style)
1586 } else {
1587 resolved_border_style
1588 };
1589 let group_name = self.group_name.clone();
1590 let is_group_container = group_name.is_some();
1591
1592 if let Some(scroll_offset) = self.scroll_offset {
1593 self.ctx.commands.push(Command::BeginScrollable {
1594 grow: self.grow,
1595 border: self.border,
1596 border_sides: self.border_sides,
1597 border_style,
1598 padding: self.padding,
1599 margin: self.margin,
1600 constraints: self.constraints,
1601 title: self.title,
1602 scroll_offset,
1603 });
1604 } else {
1605 self.ctx.commands.push(Command::BeginContainer {
1606 direction,
1607 gap: self.gap,
1608 align: self.align,
1609 justify: self.justify,
1610 border: self.border,
1611 border_sides: self.border_sides,
1612 border_style,
1613 bg_color,
1614 padding: self.padding,
1615 margin: self.margin,
1616 constraints: self.constraints,
1617 title: self.title,
1618 grow: self.grow,
1619 group_name,
1620 });
1621 }
1622 f(self.ctx);
1623 self.ctx.commands.push(Command::EndContainer);
1624 self.ctx.last_text_idx = None;
1625
1626 if is_group_container {
1627 self.ctx.group_stack.pop();
1628 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1629 }
1630
1631 self.ctx.response_for(interaction_id)
1632 }
1633}
1634
1635impl Context {
1636 #[allow(clippy::too_many_arguments)]
1637 pub(crate) fn new(
1638 events: Vec<Event>,
1639 width: u32,
1640 height: u32,
1641 tick: u64,
1642 mut focus_index: usize,
1643 prev_focus_count: usize,
1644 prev_scroll_infos: Vec<(u32, u32)>,
1645 prev_scroll_rects: Vec<Rect>,
1646 prev_hit_map: Vec<Rect>,
1647 prev_group_rects: Vec<(String, Rect)>,
1648 prev_focus_rects: Vec<(usize, Rect)>,
1649 prev_focus_groups: Vec<Option<String>>,
1650 prev_hook_states: Vec<Box<dyn std::any::Any>>,
1651 debug: bool,
1652 theme: Theme,
1653 last_mouse_pos: Option<(u32, u32)>,
1654 prev_modal_active: bool,
1655 ) -> Self {
1656 let consumed = vec![false; events.len()];
1657
1658 let mut mouse_pos = last_mouse_pos;
1659 let mut click_pos = None;
1660 for event in &events {
1661 if let Event::Mouse(mouse) = event {
1662 mouse_pos = Some((mouse.x, mouse.y));
1663 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1664 click_pos = Some((mouse.x, mouse.y));
1665 }
1666 }
1667 }
1668
1669 if let Some((mx, my)) = click_pos {
1670 let mut best: Option<(usize, u64)> = None;
1671 for &(fid, rect) in &prev_focus_rects {
1672 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1673 let area = rect.width as u64 * rect.height as u64;
1674 if best.map_or(true, |(_, ba)| area < ba) {
1675 best = Some((fid, area));
1676 }
1677 }
1678 }
1679 if let Some((fid, _)) = best {
1680 focus_index = fid;
1681 }
1682 }
1683
1684 Self {
1685 commands: Vec::new(),
1686 events,
1687 consumed,
1688 should_quit: false,
1689 area_width: width,
1690 area_height: height,
1691 tick,
1692 focus_index,
1693 focus_count: 0,
1694 hook_states: prev_hook_states,
1695 hook_cursor: 0,
1696 prev_focus_count,
1697 scroll_count: 0,
1698 prev_scroll_infos,
1699 prev_scroll_rects,
1700 interaction_count: 0,
1701 prev_hit_map,
1702 group_stack: Vec::new(),
1703 prev_group_rects,
1704 group_count: 0,
1705 prev_focus_groups,
1706 _prev_focus_rects: prev_focus_rects,
1707 mouse_pos,
1708 click_pos,
1709 last_text_idx: None,
1710 overlay_depth: 0,
1711 modal_active: false,
1712 prev_modal_active,
1713 clipboard_text: None,
1714 debug,
1715 theme,
1716 dark_mode: true,
1717 deferred_draws: Vec::new(),
1718 }
1719 }
1720
1721 pub(crate) fn process_focus_keys(&mut self) {
1722 for (i, event) in self.events.iter().enumerate() {
1723 if let Event::Key(key) = event {
1724 if key.kind != KeyEventKind::Press {
1725 continue;
1726 }
1727 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1728 if self.prev_focus_count > 0 {
1729 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1730 }
1731 self.consumed[i] = true;
1732 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1733 || key.code == KeyCode::BackTab
1734 {
1735 if self.prev_focus_count > 0 {
1736 self.focus_index = if self.focus_index == 0 {
1737 self.prev_focus_count - 1
1738 } else {
1739 self.focus_index - 1
1740 };
1741 }
1742 self.consumed[i] = true;
1743 }
1744 }
1745 }
1746 }
1747
1748 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1752 w.ui(self)
1753 }
1754
1755 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1770 self.error_boundary_with(f, |ui, msg| {
1771 ui.styled(
1772 format!("⚠ Error: {msg}"),
1773 Style::new().fg(ui.theme.error).bold(),
1774 );
1775 });
1776 }
1777
1778 pub fn error_boundary_with(
1798 &mut self,
1799 f: impl FnOnce(&mut Context),
1800 fallback: impl FnOnce(&mut Context, String),
1801 ) {
1802 let cmd_count = self.commands.len();
1803 let last_text_idx = self.last_text_idx;
1804
1805 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1806 f(self);
1807 }));
1808
1809 match result {
1810 Ok(()) => {}
1811 Err(panic_info) => {
1812 self.commands.truncate(cmd_count);
1813 self.last_text_idx = last_text_idx;
1814
1815 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1816 (*s).to_string()
1817 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1818 s.clone()
1819 } else {
1820 "widget panicked".to_string()
1821 };
1822
1823 fallback(self, msg);
1824 }
1825 }
1826 }
1827
1828 pub fn interaction(&mut self) -> Response {
1834 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1835 return Response::default();
1836 }
1837 let id = self.interaction_count;
1838 self.interaction_count += 1;
1839 self.response_for(id)
1840 }
1841
1842 pub fn register_focusable(&mut self) -> bool {
1847 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1848 return false;
1849 }
1850 let id = self.focus_count;
1851 self.focus_count += 1;
1852 self.commands.push(Command::FocusMarker(id));
1853 if self.prev_focus_count == 0 {
1854 return true;
1855 }
1856 self.focus_index % self.prev_focus_count == id
1857 }
1858
1859 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1877 let idx = self.hook_cursor;
1878 self.hook_cursor += 1;
1879
1880 if idx >= self.hook_states.len() {
1881 self.hook_states.push(Box::new(init()));
1882 }
1883
1884 State {
1885 idx,
1886 _marker: std::marker::PhantomData,
1887 }
1888 }
1889
1890 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1898 &mut self,
1899 deps: &D,
1900 compute: impl FnOnce(&D) -> T,
1901 ) -> &T {
1902 let idx = self.hook_cursor;
1903 self.hook_cursor += 1;
1904
1905 let should_recompute = if idx >= self.hook_states.len() {
1906 true
1907 } else {
1908 let (stored_deps, _) = self.hook_states[idx]
1909 .downcast_ref::<(D, T)>()
1910 .expect("use_memo type mismatch");
1911 stored_deps != deps
1912 };
1913
1914 if should_recompute {
1915 let value = compute(deps);
1916 let slot = Box::new((deps.clone(), value));
1917 if idx < self.hook_states.len() {
1918 self.hook_states[idx] = slot;
1919 } else {
1920 self.hook_states.push(slot);
1921 }
1922 }
1923
1924 let (_, value) = self.hook_states[idx]
1925 .downcast_ref::<(D, T)>()
1926 .expect("use_memo type mismatch");
1927 value
1928 }
1929
1930 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1943 let content = s.into();
1944 self.commands.push(Command::Text {
1945 content,
1946 style: Style::new(),
1947 grow: 0,
1948 align: Align::Start,
1949 wrap: false,
1950 margin: Margin::default(),
1951 constraints: Constraints::default(),
1952 });
1953 self.last_text_idx = Some(self.commands.len() - 1);
1954 self
1955 }
1956
1957 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
1963 let url_str = url.into();
1964 let focused = self.register_focusable();
1965 let interaction_id = self.interaction_count;
1966 self.interaction_count += 1;
1967 let response = self.response_for(interaction_id);
1968
1969 let mut activated = response.clicked;
1970 if focused {
1971 for (i, event) in self.events.iter().enumerate() {
1972 if let Event::Key(key) = event {
1973 if key.kind != KeyEventKind::Press {
1974 continue;
1975 }
1976 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1977 activated = true;
1978 self.consumed[i] = true;
1979 }
1980 }
1981 }
1982 }
1983
1984 if activated {
1985 let _ = open_url(&url_str);
1986 }
1987
1988 let style = if focused {
1989 Style::new()
1990 .fg(self.theme.primary)
1991 .bg(self.theme.surface_hover)
1992 .underline()
1993 .bold()
1994 } else if response.hovered {
1995 Style::new()
1996 .fg(self.theme.accent)
1997 .bg(self.theme.surface_hover)
1998 .underline()
1999 } else {
2000 Style::new().fg(self.theme.primary).underline()
2001 };
2002
2003 self.commands.push(Command::Link {
2004 text: text.into(),
2005 url: url_str,
2006 style,
2007 margin: Margin::default(),
2008 constraints: Constraints::default(),
2009 });
2010 self.last_text_idx = Some(self.commands.len() - 1);
2011 self
2012 }
2013
2014 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
2019 let content = s.into();
2020 self.commands.push(Command::Text {
2021 content,
2022 style: Style::new(),
2023 grow: 0,
2024 align: Align::Start,
2025 wrap: true,
2026 margin: Margin::default(),
2027 constraints: Constraints::default(),
2028 });
2029 self.last_text_idx = Some(self.commands.len() - 1);
2030 self
2031 }
2032
2033 pub fn bold(&mut self) -> &mut Self {
2037 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
2038 self
2039 }
2040
2041 pub fn dim(&mut self) -> &mut Self {
2046 let text_dim = self.theme.text_dim;
2047 self.modify_last_style(|s| {
2048 s.modifiers |= Modifiers::DIM;
2049 if s.fg.is_none() {
2050 s.fg = Some(text_dim);
2051 }
2052 });
2053 self
2054 }
2055
2056 pub fn italic(&mut self) -> &mut Self {
2058 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
2059 self
2060 }
2061
2062 pub fn underline(&mut self) -> &mut Self {
2064 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
2065 self
2066 }
2067
2068 pub fn reversed(&mut self) -> &mut Self {
2070 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
2071 self
2072 }
2073
2074 pub fn strikethrough(&mut self) -> &mut Self {
2076 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
2077 self
2078 }
2079
2080 pub fn fg(&mut self, color: Color) -> &mut Self {
2082 self.modify_last_style(|s| s.fg = Some(color));
2083 self
2084 }
2085
2086 pub fn bg(&mut self, color: Color) -> &mut Self {
2088 self.modify_last_style(|s| s.bg = Some(color));
2089 self
2090 }
2091
2092 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
2093 let apply_group_style = self
2094 .group_stack
2095 .last()
2096 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2097 .unwrap_or(false);
2098 if apply_group_style {
2099 self.modify_last_style(|s| s.fg = Some(color));
2100 }
2101 self
2102 }
2103
2104 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
2105 let apply_group_style = self
2106 .group_stack
2107 .last()
2108 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
2109 .unwrap_or(false);
2110 if apply_group_style {
2111 self.modify_last_style(|s| s.bg = Some(color));
2112 }
2113 self
2114 }
2115
2116 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
2121 self.commands.push(Command::Text {
2122 content: s.into(),
2123 style,
2124 grow: 0,
2125 align: Align::Start,
2126 wrap: false,
2127 margin: Margin::default(),
2128 constraints: Constraints::default(),
2129 });
2130 self.last_text_idx = Some(self.commands.len() - 1);
2131 self
2132 }
2133
2134 pub fn image(&mut self, img: &HalfBlockImage) {
2156 let width = img.width;
2157 let height = img.height;
2158
2159 self.container().w(width).h(height).gap(0).col(|ui| {
2160 for row in 0..height {
2161 ui.container().gap(0).row(|ui| {
2162 for col in 0..width {
2163 let idx = (row * width + col) as usize;
2164 if let Some(&(upper, lower)) = img.pixels.get(idx) {
2165 ui.styled("▀", Style::new().fg(upper).bg(lower));
2166 }
2167 }
2168 });
2169 }
2170 });
2171 }
2172
2173 pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
2189 if state.streaming {
2190 state.cursor_tick = state.cursor_tick.wrapping_add(1);
2191 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
2192 }
2193
2194 if state.content.is_empty() && state.streaming {
2195 let cursor = if state.cursor_visible { "▌" } else { " " };
2196 let primary = self.theme.primary;
2197 self.text(cursor).fg(primary);
2198 return;
2199 }
2200
2201 if !state.content.is_empty() {
2202 if state.streaming && state.cursor_visible {
2203 self.text_wrap(format!("{}▌", state.content));
2204 } else {
2205 self.text_wrap(&state.content);
2206 }
2207 }
2208 }
2209
2210 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
2225 let theme = self.theme;
2226 self.bordered(Border::Rounded).col(|ui| {
2227 ui.row(|ui| {
2228 ui.text("⚡").fg(theme.warning);
2229 ui.text(&state.tool_name).bold().fg(theme.primary);
2230 });
2231 ui.text(&state.description).dim();
2232
2233 if state.action == ApprovalAction::Pending {
2234 ui.row(|ui| {
2235 if ui.button("✓ Approve") {
2236 state.action = ApprovalAction::Approved;
2237 }
2238 if ui.button("✗ Reject") {
2239 state.action = ApprovalAction::Rejected;
2240 }
2241 });
2242 } else {
2243 let (label, color) = match state.action {
2244 ApprovalAction::Approved => ("✓ Approved", theme.success),
2245 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
2246 ApprovalAction::Pending => unreachable!(),
2247 };
2248 ui.text(label).fg(color).bold();
2249 }
2250 });
2251 }
2252
2253 pub fn context_bar(&mut self, items: &[ContextItem]) {
2266 if items.is_empty() {
2267 return;
2268 }
2269
2270 let theme = self.theme;
2271 let total: usize = items.iter().map(|item| item.tokens).sum();
2272
2273 self.container().row(|ui| {
2274 ui.text("📎").dim();
2275 for item in items {
2276 ui.text(format!(
2277 "{} ({})",
2278 item.label,
2279 format_token_count(item.tokens)
2280 ))
2281 .fg(theme.secondary);
2282 }
2283 ui.spacer();
2284 ui.text(format!("Σ {}", format_token_count(total))).dim();
2285 });
2286 }
2287
2288 pub fn wrap(&mut self) -> &mut Self {
2290 if let Some(idx) = self.last_text_idx {
2291 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
2292 *wrap = true;
2293 }
2294 }
2295 self
2296 }
2297
2298 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
2299 if let Some(idx) = self.last_text_idx {
2300 match &mut self.commands[idx] {
2301 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
2302 _ => {}
2303 }
2304 }
2305 }
2306
2307 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2325 self.push_container(Direction::Column, 0, f)
2326 }
2327
2328 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2332 self.push_container(Direction::Column, gap, f)
2333 }
2334
2335 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
2352 self.push_container(Direction::Row, 0, f)
2353 }
2354
2355 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
2359 self.push_container(Direction::Row, gap, f)
2360 }
2361
2362 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2379 let _ = self.push_container(Direction::Row, 0, f);
2380 self
2381 }
2382
2383 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
2402 let start = self.commands.len();
2403 f(self);
2404 let mut segments: Vec<(String, Style)> = Vec::new();
2405 for cmd in self.commands.drain(start..) {
2406 if let Command::Text { content, style, .. } = cmd {
2407 segments.push((content, style));
2408 }
2409 }
2410 self.commands.push(Command::RichText {
2411 segments,
2412 wrap: true,
2413 align: Align::Start,
2414 margin: Margin::default(),
2415 constraints: Constraints::default(),
2416 });
2417 self.last_text_idx = None;
2418 self
2419 }
2420
2421 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
2430 self.commands.push(Command::BeginOverlay { modal: true });
2431 self.overlay_depth += 1;
2432 self.modal_active = true;
2433 f(self);
2434 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2435 self.commands.push(Command::EndOverlay);
2436 self.last_text_idx = None;
2437 }
2438
2439 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
2441 self.commands.push(Command::BeginOverlay { modal: false });
2442 self.overlay_depth += 1;
2443 f(self);
2444 self.overlay_depth = self.overlay_depth.saturating_sub(1);
2445 self.commands.push(Command::EndOverlay);
2446 self.last_text_idx = None;
2447 }
2448
2449 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
2457 self.group_count = self.group_count.saturating_add(1);
2458 self.group_stack.push(name.to_string());
2459 self.container().group_name(name.to_string())
2460 }
2461
2462 pub fn container(&mut self) -> ContainerBuilder<'_> {
2483 let border = self.theme.border;
2484 ContainerBuilder {
2485 ctx: self,
2486 gap: 0,
2487 align: Align::Start,
2488 justify: Justify::Start,
2489 border: None,
2490 border_sides: BorderSides::all(),
2491 border_style: Style::new().fg(border),
2492 bg_color: None,
2493 dark_bg_color: None,
2494 dark_border_style: None,
2495 group_hover_bg: None,
2496 group_hover_border_style: None,
2497 group_name: None,
2498 padding: Padding::default(),
2499 margin: Margin::default(),
2500 constraints: Constraints::default(),
2501 title: None,
2502 grow: 0,
2503 scroll_offset: None,
2504 }
2505 }
2506
2507 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
2526 let index = self.scroll_count;
2527 self.scroll_count += 1;
2528 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
2529 state.set_bounds(ch, vh);
2530 let max = ch.saturating_sub(vh) as usize;
2531 state.offset = state.offset.min(max);
2532 }
2533
2534 let next_id = self.interaction_count;
2535 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
2536 let inner_rects: Vec<Rect> = self
2537 .prev_scroll_rects
2538 .iter()
2539 .enumerate()
2540 .filter(|&(j, sr)| {
2541 j != index
2542 && sr.width > 0
2543 && sr.height > 0
2544 && sr.x >= rect.x
2545 && sr.right() <= rect.right()
2546 && sr.y >= rect.y
2547 && sr.bottom() <= rect.bottom()
2548 })
2549 .map(|(_, sr)| *sr)
2550 .collect();
2551 self.auto_scroll_nested(&rect, state, &inner_rects);
2552 }
2553
2554 self.container().scroll_offset(state.offset as u32)
2555 }
2556
2557 pub fn scrollbar(&mut self, state: &ScrollState) {
2577 let vh = state.viewport_height();
2578 let ch = state.content_height();
2579 if vh == 0 || ch <= vh {
2580 return;
2581 }
2582
2583 let track_height = vh;
2584 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
2585 let max_offset = ch.saturating_sub(vh);
2586 let thumb_pos = if max_offset == 0 {
2587 0
2588 } else {
2589 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
2590 .round() as u32
2591 };
2592
2593 let theme = self.theme;
2594 let track_char = '│';
2595 let thumb_char = '█';
2596
2597 self.container().w(1).h(track_height).col(|ui| {
2598 for i in 0..track_height {
2599 if i >= thumb_pos && i < thumb_pos + thumb_height {
2600 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
2601 } else {
2602 ui.styled(
2603 track_char.to_string(),
2604 Style::new().fg(theme.text_dim).dim(),
2605 );
2606 }
2607 }
2608 });
2609 }
2610
2611 fn auto_scroll_nested(
2612 &mut self,
2613 rect: &Rect,
2614 state: &mut ScrollState,
2615 inner_scroll_rects: &[Rect],
2616 ) {
2617 let mut to_consume: Vec<usize> = Vec::new();
2618
2619 for (i, event) in self.events.iter().enumerate() {
2620 if self.consumed[i] {
2621 continue;
2622 }
2623 if let Event::Mouse(mouse) = event {
2624 let in_bounds = mouse.x >= rect.x
2625 && mouse.x < rect.right()
2626 && mouse.y >= rect.y
2627 && mouse.y < rect.bottom();
2628 if !in_bounds {
2629 continue;
2630 }
2631 let in_inner = inner_scroll_rects.iter().any(|sr| {
2632 mouse.x >= sr.x
2633 && mouse.x < sr.right()
2634 && mouse.y >= sr.y
2635 && mouse.y < sr.bottom()
2636 });
2637 if in_inner {
2638 continue;
2639 }
2640 match mouse.kind {
2641 MouseKind::ScrollUp => {
2642 state.scroll_up(1);
2643 to_consume.push(i);
2644 }
2645 MouseKind::ScrollDown => {
2646 state.scroll_down(1);
2647 to_consume.push(i);
2648 }
2649 MouseKind::Drag(MouseButton::Left) => {}
2650 _ => {}
2651 }
2652 }
2653 }
2654
2655 for i in to_consume {
2656 self.consumed[i] = true;
2657 }
2658 }
2659
2660 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
2664 self.container()
2665 .border(border)
2666 .border_sides(BorderSides::all())
2667 }
2668
2669 fn push_container(
2670 &mut self,
2671 direction: Direction,
2672 gap: u32,
2673 f: impl FnOnce(&mut Context),
2674 ) -> Response {
2675 let interaction_id = self.interaction_count;
2676 self.interaction_count += 1;
2677 let border = self.theme.border;
2678
2679 self.commands.push(Command::BeginContainer {
2680 direction,
2681 gap,
2682 align: Align::Start,
2683 justify: Justify::Start,
2684 border: None,
2685 border_sides: BorderSides::all(),
2686 border_style: Style::new().fg(border),
2687 bg_color: None,
2688 padding: Padding::default(),
2689 margin: Margin::default(),
2690 constraints: Constraints::default(),
2691 title: None,
2692 grow: 0,
2693 group_name: None,
2694 });
2695 f(self);
2696 self.commands.push(Command::EndContainer);
2697 self.last_text_idx = None;
2698
2699 self.response_for(interaction_id)
2700 }
2701
2702 fn response_for(&self, interaction_id: usize) -> Response {
2703 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2704 return Response::default();
2705 }
2706 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
2707 let clicked = self
2708 .click_pos
2709 .map(|(mx, my)| {
2710 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2711 })
2712 .unwrap_or(false);
2713 let hovered = self
2714 .mouse_pos
2715 .map(|(mx, my)| {
2716 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
2717 })
2718 .unwrap_or(false);
2719 Response { clicked, hovered }
2720 } else {
2721 Response::default()
2722 }
2723 }
2724
2725 pub fn is_group_hovered(&self, name: &str) -> bool {
2727 if let Some(pos) = self.mouse_pos {
2728 self.prev_group_rects.iter().any(|(n, rect)| {
2729 n == name
2730 && pos.0 >= rect.x
2731 && pos.0 < rect.x + rect.width
2732 && pos.1 >= rect.y
2733 && pos.1 < rect.y + rect.height
2734 })
2735 } else {
2736 false
2737 }
2738 }
2739
2740 pub fn is_group_focused(&self, name: &str) -> bool {
2742 if self.prev_focus_count == 0 {
2743 return false;
2744 }
2745 let focused_index = self.focus_index % self.prev_focus_count;
2746 self.prev_focus_groups
2747 .get(focused_index)
2748 .and_then(|group| group.as_deref())
2749 .map(|group| group == name)
2750 .unwrap_or(false)
2751 }
2752
2753 pub fn grow(&mut self, value: u16) -> &mut Self {
2758 if let Some(idx) = self.last_text_idx {
2759 if let Command::Text { grow, .. } = &mut self.commands[idx] {
2760 *grow = value;
2761 }
2762 }
2763 self
2764 }
2765
2766 pub fn align(&mut self, align: Align) -> &mut Self {
2768 if let Some(idx) = self.last_text_idx {
2769 if let Command::Text {
2770 align: text_align, ..
2771 } = &mut self.commands[idx]
2772 {
2773 *text_align = align;
2774 }
2775 }
2776 self
2777 }
2778
2779 pub fn spacer(&mut self) -> &mut Self {
2783 self.commands.push(Command::Spacer { grow: 1 });
2784 self.last_text_idx = None;
2785 self
2786 }
2787
2788 pub fn form(
2792 &mut self,
2793 state: &mut FormState,
2794 f: impl FnOnce(&mut Context, &mut FormState),
2795 ) -> &mut Self {
2796 self.col(|ui| {
2797 f(ui, state);
2798 });
2799 self
2800 }
2801
2802 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2806 self.col(|ui| {
2807 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2808 ui.text_input(&mut field.input);
2809 if let Some(error) = field.error.as_deref() {
2810 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2811 }
2812 });
2813 self
2814 }
2815
2816 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
2820 self.button(label)
2821 }
2822
2823 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
2839 slt_assert(
2840 !state.value.contains('\n'),
2841 "text_input got a newline — use textarea instead",
2842 );
2843 let focused = self.register_focusable();
2844 state.cursor = state.cursor.min(state.value.chars().count());
2845
2846 if focused {
2847 let mut consumed_indices = Vec::new();
2848 for (i, event) in self.events.iter().enumerate() {
2849 if let Event::Key(key) = event {
2850 if key.kind != KeyEventKind::Press {
2851 continue;
2852 }
2853 match key.code {
2854 KeyCode::Char(ch) => {
2855 if let Some(max) = state.max_length {
2856 if state.value.chars().count() >= max {
2857 continue;
2858 }
2859 }
2860 let index = byte_index_for_char(&state.value, state.cursor);
2861 state.value.insert(index, ch);
2862 state.cursor += 1;
2863 consumed_indices.push(i);
2864 }
2865 KeyCode::Backspace => {
2866 if state.cursor > 0 {
2867 let start = byte_index_for_char(&state.value, state.cursor - 1);
2868 let end = byte_index_for_char(&state.value, state.cursor);
2869 state.value.replace_range(start..end, "");
2870 state.cursor -= 1;
2871 }
2872 consumed_indices.push(i);
2873 }
2874 KeyCode::Left => {
2875 state.cursor = state.cursor.saturating_sub(1);
2876 consumed_indices.push(i);
2877 }
2878 KeyCode::Right => {
2879 state.cursor = (state.cursor + 1).min(state.value.chars().count());
2880 consumed_indices.push(i);
2881 }
2882 KeyCode::Home => {
2883 state.cursor = 0;
2884 consumed_indices.push(i);
2885 }
2886 KeyCode::Delete => {
2887 let len = state.value.chars().count();
2888 if state.cursor < len {
2889 let start = byte_index_for_char(&state.value, state.cursor);
2890 let end = byte_index_for_char(&state.value, state.cursor + 1);
2891 state.value.replace_range(start..end, "");
2892 }
2893 consumed_indices.push(i);
2894 }
2895 KeyCode::End => {
2896 state.cursor = state.value.chars().count();
2897 consumed_indices.push(i);
2898 }
2899 _ => {}
2900 }
2901 }
2902 if let Event::Paste(ref text) = event {
2903 for ch in text.chars() {
2904 if let Some(max) = state.max_length {
2905 if state.value.chars().count() >= max {
2906 break;
2907 }
2908 }
2909 let index = byte_index_for_char(&state.value, state.cursor);
2910 state.value.insert(index, ch);
2911 state.cursor += 1;
2912 }
2913 consumed_indices.push(i);
2914 }
2915 }
2916
2917 for index in consumed_indices {
2918 self.consumed[index] = true;
2919 }
2920 }
2921
2922 let visible_width = self.area_width.saturating_sub(4) as usize;
2923 let input_text = if state.value.is_empty() {
2924 if state.placeholder.len() > 100 {
2925 slt_warn(
2926 "text_input placeholder is very long (>100 chars) — consider shortening it",
2927 );
2928 }
2929 let mut ph = state.placeholder.clone();
2930 if focused {
2931 ph.insert(0, '▎');
2932 }
2933 ph
2934 } else {
2935 let chars: Vec<char> = state.value.chars().collect();
2936 let display_chars: Vec<char> = if state.masked {
2937 vec!['•'; chars.len()]
2938 } else {
2939 chars.clone()
2940 };
2941
2942 let cursor_display_pos: usize = display_chars[..state.cursor.min(display_chars.len())]
2943 .iter()
2944 .map(|c| UnicodeWidthChar::width(*c).unwrap_or(1))
2945 .sum();
2946
2947 let scroll_offset = if cursor_display_pos >= visible_width {
2948 cursor_display_pos - visible_width + 1
2949 } else {
2950 0
2951 };
2952
2953 let mut rendered = String::new();
2954 let mut current_width: usize = 0;
2955 for (idx, &ch) in display_chars.iter().enumerate() {
2956 let cw = UnicodeWidthChar::width(ch).unwrap_or(1);
2957 if current_width + cw <= scroll_offset {
2958 current_width += cw;
2959 continue;
2960 }
2961 if current_width - scroll_offset >= visible_width {
2962 break;
2963 }
2964 if focused && idx == state.cursor {
2965 rendered.push('▎');
2966 }
2967 rendered.push(ch);
2968 current_width += cw;
2969 }
2970 if focused && state.cursor >= display_chars.len() {
2971 rendered.push('▎');
2972 }
2973 rendered
2974 };
2975 let input_style = if state.value.is_empty() && !focused {
2976 Style::new().dim().fg(self.theme.text_dim)
2977 } else {
2978 Style::new().fg(self.theme.text)
2979 };
2980
2981 let border_color = if focused {
2982 self.theme.primary
2983 } else if state.validation_error.is_some() {
2984 self.theme.error
2985 } else {
2986 self.theme.border
2987 };
2988
2989 self.bordered(Border::Rounded)
2990 .border_style(Style::new().fg(border_color))
2991 .px(1)
2992 .col(|ui| {
2993 ui.styled(input_text, input_style);
2994 });
2995
2996 if let Some(error) = state.validation_error.clone() {
2997 self.styled(
2998 format!("⚠ {error}"),
2999 Style::new().dim().fg(self.theme.error),
3000 );
3001 }
3002 self
3003 }
3004
3005 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
3011 self.styled(
3012 state.frame(self.tick).to_string(),
3013 Style::new().fg(self.theme.primary),
3014 )
3015 }
3016
3017 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
3022 state.cleanup(self.tick);
3023 if state.messages.is_empty() {
3024 return self;
3025 }
3026
3027 self.interaction_count += 1;
3028 self.commands.push(Command::BeginContainer {
3029 direction: Direction::Column,
3030 gap: 0,
3031 align: Align::Start,
3032 justify: Justify::Start,
3033 border: None,
3034 border_sides: BorderSides::all(),
3035 border_style: Style::new().fg(self.theme.border),
3036 bg_color: None,
3037 padding: Padding::default(),
3038 margin: Margin::default(),
3039 constraints: Constraints::default(),
3040 title: None,
3041 grow: 0,
3042 group_name: None,
3043 });
3044 for message in state.messages.iter().rev() {
3045 let color = match message.level {
3046 ToastLevel::Info => self.theme.primary,
3047 ToastLevel::Success => self.theme.success,
3048 ToastLevel::Warning => self.theme.warning,
3049 ToastLevel::Error => self.theme.error,
3050 };
3051 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
3052 }
3053 self.commands.push(Command::EndContainer);
3054 self.last_text_idx = None;
3055
3056 self
3057 }
3058
3059 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
3067 if state.lines.is_empty() {
3068 state.lines.push(String::new());
3069 }
3070 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
3071 state.cursor_col = state
3072 .cursor_col
3073 .min(state.lines[state.cursor_row].chars().count());
3074
3075 let focused = self.register_focusable();
3076 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
3077 let wrapping = state.wrap_width.is_some();
3078
3079 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3080
3081 if focused {
3082 let mut consumed_indices = Vec::new();
3083 for (i, event) in self.events.iter().enumerate() {
3084 if let Event::Key(key) = event {
3085 if key.kind != KeyEventKind::Press {
3086 continue;
3087 }
3088 match key.code {
3089 KeyCode::Char(ch) => {
3090 if let Some(max) = state.max_length {
3091 let total: usize =
3092 state.lines.iter().map(|line| line.chars().count()).sum();
3093 if total >= max {
3094 continue;
3095 }
3096 }
3097 let index = byte_index_for_char(
3098 &state.lines[state.cursor_row],
3099 state.cursor_col,
3100 );
3101 state.lines[state.cursor_row].insert(index, ch);
3102 state.cursor_col += 1;
3103 consumed_indices.push(i);
3104 }
3105 KeyCode::Enter => {
3106 let split_index = byte_index_for_char(
3107 &state.lines[state.cursor_row],
3108 state.cursor_col,
3109 );
3110 let remainder = state.lines[state.cursor_row].split_off(split_index);
3111 state.cursor_row += 1;
3112 state.lines.insert(state.cursor_row, remainder);
3113 state.cursor_col = 0;
3114 consumed_indices.push(i);
3115 }
3116 KeyCode::Backspace => {
3117 if state.cursor_col > 0 {
3118 let start = byte_index_for_char(
3119 &state.lines[state.cursor_row],
3120 state.cursor_col - 1,
3121 );
3122 let end = byte_index_for_char(
3123 &state.lines[state.cursor_row],
3124 state.cursor_col,
3125 );
3126 state.lines[state.cursor_row].replace_range(start..end, "");
3127 state.cursor_col -= 1;
3128 } else if state.cursor_row > 0 {
3129 let current = state.lines.remove(state.cursor_row);
3130 state.cursor_row -= 1;
3131 state.cursor_col = state.lines[state.cursor_row].chars().count();
3132 state.lines[state.cursor_row].push_str(¤t);
3133 }
3134 consumed_indices.push(i);
3135 }
3136 KeyCode::Left => {
3137 if state.cursor_col > 0 {
3138 state.cursor_col -= 1;
3139 } else if state.cursor_row > 0 {
3140 state.cursor_row -= 1;
3141 state.cursor_col = state.lines[state.cursor_row].chars().count();
3142 }
3143 consumed_indices.push(i);
3144 }
3145 KeyCode::Right => {
3146 let line_len = state.lines[state.cursor_row].chars().count();
3147 if state.cursor_col < line_len {
3148 state.cursor_col += 1;
3149 } else if state.cursor_row + 1 < state.lines.len() {
3150 state.cursor_row += 1;
3151 state.cursor_col = 0;
3152 }
3153 consumed_indices.push(i);
3154 }
3155 KeyCode::Up => {
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 > 0 {
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 > 0 {
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::Down => {
3177 if wrapping {
3178 let (vrow, vcol) = textarea_logical_to_visual(
3179 &pre_vlines,
3180 state.cursor_row,
3181 state.cursor_col,
3182 );
3183 if vrow + 1 < pre_vlines.len() {
3184 let (lr, lc) =
3185 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
3186 state.cursor_row = lr;
3187 state.cursor_col = lc;
3188 }
3189 } else if state.cursor_row + 1 < state.lines.len() {
3190 state.cursor_row += 1;
3191 state.cursor_col = state
3192 .cursor_col
3193 .min(state.lines[state.cursor_row].chars().count());
3194 }
3195 consumed_indices.push(i);
3196 }
3197 KeyCode::Home => {
3198 state.cursor_col = 0;
3199 consumed_indices.push(i);
3200 }
3201 KeyCode::Delete => {
3202 let line_len = state.lines[state.cursor_row].chars().count();
3203 if state.cursor_col < line_len {
3204 let start = byte_index_for_char(
3205 &state.lines[state.cursor_row],
3206 state.cursor_col,
3207 );
3208 let end = byte_index_for_char(
3209 &state.lines[state.cursor_row],
3210 state.cursor_col + 1,
3211 );
3212 state.lines[state.cursor_row].replace_range(start..end, "");
3213 } else if state.cursor_row + 1 < state.lines.len() {
3214 let next = state.lines.remove(state.cursor_row + 1);
3215 state.lines[state.cursor_row].push_str(&next);
3216 }
3217 consumed_indices.push(i);
3218 }
3219 KeyCode::End => {
3220 state.cursor_col = state.lines[state.cursor_row].chars().count();
3221 consumed_indices.push(i);
3222 }
3223 _ => {}
3224 }
3225 }
3226 if let Event::Paste(ref text) = event {
3227 for ch in text.chars() {
3228 if ch == '\n' || ch == '\r' {
3229 let split_index = byte_index_for_char(
3230 &state.lines[state.cursor_row],
3231 state.cursor_col,
3232 );
3233 let remainder = state.lines[state.cursor_row].split_off(split_index);
3234 state.cursor_row += 1;
3235 state.lines.insert(state.cursor_row, remainder);
3236 state.cursor_col = 0;
3237 } else {
3238 if let Some(max) = state.max_length {
3239 let total: usize =
3240 state.lines.iter().map(|l| l.chars().count()).sum();
3241 if total >= max {
3242 break;
3243 }
3244 }
3245 let index = byte_index_for_char(
3246 &state.lines[state.cursor_row],
3247 state.cursor_col,
3248 );
3249 state.lines[state.cursor_row].insert(index, ch);
3250 state.cursor_col += 1;
3251 }
3252 }
3253 consumed_indices.push(i);
3254 }
3255 }
3256
3257 for index in consumed_indices {
3258 self.consumed[index] = true;
3259 }
3260 }
3261
3262 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
3263 let (cursor_vrow, cursor_vcol) =
3264 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
3265
3266 if cursor_vrow < state.scroll_offset {
3267 state.scroll_offset = cursor_vrow;
3268 }
3269 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
3270 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
3271 }
3272
3273 self.interaction_count += 1;
3274 self.commands.push(Command::BeginContainer {
3275 direction: Direction::Column,
3276 gap: 0,
3277 align: Align::Start,
3278 justify: Justify::Start,
3279 border: None,
3280 border_sides: BorderSides::all(),
3281 border_style: Style::new().fg(self.theme.border),
3282 bg_color: None,
3283 padding: Padding::default(),
3284 margin: Margin::default(),
3285 constraints: Constraints::default(),
3286 title: None,
3287 grow: 0,
3288 group_name: None,
3289 });
3290
3291 for vi in 0..visible_rows as usize {
3292 let actual_vi = state.scroll_offset + vi;
3293 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
3294 let line = &state.lines[vl.logical_row];
3295 let text: String = line
3296 .chars()
3297 .skip(vl.char_start)
3298 .take(vl.char_count)
3299 .collect();
3300 (text, actual_vi == cursor_vrow)
3301 } else {
3302 (String::new(), false)
3303 };
3304
3305 let mut rendered = seg_text.clone();
3306 let mut style = if seg_text.is_empty() {
3307 Style::new().fg(self.theme.text_dim)
3308 } else {
3309 Style::new().fg(self.theme.text)
3310 };
3311
3312 if is_cursor_line && focused {
3313 rendered.clear();
3314 for (idx, ch) in seg_text.chars().enumerate() {
3315 if idx == cursor_vcol {
3316 rendered.push('▎');
3317 }
3318 rendered.push(ch);
3319 }
3320 if cursor_vcol >= seg_text.chars().count() {
3321 rendered.push('▎');
3322 }
3323 style = Style::new().fg(self.theme.text);
3324 }
3325
3326 self.styled(rendered, style);
3327 }
3328 self.commands.push(Command::EndContainer);
3329 self.last_text_idx = None;
3330
3331 self
3332 }
3333
3334 pub fn progress(&mut self, ratio: f64) -> &mut Self {
3339 self.progress_bar(ratio, 20)
3340 }
3341
3342 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
3347 let clamped = ratio.clamp(0.0, 1.0);
3348 let filled = (clamped * width as f64).round() as u32;
3349 let empty = width.saturating_sub(filled);
3350 let mut bar = String::new();
3351 for _ in 0..filled {
3352 bar.push('█');
3353 }
3354 for _ in 0..empty {
3355 bar.push('░');
3356 }
3357 self.text(bar)
3358 }
3359
3360 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
3381 if data.is_empty() {
3382 return self;
3383 }
3384
3385 let max_label_width = data
3386 .iter()
3387 .map(|(label, _)| UnicodeWidthStr::width(*label))
3388 .max()
3389 .unwrap_or(0);
3390 let max_value = data
3391 .iter()
3392 .map(|(_, value)| *value)
3393 .fold(f64::NEG_INFINITY, f64::max);
3394 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3395
3396 self.interaction_count += 1;
3397 self.commands.push(Command::BeginContainer {
3398 direction: Direction::Column,
3399 gap: 0,
3400 align: Align::Start,
3401 justify: Justify::Start,
3402 border: None,
3403 border_sides: BorderSides::all(),
3404 border_style: Style::new().fg(self.theme.border),
3405 bg_color: None,
3406 padding: Padding::default(),
3407 margin: Margin::default(),
3408 constraints: Constraints::default(),
3409 title: None,
3410 grow: 0,
3411 group_name: None,
3412 });
3413
3414 for (label, value) in data {
3415 let label_width = UnicodeWidthStr::width(*label);
3416 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3417 let normalized = (*value / denom).clamp(0.0, 1.0);
3418 let bar_len = (normalized * max_width as f64).round() as usize;
3419 let bar = "█".repeat(bar_len);
3420
3421 self.interaction_count += 1;
3422 self.commands.push(Command::BeginContainer {
3423 direction: Direction::Row,
3424 gap: 1,
3425 align: Align::Start,
3426 justify: Justify::Start,
3427 border: None,
3428 border_sides: BorderSides::all(),
3429 border_style: Style::new().fg(self.theme.border),
3430 bg_color: None,
3431 padding: Padding::default(),
3432 margin: Margin::default(),
3433 constraints: Constraints::default(),
3434 title: None,
3435 grow: 0,
3436 group_name: None,
3437 });
3438 self.styled(
3439 format!("{label}{label_padding}"),
3440 Style::new().fg(self.theme.text),
3441 );
3442 self.styled(bar, Style::new().fg(self.theme.primary));
3443 self.styled(
3444 format_compact_number(*value),
3445 Style::new().fg(self.theme.text_dim),
3446 );
3447 self.commands.push(Command::EndContainer);
3448 self.last_text_idx = None;
3449 }
3450
3451 self.commands.push(Command::EndContainer);
3452 self.last_text_idx = None;
3453
3454 self
3455 }
3456
3457 pub fn bar_chart_styled(
3473 &mut self,
3474 bars: &[Bar],
3475 max_width: u32,
3476 direction: BarDirection,
3477 ) -> &mut Self {
3478 if bars.is_empty() {
3479 return self;
3480 }
3481
3482 let max_value = bars
3483 .iter()
3484 .map(|bar| bar.value)
3485 .fold(f64::NEG_INFINITY, f64::max);
3486 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3487
3488 match direction {
3489 BarDirection::Horizontal => {
3490 let max_label_width = bars
3491 .iter()
3492 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3493 .max()
3494 .unwrap_or(0);
3495
3496 self.interaction_count += 1;
3497 self.commands.push(Command::BeginContainer {
3498 direction: Direction::Column,
3499 gap: 0,
3500 align: Align::Start,
3501 justify: Justify::Start,
3502 border: None,
3503 border_sides: BorderSides::all(),
3504 border_style: Style::new().fg(self.theme.border),
3505 bg_color: None,
3506 padding: Padding::default(),
3507 margin: Margin::default(),
3508 constraints: Constraints::default(),
3509 title: None,
3510 grow: 0,
3511 group_name: None,
3512 });
3513
3514 for bar in bars {
3515 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3516 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3517 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3518 let bar_len = (normalized * max_width as f64).round() as usize;
3519 let bar_text = "█".repeat(bar_len);
3520 let color = bar.color.unwrap_or(self.theme.primary);
3521
3522 self.interaction_count += 1;
3523 self.commands.push(Command::BeginContainer {
3524 direction: Direction::Row,
3525 gap: 1,
3526 align: Align::Start,
3527 justify: Justify::Start,
3528 border: None,
3529 border_sides: BorderSides::all(),
3530 border_style: Style::new().fg(self.theme.border),
3531 bg_color: None,
3532 padding: Padding::default(),
3533 margin: Margin::default(),
3534 constraints: Constraints::default(),
3535 title: None,
3536 grow: 0,
3537 group_name: None,
3538 });
3539 self.styled(
3540 format!("{}{label_padding}", bar.label),
3541 Style::new().fg(self.theme.text),
3542 );
3543 self.styled(bar_text, Style::new().fg(color));
3544 self.styled(
3545 format_compact_number(bar.value),
3546 Style::new().fg(self.theme.text_dim),
3547 );
3548 self.commands.push(Command::EndContainer);
3549 self.last_text_idx = None;
3550 }
3551
3552 self.commands.push(Command::EndContainer);
3553 self.last_text_idx = None;
3554 }
3555 BarDirection::Vertical => {
3556 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
3557
3558 let chart_height = max_width.max(1) as usize;
3559 let value_labels: Vec<String> = bars
3560 .iter()
3561 .map(|bar| format_compact_number(bar.value))
3562 .collect();
3563 let col_width = bars
3564 .iter()
3565 .zip(value_labels.iter())
3566 .map(|(bar, value)| {
3567 UnicodeWidthStr::width(bar.label.as_str())
3568 .max(UnicodeWidthStr::width(value.as_str()))
3569 .max(1)
3570 })
3571 .max()
3572 .unwrap_or(1);
3573
3574 let bar_units: Vec<usize> = bars
3575 .iter()
3576 .map(|bar| {
3577 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3578 (normalized * chart_height as f64 * 8.0).round() as usize
3579 })
3580 .collect();
3581
3582 self.interaction_count += 1;
3583 self.commands.push(Command::BeginContainer {
3584 direction: Direction::Column,
3585 gap: 0,
3586 align: Align::Start,
3587 justify: Justify::Start,
3588 border: None,
3589 border_sides: BorderSides::all(),
3590 border_style: Style::new().fg(self.theme.border),
3591 bg_color: None,
3592 padding: Padding::default(),
3593 margin: Margin::default(),
3594 constraints: Constraints::default(),
3595 title: None,
3596 grow: 0,
3597 group_name: None,
3598 });
3599
3600 self.interaction_count += 1;
3601 self.commands.push(Command::BeginContainer {
3602 direction: Direction::Row,
3603 gap: 1,
3604 align: Align::Start,
3605 justify: Justify::Start,
3606 border: None,
3607 border_sides: BorderSides::all(),
3608 border_style: Style::new().fg(self.theme.border),
3609 bg_color: None,
3610 padding: Padding::default(),
3611 margin: Margin::default(),
3612 constraints: Constraints::default(),
3613 title: None,
3614 grow: 0,
3615 group_name: None,
3616 });
3617 for value in &value_labels {
3618 self.styled(
3619 center_text(value, col_width),
3620 Style::new().fg(self.theme.text_dim),
3621 );
3622 }
3623 self.commands.push(Command::EndContainer);
3624 self.last_text_idx = None;
3625
3626 for row in (0..chart_height).rev() {
3627 self.interaction_count += 1;
3628 self.commands.push(Command::BeginContainer {
3629 direction: Direction::Row,
3630 gap: 1,
3631 align: Align::Start,
3632 justify: Justify::Start,
3633 border: None,
3634 border_sides: BorderSides::all(),
3635 border_style: Style::new().fg(self.theme.border),
3636 bg_color: None,
3637 padding: Padding::default(),
3638 margin: Margin::default(),
3639 constraints: Constraints::default(),
3640 title: None,
3641 grow: 0,
3642 group_name: None,
3643 });
3644
3645 let row_base = row * 8;
3646 for (bar, units) in bars.iter().zip(bar_units.iter()) {
3647 let fill = if *units <= row_base {
3648 ' '
3649 } else {
3650 let delta = *units - row_base;
3651 if delta >= 8 {
3652 '█'
3653 } else {
3654 FRACTION_BLOCKS[delta]
3655 }
3656 };
3657
3658 self.styled(
3659 center_text(&fill.to_string(), col_width),
3660 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3661 );
3662 }
3663
3664 self.commands.push(Command::EndContainer);
3665 self.last_text_idx = None;
3666 }
3667
3668 self.interaction_count += 1;
3669 self.commands.push(Command::BeginContainer {
3670 direction: Direction::Row,
3671 gap: 1,
3672 align: Align::Start,
3673 justify: Justify::Start,
3674 border: None,
3675 border_sides: BorderSides::all(),
3676 border_style: Style::new().fg(self.theme.border),
3677 bg_color: None,
3678 padding: Padding::default(),
3679 margin: Margin::default(),
3680 constraints: Constraints::default(),
3681 title: None,
3682 grow: 0,
3683 group_name: None,
3684 });
3685 for bar in bars {
3686 self.styled(
3687 center_text(&bar.label, col_width),
3688 Style::new().fg(self.theme.text),
3689 );
3690 }
3691 self.commands.push(Command::EndContainer);
3692 self.last_text_idx = None;
3693
3694 self.commands.push(Command::EndContainer);
3695 self.last_text_idx = None;
3696 }
3697 }
3698
3699 self
3700 }
3701
3702 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
3719 if groups.is_empty() {
3720 return self;
3721 }
3722
3723 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
3724 if all_bars.is_empty() {
3725 return self;
3726 }
3727
3728 let max_label_width = all_bars
3729 .iter()
3730 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
3731 .max()
3732 .unwrap_or(0);
3733 let max_value = all_bars
3734 .iter()
3735 .map(|bar| bar.value)
3736 .fold(f64::NEG_INFINITY, f64::max);
3737 let denom = if max_value > 0.0 { max_value } else { 1.0 };
3738
3739 self.interaction_count += 1;
3740 self.commands.push(Command::BeginContainer {
3741 direction: Direction::Column,
3742 gap: 1,
3743 align: Align::Start,
3744 justify: Justify::Start,
3745 border: None,
3746 border_sides: BorderSides::all(),
3747 border_style: Style::new().fg(self.theme.border),
3748 bg_color: None,
3749 padding: Padding::default(),
3750 margin: Margin::default(),
3751 constraints: Constraints::default(),
3752 title: None,
3753 grow: 0,
3754 group_name: None,
3755 });
3756
3757 for group in groups {
3758 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
3759
3760 for bar in &group.bars {
3761 let label_width = UnicodeWidthStr::width(bar.label.as_str());
3762 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
3763 let normalized = (bar.value / denom).clamp(0.0, 1.0);
3764 let bar_len = (normalized * max_width as f64).round() as usize;
3765 let bar_text = "█".repeat(bar_len);
3766
3767 self.interaction_count += 1;
3768 self.commands.push(Command::BeginContainer {
3769 direction: Direction::Row,
3770 gap: 1,
3771 align: Align::Start,
3772 justify: Justify::Start,
3773 border: None,
3774 border_sides: BorderSides::all(),
3775 border_style: Style::new().fg(self.theme.border),
3776 bg_color: None,
3777 padding: Padding::default(),
3778 margin: Margin::default(),
3779 constraints: Constraints::default(),
3780 title: None,
3781 grow: 0,
3782 group_name: None,
3783 });
3784 self.styled(
3785 format!(" {}{label_padding}", bar.label),
3786 Style::new().fg(self.theme.text),
3787 );
3788 self.styled(
3789 bar_text,
3790 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
3791 );
3792 self.styled(
3793 format_compact_number(bar.value),
3794 Style::new().fg(self.theme.text_dim),
3795 );
3796 self.commands.push(Command::EndContainer);
3797 self.last_text_idx = None;
3798 }
3799 }
3800
3801 self.commands.push(Command::EndContainer);
3802 self.last_text_idx = None;
3803
3804 self
3805 }
3806
3807 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
3823 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3824
3825 let w = width as usize;
3826 let window = if data.len() > w {
3827 &data[data.len() - w..]
3828 } else {
3829 data
3830 };
3831
3832 if window.is_empty() {
3833 return self;
3834 }
3835
3836 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
3837 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3838 let range = max - min;
3839
3840 let line: String = window
3841 .iter()
3842 .map(|&value| {
3843 let normalized = if range == 0.0 {
3844 0.5
3845 } else {
3846 (value - min) / range
3847 };
3848 let idx = (normalized * 7.0).round() as usize;
3849 BLOCKS[idx.min(7)]
3850 })
3851 .collect();
3852
3853 self.styled(line, Style::new().fg(self.theme.primary))
3854 }
3855
3856 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
3876 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3877
3878 let w = width as usize;
3879 let window = if data.len() > w {
3880 &data[data.len() - w..]
3881 } else {
3882 data
3883 };
3884
3885 if window.is_empty() {
3886 return self;
3887 }
3888
3889 let mut finite_values = window
3890 .iter()
3891 .map(|(value, _)| *value)
3892 .filter(|value| !value.is_nan());
3893 let Some(first) = finite_values.next() else {
3894 return self.styled(
3895 " ".repeat(window.len()),
3896 Style::new().fg(self.theme.text_dim),
3897 );
3898 };
3899
3900 let mut min = first;
3901 let mut max = first;
3902 for value in finite_values {
3903 min = f64::min(min, value);
3904 max = f64::max(max, value);
3905 }
3906 let range = max - min;
3907
3908 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
3909 for (value, color) in window {
3910 if value.is_nan() {
3911 cells.push((' ', self.theme.text_dim));
3912 continue;
3913 }
3914
3915 let normalized = if range == 0.0 {
3916 0.5
3917 } else {
3918 ((*value - min) / range).clamp(0.0, 1.0)
3919 };
3920 let idx = (normalized * 7.0).round() as usize;
3921 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
3922 }
3923
3924 self.interaction_count += 1;
3925 self.commands.push(Command::BeginContainer {
3926 direction: Direction::Row,
3927 gap: 0,
3928 align: Align::Start,
3929 justify: Justify::Start,
3930 border: None,
3931 border_sides: BorderSides::all(),
3932 border_style: Style::new().fg(self.theme.border),
3933 bg_color: None,
3934 padding: Padding::default(),
3935 margin: Margin::default(),
3936 constraints: Constraints::default(),
3937 title: None,
3938 grow: 0,
3939 group_name: None,
3940 });
3941
3942 let mut seg = String::new();
3943 let mut seg_color = cells[0].1;
3944 for (ch, color) in cells {
3945 if color != seg_color {
3946 self.styled(seg, Style::new().fg(seg_color));
3947 seg = String::new();
3948 seg_color = color;
3949 }
3950 seg.push(ch);
3951 }
3952 if !seg.is_empty() {
3953 self.styled(seg, Style::new().fg(seg_color));
3954 }
3955
3956 self.commands.push(Command::EndContainer);
3957 self.last_text_idx = None;
3958
3959 self
3960 }
3961
3962 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
3976 if data.is_empty() || width == 0 || height == 0 {
3977 return self;
3978 }
3979
3980 let cols = width as usize;
3981 let rows = height as usize;
3982 let px_w = cols * 2;
3983 let px_h = rows * 4;
3984
3985 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
3986 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
3987 let range = if (max - min).abs() < f64::EPSILON {
3988 1.0
3989 } else {
3990 max - min
3991 };
3992
3993 let points: Vec<usize> = (0..px_w)
3994 .map(|px| {
3995 let data_idx = if px_w <= 1 {
3996 0.0
3997 } else {
3998 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
3999 };
4000 let idx = data_idx.floor() as usize;
4001 let frac = data_idx - idx as f64;
4002 let value = if idx + 1 < data.len() {
4003 data[idx] * (1.0 - frac) + data[idx + 1] * frac
4004 } else {
4005 data[idx.min(data.len() - 1)]
4006 };
4007
4008 let normalized = (value - min) / range;
4009 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
4010 py.min(px_h - 1)
4011 })
4012 .collect();
4013
4014 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
4015 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
4016
4017 let mut grid = vec![vec![0u32; cols]; rows];
4018
4019 for i in 0..points.len() {
4020 let px = i;
4021 let py = points[i];
4022 let char_col = px / 2;
4023 let char_row = py / 4;
4024 let sub_col = px % 2;
4025 let sub_row = py % 4;
4026
4027 if char_col < cols && char_row < rows {
4028 grid[char_row][char_col] |= if sub_col == 0 {
4029 LEFT_BITS[sub_row]
4030 } else {
4031 RIGHT_BITS[sub_row]
4032 };
4033 }
4034
4035 if i + 1 < points.len() {
4036 let py_next = points[i + 1];
4037 let (y_start, y_end) = if py <= py_next {
4038 (py, py_next)
4039 } else {
4040 (py_next, py)
4041 };
4042 for y in y_start..=y_end {
4043 let cell_row = y / 4;
4044 let sub_y = y % 4;
4045 if char_col < cols && cell_row < rows {
4046 grid[cell_row][char_col] |= if sub_col == 0 {
4047 LEFT_BITS[sub_y]
4048 } else {
4049 RIGHT_BITS[sub_y]
4050 };
4051 }
4052 }
4053 }
4054 }
4055
4056 let style = Style::new().fg(self.theme.primary);
4057 for row in grid {
4058 let line: String = row
4059 .iter()
4060 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
4061 .collect();
4062 self.styled(line, style);
4063 }
4064
4065 self
4066 }
4067
4068 pub fn canvas(
4085 &mut self,
4086 width: u32,
4087 height: u32,
4088 draw: impl FnOnce(&mut CanvasContext),
4089 ) -> &mut Self {
4090 if width == 0 || height == 0 {
4091 return self;
4092 }
4093
4094 let mut canvas = CanvasContext::new(width as usize, height as usize);
4095 draw(&mut canvas);
4096
4097 for segments in canvas.render() {
4098 self.interaction_count += 1;
4099 self.commands.push(Command::BeginContainer {
4100 direction: Direction::Row,
4101 gap: 0,
4102 align: Align::Start,
4103 justify: Justify::Start,
4104 border: None,
4105 border_sides: BorderSides::all(),
4106 border_style: Style::new(),
4107 bg_color: None,
4108 padding: Padding::default(),
4109 margin: Margin::default(),
4110 constraints: Constraints::default(),
4111 title: None,
4112 grow: 0,
4113 group_name: None,
4114 });
4115 for (text, color) in segments {
4116 let c = if color == Color::Reset {
4117 self.theme.primary
4118 } else {
4119 color
4120 };
4121 self.styled(text, Style::new().fg(c));
4122 }
4123 self.commands.push(Command::EndContainer);
4124 self.last_text_idx = None;
4125 }
4126
4127 self
4128 }
4129
4130 pub fn chart(
4132 &mut self,
4133 configure: impl FnOnce(&mut ChartBuilder),
4134 width: u32,
4135 height: u32,
4136 ) -> &mut Self {
4137 if width == 0 || height == 0 {
4138 return self;
4139 }
4140
4141 let axis_style = Style::new().fg(self.theme.text_dim);
4142 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
4143 configure(&mut builder);
4144
4145 let config = builder.build();
4146 let rows = render_chart(&config);
4147
4148 for row in rows {
4149 self.interaction_count += 1;
4150 self.commands.push(Command::BeginContainer {
4151 direction: Direction::Row,
4152 gap: 0,
4153 align: Align::Start,
4154 justify: Justify::Start,
4155 border: None,
4156 border_sides: BorderSides::all(),
4157 border_style: Style::new().fg(self.theme.border),
4158 bg_color: None,
4159 padding: Padding::default(),
4160 margin: Margin::default(),
4161 constraints: Constraints::default(),
4162 title: None,
4163 grow: 0,
4164 group_name: None,
4165 });
4166 for (text, style) in row.segments {
4167 self.styled(text, style);
4168 }
4169 self.commands.push(Command::EndContainer);
4170 self.last_text_idx = None;
4171 }
4172
4173 self
4174 }
4175
4176 pub fn scatter(&mut self, data: &[(f64, f64)], width: u32, height: u32) -> &mut Self {
4180 self.chart(
4181 |c| {
4182 c.scatter(data);
4183 c.grid(true);
4184 },
4185 width,
4186 height,
4187 )
4188 }
4189
4190 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
4192 self.histogram_with(data, |_| {}, width, height)
4193 }
4194
4195 pub fn histogram_with(
4197 &mut self,
4198 data: &[f64],
4199 configure: impl FnOnce(&mut HistogramBuilder),
4200 width: u32,
4201 height: u32,
4202 ) -> &mut Self {
4203 if width == 0 || height == 0 {
4204 return self;
4205 }
4206
4207 let mut options = HistogramBuilder::default();
4208 configure(&mut options);
4209 let axis_style = Style::new().fg(self.theme.text_dim);
4210 let config = build_histogram_config(data, &options, width, height, axis_style);
4211 let rows = render_chart(&config);
4212
4213 for row in rows {
4214 self.interaction_count += 1;
4215 self.commands.push(Command::BeginContainer {
4216 direction: Direction::Row,
4217 gap: 0,
4218 align: Align::Start,
4219 justify: Justify::Start,
4220 border: None,
4221 border_sides: BorderSides::all(),
4222 border_style: Style::new().fg(self.theme.border),
4223 bg_color: None,
4224 padding: Padding::default(),
4225 margin: Margin::default(),
4226 constraints: Constraints::default(),
4227 title: None,
4228 grow: 0,
4229 group_name: None,
4230 });
4231 for (text, style) in row.segments {
4232 self.styled(text, style);
4233 }
4234 self.commands.push(Command::EndContainer);
4235 self.last_text_idx = None;
4236 }
4237
4238 self
4239 }
4240
4241 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
4258 slt_assert(cols > 0, "grid() requires at least 1 column");
4259 let interaction_id = self.interaction_count;
4260 self.interaction_count += 1;
4261 let border = self.theme.border;
4262
4263 self.commands.push(Command::BeginContainer {
4264 direction: Direction::Column,
4265 gap: 0,
4266 align: Align::Start,
4267 justify: Justify::Start,
4268 border: None,
4269 border_sides: BorderSides::all(),
4270 border_style: Style::new().fg(border),
4271 bg_color: None,
4272 padding: Padding::default(),
4273 margin: Margin::default(),
4274 constraints: Constraints::default(),
4275 title: None,
4276 grow: 0,
4277 group_name: None,
4278 });
4279
4280 let children_start = self.commands.len();
4281 f(self);
4282 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
4283
4284 let mut elements: Vec<Vec<Command>> = Vec::new();
4285 let mut iter = child_commands.into_iter().peekable();
4286 while let Some(cmd) = iter.next() {
4287 match cmd {
4288 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4289 let mut depth = 1_u32;
4290 let mut element = vec![cmd];
4291 for next in iter.by_ref() {
4292 match next {
4293 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
4294 depth += 1;
4295 }
4296 Command::EndContainer => {
4297 depth = depth.saturating_sub(1);
4298 }
4299 _ => {}
4300 }
4301 let at_end = matches!(next, Command::EndContainer) && depth == 0;
4302 element.push(next);
4303 if at_end {
4304 break;
4305 }
4306 }
4307 elements.push(element);
4308 }
4309 Command::EndContainer => {}
4310 _ => elements.push(vec![cmd]),
4311 }
4312 }
4313
4314 let cols = cols.max(1) as usize;
4315 for row in elements.chunks(cols) {
4316 self.interaction_count += 1;
4317 self.commands.push(Command::BeginContainer {
4318 direction: Direction::Row,
4319 gap: 0,
4320 align: Align::Start,
4321 justify: Justify::Start,
4322 border: None,
4323 border_sides: BorderSides::all(),
4324 border_style: Style::new().fg(border),
4325 bg_color: None,
4326 padding: Padding::default(),
4327 margin: Margin::default(),
4328 constraints: Constraints::default(),
4329 title: None,
4330 grow: 0,
4331 group_name: None,
4332 });
4333
4334 for element in row {
4335 self.interaction_count += 1;
4336 self.commands.push(Command::BeginContainer {
4337 direction: Direction::Column,
4338 gap: 0,
4339 align: Align::Start,
4340 justify: Justify::Start,
4341 border: None,
4342 border_sides: BorderSides::all(),
4343 border_style: Style::new().fg(border),
4344 bg_color: None,
4345 padding: Padding::default(),
4346 margin: Margin::default(),
4347 constraints: Constraints::default(),
4348 title: None,
4349 grow: 1,
4350 group_name: None,
4351 });
4352 self.commands.extend(element.iter().cloned());
4353 self.commands.push(Command::EndContainer);
4354 }
4355
4356 self.commands.push(Command::EndContainer);
4357 }
4358
4359 self.commands.push(Command::EndContainer);
4360 self.last_text_idx = None;
4361
4362 self.response_for(interaction_id)
4363 }
4364
4365 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
4370 let visible = state.visible_indices().to_vec();
4371 if visible.is_empty() && state.items.is_empty() {
4372 state.selected = 0;
4373 return self;
4374 }
4375
4376 if !visible.is_empty() {
4377 state.selected = state.selected.min(visible.len().saturating_sub(1));
4378 }
4379
4380 let focused = self.register_focusable();
4381 let interaction_id = self.interaction_count;
4382 self.interaction_count += 1;
4383
4384 if focused {
4385 let mut consumed_indices = Vec::new();
4386 for (i, event) in self.events.iter().enumerate() {
4387 if let Event::Key(key) = event {
4388 if key.kind != KeyEventKind::Press {
4389 continue;
4390 }
4391 match key.code {
4392 KeyCode::Up | KeyCode::Char('k') => {
4393 state.selected = state.selected.saturating_sub(1);
4394 consumed_indices.push(i);
4395 }
4396 KeyCode::Down | KeyCode::Char('j') => {
4397 state.selected =
4398 (state.selected + 1).min(visible.len().saturating_sub(1));
4399 consumed_indices.push(i);
4400 }
4401 _ => {}
4402 }
4403 }
4404 }
4405
4406 for index in consumed_indices {
4407 self.consumed[index] = true;
4408 }
4409 }
4410
4411 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4412 for (i, event) in self.events.iter().enumerate() {
4413 if self.consumed[i] {
4414 continue;
4415 }
4416 if let Event::Mouse(mouse) = event {
4417 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4418 continue;
4419 }
4420 let in_bounds = mouse.x >= rect.x
4421 && mouse.x < rect.right()
4422 && mouse.y >= rect.y
4423 && mouse.y < rect.bottom();
4424 if !in_bounds {
4425 continue;
4426 }
4427 let clicked_idx = (mouse.y - rect.y) as usize;
4428 if clicked_idx < visible.len() {
4429 state.selected = clicked_idx;
4430 self.consumed[i] = true;
4431 }
4432 }
4433 }
4434 }
4435
4436 self.commands.push(Command::BeginContainer {
4437 direction: Direction::Column,
4438 gap: 0,
4439 align: Align::Start,
4440 justify: Justify::Start,
4441 border: None,
4442 border_sides: BorderSides::all(),
4443 border_style: Style::new().fg(self.theme.border),
4444 bg_color: None,
4445 padding: Padding::default(),
4446 margin: Margin::default(),
4447 constraints: Constraints::default(),
4448 title: None,
4449 grow: 0,
4450 group_name: None,
4451 });
4452
4453 for (view_idx, &item_idx) in visible.iter().enumerate() {
4454 let item = &state.items[item_idx];
4455 if view_idx == state.selected {
4456 if focused {
4457 self.styled(
4458 format!("▸ {item}"),
4459 Style::new().bold().fg(self.theme.primary),
4460 );
4461 } else {
4462 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
4463 }
4464 } else {
4465 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
4466 }
4467 }
4468
4469 self.commands.push(Command::EndContainer);
4470 self.last_text_idx = None;
4471
4472 self
4473 }
4474
4475 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
4480 if state.is_dirty() {
4481 state.recompute_widths();
4482 }
4483
4484 let focused = self.register_focusable();
4485 let interaction_id = self.interaction_count;
4486 self.interaction_count += 1;
4487
4488 if focused && !state.visible_indices().is_empty() {
4489 let mut consumed_indices = Vec::new();
4490 for (i, event) in self.events.iter().enumerate() {
4491 if let Event::Key(key) = event {
4492 if key.kind != KeyEventKind::Press {
4493 continue;
4494 }
4495 match key.code {
4496 KeyCode::Up | KeyCode::Char('k') => {
4497 let visible_len = if state.page_size > 0 {
4498 let start = state
4499 .page
4500 .saturating_mul(state.page_size)
4501 .min(state.visible_indices().len());
4502 let end =
4503 (start + state.page_size).min(state.visible_indices().len());
4504 end.saturating_sub(start)
4505 } else {
4506 state.visible_indices().len()
4507 };
4508 state.selected = state.selected.min(visible_len.saturating_sub(1));
4509 state.selected = state.selected.saturating_sub(1);
4510 consumed_indices.push(i);
4511 }
4512 KeyCode::Down | KeyCode::Char('j') => {
4513 let visible_len = if state.page_size > 0 {
4514 let start = state
4515 .page
4516 .saturating_mul(state.page_size)
4517 .min(state.visible_indices().len());
4518 let end =
4519 (start + state.page_size).min(state.visible_indices().len());
4520 end.saturating_sub(start)
4521 } else {
4522 state.visible_indices().len()
4523 };
4524 state.selected =
4525 (state.selected + 1).min(visible_len.saturating_sub(1));
4526 consumed_indices.push(i);
4527 }
4528 KeyCode::PageUp => {
4529 let old_page = state.page;
4530 state.prev_page();
4531 if state.page != old_page {
4532 state.selected = 0;
4533 }
4534 consumed_indices.push(i);
4535 }
4536 KeyCode::PageDown => {
4537 let old_page = state.page;
4538 state.next_page();
4539 if state.page != old_page {
4540 state.selected = 0;
4541 }
4542 consumed_indices.push(i);
4543 }
4544 _ => {}
4545 }
4546 }
4547 }
4548 for index in consumed_indices {
4549 self.consumed[index] = true;
4550 }
4551 }
4552
4553 if !state.visible_indices().is_empty() || !state.headers.is_empty() {
4554 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4555 for (i, event) in self.events.iter().enumerate() {
4556 if self.consumed[i] {
4557 continue;
4558 }
4559 if let Event::Mouse(mouse) = event {
4560 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4561 continue;
4562 }
4563 let in_bounds = mouse.x >= rect.x
4564 && mouse.x < rect.right()
4565 && mouse.y >= rect.y
4566 && mouse.y < rect.bottom();
4567 if !in_bounds {
4568 continue;
4569 }
4570
4571 if mouse.y == rect.y {
4572 let rel_x = mouse.x.saturating_sub(rect.x);
4573 let mut x_offset = 0u32;
4574 for (col_idx, width) in state.column_widths().iter().enumerate() {
4575 if rel_x >= x_offset && rel_x < x_offset + *width {
4576 state.toggle_sort(col_idx);
4577 state.selected = 0;
4578 self.consumed[i] = true;
4579 break;
4580 }
4581 x_offset += *width;
4582 if col_idx + 1 < state.column_widths().len() {
4583 x_offset += 3;
4584 }
4585 }
4586 continue;
4587 }
4588
4589 if mouse.y < rect.y + 2 {
4590 continue;
4591 }
4592
4593 let visible_len = if state.page_size > 0 {
4594 let start = state
4595 .page
4596 .saturating_mul(state.page_size)
4597 .min(state.visible_indices().len());
4598 let end = (start + state.page_size).min(state.visible_indices().len());
4599 end.saturating_sub(start)
4600 } else {
4601 state.visible_indices().len()
4602 };
4603 let clicked_idx = (mouse.y - rect.y - 2) as usize;
4604 if clicked_idx < visible_len {
4605 state.selected = clicked_idx;
4606 self.consumed[i] = true;
4607 }
4608 }
4609 }
4610 }
4611 }
4612
4613 if state.is_dirty() {
4614 state.recompute_widths();
4615 }
4616
4617 let total_visible = state.visible_indices().len();
4618 let page_start = if state.page_size > 0 {
4619 state
4620 .page
4621 .saturating_mul(state.page_size)
4622 .min(total_visible)
4623 } else {
4624 0
4625 };
4626 let page_end = if state.page_size > 0 {
4627 (page_start + state.page_size).min(total_visible)
4628 } else {
4629 total_visible
4630 };
4631 let visible_len = page_end.saturating_sub(page_start);
4632 state.selected = state.selected.min(visible_len.saturating_sub(1));
4633
4634 self.commands.push(Command::BeginContainer {
4635 direction: Direction::Column,
4636 gap: 0,
4637 align: Align::Start,
4638 justify: Justify::Start,
4639 border: None,
4640 border_sides: BorderSides::all(),
4641 border_style: Style::new().fg(self.theme.border),
4642 bg_color: None,
4643 padding: Padding::default(),
4644 margin: Margin::default(),
4645 constraints: Constraints::default(),
4646 title: None,
4647 grow: 0,
4648 group_name: None,
4649 });
4650
4651 let header_cells = state
4652 .headers
4653 .iter()
4654 .enumerate()
4655 .map(|(i, header)| {
4656 if state.sort_column == Some(i) {
4657 if state.sort_ascending {
4658 format!("{header} ▲")
4659 } else {
4660 format!("{header} ▼")
4661 }
4662 } else {
4663 header.clone()
4664 }
4665 })
4666 .collect::<Vec<_>>();
4667 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
4668 self.styled(header_line, Style::new().bold().fg(self.theme.text));
4669
4670 let separator = state
4671 .column_widths()
4672 .iter()
4673 .map(|w| "─".repeat(*w as usize))
4674 .collect::<Vec<_>>()
4675 .join("─┼─");
4676 self.text(separator);
4677
4678 for idx in 0..visible_len {
4679 let data_idx = state.visible_indices()[page_start + idx];
4680 let Some(row) = state.rows.get(data_idx) else {
4681 continue;
4682 };
4683 let line = format_table_row(row, state.column_widths(), " │ ");
4684 if idx == state.selected {
4685 let mut style = Style::new()
4686 .bg(self.theme.selected_bg)
4687 .fg(self.theme.selected_fg);
4688 if focused {
4689 style = style.bold();
4690 }
4691 self.styled(line, style);
4692 } else {
4693 self.styled(line, Style::new().fg(self.theme.text));
4694 }
4695 }
4696
4697 if state.page_size > 0 && state.total_pages() > 1 {
4698 self.styled(
4699 format!("Page {}/{}", state.page + 1, state.total_pages()),
4700 Style::new().dim().fg(self.theme.text_dim),
4701 );
4702 }
4703
4704 self.commands.push(Command::EndContainer);
4705 self.last_text_idx = None;
4706
4707 self
4708 }
4709
4710 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
4715 if state.labels.is_empty() {
4716 state.selected = 0;
4717 return self;
4718 }
4719
4720 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
4721 let focused = self.register_focusable();
4722 let interaction_id = self.interaction_count;
4723
4724 if focused {
4725 let mut consumed_indices = Vec::new();
4726 for (i, event) in self.events.iter().enumerate() {
4727 if let Event::Key(key) = event {
4728 if key.kind != KeyEventKind::Press {
4729 continue;
4730 }
4731 match key.code {
4732 KeyCode::Left => {
4733 state.selected = if state.selected == 0 {
4734 state.labels.len().saturating_sub(1)
4735 } else {
4736 state.selected - 1
4737 };
4738 consumed_indices.push(i);
4739 }
4740 KeyCode::Right => {
4741 if !state.labels.is_empty() {
4742 state.selected = (state.selected + 1) % state.labels.len();
4743 }
4744 consumed_indices.push(i);
4745 }
4746 _ => {}
4747 }
4748 }
4749 }
4750
4751 for index in consumed_indices {
4752 self.consumed[index] = true;
4753 }
4754 }
4755
4756 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
4757 for (i, event) in self.events.iter().enumerate() {
4758 if self.consumed[i] {
4759 continue;
4760 }
4761 if let Event::Mouse(mouse) = event {
4762 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
4763 continue;
4764 }
4765 let in_bounds = mouse.x >= rect.x
4766 && mouse.x < rect.right()
4767 && mouse.y >= rect.y
4768 && mouse.y < rect.bottom();
4769 if !in_bounds {
4770 continue;
4771 }
4772
4773 let mut x_offset = 0u32;
4774 let rel_x = mouse.x - rect.x;
4775 for (idx, label) in state.labels.iter().enumerate() {
4776 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
4777 if rel_x >= x_offset && rel_x < x_offset + tab_width {
4778 state.selected = idx;
4779 self.consumed[i] = true;
4780 break;
4781 }
4782 x_offset += tab_width + 1;
4783 }
4784 }
4785 }
4786 }
4787
4788 self.interaction_count += 1;
4789 self.commands.push(Command::BeginContainer {
4790 direction: Direction::Row,
4791 gap: 1,
4792 align: Align::Start,
4793 justify: Justify::Start,
4794 border: None,
4795 border_sides: BorderSides::all(),
4796 border_style: Style::new().fg(self.theme.border),
4797 bg_color: None,
4798 padding: Padding::default(),
4799 margin: Margin::default(),
4800 constraints: Constraints::default(),
4801 title: None,
4802 grow: 0,
4803 group_name: None,
4804 });
4805 for (idx, label) in state.labels.iter().enumerate() {
4806 let style = if idx == state.selected {
4807 let s = Style::new().fg(self.theme.primary).bold();
4808 if focused {
4809 s.underline()
4810 } else {
4811 s
4812 }
4813 } else {
4814 Style::new().fg(self.theme.text_dim)
4815 };
4816 self.styled(format!("[ {label} ]"), style);
4817 }
4818 self.commands.push(Command::EndContainer);
4819 self.last_text_idx = None;
4820
4821 self
4822 }
4823
4824 pub fn button(&mut self, label: impl Into<String>) -> bool {
4829 let focused = self.register_focusable();
4830 let interaction_id = self.interaction_count;
4831 self.interaction_count += 1;
4832 let response = self.response_for(interaction_id);
4833
4834 let mut activated = response.clicked;
4835 if focused {
4836 let mut consumed_indices = Vec::new();
4837 for (i, event) in self.events.iter().enumerate() {
4838 if let Event::Key(key) = event {
4839 if key.kind != KeyEventKind::Press {
4840 continue;
4841 }
4842 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4843 activated = true;
4844 consumed_indices.push(i);
4845 }
4846 }
4847 }
4848
4849 for index in consumed_indices {
4850 self.consumed[index] = true;
4851 }
4852 }
4853
4854 let hovered = response.hovered;
4855 let style = if focused {
4856 Style::new().fg(self.theme.primary).bold()
4857 } else if hovered {
4858 Style::new().fg(self.theme.accent)
4859 } else {
4860 Style::new().fg(self.theme.text)
4861 };
4862 let hover_bg = if hovered || focused {
4863 Some(self.theme.surface_hover)
4864 } else {
4865 None
4866 };
4867
4868 self.commands.push(Command::BeginContainer {
4869 direction: Direction::Row,
4870 gap: 0,
4871 align: Align::Start,
4872 justify: Justify::Start,
4873 border: None,
4874 border_sides: BorderSides::all(),
4875 border_style: Style::new().fg(self.theme.border),
4876 bg_color: hover_bg,
4877 padding: Padding::default(),
4878 margin: Margin::default(),
4879 constraints: Constraints::default(),
4880 title: None,
4881 grow: 0,
4882 group_name: None,
4883 });
4884 self.styled(format!("[ {} ]", label.into()), style);
4885 self.commands.push(Command::EndContainer);
4886 self.last_text_idx = None;
4887
4888 activated
4889 }
4890
4891 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
4896 let focused = self.register_focusable();
4897 let interaction_id = self.interaction_count;
4898 self.interaction_count += 1;
4899 let response = self.response_for(interaction_id);
4900
4901 let mut activated = response.clicked;
4902 if focused {
4903 let mut consumed_indices = Vec::new();
4904 for (i, event) in self.events.iter().enumerate() {
4905 if let Event::Key(key) = event {
4906 if key.kind != KeyEventKind::Press {
4907 continue;
4908 }
4909 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
4910 activated = true;
4911 consumed_indices.push(i);
4912 }
4913 }
4914 }
4915 for index in consumed_indices {
4916 self.consumed[index] = true;
4917 }
4918 }
4919
4920 let label = label.into();
4921 let hover_bg = if response.hovered || focused {
4922 Some(self.theme.surface_hover)
4923 } else {
4924 None
4925 };
4926 let (text, style, bg_color, border) = match variant {
4927 ButtonVariant::Default => {
4928 let style = if focused {
4929 Style::new().fg(self.theme.primary).bold()
4930 } else if response.hovered {
4931 Style::new().fg(self.theme.accent)
4932 } else {
4933 Style::new().fg(self.theme.text)
4934 };
4935 (format!("[ {label} ]"), style, hover_bg, None)
4936 }
4937 ButtonVariant::Primary => {
4938 let style = if focused {
4939 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
4940 } else if response.hovered {
4941 Style::new().fg(self.theme.bg).bg(self.theme.accent)
4942 } else {
4943 Style::new().fg(self.theme.bg).bg(self.theme.primary)
4944 };
4945 (format!(" {label} "), style, hover_bg, None)
4946 }
4947 ButtonVariant::Danger => {
4948 let style = if focused {
4949 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
4950 } else if response.hovered {
4951 Style::new().fg(self.theme.bg).bg(self.theme.warning)
4952 } else {
4953 Style::new().fg(self.theme.bg).bg(self.theme.error)
4954 };
4955 (format!(" {label} "), style, hover_bg, None)
4956 }
4957 ButtonVariant::Outline => {
4958 let border_color = if focused {
4959 self.theme.primary
4960 } else if response.hovered {
4961 self.theme.accent
4962 } else {
4963 self.theme.border
4964 };
4965 let style = if focused {
4966 Style::new().fg(self.theme.primary).bold()
4967 } else if response.hovered {
4968 Style::new().fg(self.theme.accent)
4969 } else {
4970 Style::new().fg(self.theme.text)
4971 };
4972 (
4973 format!(" {label} "),
4974 style,
4975 hover_bg,
4976 Some((Border::Rounded, Style::new().fg(border_color))),
4977 )
4978 }
4979 };
4980
4981 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
4982 self.commands.push(Command::BeginContainer {
4983 direction: Direction::Row,
4984 gap: 0,
4985 align: Align::Center,
4986 justify: Justify::Center,
4987 border: if border.is_some() {
4988 Some(btn_border)
4989 } else {
4990 None
4991 },
4992 border_sides: BorderSides::all(),
4993 border_style: btn_border_style,
4994 bg_color,
4995 padding: Padding::default(),
4996 margin: Margin::default(),
4997 constraints: Constraints::default(),
4998 title: None,
4999 grow: 0,
5000 group_name: None,
5001 });
5002 self.styled(text, style);
5003 self.commands.push(Command::EndContainer);
5004 self.last_text_idx = None;
5005
5006 activated
5007 }
5008
5009 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
5014 let focused = self.register_focusable();
5015 let interaction_id = self.interaction_count;
5016 self.interaction_count += 1;
5017 let response = self.response_for(interaction_id);
5018 let mut should_toggle = response.clicked;
5019
5020 if focused {
5021 let mut consumed_indices = Vec::new();
5022 for (i, event) in self.events.iter().enumerate() {
5023 if let Event::Key(key) = event {
5024 if key.kind != KeyEventKind::Press {
5025 continue;
5026 }
5027 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5028 should_toggle = true;
5029 consumed_indices.push(i);
5030 }
5031 }
5032 }
5033
5034 for index in consumed_indices {
5035 self.consumed[index] = true;
5036 }
5037 }
5038
5039 if should_toggle {
5040 *checked = !*checked;
5041 }
5042
5043 let hover_bg = if response.hovered || focused {
5044 Some(self.theme.surface_hover)
5045 } else {
5046 None
5047 };
5048 self.commands.push(Command::BeginContainer {
5049 direction: Direction::Row,
5050 gap: 1,
5051 align: Align::Start,
5052 justify: Justify::Start,
5053 border: None,
5054 border_sides: BorderSides::all(),
5055 border_style: Style::new().fg(self.theme.border),
5056 bg_color: hover_bg,
5057 padding: Padding::default(),
5058 margin: Margin::default(),
5059 constraints: Constraints::default(),
5060 title: None,
5061 grow: 0,
5062 group_name: None,
5063 });
5064 let marker_style = if *checked {
5065 Style::new().fg(self.theme.success)
5066 } else {
5067 Style::new().fg(self.theme.text_dim)
5068 };
5069 let marker = if *checked { "[x]" } else { "[ ]" };
5070 let label_text = label.into();
5071 if focused {
5072 self.styled(format!("▸ {marker}"), marker_style.bold());
5073 self.styled(label_text, Style::new().fg(self.theme.text).bold());
5074 } else {
5075 self.styled(marker, marker_style);
5076 self.styled(label_text, Style::new().fg(self.theme.text));
5077 }
5078 self.commands.push(Command::EndContainer);
5079 self.last_text_idx = None;
5080
5081 self
5082 }
5083
5084 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
5090 let focused = self.register_focusable();
5091 let interaction_id = self.interaction_count;
5092 self.interaction_count += 1;
5093 let response = self.response_for(interaction_id);
5094 let mut should_toggle = response.clicked;
5095
5096 if focused {
5097 let mut consumed_indices = Vec::new();
5098 for (i, event) in self.events.iter().enumerate() {
5099 if let Event::Key(key) = event {
5100 if key.kind != KeyEventKind::Press {
5101 continue;
5102 }
5103 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5104 should_toggle = true;
5105 consumed_indices.push(i);
5106 }
5107 }
5108 }
5109
5110 for index in consumed_indices {
5111 self.consumed[index] = true;
5112 }
5113 }
5114
5115 if should_toggle {
5116 *on = !*on;
5117 }
5118
5119 let hover_bg = if response.hovered || focused {
5120 Some(self.theme.surface_hover)
5121 } else {
5122 None
5123 };
5124 self.commands.push(Command::BeginContainer {
5125 direction: Direction::Row,
5126 gap: 2,
5127 align: Align::Start,
5128 justify: Justify::Start,
5129 border: None,
5130 border_sides: BorderSides::all(),
5131 border_style: Style::new().fg(self.theme.border),
5132 bg_color: hover_bg,
5133 padding: Padding::default(),
5134 margin: Margin::default(),
5135 constraints: Constraints::default(),
5136 title: None,
5137 grow: 0,
5138 group_name: None,
5139 });
5140 let label_text = label.into();
5141 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
5142 let switch_style = if *on {
5143 Style::new().fg(self.theme.success)
5144 } else {
5145 Style::new().fg(self.theme.text_dim)
5146 };
5147 if focused {
5148 self.styled(
5149 format!("▸ {label_text}"),
5150 Style::new().fg(self.theme.text).bold(),
5151 );
5152 self.styled(switch, switch_style.bold());
5153 } else {
5154 self.styled(label_text, Style::new().fg(self.theme.text));
5155 self.styled(switch, switch_style);
5156 }
5157 self.commands.push(Command::EndContainer);
5158 self.last_text_idx = None;
5159
5160 self
5161 }
5162
5163 pub fn select(&mut self, state: &mut SelectState) -> bool {
5169 if state.items.is_empty() {
5170 return false;
5171 }
5172 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5173
5174 let focused = self.register_focusable();
5175 let interaction_id = self.interaction_count;
5176 self.interaction_count += 1;
5177 let response = self.response_for(interaction_id);
5178 let old_selected = state.selected;
5179
5180 if response.clicked {
5181 state.open = !state.open;
5182 if state.open {
5183 state.set_cursor(state.selected);
5184 }
5185 }
5186
5187 if focused {
5188 let mut consumed_indices = Vec::new();
5189 for (i, event) in self.events.iter().enumerate() {
5190 if self.consumed[i] {
5191 continue;
5192 }
5193 if let Event::Key(key) = event {
5194 if key.kind != KeyEventKind::Press {
5195 continue;
5196 }
5197 if state.open {
5198 match key.code {
5199 KeyCode::Up | KeyCode::Char('k') => {
5200 let c = state.cursor();
5201 state.set_cursor(c.saturating_sub(1));
5202 consumed_indices.push(i);
5203 }
5204 KeyCode::Down | KeyCode::Char('j') => {
5205 let c = state.cursor();
5206 state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
5207 consumed_indices.push(i);
5208 }
5209 KeyCode::Enter | KeyCode::Char(' ') => {
5210 state.selected = state.cursor();
5211 state.open = false;
5212 consumed_indices.push(i);
5213 }
5214 KeyCode::Esc => {
5215 state.open = false;
5216 consumed_indices.push(i);
5217 }
5218 _ => {}
5219 }
5220 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
5221 state.open = true;
5222 state.set_cursor(state.selected);
5223 consumed_indices.push(i);
5224 }
5225 }
5226 }
5227 for idx in consumed_indices {
5228 self.consumed[idx] = true;
5229 }
5230 }
5231
5232 let changed = state.selected != old_selected;
5233
5234 let border_color = if focused {
5235 self.theme.primary
5236 } else {
5237 self.theme.border
5238 };
5239 let display_text = state
5240 .items
5241 .get(state.selected)
5242 .cloned()
5243 .unwrap_or_else(|| state.placeholder.clone());
5244 let arrow = if state.open { "▲" } else { "▼" };
5245
5246 self.commands.push(Command::BeginContainer {
5247 direction: Direction::Column,
5248 gap: 0,
5249 align: Align::Start,
5250 justify: Justify::Start,
5251 border: None,
5252 border_sides: BorderSides::all(),
5253 border_style: Style::new().fg(self.theme.border),
5254 bg_color: None,
5255 padding: Padding::default(),
5256 margin: Margin::default(),
5257 constraints: Constraints::default(),
5258 title: None,
5259 grow: 0,
5260 group_name: None,
5261 });
5262
5263 self.commands.push(Command::BeginContainer {
5264 direction: Direction::Row,
5265 gap: 1,
5266 align: Align::Start,
5267 justify: Justify::Start,
5268 border: Some(Border::Rounded),
5269 border_sides: BorderSides::all(),
5270 border_style: Style::new().fg(border_color),
5271 bg_color: None,
5272 padding: Padding {
5273 left: 1,
5274 right: 1,
5275 top: 0,
5276 bottom: 0,
5277 },
5278 margin: Margin::default(),
5279 constraints: Constraints::default(),
5280 title: None,
5281 grow: 0,
5282 group_name: None,
5283 });
5284 self.interaction_count += 1;
5285 self.styled(&display_text, Style::new().fg(self.theme.text));
5286 self.styled(arrow, Style::new().fg(self.theme.text_dim));
5287 self.commands.push(Command::EndContainer);
5288 self.last_text_idx = None;
5289
5290 if state.open {
5291 for (idx, item) in state.items.iter().enumerate() {
5292 let is_cursor = idx == state.cursor();
5293 let style = if is_cursor {
5294 Style::new().bold().fg(self.theme.primary)
5295 } else {
5296 Style::new().fg(self.theme.text)
5297 };
5298 let prefix = if is_cursor { "▸ " } else { " " };
5299 self.styled(format!("{prefix}{item}"), style);
5300 }
5301 }
5302
5303 self.commands.push(Command::EndContainer);
5304 self.last_text_idx = None;
5305 changed
5306 }
5307
5308 pub fn radio(&mut self, state: &mut RadioState) -> bool {
5312 if state.items.is_empty() {
5313 return false;
5314 }
5315 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5316 let focused = self.register_focusable();
5317 let old_selected = state.selected;
5318
5319 if focused {
5320 let mut consumed_indices = Vec::new();
5321 for (i, event) in self.events.iter().enumerate() {
5322 if self.consumed[i] {
5323 continue;
5324 }
5325 if let Event::Key(key) = event {
5326 if key.kind != KeyEventKind::Press {
5327 continue;
5328 }
5329 match key.code {
5330 KeyCode::Up | KeyCode::Char('k') => {
5331 state.selected = state.selected.saturating_sub(1);
5332 consumed_indices.push(i);
5333 }
5334 KeyCode::Down | KeyCode::Char('j') => {
5335 state.selected =
5336 (state.selected + 1).min(state.items.len().saturating_sub(1));
5337 consumed_indices.push(i);
5338 }
5339 KeyCode::Enter | KeyCode::Char(' ') => {
5340 consumed_indices.push(i);
5341 }
5342 _ => {}
5343 }
5344 }
5345 }
5346 for idx in consumed_indices {
5347 self.consumed[idx] = true;
5348 }
5349 }
5350
5351 let interaction_id = self.interaction_count;
5352 self.interaction_count += 1;
5353
5354 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5355 for (i, event) in self.events.iter().enumerate() {
5356 if self.consumed[i] {
5357 continue;
5358 }
5359 if let Event::Mouse(mouse) = event {
5360 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5361 continue;
5362 }
5363 let in_bounds = mouse.x >= rect.x
5364 && mouse.x < rect.right()
5365 && mouse.y >= rect.y
5366 && mouse.y < rect.bottom();
5367 if !in_bounds {
5368 continue;
5369 }
5370 let clicked_idx = (mouse.y - rect.y) as usize;
5371 if clicked_idx < state.items.len() {
5372 state.selected = clicked_idx;
5373 self.consumed[i] = true;
5374 }
5375 }
5376 }
5377 }
5378
5379 self.commands.push(Command::BeginContainer {
5380 direction: Direction::Column,
5381 gap: 0,
5382 align: Align::Start,
5383 justify: Justify::Start,
5384 border: None,
5385 border_sides: BorderSides::all(),
5386 border_style: Style::new().fg(self.theme.border),
5387 bg_color: None,
5388 padding: Padding::default(),
5389 margin: Margin::default(),
5390 constraints: Constraints::default(),
5391 title: None,
5392 grow: 0,
5393 group_name: None,
5394 });
5395
5396 for (idx, item) in state.items.iter().enumerate() {
5397 let is_selected = idx == state.selected;
5398 let marker = if is_selected { "●" } else { "○" };
5399 let style = if is_selected {
5400 if focused {
5401 Style::new().bold().fg(self.theme.primary)
5402 } else {
5403 Style::new().fg(self.theme.primary)
5404 }
5405 } else {
5406 Style::new().fg(self.theme.text)
5407 };
5408 let prefix = if focused && idx == state.selected {
5409 "▸ "
5410 } else {
5411 " "
5412 };
5413 self.styled(format!("{prefix}{marker} {item}"), style);
5414 }
5415
5416 self.commands.push(Command::EndContainer);
5417 self.last_text_idx = None;
5418 state.selected != old_selected
5419 }
5420
5421 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
5425 if state.items.is_empty() {
5426 return self;
5427 }
5428 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
5429 let focused = self.register_focusable();
5430
5431 if focused {
5432 let mut consumed_indices = Vec::new();
5433 for (i, event) in self.events.iter().enumerate() {
5434 if self.consumed[i] {
5435 continue;
5436 }
5437 if let Event::Key(key) = event {
5438 if key.kind != KeyEventKind::Press {
5439 continue;
5440 }
5441 match key.code {
5442 KeyCode::Up | KeyCode::Char('k') => {
5443 state.cursor = state.cursor.saturating_sub(1);
5444 consumed_indices.push(i);
5445 }
5446 KeyCode::Down | KeyCode::Char('j') => {
5447 state.cursor =
5448 (state.cursor + 1).min(state.items.len().saturating_sub(1));
5449 consumed_indices.push(i);
5450 }
5451 KeyCode::Char(' ') | KeyCode::Enter => {
5452 state.toggle(state.cursor);
5453 consumed_indices.push(i);
5454 }
5455 _ => {}
5456 }
5457 }
5458 }
5459 for idx in consumed_indices {
5460 self.consumed[idx] = true;
5461 }
5462 }
5463
5464 let interaction_id = self.interaction_count;
5465 self.interaction_count += 1;
5466
5467 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
5468 for (i, event) in self.events.iter().enumerate() {
5469 if self.consumed[i] {
5470 continue;
5471 }
5472 if let Event::Mouse(mouse) = event {
5473 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
5474 continue;
5475 }
5476 let in_bounds = mouse.x >= rect.x
5477 && mouse.x < rect.right()
5478 && mouse.y >= rect.y
5479 && mouse.y < rect.bottom();
5480 if !in_bounds {
5481 continue;
5482 }
5483 let clicked_idx = (mouse.y - rect.y) as usize;
5484 if clicked_idx < state.items.len() {
5485 state.toggle(clicked_idx);
5486 state.cursor = clicked_idx;
5487 self.consumed[i] = true;
5488 }
5489 }
5490 }
5491 }
5492
5493 self.commands.push(Command::BeginContainer {
5494 direction: Direction::Column,
5495 gap: 0,
5496 align: Align::Start,
5497 justify: Justify::Start,
5498 border: None,
5499 border_sides: BorderSides::all(),
5500 border_style: Style::new().fg(self.theme.border),
5501 bg_color: None,
5502 padding: Padding::default(),
5503 margin: Margin::default(),
5504 constraints: Constraints::default(),
5505 title: None,
5506 grow: 0,
5507 group_name: None,
5508 });
5509
5510 for (idx, item) in state.items.iter().enumerate() {
5511 let checked = state.selected.contains(&idx);
5512 let marker = if checked { "[x]" } else { "[ ]" };
5513 let is_cursor = idx == state.cursor;
5514 let style = if is_cursor && focused {
5515 Style::new().bold().fg(self.theme.primary)
5516 } else if checked {
5517 Style::new().fg(self.theme.success)
5518 } else {
5519 Style::new().fg(self.theme.text)
5520 };
5521 let prefix = if is_cursor && focused { "▸ " } else { " " };
5522 self.styled(format!("{prefix}{marker} {item}"), style);
5523 }
5524
5525 self.commands.push(Command::EndContainer);
5526 self.last_text_idx = None;
5527 self
5528 }
5529
5530 pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
5534 let entries = state.flatten();
5535 if entries.is_empty() {
5536 return self;
5537 }
5538 state.selected = state.selected.min(entries.len().saturating_sub(1));
5539 let focused = self.register_focusable();
5540
5541 if focused {
5542 let mut consumed_indices = Vec::new();
5543 for (i, event) in self.events.iter().enumerate() {
5544 if self.consumed[i] {
5545 continue;
5546 }
5547 if let Event::Key(key) = event {
5548 if key.kind != KeyEventKind::Press {
5549 continue;
5550 }
5551 match key.code {
5552 KeyCode::Up | KeyCode::Char('k') => {
5553 state.selected = state.selected.saturating_sub(1);
5554 consumed_indices.push(i);
5555 }
5556 KeyCode::Down | KeyCode::Char('j') => {
5557 let max = state.flatten().len().saturating_sub(1);
5558 state.selected = (state.selected + 1).min(max);
5559 consumed_indices.push(i);
5560 }
5561 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
5562 state.toggle_at(state.selected);
5563 consumed_indices.push(i);
5564 }
5565 KeyCode::Left => {
5566 let entry = &entries[state.selected.min(entries.len() - 1)];
5567 if entry.expanded {
5568 state.toggle_at(state.selected);
5569 }
5570 consumed_indices.push(i);
5571 }
5572 _ => {}
5573 }
5574 }
5575 }
5576 for idx in consumed_indices {
5577 self.consumed[idx] = true;
5578 }
5579 }
5580
5581 self.interaction_count += 1;
5582 self.commands.push(Command::BeginContainer {
5583 direction: Direction::Column,
5584 gap: 0,
5585 align: Align::Start,
5586 justify: Justify::Start,
5587 border: None,
5588 border_sides: BorderSides::all(),
5589 border_style: Style::new().fg(self.theme.border),
5590 bg_color: None,
5591 padding: Padding::default(),
5592 margin: Margin::default(),
5593 constraints: Constraints::default(),
5594 title: None,
5595 grow: 0,
5596 group_name: None,
5597 });
5598
5599 let entries = state.flatten();
5600 for (idx, entry) in entries.iter().enumerate() {
5601 let indent = " ".repeat(entry.depth);
5602 let icon = if entry.is_leaf {
5603 " "
5604 } else if entry.expanded {
5605 "▾ "
5606 } else {
5607 "▸ "
5608 };
5609 let is_selected = idx == state.selected;
5610 let style = if is_selected && focused {
5611 Style::new().bold().fg(self.theme.primary)
5612 } else if is_selected {
5613 Style::new().fg(self.theme.primary)
5614 } else {
5615 Style::new().fg(self.theme.text)
5616 };
5617 let cursor = if is_selected && focused { "▸" } else { " " };
5618 self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
5619 }
5620
5621 self.commands.push(Command::EndContainer);
5622 self.last_text_idx = None;
5623 self
5624 }
5625
5626 pub fn virtual_list(
5633 &mut self,
5634 state: &mut ListState,
5635 visible_height: usize,
5636 f: impl Fn(&mut Context, usize),
5637 ) -> &mut Self {
5638 if state.items.is_empty() {
5639 return self;
5640 }
5641 state.selected = state.selected.min(state.items.len().saturating_sub(1));
5642 let focused = self.register_focusable();
5643
5644 if focused {
5645 let mut consumed_indices = Vec::new();
5646 for (i, event) in self.events.iter().enumerate() {
5647 if self.consumed[i] {
5648 continue;
5649 }
5650 if let Event::Key(key) = event {
5651 if key.kind != KeyEventKind::Press {
5652 continue;
5653 }
5654 match key.code {
5655 KeyCode::Up | KeyCode::Char('k') => {
5656 state.selected = state.selected.saturating_sub(1);
5657 consumed_indices.push(i);
5658 }
5659 KeyCode::Down | KeyCode::Char('j') => {
5660 state.selected =
5661 (state.selected + 1).min(state.items.len().saturating_sub(1));
5662 consumed_indices.push(i);
5663 }
5664 KeyCode::PageUp => {
5665 state.selected = state.selected.saturating_sub(visible_height);
5666 consumed_indices.push(i);
5667 }
5668 KeyCode::PageDown => {
5669 state.selected = (state.selected + visible_height)
5670 .min(state.items.len().saturating_sub(1));
5671 consumed_indices.push(i);
5672 }
5673 KeyCode::Home => {
5674 state.selected = 0;
5675 consumed_indices.push(i);
5676 }
5677 KeyCode::End => {
5678 state.selected = state.items.len().saturating_sub(1);
5679 consumed_indices.push(i);
5680 }
5681 _ => {}
5682 }
5683 }
5684 }
5685 for idx in consumed_indices {
5686 self.consumed[idx] = true;
5687 }
5688 }
5689
5690 let start = if state.selected >= visible_height {
5691 state.selected - visible_height + 1
5692 } else {
5693 0
5694 };
5695 let end = (start + visible_height).min(state.items.len());
5696
5697 self.interaction_count += 1;
5698 self.commands.push(Command::BeginContainer {
5699 direction: Direction::Column,
5700 gap: 0,
5701 align: Align::Start,
5702 justify: Justify::Start,
5703 border: None,
5704 border_sides: BorderSides::all(),
5705 border_style: Style::new().fg(self.theme.border),
5706 bg_color: None,
5707 padding: Padding::default(),
5708 margin: Margin::default(),
5709 constraints: Constraints::default(),
5710 title: None,
5711 grow: 0,
5712 group_name: None,
5713 });
5714
5715 if start > 0 {
5716 self.styled(
5717 format!(" ↑ {} more", start),
5718 Style::new().fg(self.theme.text_dim).dim(),
5719 );
5720 }
5721
5722 for idx in start..end {
5723 f(self, idx);
5724 }
5725
5726 let remaining = state.items.len().saturating_sub(end);
5727 if remaining > 0 {
5728 self.styled(
5729 format!(" ↓ {} more", remaining),
5730 Style::new().fg(self.theme.text_dim).dim(),
5731 );
5732 }
5733
5734 self.commands.push(Command::EndContainer);
5735 self.last_text_idx = None;
5736 self
5737 }
5738
5739 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
5743 if !state.open {
5744 return None;
5745 }
5746
5747 let filtered = state.filtered_indices();
5748 let sel = state.selected().min(filtered.len().saturating_sub(1));
5749 state.set_selected(sel);
5750
5751 let mut consumed_indices = Vec::new();
5752 let mut result: Option<usize> = None;
5753
5754 for (i, event) in self.events.iter().enumerate() {
5755 if self.consumed[i] {
5756 continue;
5757 }
5758 if let Event::Key(key) = event {
5759 if key.kind != KeyEventKind::Press {
5760 continue;
5761 }
5762 match key.code {
5763 KeyCode::Esc => {
5764 state.open = false;
5765 consumed_indices.push(i);
5766 }
5767 KeyCode::Up => {
5768 let s = state.selected();
5769 state.set_selected(s.saturating_sub(1));
5770 consumed_indices.push(i);
5771 }
5772 KeyCode::Down => {
5773 let s = state.selected();
5774 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
5775 consumed_indices.push(i);
5776 }
5777 KeyCode::Enter => {
5778 if let Some(&cmd_idx) = filtered.get(state.selected()) {
5779 result = Some(cmd_idx);
5780 state.open = false;
5781 }
5782 consumed_indices.push(i);
5783 }
5784 KeyCode::Backspace => {
5785 if state.cursor > 0 {
5786 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
5787 let end_idx = byte_index_for_char(&state.input, state.cursor);
5788 state.input.replace_range(byte_idx..end_idx, "");
5789 state.cursor -= 1;
5790 state.set_selected(0);
5791 }
5792 consumed_indices.push(i);
5793 }
5794 KeyCode::Char(ch) => {
5795 let byte_idx = byte_index_for_char(&state.input, state.cursor);
5796 state.input.insert(byte_idx, ch);
5797 state.cursor += 1;
5798 state.set_selected(0);
5799 consumed_indices.push(i);
5800 }
5801 _ => {}
5802 }
5803 }
5804 }
5805 for idx in consumed_indices {
5806 self.consumed[idx] = true;
5807 }
5808
5809 let filtered = state.filtered_indices();
5810
5811 self.modal(|ui| {
5812 let primary = ui.theme.primary;
5813 ui.container()
5814 .border(Border::Rounded)
5815 .border_style(Style::new().fg(primary))
5816 .pad(1)
5817 .max_w(60)
5818 .col(|ui| {
5819 let border_color = ui.theme.primary;
5820 ui.bordered(Border::Rounded)
5821 .border_style(Style::new().fg(border_color))
5822 .px(1)
5823 .col(|ui| {
5824 let display = if state.input.is_empty() {
5825 "Type to search...".to_string()
5826 } else {
5827 state.input.clone()
5828 };
5829 let style = if state.input.is_empty() {
5830 Style::new().dim().fg(ui.theme.text_dim)
5831 } else {
5832 Style::new().fg(ui.theme.text)
5833 };
5834 ui.styled(display, style);
5835 });
5836
5837 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
5838 let cmd = &state.commands[cmd_idx];
5839 let is_selected = list_idx == state.selected();
5840 let style = if is_selected {
5841 Style::new().bold().fg(ui.theme.primary)
5842 } else {
5843 Style::new().fg(ui.theme.text)
5844 };
5845 let prefix = if is_selected { "▸ " } else { " " };
5846 let shortcut_text = cmd
5847 .shortcut
5848 .as_deref()
5849 .map(|s| format!(" ({s})"))
5850 .unwrap_or_default();
5851 ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
5852 if is_selected && !cmd.description.is_empty() {
5853 ui.styled(
5854 format!(" {}", cmd.description),
5855 Style::new().dim().fg(ui.theme.text_dim),
5856 );
5857 }
5858 }
5859
5860 if filtered.is_empty() {
5861 ui.styled(
5862 " No matching commands",
5863 Style::new().dim().fg(ui.theme.text_dim),
5864 );
5865 }
5866 });
5867 });
5868
5869 result
5870 }
5871
5872 pub fn markdown(&mut self, text: &str) -> &mut Self {
5879 self.commands.push(Command::BeginContainer {
5880 direction: Direction::Column,
5881 gap: 0,
5882 align: Align::Start,
5883 justify: Justify::Start,
5884 border: None,
5885 border_sides: BorderSides::all(),
5886 border_style: Style::new().fg(self.theme.border),
5887 bg_color: None,
5888 padding: Padding::default(),
5889 margin: Margin::default(),
5890 constraints: Constraints::default(),
5891 title: None,
5892 grow: 0,
5893 group_name: None,
5894 });
5895 self.interaction_count += 1;
5896
5897 let text_style = Style::new().fg(self.theme.text);
5898 let bold_style = Style::new().fg(self.theme.text).bold();
5899 let code_style = Style::new().fg(self.theme.accent);
5900
5901 for line in text.lines() {
5902 let trimmed = line.trim();
5903 if trimmed.is_empty() {
5904 self.text(" ");
5905 continue;
5906 }
5907 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
5908 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
5909 continue;
5910 }
5911 if let Some(heading) = trimmed.strip_prefix("### ") {
5912 self.styled(heading, Style::new().bold().fg(self.theme.accent));
5913 } else if let Some(heading) = trimmed.strip_prefix("## ") {
5914 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
5915 } else if let Some(heading) = trimmed.strip_prefix("# ") {
5916 self.styled(heading, Style::new().bold().fg(self.theme.primary));
5917 } else if let Some(item) = trimmed
5918 .strip_prefix("- ")
5919 .or_else(|| trimmed.strip_prefix("* "))
5920 {
5921 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
5922 if segs.len() <= 1 {
5923 self.styled(format!(" • {item}"), text_style);
5924 } else {
5925 self.line(|ui| {
5926 ui.styled(" • ", text_style);
5927 for (s, st) in segs {
5928 ui.styled(s, st);
5929 }
5930 });
5931 }
5932 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
5933 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
5934 if parts.len() == 2 {
5935 let segs =
5936 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
5937 if segs.len() <= 1 {
5938 self.styled(format!(" {}. {}", parts[0], parts[1]), text_style);
5939 } else {
5940 self.line(|ui| {
5941 ui.styled(format!(" {}. ", parts[0]), text_style);
5942 for (s, st) in segs {
5943 ui.styled(s, st);
5944 }
5945 });
5946 }
5947 } else {
5948 self.text(trimmed);
5949 }
5950 } else if let Some(code) = trimmed.strip_prefix("```") {
5951 let _ = code;
5952 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
5953 } else {
5954 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
5955 if segs.len() <= 1 {
5956 self.styled(trimmed, text_style);
5957 } else {
5958 self.line(|ui| {
5959 for (s, st) in segs {
5960 ui.styled(s, st);
5961 }
5962 });
5963 }
5964 }
5965 }
5966
5967 self.commands.push(Command::EndContainer);
5968 self.last_text_idx = None;
5969 self
5970 }
5971
5972 fn parse_inline_segments(
5973 text: &str,
5974 base: Style,
5975 bold: Style,
5976 code: Style,
5977 ) -> Vec<(String, Style)> {
5978 let mut segments: Vec<(String, Style)> = Vec::new();
5979 let mut current = String::new();
5980 let chars: Vec<char> = text.chars().collect();
5981 let mut i = 0;
5982 while i < chars.len() {
5983 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
5984 let rest: String = chars[i + 2..].iter().collect();
5985 if let Some(end) = rest.find("**") {
5986 if !current.is_empty() {
5987 segments.push((std::mem::take(&mut current), base));
5988 }
5989 let inner: String = rest[..end].to_string();
5990 let char_count = inner.chars().count();
5991 segments.push((inner, bold));
5992 i += 2 + char_count + 2;
5993 continue;
5994 }
5995 }
5996 if chars[i] == '*'
5997 && (i + 1 >= chars.len() || chars[i + 1] != '*')
5998 && (i == 0 || chars[i - 1] != '*')
5999 {
6000 let rest: String = chars[i + 1..].iter().collect();
6001 if let Some(end) = rest.find('*') {
6002 if !current.is_empty() {
6003 segments.push((std::mem::take(&mut current), base));
6004 }
6005 let inner: String = rest[..end].to_string();
6006 let char_count = inner.chars().count();
6007 segments.push((inner, base.italic()));
6008 i += 1 + char_count + 1;
6009 continue;
6010 }
6011 }
6012 if chars[i] == '`' {
6013 let rest: String = chars[i + 1..].iter().collect();
6014 if let Some(end) = rest.find('`') {
6015 if !current.is_empty() {
6016 segments.push((std::mem::take(&mut current), base));
6017 }
6018 let inner: String = rest[..end].to_string();
6019 let char_count = inner.chars().count();
6020 segments.push((inner, code));
6021 i += 1 + char_count + 1;
6022 continue;
6023 }
6024 }
6025 current.push(chars[i]);
6026 i += 1;
6027 }
6028 if !current.is_empty() {
6029 segments.push((current, base));
6030 }
6031 segments
6032 }
6033
6034 pub fn key_seq(&self, seq: &str) -> bool {
6041 if seq.is_empty() {
6042 return false;
6043 }
6044 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6045 return false;
6046 }
6047 let target: Vec<char> = seq.chars().collect();
6048 let mut matched = 0;
6049 for (i, event) in self.events.iter().enumerate() {
6050 if self.consumed[i] {
6051 continue;
6052 }
6053 if let Event::Key(key) = event {
6054 if key.kind != KeyEventKind::Press {
6055 continue;
6056 }
6057 if let KeyCode::Char(c) = key.code {
6058 if c == target[matched] {
6059 matched += 1;
6060 if matched == target.len() {
6061 return true;
6062 }
6063 } else {
6064 matched = 0;
6065 if c == target[0] {
6066 matched = 1;
6067 }
6068 }
6069 }
6070 }
6071 }
6072 false
6073 }
6074
6075 pub fn separator(&mut self) -> &mut Self {
6080 self.commands.push(Command::Text {
6081 content: "─".repeat(200),
6082 style: Style::new().fg(self.theme.border).dim(),
6083 grow: 0,
6084 align: Align::Start,
6085 wrap: false,
6086 margin: Margin::default(),
6087 constraints: Constraints::default(),
6088 });
6089 self.last_text_idx = Some(self.commands.len() - 1);
6090 self
6091 }
6092
6093 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
6099 if bindings.is_empty() {
6100 return self;
6101 }
6102
6103 self.interaction_count += 1;
6104 self.commands.push(Command::BeginContainer {
6105 direction: Direction::Row,
6106 gap: 2,
6107 align: Align::Start,
6108 justify: Justify::Start,
6109 border: None,
6110 border_sides: BorderSides::all(),
6111 border_style: Style::new().fg(self.theme.border),
6112 bg_color: None,
6113 padding: Padding::default(),
6114 margin: Margin::default(),
6115 constraints: Constraints::default(),
6116 title: None,
6117 grow: 0,
6118 group_name: None,
6119 });
6120 for (idx, (key, action)) in bindings.iter().enumerate() {
6121 if idx > 0 {
6122 self.styled("·", Style::new().fg(self.theme.text_dim));
6123 }
6124 self.styled(*key, Style::new().bold().fg(self.theme.primary));
6125 self.styled(*action, Style::new().fg(self.theme.text_dim));
6126 }
6127 self.commands.push(Command::EndContainer);
6128 self.last_text_idx = None;
6129
6130 self
6131 }
6132
6133 pub fn key(&self, c: char) -> bool {
6139 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6140 return false;
6141 }
6142 self.events.iter().enumerate().any(|(i, e)| {
6143 !self.consumed[i]
6144 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
6145 })
6146 }
6147
6148 pub fn key_code(&self, code: KeyCode) -> bool {
6152 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6153 return false;
6154 }
6155 self.events.iter().enumerate().any(|(i, e)| {
6156 !self.consumed[i]
6157 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
6158 })
6159 }
6160
6161 pub fn key_release(&self, c: char) -> bool {
6165 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6166 return false;
6167 }
6168 self.events.iter().enumerate().any(|(i, e)| {
6169 !self.consumed[i]
6170 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
6171 })
6172 }
6173
6174 pub fn key_code_release(&self, code: KeyCode) -> bool {
6178 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6179 return false;
6180 }
6181 self.events.iter().enumerate().any(|(i, e)| {
6182 !self.consumed[i]
6183 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
6184 })
6185 }
6186
6187 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
6191 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6192 return false;
6193 }
6194 self.events.iter().enumerate().any(|(i, e)| {
6195 !self.consumed[i]
6196 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
6197 })
6198 }
6199
6200 pub fn mouse_down(&self) -> Option<(u32, u32)> {
6204 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6205 return None;
6206 }
6207 self.events.iter().enumerate().find_map(|(i, event)| {
6208 if self.consumed[i] {
6209 return None;
6210 }
6211 if let Event::Mouse(mouse) = event {
6212 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
6213 return Some((mouse.x, mouse.y));
6214 }
6215 }
6216 None
6217 })
6218 }
6219
6220 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
6225 self.mouse_pos
6226 }
6227
6228 pub fn paste(&self) -> Option<&str> {
6230 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6231 return None;
6232 }
6233 self.events.iter().enumerate().find_map(|(i, event)| {
6234 if self.consumed[i] {
6235 return None;
6236 }
6237 if let Event::Paste(ref text) = event {
6238 return Some(text.as_str());
6239 }
6240 None
6241 })
6242 }
6243
6244 pub fn scroll_up(&self) -> bool {
6246 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6247 return false;
6248 }
6249 self.events.iter().enumerate().any(|(i, event)| {
6250 !self.consumed[i]
6251 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
6252 })
6253 }
6254
6255 pub fn scroll_down(&self) -> bool {
6257 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
6258 return false;
6259 }
6260 self.events.iter().enumerate().any(|(i, event)| {
6261 !self.consumed[i]
6262 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
6263 })
6264 }
6265
6266 pub fn quit(&mut self) {
6268 self.should_quit = true;
6269 }
6270
6271 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
6279 self.clipboard_text = Some(text.into());
6280 }
6281
6282 pub fn theme(&self) -> &Theme {
6284 &self.theme
6285 }
6286
6287 pub fn set_theme(&mut self, theme: Theme) {
6291 self.theme = theme;
6292 }
6293
6294 pub fn is_dark_mode(&self) -> bool {
6296 self.dark_mode
6297 }
6298
6299 pub fn set_dark_mode(&mut self, dark: bool) {
6301 self.dark_mode = dark;
6302 }
6303
6304 pub fn width(&self) -> u32 {
6308 self.area_width
6309 }
6310
6311 pub fn breakpoint(&self) -> Breakpoint {
6335 let w = self.area_width;
6336 if w < 40 {
6337 Breakpoint::Xs
6338 } else if w < 80 {
6339 Breakpoint::Sm
6340 } else if w < 120 {
6341 Breakpoint::Md
6342 } else if w < 160 {
6343 Breakpoint::Lg
6344 } else {
6345 Breakpoint::Xl
6346 }
6347 }
6348
6349 pub fn height(&self) -> u32 {
6351 self.area_height
6352 }
6353
6354 pub fn tick(&self) -> u64 {
6359 self.tick
6360 }
6361
6362 pub fn debug_enabled(&self) -> bool {
6366 self.debug
6367 }
6368}
6369
6370#[inline]
6371fn byte_index_for_char(value: &str, char_index: usize) -> usize {
6372 if char_index == 0 {
6373 return 0;
6374 }
6375 value
6376 .char_indices()
6377 .nth(char_index)
6378 .map_or(value.len(), |(idx, _)| idx)
6379}
6380
6381fn format_token_count(count: usize) -> String {
6382 if count >= 1_000_000 {
6383 format!("{:.1}M", count as f64 / 1_000_000.0)
6384 } else if count >= 1_000 {
6385 format!("{:.1}k", count as f64 / 1_000.0)
6386 } else {
6387 format!("{count}")
6388 }
6389}
6390
6391fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
6392 let mut parts: Vec<String> = Vec::new();
6393 for (i, width) in widths.iter().enumerate() {
6394 let cell = cells.get(i).map(String::as_str).unwrap_or("");
6395 let cell_width = UnicodeWidthStr::width(cell) as u32;
6396 let padding = (*width).saturating_sub(cell_width) as usize;
6397 parts.push(format!("{cell}{}", " ".repeat(padding)));
6398 }
6399 parts.join(separator)
6400}
6401
6402fn format_compact_number(value: f64) -> String {
6403 if value.fract().abs() < f64::EPSILON {
6404 return format!("{value:.0}");
6405 }
6406
6407 let mut s = format!("{value:.2}");
6408 while s.contains('.') && s.ends_with('0') {
6409 s.pop();
6410 }
6411 if s.ends_with('.') {
6412 s.pop();
6413 }
6414 s
6415}
6416
6417fn center_text(text: &str, width: usize) -> String {
6418 let text_width = UnicodeWidthStr::width(text);
6419 if text_width >= width {
6420 return text.to_string();
6421 }
6422
6423 let total = width - text_width;
6424 let left = total / 2;
6425 let right = total - left;
6426 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
6427}
6428
6429struct TextareaVLine {
6430 logical_row: usize,
6431 char_start: usize,
6432 char_count: usize,
6433}
6434
6435fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
6436 let mut out = Vec::new();
6437 for (row, line) in lines.iter().enumerate() {
6438 if line.is_empty() || wrap_width == u32::MAX {
6439 out.push(TextareaVLine {
6440 logical_row: row,
6441 char_start: 0,
6442 char_count: line.chars().count(),
6443 });
6444 continue;
6445 }
6446 let mut seg_start = 0usize;
6447 let mut seg_chars = 0usize;
6448 let mut seg_width = 0u32;
6449 for (idx, ch) in line.chars().enumerate() {
6450 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
6451 if seg_width + cw > wrap_width && seg_chars > 0 {
6452 out.push(TextareaVLine {
6453 logical_row: row,
6454 char_start: seg_start,
6455 char_count: seg_chars,
6456 });
6457 seg_start = idx;
6458 seg_chars = 0;
6459 seg_width = 0;
6460 }
6461 seg_chars += 1;
6462 seg_width += cw;
6463 }
6464 out.push(TextareaVLine {
6465 logical_row: row,
6466 char_start: seg_start,
6467 char_count: seg_chars,
6468 });
6469 }
6470 out
6471}
6472
6473fn textarea_logical_to_visual(
6474 vlines: &[TextareaVLine],
6475 logical_row: usize,
6476 logical_col: usize,
6477) -> (usize, usize) {
6478 for (i, vl) in vlines.iter().enumerate() {
6479 if vl.logical_row != logical_row {
6480 continue;
6481 }
6482 let seg_end = vl.char_start + vl.char_count;
6483 if logical_col >= vl.char_start && logical_col < seg_end {
6484 return (i, logical_col - vl.char_start);
6485 }
6486 if logical_col == seg_end {
6487 let is_last_seg = vlines
6488 .get(i + 1)
6489 .map_or(true, |next| next.logical_row != logical_row);
6490 if is_last_seg {
6491 return (i, logical_col - vl.char_start);
6492 }
6493 }
6494 }
6495 (vlines.len().saturating_sub(1), 0)
6496}
6497
6498fn textarea_visual_to_logical(
6499 vlines: &[TextareaVLine],
6500 visual_row: usize,
6501 visual_col: usize,
6502) -> (usize, usize) {
6503 if let Some(vl) = vlines.get(visual_row) {
6504 let logical_col = vl.char_start + visual_col.min(vl.char_count);
6505 (vl.logical_row, logical_col)
6506 } else {
6507 (0, 0)
6508 }
6509}
6510
6511fn open_url(url: &str) -> std::io::Result<()> {
6512 #[cfg(target_os = "macos")]
6513 {
6514 std::process::Command::new("open").arg(url).spawn()?;
6515 }
6516 #[cfg(target_os = "linux")]
6517 {
6518 std::process::Command::new("xdg-open").arg(url).spawn()?;
6519 }
6520 #[cfg(target_os = "windows")]
6521 {
6522 std::process::Command::new("cmd")
6523 .args(["/c", "start", "", url])
6524 .spawn()?;
6525 }
6526 Ok(())
6527}