1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseKind};
3use crate::halfblock::HalfBlockImage;
4use crate::layout::{Command, Direction};
5use crate::rect::Rect;
6use crate::style::{
7 Align, Border, BorderSides, Breakpoint, Color, Constraints, ContainerStyle, Justify, Margin,
8 Modifiers, Padding, Style, Theme,
9};
10use crate::widgets::{
11 ApprovalAction, ButtonVariant, CommandPaletteState, ContextItem, FormField, FormState,
12 ListState, MultiSelectState, RadioState, ScrollState, SelectState, SpinnerState,
13 StreamingTextState, TableState, TabsState, TextInputState, TextareaState, ToastLevel,
14 ToastState, ToolApprovalState, TreeState,
15};
16use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
17
18#[allow(dead_code)]
19fn slt_assert(condition: bool, msg: &str) {
20 if !condition {
21 panic!("[SLT] {}", msg);
22 }
23}
24
25#[cfg(debug_assertions)]
26#[allow(dead_code)]
27fn slt_warn(msg: &str) {
28 eprintln!("\x1b[33m[SLT warning]\x1b[0m {}", msg);
29}
30
31#[cfg(not(debug_assertions))]
32#[allow(dead_code)]
33fn slt_warn(_msg: &str) {}
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37pub struct State<T> {
38 idx: usize,
39 _marker: std::marker::PhantomData<T>,
40}
41
42impl<T: 'static> State<T> {
43 pub fn get<'a>(&self, ui: &'a Context) -> &'a T {
45 ui.hook_states[self.idx]
46 .downcast_ref::<T>()
47 .unwrap_or_else(|| {
48 panic!(
49 "use_state type mismatch at hook index {} — expected {}",
50 self.idx,
51 std::any::type_name::<T>()
52 )
53 })
54 }
55
56 pub fn get_mut<'a>(&self, ui: &'a mut Context) -> &'a mut T {
58 ui.hook_states[self.idx]
59 .downcast_mut::<T>()
60 .unwrap_or_else(|| {
61 panic!(
62 "use_state type mismatch at hook index {} — expected {}",
63 self.idx,
64 std::any::type_name::<T>()
65 )
66 })
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default)]
76pub struct Response {
77 pub clicked: bool,
79 pub hovered: bool,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum BarDirection {
86 Horizontal,
88 Vertical,
90}
91
92#[derive(Debug, Clone)]
94pub struct Bar {
95 pub label: String,
97 pub value: f64,
99 pub color: Option<Color>,
101}
102
103impl Bar {
104 pub fn new(label: impl Into<String>, value: f64) -> Self {
106 Self {
107 label: label.into(),
108 value,
109 color: None,
110 }
111 }
112
113 pub fn color(mut self, color: Color) -> Self {
115 self.color = Some(color);
116 self
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct BarGroup {
123 pub label: String,
125 pub bars: Vec<Bar>,
127}
128
129impl BarGroup {
130 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
132 Self {
133 label: label.into(),
134 bars,
135 }
136 }
137}
138
139pub trait Widget {
201 type Response;
204
205 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
211}
212
213pub struct Context {
229 pub(crate) commands: Vec<Command>,
230 pub(crate) events: Vec<Event>,
231 pub(crate) consumed: Vec<bool>,
232 pub(crate) should_quit: bool,
233 pub(crate) area_width: u32,
234 pub(crate) area_height: u32,
235 pub(crate) tick: u64,
236 pub(crate) focus_index: usize,
237 pub(crate) focus_count: usize,
238 pub(crate) hook_states: Vec<Box<dyn std::any::Any>>,
239 pub(crate) hook_cursor: usize,
240 prev_focus_count: usize,
241 scroll_count: usize,
242 prev_scroll_infos: Vec<(u32, u32)>,
243 prev_scroll_rects: Vec<Rect>,
244 interaction_count: usize,
245 pub(crate) prev_hit_map: Vec<Rect>,
246 pub(crate) group_stack: Vec<String>,
247 pub(crate) prev_group_rects: Vec<(String, Rect)>,
248 group_count: usize,
249 prev_focus_groups: Vec<Option<String>>,
250 _prev_focus_rects: Vec<(usize, Rect)>,
251 mouse_pos: Option<(u32, u32)>,
252 click_pos: Option<(u32, u32)>,
253 last_text_idx: Option<usize>,
254 overlay_depth: usize,
255 pub(crate) modal_active: bool,
256 prev_modal_active: bool,
257 pub(crate) clipboard_text: Option<String>,
258 debug: bool,
259 theme: Theme,
260 pub(crate) dark_mode: bool,
261 pub(crate) deferred_draws: Vec<Option<RawDrawCallback>>,
262}
263
264type RawDrawCallback = Box<dyn FnOnce(&mut crate::buffer::Buffer, Rect)>;
265
266#[must_use = "configure and finalize with .col() or .row()"]
287pub struct ContainerBuilder<'a> {
288 ctx: &'a mut Context,
289 gap: u32,
290 align: Align,
291 justify: Justify,
292 border: Option<Border>,
293 border_sides: BorderSides,
294 border_style: Style,
295 bg_color: Option<Color>,
296 dark_bg_color: Option<Color>,
297 dark_border_style: Option<Style>,
298 group_hover_bg: Option<Color>,
299 group_hover_border_style: Option<Style>,
300 group_name: Option<String>,
301 padding: Padding,
302 margin: Margin,
303 constraints: Constraints,
304 title: Option<(String, Style)>,
305 grow: u16,
306 scroll_offset: Option<u32>,
307}
308
309#[derive(Debug, Clone, Copy)]
316struct CanvasPixel {
317 bits: u32,
318 color: Color,
319}
320
321#[derive(Debug, Clone)]
323struct CanvasLabel {
324 x: usize,
325 y: usize,
326 text: String,
327 color: Color,
328}
329
330#[derive(Debug, Clone)]
332struct CanvasLayer {
333 grid: Vec<Vec<CanvasPixel>>,
334 labels: Vec<CanvasLabel>,
335}
336
337pub struct CanvasContext {
338 layers: Vec<CanvasLayer>,
339 cols: usize,
340 rows: usize,
341 px_w: usize,
342 px_h: usize,
343 current_color: Color,
344}
345
346impl CanvasContext {
347 fn new(cols: usize, rows: usize) -> Self {
348 Self {
349 layers: vec![Self::new_layer(cols, rows)],
350 cols,
351 rows,
352 px_w: cols * 2,
353 px_h: rows * 4,
354 current_color: Color::Reset,
355 }
356 }
357
358 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
359 CanvasLayer {
360 grid: vec![
361 vec![
362 CanvasPixel {
363 bits: 0,
364 color: Color::Reset,
365 };
366 cols
367 ];
368 rows
369 ],
370 labels: Vec::new(),
371 }
372 }
373
374 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
375 self.layers.last_mut()
376 }
377
378 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
379 if x >= self.px_w || y >= self.px_h {
380 return;
381 }
382
383 let char_col = x / 2;
384 let char_row = y / 4;
385 let sub_col = x % 2;
386 let sub_row = y % 4;
387 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
388 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
389
390 let bit = if sub_col == 0 {
391 LEFT_BITS[sub_row]
392 } else {
393 RIGHT_BITS[sub_row]
394 };
395
396 if let Some(layer) = self.current_layer_mut() {
397 let cell = &mut layer.grid[char_row][char_col];
398 let new_bits = cell.bits | bit;
399 if new_bits != cell.bits {
400 cell.bits = new_bits;
401 cell.color = color;
402 }
403 }
404 }
405
406 fn dot_isize(&mut self, x: isize, y: isize) {
407 if x >= 0 && y >= 0 {
408 self.dot(x as usize, y as usize);
409 }
410 }
411
412 pub fn width(&self) -> usize {
414 self.px_w
415 }
416
417 pub fn height(&self) -> usize {
419 self.px_h
420 }
421
422 pub fn dot(&mut self, x: usize, y: usize) {
424 self.dot_with_color(x, y, self.current_color);
425 }
426
427 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
429 let (mut x, mut y) = (x0 as isize, y0 as isize);
430 let (x1, y1) = (x1 as isize, y1 as isize);
431 let dx = (x1 - x).abs();
432 let dy = -(y1 - y).abs();
433 let sx = if x < x1 { 1 } else { -1 };
434 let sy = if y < y1 { 1 } else { -1 };
435 let mut err = dx + dy;
436
437 loop {
438 self.dot_isize(x, y);
439 if x == x1 && y == y1 {
440 break;
441 }
442 let e2 = 2 * err;
443 if e2 >= dy {
444 err += dy;
445 x += sx;
446 }
447 if e2 <= dx {
448 err += dx;
449 y += sy;
450 }
451 }
452 }
453
454 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
456 if w == 0 || h == 0 {
457 return;
458 }
459
460 self.line(x, y, x + w.saturating_sub(1), y);
461 self.line(
462 x + w.saturating_sub(1),
463 y,
464 x + w.saturating_sub(1),
465 y + h.saturating_sub(1),
466 );
467 self.line(
468 x + w.saturating_sub(1),
469 y + h.saturating_sub(1),
470 x,
471 y + h.saturating_sub(1),
472 );
473 self.line(x, y + h.saturating_sub(1), x, y);
474 }
475
476 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
478 let mut x = r as isize;
479 let mut y: isize = 0;
480 let mut err: isize = 1 - x;
481 let (cx, cy) = (cx as isize, cy as isize);
482
483 while x >= y {
484 for &(dx, dy) in &[
485 (x, y),
486 (y, x),
487 (-x, y),
488 (-y, x),
489 (x, -y),
490 (y, -x),
491 (-x, -y),
492 (-y, -x),
493 ] {
494 let px = cx + dx;
495 let py = cy + dy;
496 self.dot_isize(px, py);
497 }
498
499 y += 1;
500 if err < 0 {
501 err += 2 * y + 1;
502 } else {
503 x -= 1;
504 err += 2 * (y - x) + 1;
505 }
506 }
507 }
508
509 pub fn set_color(&mut self, color: Color) {
511 self.current_color = color;
512 }
513
514 pub fn color(&self) -> Color {
516 self.current_color
517 }
518
519 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
521 if w == 0 || h == 0 {
522 return;
523 }
524
525 let x_end = x.saturating_add(w).min(self.px_w);
526 let y_end = y.saturating_add(h).min(self.px_h);
527 if x >= x_end || y >= y_end {
528 return;
529 }
530
531 for yy in y..y_end {
532 self.line(x, yy, x_end.saturating_sub(1), yy);
533 }
534 }
535
536 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
538 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
539 for y in (cy - r)..=(cy + r) {
540 let dy = y - cy;
541 let span_sq = (r * r - dy * dy).max(0);
542 let dx = (span_sq as f64).sqrt() as isize;
543 for x in (cx - dx)..=(cx + dx) {
544 self.dot_isize(x, y);
545 }
546 }
547 }
548
549 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
551 self.line(x0, y0, x1, y1);
552 self.line(x1, y1, x2, y2);
553 self.line(x2, y2, x0, y0);
554 }
555
556 pub fn filled_triangle(
558 &mut self,
559 x0: usize,
560 y0: usize,
561 x1: usize,
562 y1: usize,
563 x2: usize,
564 y2: usize,
565 ) {
566 let vertices = [
567 (x0 as isize, y0 as isize),
568 (x1 as isize, y1 as isize),
569 (x2 as isize, y2 as isize),
570 ];
571 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
572 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
573
574 for y in min_y..=max_y {
575 let mut intersections: Vec<f64> = Vec::new();
576
577 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
578 let (x_a, y_a) = vertices[edge.0];
579 let (x_b, y_b) = vertices[edge.1];
580 if y_a == y_b {
581 continue;
582 }
583
584 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
585 (x_a, y_a, x_b, y_b)
586 } else {
587 (x_b, y_b, x_a, y_a)
588 };
589
590 if y < y_start || y >= y_end {
591 continue;
592 }
593
594 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
595 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
596 }
597
598 intersections.sort_by(|a, b| a.total_cmp(b));
599 let mut i = 0usize;
600 while i + 1 < intersections.len() {
601 let x_start = intersections[i].ceil() as isize;
602 let x_end = intersections[i + 1].floor() as isize;
603 for x in x_start..=x_end {
604 self.dot_isize(x, y);
605 }
606 i += 2;
607 }
608 }
609
610 self.triangle(x0, y0, x1, y1, x2, y2);
611 }
612
613 pub fn points(&mut self, pts: &[(usize, usize)]) {
615 for &(x, y) in pts {
616 self.dot(x, y);
617 }
618 }
619
620 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
622 for window in pts.windows(2) {
623 if let [(x0, y0), (x1, y1)] = window {
624 self.line(*x0, *y0, *x1, *y1);
625 }
626 }
627 }
628
629 pub fn print(&mut self, x: usize, y: usize, text: &str) {
632 if text.is_empty() {
633 return;
634 }
635
636 let color = self.current_color;
637 if let Some(layer) = self.current_layer_mut() {
638 layer.labels.push(CanvasLabel {
639 x,
640 y,
641 text: text.to_string(),
642 color,
643 });
644 }
645 }
646
647 pub fn layer(&mut self) {
649 self.layers.push(Self::new_layer(self.cols, self.rows));
650 }
651
652 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
653 let mut final_grid = vec![
654 vec![
655 CanvasPixel {
656 bits: 0,
657 color: Color::Reset,
658 };
659 self.cols
660 ];
661 self.rows
662 ];
663 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
664 vec![vec![None; self.cols]; self.rows];
665
666 for layer in &self.layers {
667 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
668 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
669 let src = layer.grid[row][col];
670 if src.bits == 0 {
671 continue;
672 }
673
674 let merged = dst.bits | src.bits;
675 if merged != dst.bits {
676 dst.bits = merged;
677 dst.color = src.color;
678 }
679 }
680 }
681
682 for label in &layer.labels {
683 let row = label.y / 4;
684 if row >= self.rows {
685 continue;
686 }
687 let start_col = label.x / 2;
688 for (offset, ch) in label.text.chars().enumerate() {
689 let col = start_col + offset;
690 if col >= self.cols {
691 break;
692 }
693 labels_overlay[row][col] = Some((ch, label.color));
694 }
695 }
696 }
697
698 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
699 for row in 0..self.rows {
700 let mut segments: Vec<(String, Color)> = Vec::new();
701 let mut current_color: Option<Color> = None;
702 let mut current_text = String::new();
703
704 for col in 0..self.cols {
705 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
706 (label_ch, label_color)
707 } else {
708 let bits = final_grid[row][col].bits;
709 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
710 (ch, final_grid[row][col].color)
711 };
712
713 match current_color {
714 Some(c) if c == color => {
715 current_text.push(ch);
716 }
717 Some(c) => {
718 segments.push((std::mem::take(&mut current_text), c));
719 current_text.push(ch);
720 current_color = Some(color);
721 }
722 None => {
723 current_text.push(ch);
724 current_color = Some(color);
725 }
726 }
727 }
728
729 if let Some(color) = current_color {
730 segments.push((current_text, color));
731 }
732 lines.push(segments);
733 }
734
735 lines
736 }
737}
738
739impl<'a> ContainerBuilder<'a> {
740 pub fn apply(mut self, style: &ContainerStyle) -> Self {
745 if let Some(v) = style.border {
746 self.border = Some(v);
747 }
748 if let Some(v) = style.border_sides {
749 self.border_sides = v;
750 }
751 if let Some(v) = style.border_style {
752 self.border_style = v;
753 }
754 if let Some(v) = style.bg {
755 self.bg_color = Some(v);
756 }
757 if let Some(v) = style.dark_bg {
758 self.dark_bg_color = Some(v);
759 }
760 if let Some(v) = style.dark_border_style {
761 self.dark_border_style = Some(v);
762 }
763 if let Some(v) = style.padding {
764 self.padding = v;
765 }
766 if let Some(v) = style.margin {
767 self.margin = v;
768 }
769 if let Some(v) = style.gap {
770 self.gap = v;
771 }
772 if let Some(v) = style.grow {
773 self.grow = v;
774 }
775 if let Some(v) = style.align {
776 self.align = v;
777 }
778 if let Some(v) = style.justify {
779 self.justify = v;
780 }
781 if let Some(w) = style.w {
782 self.constraints.min_width = Some(w);
783 self.constraints.max_width = Some(w);
784 }
785 if let Some(h) = style.h {
786 self.constraints.min_height = Some(h);
787 self.constraints.max_height = Some(h);
788 }
789 if let Some(v) = style.min_w {
790 self.constraints.min_width = Some(v);
791 }
792 if let Some(v) = style.max_w {
793 self.constraints.max_width = Some(v);
794 }
795 if let Some(v) = style.min_h {
796 self.constraints.min_height = Some(v);
797 }
798 if let Some(v) = style.max_h {
799 self.constraints.max_height = Some(v);
800 }
801 if let Some(v) = style.w_pct {
802 self.constraints.width_pct = Some(v);
803 }
804 if let Some(v) = style.h_pct {
805 self.constraints.height_pct = Some(v);
806 }
807 self
808 }
809
810 pub fn border(mut self, border: Border) -> Self {
812 self.border = Some(border);
813 self
814 }
815
816 pub fn border_top(mut self, show: bool) -> Self {
818 self.border_sides.top = show;
819 self
820 }
821
822 pub fn border_right(mut self, show: bool) -> Self {
824 self.border_sides.right = show;
825 self
826 }
827
828 pub fn border_bottom(mut self, show: bool) -> Self {
830 self.border_sides.bottom = show;
831 self
832 }
833
834 pub fn border_left(mut self, show: bool) -> Self {
836 self.border_sides.left = show;
837 self
838 }
839
840 pub fn border_sides(mut self, sides: BorderSides) -> Self {
842 self.border_sides = sides;
843 self
844 }
845
846 pub fn rounded(self) -> Self {
848 self.border(Border::Rounded)
849 }
850
851 pub fn border_style(mut self, style: Style) -> Self {
853 self.border_style = style;
854 self
855 }
856
857 pub fn dark_border_style(mut self, style: Style) -> Self {
859 self.dark_border_style = Some(style);
860 self
861 }
862
863 pub fn bg(mut self, color: Color) -> Self {
864 self.bg_color = Some(color);
865 self
866 }
867
868 pub fn dark_bg(mut self, color: Color) -> Self {
870 self.dark_bg_color = Some(color);
871 self
872 }
873
874 pub fn group_hover_bg(mut self, color: Color) -> Self {
876 self.group_hover_bg = Some(color);
877 self
878 }
879
880 pub fn group_hover_border_style(mut self, style: Style) -> Self {
882 self.group_hover_border_style = Some(style);
883 self
884 }
885
886 pub fn p(self, value: u32) -> Self {
890 self.pad(value)
891 }
892
893 pub fn pad(mut self, value: u32) -> Self {
895 self.padding = Padding::all(value);
896 self
897 }
898
899 pub fn px(mut self, value: u32) -> Self {
901 self.padding.left = value;
902 self.padding.right = value;
903 self
904 }
905
906 pub fn py(mut self, value: u32) -> Self {
908 self.padding.top = value;
909 self.padding.bottom = value;
910 self
911 }
912
913 pub fn pt(mut self, value: u32) -> Self {
915 self.padding.top = value;
916 self
917 }
918
919 pub fn pr(mut self, value: u32) -> Self {
921 self.padding.right = value;
922 self
923 }
924
925 pub fn pb(mut self, value: u32) -> Self {
927 self.padding.bottom = value;
928 self
929 }
930
931 pub fn pl(mut self, value: u32) -> Self {
933 self.padding.left = value;
934 self
935 }
936
937 pub fn padding(mut self, padding: Padding) -> Self {
939 self.padding = padding;
940 self
941 }
942
943 pub fn m(mut self, value: u32) -> Self {
947 self.margin = Margin::all(value);
948 self
949 }
950
951 pub fn mx(mut self, value: u32) -> Self {
953 self.margin.left = value;
954 self.margin.right = value;
955 self
956 }
957
958 pub fn my(mut self, value: u32) -> Self {
960 self.margin.top = value;
961 self.margin.bottom = value;
962 self
963 }
964
965 pub fn mt(mut self, value: u32) -> Self {
967 self.margin.top = value;
968 self
969 }
970
971 pub fn mr(mut self, value: u32) -> Self {
973 self.margin.right = value;
974 self
975 }
976
977 pub fn mb(mut self, value: u32) -> Self {
979 self.margin.bottom = value;
980 self
981 }
982
983 pub fn ml(mut self, value: u32) -> Self {
985 self.margin.left = value;
986 self
987 }
988
989 pub fn margin(mut self, margin: Margin) -> Self {
991 self.margin = margin;
992 self
993 }
994
995 pub fn w(mut self, value: u32) -> Self {
999 self.constraints.min_width = Some(value);
1000 self.constraints.max_width = Some(value);
1001 self
1002 }
1003
1004 pub fn xs_w(self, value: u32) -> Self {
1011 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1012 if is_xs {
1013 self.w(value)
1014 } else {
1015 self
1016 }
1017 }
1018
1019 pub fn sm_w(self, value: u32) -> Self {
1021 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1022 if is_sm {
1023 self.w(value)
1024 } else {
1025 self
1026 }
1027 }
1028
1029 pub fn md_w(self, value: u32) -> Self {
1031 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1032 if is_md {
1033 self.w(value)
1034 } else {
1035 self
1036 }
1037 }
1038
1039 pub fn lg_w(self, value: u32) -> Self {
1041 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1042 if is_lg {
1043 self.w(value)
1044 } else {
1045 self
1046 }
1047 }
1048
1049 pub fn xl_w(self, value: u32) -> Self {
1051 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1052 if is_xl {
1053 self.w(value)
1054 } else {
1055 self
1056 }
1057 }
1058 pub fn w_at(self, bp: Breakpoint, value: u32) -> Self {
1059 if self.ctx.breakpoint() == bp {
1060 self.w(value)
1061 } else {
1062 self
1063 }
1064 }
1065
1066 pub fn h(mut self, value: u32) -> Self {
1068 self.constraints.min_height = Some(value);
1069 self.constraints.max_height = Some(value);
1070 self
1071 }
1072
1073 pub fn xs_h(self, value: u32) -> Self {
1075 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1076 if is_xs {
1077 self.h(value)
1078 } else {
1079 self
1080 }
1081 }
1082
1083 pub fn sm_h(self, value: u32) -> Self {
1085 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1086 if is_sm {
1087 self.h(value)
1088 } else {
1089 self
1090 }
1091 }
1092
1093 pub fn md_h(self, value: u32) -> Self {
1095 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1096 if is_md {
1097 self.h(value)
1098 } else {
1099 self
1100 }
1101 }
1102
1103 pub fn lg_h(self, value: u32) -> Self {
1105 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1106 if is_lg {
1107 self.h(value)
1108 } else {
1109 self
1110 }
1111 }
1112
1113 pub fn xl_h(self, value: u32) -> Self {
1115 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1116 if is_xl {
1117 self.h(value)
1118 } else {
1119 self
1120 }
1121 }
1122 pub fn h_at(self, bp: Breakpoint, value: u32) -> Self {
1123 if self.ctx.breakpoint() == bp {
1124 self.h(value)
1125 } else {
1126 self
1127 }
1128 }
1129
1130 pub fn min_w(mut self, value: u32) -> Self {
1132 self.constraints.min_width = Some(value);
1133 self
1134 }
1135
1136 pub fn xs_min_w(self, value: u32) -> Self {
1138 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1139 if is_xs {
1140 self.min_w(value)
1141 } else {
1142 self
1143 }
1144 }
1145
1146 pub fn sm_min_w(self, value: u32) -> Self {
1148 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1149 if is_sm {
1150 self.min_w(value)
1151 } else {
1152 self
1153 }
1154 }
1155
1156 pub fn md_min_w(self, value: u32) -> Self {
1158 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1159 if is_md {
1160 self.min_w(value)
1161 } else {
1162 self
1163 }
1164 }
1165
1166 pub fn lg_min_w(self, value: u32) -> Self {
1168 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1169 if is_lg {
1170 self.min_w(value)
1171 } else {
1172 self
1173 }
1174 }
1175
1176 pub fn xl_min_w(self, value: u32) -> Self {
1178 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1179 if is_xl {
1180 self.min_w(value)
1181 } else {
1182 self
1183 }
1184 }
1185 pub fn min_w_at(self, bp: Breakpoint, value: u32) -> Self {
1186 if self.ctx.breakpoint() == bp {
1187 self.min_w(value)
1188 } else {
1189 self
1190 }
1191 }
1192
1193 pub fn max_w(mut self, value: u32) -> Self {
1195 self.constraints.max_width = Some(value);
1196 self
1197 }
1198
1199 pub fn xs_max_w(self, value: u32) -> Self {
1201 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1202 if is_xs {
1203 self.max_w(value)
1204 } else {
1205 self
1206 }
1207 }
1208
1209 pub fn sm_max_w(self, value: u32) -> Self {
1211 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1212 if is_sm {
1213 self.max_w(value)
1214 } else {
1215 self
1216 }
1217 }
1218
1219 pub fn md_max_w(self, value: u32) -> Self {
1221 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1222 if is_md {
1223 self.max_w(value)
1224 } else {
1225 self
1226 }
1227 }
1228
1229 pub fn lg_max_w(self, value: u32) -> Self {
1231 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1232 if is_lg {
1233 self.max_w(value)
1234 } else {
1235 self
1236 }
1237 }
1238
1239 pub fn xl_max_w(self, value: u32) -> Self {
1241 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1242 if is_xl {
1243 self.max_w(value)
1244 } else {
1245 self
1246 }
1247 }
1248 pub fn max_w_at(self, bp: Breakpoint, value: u32) -> Self {
1249 if self.ctx.breakpoint() == bp {
1250 self.max_w(value)
1251 } else {
1252 self
1253 }
1254 }
1255
1256 pub fn min_h(mut self, value: u32) -> Self {
1258 self.constraints.min_height = Some(value);
1259 self
1260 }
1261
1262 pub fn max_h(mut self, value: u32) -> Self {
1264 self.constraints.max_height = Some(value);
1265 self
1266 }
1267
1268 pub fn min_width(mut self, value: u32) -> Self {
1270 self.constraints.min_width = Some(value);
1271 self
1272 }
1273
1274 pub fn max_width(mut self, value: u32) -> Self {
1276 self.constraints.max_width = Some(value);
1277 self
1278 }
1279
1280 pub fn min_height(mut self, value: u32) -> Self {
1282 self.constraints.min_height = Some(value);
1283 self
1284 }
1285
1286 pub fn max_height(mut self, value: u32) -> Self {
1288 self.constraints.max_height = Some(value);
1289 self
1290 }
1291
1292 pub fn w_pct(mut self, pct: u8) -> Self {
1294 self.constraints.width_pct = Some(pct.min(100));
1295 self
1296 }
1297
1298 pub fn h_pct(mut self, pct: u8) -> Self {
1300 self.constraints.height_pct = Some(pct.min(100));
1301 self
1302 }
1303
1304 pub fn constraints(mut self, constraints: Constraints) -> Self {
1306 self.constraints = constraints;
1307 self
1308 }
1309
1310 pub fn gap(mut self, gap: u32) -> Self {
1314 self.gap = gap;
1315 self
1316 }
1317
1318 pub fn xs_gap(self, value: u32) -> Self {
1320 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1321 if is_xs {
1322 self.gap(value)
1323 } else {
1324 self
1325 }
1326 }
1327
1328 pub fn sm_gap(self, value: u32) -> Self {
1330 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1331 if is_sm {
1332 self.gap(value)
1333 } else {
1334 self
1335 }
1336 }
1337
1338 pub fn md_gap(self, value: u32) -> Self {
1345 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1346 if is_md {
1347 self.gap(value)
1348 } else {
1349 self
1350 }
1351 }
1352
1353 pub fn lg_gap(self, value: u32) -> Self {
1355 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1356 if is_lg {
1357 self.gap(value)
1358 } else {
1359 self
1360 }
1361 }
1362
1363 pub fn xl_gap(self, value: u32) -> Self {
1365 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1366 if is_xl {
1367 self.gap(value)
1368 } else {
1369 self
1370 }
1371 }
1372
1373 pub fn gap_at(self, bp: Breakpoint, value: u32) -> Self {
1374 if self.ctx.breakpoint() == bp {
1375 self.gap(value)
1376 } else {
1377 self
1378 }
1379 }
1380
1381 pub fn grow(mut self, grow: u16) -> Self {
1383 self.grow = grow;
1384 self
1385 }
1386
1387 pub fn xs_grow(self, value: u16) -> Self {
1389 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1390 if is_xs {
1391 self.grow(value)
1392 } else {
1393 self
1394 }
1395 }
1396
1397 pub fn sm_grow(self, value: u16) -> Self {
1399 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1400 if is_sm {
1401 self.grow(value)
1402 } else {
1403 self
1404 }
1405 }
1406
1407 pub fn md_grow(self, value: u16) -> Self {
1409 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1410 if is_md {
1411 self.grow(value)
1412 } else {
1413 self
1414 }
1415 }
1416
1417 pub fn lg_grow(self, value: u16) -> Self {
1419 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1420 if is_lg {
1421 self.grow(value)
1422 } else {
1423 self
1424 }
1425 }
1426
1427 pub fn xl_grow(self, value: u16) -> Self {
1429 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1430 if is_xl {
1431 self.grow(value)
1432 } else {
1433 self
1434 }
1435 }
1436 pub fn grow_at(self, bp: Breakpoint, value: u16) -> Self {
1437 if self.ctx.breakpoint() == bp {
1438 self.grow(value)
1439 } else {
1440 self
1441 }
1442 }
1443
1444 pub fn xs_p(self, value: u32) -> Self {
1446 let is_xs = self.ctx.breakpoint() == Breakpoint::Xs;
1447 if is_xs {
1448 self.p(value)
1449 } else {
1450 self
1451 }
1452 }
1453
1454 pub fn sm_p(self, value: u32) -> Self {
1456 let is_sm = self.ctx.breakpoint() == Breakpoint::Sm;
1457 if is_sm {
1458 self.p(value)
1459 } else {
1460 self
1461 }
1462 }
1463
1464 pub fn md_p(self, value: u32) -> Self {
1466 let is_md = self.ctx.breakpoint() == Breakpoint::Md;
1467 if is_md {
1468 self.p(value)
1469 } else {
1470 self
1471 }
1472 }
1473
1474 pub fn lg_p(self, value: u32) -> Self {
1476 let is_lg = self.ctx.breakpoint() == Breakpoint::Lg;
1477 if is_lg {
1478 self.p(value)
1479 } else {
1480 self
1481 }
1482 }
1483
1484 pub fn xl_p(self, value: u32) -> Self {
1486 let is_xl = self.ctx.breakpoint() == Breakpoint::Xl;
1487 if is_xl {
1488 self.p(value)
1489 } else {
1490 self
1491 }
1492 }
1493 pub fn p_at(self, bp: Breakpoint, value: u32) -> Self {
1494 if self.ctx.breakpoint() == bp {
1495 self.p(value)
1496 } else {
1497 self
1498 }
1499 }
1500
1501 pub fn align(mut self, align: Align) -> Self {
1505 self.align = align;
1506 self
1507 }
1508
1509 pub fn center(self) -> Self {
1511 self.align(Align::Center)
1512 }
1513
1514 pub fn justify(mut self, justify: Justify) -> Self {
1516 self.justify = justify;
1517 self
1518 }
1519
1520 pub fn space_between(self) -> Self {
1522 self.justify(Justify::SpaceBetween)
1523 }
1524
1525 pub fn space_around(self) -> Self {
1527 self.justify(Justify::SpaceAround)
1528 }
1529
1530 pub fn space_evenly(self) -> Self {
1532 self.justify(Justify::SpaceEvenly)
1533 }
1534
1535 pub fn title(self, title: impl Into<String>) -> Self {
1539 self.title_styled(title, Style::new())
1540 }
1541
1542 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
1544 self.title = Some((title.into(), style));
1545 self
1546 }
1547
1548 pub fn scroll_offset(mut self, offset: u32) -> Self {
1552 self.scroll_offset = Some(offset);
1553 self
1554 }
1555
1556 fn group_name(mut self, name: String) -> Self {
1557 self.group_name = Some(name);
1558 self
1559 }
1560
1561 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
1566 self.finish(Direction::Column, f)
1567 }
1568
1569 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
1574 self.finish(Direction::Row, f)
1575 }
1576
1577 pub fn line(mut self, f: impl FnOnce(&mut Context)) -> Response {
1582 self.gap = 0;
1583 self.finish(Direction::Row, f)
1584 }
1585
1586 pub fn draw(self, f: impl FnOnce(&mut crate::buffer::Buffer, Rect) + 'static) {
1601 let draw_id = self.ctx.deferred_draws.len();
1602 self.ctx.deferred_draws.push(Some(Box::new(f)));
1603 self.ctx.interaction_count += 1;
1604 self.ctx.commands.push(Command::RawDraw {
1605 draw_id,
1606 constraints: self.constraints,
1607 grow: self.grow,
1608 margin: self.margin,
1609 });
1610 }
1611
1612 fn finish(mut self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
1613 let interaction_id = self.ctx.interaction_count;
1614 self.ctx.interaction_count += 1;
1615
1616 let in_hovered_group = self
1617 .group_name
1618 .as_ref()
1619 .map(|name| self.ctx.is_group_hovered(name))
1620 .unwrap_or(false)
1621 || self
1622 .ctx
1623 .group_stack
1624 .last()
1625 .map(|name| self.ctx.is_group_hovered(name))
1626 .unwrap_or(false);
1627 let in_focused_group = self
1628 .group_name
1629 .as_ref()
1630 .map(|name| self.ctx.is_group_focused(name))
1631 .unwrap_or(false)
1632 || self
1633 .ctx
1634 .group_stack
1635 .last()
1636 .map(|name| self.ctx.is_group_focused(name))
1637 .unwrap_or(false);
1638
1639 let resolved_bg = if self.ctx.dark_mode {
1640 self.dark_bg_color.or(self.bg_color)
1641 } else {
1642 self.bg_color
1643 };
1644 let resolved_border_style = if self.ctx.dark_mode {
1645 self.dark_border_style.unwrap_or(self.border_style)
1646 } else {
1647 self.border_style
1648 };
1649 let bg_color = if in_hovered_group || in_focused_group {
1650 self.group_hover_bg.or(resolved_bg)
1651 } else {
1652 resolved_bg
1653 };
1654 let border_style = if in_hovered_group || in_focused_group {
1655 self.group_hover_border_style
1656 .unwrap_or(resolved_border_style)
1657 } else {
1658 resolved_border_style
1659 };
1660 let group_name = self.group_name.take();
1661 let is_group_container = group_name.is_some();
1662
1663 if let Some(scroll_offset) = self.scroll_offset {
1664 self.ctx.commands.push(Command::BeginScrollable {
1665 grow: self.grow,
1666 border: self.border,
1667 border_sides: self.border_sides,
1668 border_style,
1669 padding: self.padding,
1670 margin: self.margin,
1671 constraints: self.constraints,
1672 title: self.title,
1673 scroll_offset,
1674 });
1675 } else {
1676 self.ctx.commands.push(Command::BeginContainer {
1677 direction,
1678 gap: self.gap,
1679 align: self.align,
1680 justify: self.justify,
1681 border: self.border,
1682 border_sides: self.border_sides,
1683 border_style,
1684 bg_color,
1685 padding: self.padding,
1686 margin: self.margin,
1687 constraints: self.constraints,
1688 title: self.title,
1689 grow: self.grow,
1690 group_name,
1691 });
1692 }
1693 f(self.ctx);
1694 self.ctx.commands.push(Command::EndContainer);
1695 self.ctx.last_text_idx = None;
1696
1697 if is_group_container {
1698 self.ctx.group_stack.pop();
1699 self.ctx.group_count = self.ctx.group_count.saturating_sub(1);
1700 }
1701
1702 self.ctx.response_for(interaction_id)
1703 }
1704}
1705
1706impl Context {
1707 #[allow(clippy::too_many_arguments)]
1708 pub(crate) fn new(
1709 events: Vec<Event>,
1710 width: u32,
1711 height: u32,
1712 tick: u64,
1713 mut focus_index: usize,
1714 prev_focus_count: usize,
1715 prev_scroll_infos: Vec<(u32, u32)>,
1716 prev_scroll_rects: Vec<Rect>,
1717 prev_hit_map: Vec<Rect>,
1718 prev_group_rects: Vec<(String, Rect)>,
1719 prev_focus_rects: Vec<(usize, Rect)>,
1720 prev_focus_groups: Vec<Option<String>>,
1721 prev_hook_states: Vec<Box<dyn std::any::Any>>,
1722 debug: bool,
1723 theme: Theme,
1724 last_mouse_pos: Option<(u32, u32)>,
1725 prev_modal_active: bool,
1726 ) -> Self {
1727 let consumed = vec![false; events.len()];
1728
1729 let mut mouse_pos = last_mouse_pos;
1730 let mut click_pos = None;
1731 for event in &events {
1732 if let Event::Mouse(mouse) = event {
1733 mouse_pos = Some((mouse.x, mouse.y));
1734 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1735 click_pos = Some((mouse.x, mouse.y));
1736 }
1737 }
1738 }
1739
1740 if let Some((mx, my)) = click_pos {
1741 let mut best: Option<(usize, u64)> = None;
1742 for &(fid, rect) in &prev_focus_rects {
1743 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
1744 let area = rect.width as u64 * rect.height as u64;
1745 if best.map_or(true, |(_, ba)| area < ba) {
1746 best = Some((fid, area));
1747 }
1748 }
1749 }
1750 if let Some((fid, _)) = best {
1751 focus_index = fid;
1752 }
1753 }
1754
1755 Self {
1756 commands: Vec::new(),
1757 events,
1758 consumed,
1759 should_quit: false,
1760 area_width: width,
1761 area_height: height,
1762 tick,
1763 focus_index,
1764 focus_count: 0,
1765 hook_states: prev_hook_states,
1766 hook_cursor: 0,
1767 prev_focus_count,
1768 scroll_count: 0,
1769 prev_scroll_infos,
1770 prev_scroll_rects,
1771 interaction_count: 0,
1772 prev_hit_map,
1773 group_stack: Vec::new(),
1774 prev_group_rects,
1775 group_count: 0,
1776 prev_focus_groups,
1777 _prev_focus_rects: prev_focus_rects,
1778 mouse_pos,
1779 click_pos,
1780 last_text_idx: None,
1781 overlay_depth: 0,
1782 modal_active: false,
1783 prev_modal_active,
1784 clipboard_text: None,
1785 debug,
1786 theme,
1787 dark_mode: true,
1788 deferred_draws: Vec::new(),
1789 }
1790 }
1791
1792 pub(crate) fn process_focus_keys(&mut self) {
1793 for (i, event) in self.events.iter().enumerate() {
1794 if let Event::Key(key) = event {
1795 if key.kind != KeyEventKind::Press {
1796 continue;
1797 }
1798 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1799 if self.prev_focus_count > 0 {
1800 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1801 }
1802 self.consumed[i] = true;
1803 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1804 || key.code == KeyCode::BackTab
1805 {
1806 if self.prev_focus_count > 0 {
1807 self.focus_index = if self.focus_index == 0 {
1808 self.prev_focus_count - 1
1809 } else {
1810 self.focus_index - 1
1811 };
1812 }
1813 self.consumed[i] = true;
1814 }
1815 }
1816 }
1817 }
1818
1819 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1823 w.ui(self)
1824 }
1825
1826 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1841 self.error_boundary_with(f, |ui, msg| {
1842 ui.styled(
1843 format!("⚠ Error: {msg}"),
1844 Style::new().fg(ui.theme.error).bold(),
1845 );
1846 });
1847 }
1848
1849 pub fn error_boundary_with(
1869 &mut self,
1870 f: impl FnOnce(&mut Context),
1871 fallback: impl FnOnce(&mut Context, String),
1872 ) {
1873 let cmd_count = self.commands.len();
1874 let last_text_idx = self.last_text_idx;
1875
1876 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1877 f(self);
1878 }));
1879
1880 match result {
1881 Ok(()) => {}
1882 Err(panic_info) => {
1883 self.commands.truncate(cmd_count);
1884 self.last_text_idx = last_text_idx;
1885
1886 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1887 (*s).to_string()
1888 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1889 s.clone()
1890 } else {
1891 "widget panicked".to_string()
1892 };
1893
1894 fallback(self, msg);
1895 }
1896 }
1897 }
1898
1899 pub fn interaction(&mut self) -> Response {
1905 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1906 return Response::default();
1907 }
1908 let id = self.interaction_count;
1909 self.interaction_count += 1;
1910 self.response_for(id)
1911 }
1912
1913 pub fn register_focusable(&mut self) -> bool {
1918 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1919 return false;
1920 }
1921 let id = self.focus_count;
1922 self.focus_count += 1;
1923 self.commands.push(Command::FocusMarker(id));
1924 if self.prev_focus_count == 0 {
1925 return true;
1926 }
1927 self.focus_index % self.prev_focus_count == id
1928 }
1929
1930 pub fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T> {
1948 let idx = self.hook_cursor;
1949 self.hook_cursor += 1;
1950
1951 if idx >= self.hook_states.len() {
1952 self.hook_states.push(Box::new(init()));
1953 }
1954
1955 State {
1956 idx,
1957 _marker: std::marker::PhantomData,
1958 }
1959 }
1960
1961 pub fn use_memo<T: 'static, D: PartialEq + Clone + 'static>(
1969 &mut self,
1970 deps: &D,
1971 compute: impl FnOnce(&D) -> T,
1972 ) -> &T {
1973 let idx = self.hook_cursor;
1974 self.hook_cursor += 1;
1975
1976 let should_recompute = if idx >= self.hook_states.len() {
1977 true
1978 } else {
1979 let (stored_deps, _) = self.hook_states[idx]
1980 .downcast_ref::<(D, T)>()
1981 .expect("use_memo type mismatch");
1982 stored_deps != deps
1983 };
1984
1985 if should_recompute {
1986 let value = compute(deps);
1987 let slot = Box::new((deps.clone(), value));
1988 if idx < self.hook_states.len() {
1989 self.hook_states[idx] = slot;
1990 } else {
1991 self.hook_states.push(slot);
1992 }
1993 }
1994
1995 let (_, value) = self.hook_states[idx]
1996 .downcast_ref::<(D, T)>()
1997 .expect("use_memo type mismatch");
1998 value
1999 }
2000}
2001
2002mod widgets_display;
2003mod widgets_input;
2004mod widgets_viz;
2005
2006#[inline]
2007fn byte_index_for_char(value: &str, char_index: usize) -> usize {
2008 if char_index == 0 {
2009 return 0;
2010 }
2011 value
2012 .char_indices()
2013 .nth(char_index)
2014 .map_or(value.len(), |(idx, _)| idx)
2015}
2016
2017fn format_token_count(count: usize) -> String {
2018 if count >= 1_000_000 {
2019 format!("{:.1}M", count as f64 / 1_000_000.0)
2020 } else if count >= 1_000 {
2021 format!("{:.1}k", count as f64 / 1_000.0)
2022 } else {
2023 format!("{count}")
2024 }
2025}
2026
2027fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
2028 let mut parts: Vec<String> = Vec::new();
2029 for (i, width) in widths.iter().enumerate() {
2030 let cell = cells.get(i).map(String::as_str).unwrap_or("");
2031 let cell_width = UnicodeWidthStr::width(cell) as u32;
2032 let padding = (*width).saturating_sub(cell_width) as usize;
2033 parts.push(format!("{cell}{}", " ".repeat(padding)));
2034 }
2035 parts.join(separator)
2036}
2037
2038fn format_compact_number(value: f64) -> String {
2039 if value.fract().abs() < f64::EPSILON {
2040 return format!("{value:.0}");
2041 }
2042
2043 let mut s = format!("{value:.2}");
2044 while s.contains('.') && s.ends_with('0') {
2045 s.pop();
2046 }
2047 if s.ends_with('.') {
2048 s.pop();
2049 }
2050 s
2051}
2052
2053fn center_text(text: &str, width: usize) -> String {
2054 let text_width = UnicodeWidthStr::width(text);
2055 if text_width >= width {
2056 return text.to_string();
2057 }
2058
2059 let total = width - text_width;
2060 let left = total / 2;
2061 let right = total - left;
2062 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
2063}
2064
2065struct TextareaVLine {
2066 logical_row: usize,
2067 char_start: usize,
2068 char_count: usize,
2069}
2070
2071fn textarea_build_visual_lines(lines: &[String], wrap_width: u32) -> Vec<TextareaVLine> {
2072 let mut out = Vec::new();
2073 for (row, line) in lines.iter().enumerate() {
2074 if line.is_empty() || wrap_width == u32::MAX {
2075 out.push(TextareaVLine {
2076 logical_row: row,
2077 char_start: 0,
2078 char_count: line.chars().count(),
2079 });
2080 continue;
2081 }
2082 let mut seg_start = 0usize;
2083 let mut seg_chars = 0usize;
2084 let mut seg_width = 0u32;
2085 for (idx, ch) in line.chars().enumerate() {
2086 let cw = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
2087 if seg_width + cw > wrap_width && seg_chars > 0 {
2088 out.push(TextareaVLine {
2089 logical_row: row,
2090 char_start: seg_start,
2091 char_count: seg_chars,
2092 });
2093 seg_start = idx;
2094 seg_chars = 0;
2095 seg_width = 0;
2096 }
2097 seg_chars += 1;
2098 seg_width += cw;
2099 }
2100 out.push(TextareaVLine {
2101 logical_row: row,
2102 char_start: seg_start,
2103 char_count: seg_chars,
2104 });
2105 }
2106 out
2107}
2108
2109fn textarea_logical_to_visual(
2110 vlines: &[TextareaVLine],
2111 logical_row: usize,
2112 logical_col: usize,
2113) -> (usize, usize) {
2114 for (i, vl) in vlines.iter().enumerate() {
2115 if vl.logical_row != logical_row {
2116 continue;
2117 }
2118 let seg_end = vl.char_start + vl.char_count;
2119 if logical_col >= vl.char_start && logical_col < seg_end {
2120 return (i, logical_col - vl.char_start);
2121 }
2122 if logical_col == seg_end {
2123 let is_last_seg = vlines
2124 .get(i + 1)
2125 .map_or(true, |next| next.logical_row != logical_row);
2126 if is_last_seg {
2127 return (i, logical_col - vl.char_start);
2128 }
2129 }
2130 }
2131 (vlines.len().saturating_sub(1), 0)
2132}
2133
2134fn textarea_visual_to_logical(
2135 vlines: &[TextareaVLine],
2136 visual_row: usize,
2137 visual_col: usize,
2138) -> (usize, usize) {
2139 if let Some(vl) = vlines.get(visual_row) {
2140 let logical_col = vl.char_start + visual_col.min(vl.char_count);
2141 (vl.logical_row, logical_col)
2142 } else {
2143 (0, 0)
2144 }
2145}
2146
2147fn open_url(url: &str) -> std::io::Result<()> {
2148 #[cfg(target_os = "macos")]
2149 {
2150 std::process::Command::new("open").arg(url).spawn()?;
2151 }
2152 #[cfg(target_os = "linux")]
2153 {
2154 std::process::Command::new("xdg-open").arg(url).spawn()?;
2155 }
2156 #[cfg(target_os = "windows")]
2157 {
2158 std::process::Command::new("cmd")
2159 .args(["/c", "start", "", url])
2160 .spawn()?;
2161 }
2162 Ok(())
2163}