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