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