1use crate::chart::{build_histogram_config, render_chart, ChartBuilder, HistogramBuilder};
2use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
3use crate::layout::{Command, Direction};
4use crate::rect::Rect;
5use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
6use crate::widgets::{
7 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
8 ToastLevel, ToastState,
9};
10use unicode_width::UnicodeWidthStr;
11
12#[derive(Debug, Clone, Copy, Default)]
18pub struct Response {
19 pub clicked: bool,
21 pub hovered: bool,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BarDirection {
28 Horizontal,
30 Vertical,
32}
33
34#[derive(Debug, Clone)]
36pub struct Bar {
37 pub label: String,
39 pub value: f64,
41 pub color: Option<Color>,
43}
44
45impl Bar {
46 pub fn new(label: impl Into<String>, value: f64) -> Self {
48 Self {
49 label: label.into(),
50 value,
51 color: None,
52 }
53 }
54
55 pub fn color(mut self, color: Color) -> Self {
57 self.color = Some(color);
58 self
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct BarGroup {
65 pub label: String,
67 pub bars: Vec<Bar>,
69}
70
71impl BarGroup {
72 pub fn new(label: impl Into<String>, bars: Vec<Bar>) -> Self {
74 Self {
75 label: label.into(),
76 bars,
77 }
78 }
79}
80
81pub trait Widget {
143 type Response;
146
147 fn ui(&mut self, ctx: &mut Context) -> Self::Response;
153}
154
155pub struct Context {
171 pub(crate) commands: Vec<Command>,
172 pub(crate) events: Vec<Event>,
173 pub(crate) consumed: Vec<bool>,
174 pub(crate) should_quit: bool,
175 pub(crate) area_width: u32,
176 pub(crate) area_height: u32,
177 pub(crate) tick: u64,
178 pub(crate) focus_index: usize,
179 pub(crate) focus_count: usize,
180 prev_focus_count: usize,
181 scroll_count: usize,
182 prev_scroll_infos: Vec<(u32, u32)>,
183 interaction_count: usize,
184 pub(crate) prev_hit_map: Vec<Rect>,
185 _prev_focus_rects: Vec<(usize, Rect)>,
186 mouse_pos: Option<(u32, u32)>,
187 click_pos: Option<(u32, u32)>,
188 last_text_idx: Option<usize>,
189 debug: bool,
190 theme: Theme,
191}
192
193#[must_use = "configure and finalize with .col() or .row()"]
214pub struct ContainerBuilder<'a> {
215 ctx: &'a mut Context,
216 gap: u32,
217 align: Align,
218 border: Option<Border>,
219 border_style: Style,
220 padding: Padding,
221 margin: Margin,
222 constraints: Constraints,
223 title: Option<(String, Style)>,
224 grow: u16,
225 scroll_offset: Option<u32>,
226}
227
228#[derive(Debug, Clone, Copy)]
235struct CanvasPixel {
236 bits: u32,
237 color: Color,
238}
239
240#[derive(Debug, Clone)]
242struct CanvasLabel {
243 x: usize,
244 y: usize,
245 text: String,
246 color: Color,
247}
248
249#[derive(Debug, Clone)]
251struct CanvasLayer {
252 grid: Vec<Vec<CanvasPixel>>,
253 labels: Vec<CanvasLabel>,
254}
255
256pub struct CanvasContext {
257 layers: Vec<CanvasLayer>,
258 cols: usize,
259 rows: usize,
260 px_w: usize,
261 px_h: usize,
262 current_color: Color,
263}
264
265impl CanvasContext {
266 fn new(cols: usize, rows: usize) -> Self {
267 Self {
268 layers: vec![Self::new_layer(cols, rows)],
269 cols,
270 rows,
271 px_w: cols * 2,
272 px_h: rows * 4,
273 current_color: Color::Reset,
274 }
275 }
276
277 fn new_layer(cols: usize, rows: usize) -> CanvasLayer {
278 CanvasLayer {
279 grid: vec![
280 vec![
281 CanvasPixel {
282 bits: 0,
283 color: Color::Reset,
284 };
285 cols
286 ];
287 rows
288 ],
289 labels: Vec::new(),
290 }
291 }
292
293 fn current_layer_mut(&mut self) -> Option<&mut CanvasLayer> {
294 self.layers.last_mut()
295 }
296
297 fn dot_with_color(&mut self, x: usize, y: usize, color: Color) {
298 if x >= self.px_w || y >= self.px_h {
299 return;
300 }
301
302 let char_col = x / 2;
303 let char_row = y / 4;
304 let sub_col = x % 2;
305 let sub_row = y % 4;
306 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
307 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
308
309 let bit = if sub_col == 0 {
310 LEFT_BITS[sub_row]
311 } else {
312 RIGHT_BITS[sub_row]
313 };
314
315 if let Some(layer) = self.current_layer_mut() {
316 let cell = &mut layer.grid[char_row][char_col];
317 let new_bits = cell.bits | bit;
318 if new_bits != cell.bits {
319 cell.bits = new_bits;
320 cell.color = color;
321 }
322 }
323 }
324
325 fn dot_isize(&mut self, x: isize, y: isize) {
326 if x >= 0 && y >= 0 {
327 self.dot(x as usize, y as usize);
328 }
329 }
330
331 pub fn width(&self) -> usize {
333 self.px_w
334 }
335
336 pub fn height(&self) -> usize {
338 self.px_h
339 }
340
341 pub fn dot(&mut self, x: usize, y: usize) {
343 self.dot_with_color(x, y, self.current_color);
344 }
345
346 pub fn line(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
348 let (mut x, mut y) = (x0 as isize, y0 as isize);
349 let (x1, y1) = (x1 as isize, y1 as isize);
350 let dx = (x1 - x).abs();
351 let dy = -(y1 - y).abs();
352 let sx = if x < x1 { 1 } else { -1 };
353 let sy = if y < y1 { 1 } else { -1 };
354 let mut err = dx + dy;
355
356 loop {
357 self.dot_isize(x, y);
358 if x == x1 && y == y1 {
359 break;
360 }
361 let e2 = 2 * err;
362 if e2 >= dy {
363 err += dy;
364 x += sx;
365 }
366 if e2 <= dx {
367 err += dx;
368 y += sy;
369 }
370 }
371 }
372
373 pub fn rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
375 if w == 0 || h == 0 {
376 return;
377 }
378
379 self.line(x, y, x + w.saturating_sub(1), y);
380 self.line(
381 x + w.saturating_sub(1),
382 y,
383 x + w.saturating_sub(1),
384 y + h.saturating_sub(1),
385 );
386 self.line(
387 x + w.saturating_sub(1),
388 y + h.saturating_sub(1),
389 x,
390 y + h.saturating_sub(1),
391 );
392 self.line(x, y + h.saturating_sub(1), x, y);
393 }
394
395 pub fn circle(&mut self, cx: usize, cy: usize, r: usize) {
397 let mut x = r as isize;
398 let mut y: isize = 0;
399 let mut err: isize = 1 - x;
400 let (cx, cy) = (cx as isize, cy as isize);
401
402 while x >= y {
403 for &(dx, dy) in &[
404 (x, y),
405 (y, x),
406 (-x, y),
407 (-y, x),
408 (x, -y),
409 (y, -x),
410 (-x, -y),
411 (-y, -x),
412 ] {
413 let px = cx + dx;
414 let py = cy + dy;
415 self.dot_isize(px, py);
416 }
417
418 y += 1;
419 if err < 0 {
420 err += 2 * y + 1;
421 } else {
422 x -= 1;
423 err += 2 * (y - x) + 1;
424 }
425 }
426 }
427
428 pub fn set_color(&mut self, color: Color) {
430 self.current_color = color;
431 }
432
433 pub fn color(&self) -> Color {
435 self.current_color
436 }
437
438 pub fn filled_rect(&mut self, x: usize, y: usize, w: usize, h: usize) {
440 if w == 0 || h == 0 {
441 return;
442 }
443
444 let x_end = x.saturating_add(w).min(self.px_w);
445 let y_end = y.saturating_add(h).min(self.px_h);
446 if x >= x_end || y >= y_end {
447 return;
448 }
449
450 for yy in y..y_end {
451 self.line(x, yy, x_end.saturating_sub(1), yy);
452 }
453 }
454
455 pub fn filled_circle(&mut self, cx: usize, cy: usize, r: usize) {
457 let (cx, cy, r) = (cx as isize, cy as isize, r as isize);
458 for y in (cy - r)..=(cy + r) {
459 let dy = y - cy;
460 let span_sq = (r * r - dy * dy).max(0);
461 let dx = (span_sq as f64).sqrt() as isize;
462 for x in (cx - dx)..=(cx + dx) {
463 self.dot_isize(x, y);
464 }
465 }
466 }
467
468 pub fn triangle(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, x2: usize, y2: usize) {
470 self.line(x0, y0, x1, y1);
471 self.line(x1, y1, x2, y2);
472 self.line(x2, y2, x0, y0);
473 }
474
475 pub fn filled_triangle(
477 &mut self,
478 x0: usize,
479 y0: usize,
480 x1: usize,
481 y1: usize,
482 x2: usize,
483 y2: usize,
484 ) {
485 let vertices = [
486 (x0 as isize, y0 as isize),
487 (x1 as isize, y1 as isize),
488 (x2 as isize, y2 as isize),
489 ];
490 let min_y = vertices.iter().map(|(_, y)| *y).min().unwrap_or(0);
491 let max_y = vertices.iter().map(|(_, y)| *y).max().unwrap_or(-1);
492
493 for y in min_y..=max_y {
494 let mut intersections: Vec<f64> = Vec::new();
495
496 for edge in [(0usize, 1usize), (1usize, 2usize), (2usize, 0usize)] {
497 let (x_a, y_a) = vertices[edge.0];
498 let (x_b, y_b) = vertices[edge.1];
499 if y_a == y_b {
500 continue;
501 }
502
503 let (x_start, y_start, x_end, y_end) = if y_a < y_b {
504 (x_a, y_a, x_b, y_b)
505 } else {
506 (x_b, y_b, x_a, y_a)
507 };
508
509 if y < y_start || y >= y_end {
510 continue;
511 }
512
513 let t = (y - y_start) as f64 / (y_end - y_start) as f64;
514 intersections.push(x_start as f64 + t * (x_end - x_start) as f64);
515 }
516
517 intersections.sort_by(|a, b| a.total_cmp(b));
518 let mut i = 0usize;
519 while i + 1 < intersections.len() {
520 let x_start = intersections[i].ceil() as isize;
521 let x_end = intersections[i + 1].floor() as isize;
522 for x in x_start..=x_end {
523 self.dot_isize(x, y);
524 }
525 i += 2;
526 }
527 }
528
529 self.triangle(x0, y0, x1, y1, x2, y2);
530 }
531
532 pub fn points(&mut self, pts: &[(usize, usize)]) {
534 for &(x, y) in pts {
535 self.dot(x, y);
536 }
537 }
538
539 pub fn polyline(&mut self, pts: &[(usize, usize)]) {
541 for window in pts.windows(2) {
542 if let [(x0, y0), (x1, y1)] = window {
543 self.line(*x0, *y0, *x1, *y1);
544 }
545 }
546 }
547
548 pub fn print(&mut self, x: usize, y: usize, text: &str) {
551 if text.is_empty() {
552 return;
553 }
554
555 let color = self.current_color;
556 if let Some(layer) = self.current_layer_mut() {
557 layer.labels.push(CanvasLabel {
558 x,
559 y,
560 text: text.to_string(),
561 color,
562 });
563 }
564 }
565
566 pub fn layer(&mut self) {
568 self.layers.push(Self::new_layer(self.cols, self.rows));
569 }
570
571 pub(crate) fn render(&self) -> Vec<Vec<(String, Color)>> {
572 let mut final_grid = vec![
573 vec![
574 CanvasPixel {
575 bits: 0,
576 color: Color::Reset,
577 };
578 self.cols
579 ];
580 self.rows
581 ];
582 let mut labels_overlay: Vec<Vec<Option<(char, Color)>>> =
583 vec![vec![None; self.cols]; self.rows];
584
585 for layer in &self.layers {
586 for (row, final_row) in final_grid.iter_mut().enumerate().take(self.rows) {
587 for (col, dst) in final_row.iter_mut().enumerate().take(self.cols) {
588 let src = layer.grid[row][col];
589 if src.bits == 0 {
590 continue;
591 }
592
593 let merged = dst.bits | src.bits;
594 if merged != dst.bits {
595 dst.bits = merged;
596 dst.color = src.color;
597 }
598 }
599 }
600
601 for label in &layer.labels {
602 let row = label.y / 4;
603 if row >= self.rows {
604 continue;
605 }
606 let start_col = label.x / 2;
607 for (offset, ch) in label.text.chars().enumerate() {
608 let col = start_col + offset;
609 if col >= self.cols {
610 break;
611 }
612 labels_overlay[row][col] = Some((ch, label.color));
613 }
614 }
615 }
616
617 let mut lines: Vec<Vec<(String, Color)>> = Vec::with_capacity(self.rows);
618 for row in 0..self.rows {
619 let mut segments: Vec<(String, Color)> = Vec::new();
620 let mut current_color: Option<Color> = None;
621 let mut current_text = String::new();
622
623 for col in 0..self.cols {
624 let (ch, color) = if let Some((label_ch, label_color)) = labels_overlay[row][col] {
625 (label_ch, label_color)
626 } else {
627 let bits = final_grid[row][col].bits;
628 let ch = char::from_u32(0x2800 + bits).unwrap_or(' ');
629 (ch, final_grid[row][col].color)
630 };
631
632 match current_color {
633 Some(c) if c == color => {
634 current_text.push(ch);
635 }
636 Some(c) => {
637 segments.push((std::mem::take(&mut current_text), c));
638 current_text.push(ch);
639 current_color = Some(color);
640 }
641 None => {
642 current_text.push(ch);
643 current_color = Some(color);
644 }
645 }
646 }
647
648 if let Some(color) = current_color {
649 segments.push((current_text, color));
650 }
651 lines.push(segments);
652 }
653
654 lines
655 }
656}
657
658impl<'a> ContainerBuilder<'a> {
659 pub fn border(mut self, border: Border) -> Self {
663 self.border = Some(border);
664 self
665 }
666
667 pub fn rounded(self) -> Self {
669 self.border(Border::Rounded)
670 }
671
672 pub fn border_style(mut self, style: Style) -> Self {
674 self.border_style = style;
675 self
676 }
677
678 pub fn p(self, value: u32) -> Self {
682 self.pad(value)
683 }
684
685 pub fn pad(mut self, value: u32) -> Self {
687 self.padding = Padding::all(value);
688 self
689 }
690
691 pub fn px(mut self, value: u32) -> Self {
693 self.padding.left = value;
694 self.padding.right = value;
695 self
696 }
697
698 pub fn py(mut self, value: u32) -> Self {
700 self.padding.top = value;
701 self.padding.bottom = value;
702 self
703 }
704
705 pub fn pt(mut self, value: u32) -> Self {
707 self.padding.top = value;
708 self
709 }
710
711 pub fn pr(mut self, value: u32) -> Self {
713 self.padding.right = value;
714 self
715 }
716
717 pub fn pb(mut self, value: u32) -> Self {
719 self.padding.bottom = value;
720 self
721 }
722
723 pub fn pl(mut self, value: u32) -> Self {
725 self.padding.left = value;
726 self
727 }
728
729 pub fn padding(mut self, padding: Padding) -> Self {
731 self.padding = padding;
732 self
733 }
734
735 pub fn m(mut self, value: u32) -> Self {
739 self.margin = Margin::all(value);
740 self
741 }
742
743 pub fn mx(mut self, value: u32) -> Self {
745 self.margin.left = value;
746 self.margin.right = value;
747 self
748 }
749
750 pub fn my(mut self, value: u32) -> Self {
752 self.margin.top = value;
753 self.margin.bottom = value;
754 self
755 }
756
757 pub fn mt(mut self, value: u32) -> Self {
759 self.margin.top = value;
760 self
761 }
762
763 pub fn mr(mut self, value: u32) -> Self {
765 self.margin.right = value;
766 self
767 }
768
769 pub fn mb(mut self, value: u32) -> Self {
771 self.margin.bottom = value;
772 self
773 }
774
775 pub fn ml(mut self, value: u32) -> Self {
777 self.margin.left = value;
778 self
779 }
780
781 pub fn margin(mut self, margin: Margin) -> Self {
783 self.margin = margin;
784 self
785 }
786
787 pub fn w(mut self, value: u32) -> Self {
791 self.constraints.min_width = Some(value);
792 self.constraints.max_width = Some(value);
793 self
794 }
795
796 pub fn h(mut self, value: u32) -> Self {
798 self.constraints.min_height = Some(value);
799 self.constraints.max_height = Some(value);
800 self
801 }
802
803 pub fn min_w(mut self, value: u32) -> Self {
805 self.constraints.min_width = Some(value);
806 self
807 }
808
809 pub fn max_w(mut self, value: u32) -> Self {
811 self.constraints.max_width = Some(value);
812 self
813 }
814
815 pub fn min_h(mut self, value: u32) -> Self {
817 self.constraints.min_height = Some(value);
818 self
819 }
820
821 pub fn max_h(mut self, value: u32) -> Self {
823 self.constraints.max_height = Some(value);
824 self
825 }
826
827 pub fn min_width(mut self, value: u32) -> Self {
829 self.constraints.min_width = Some(value);
830 self
831 }
832
833 pub fn max_width(mut self, value: u32) -> Self {
835 self.constraints.max_width = Some(value);
836 self
837 }
838
839 pub fn min_height(mut self, value: u32) -> Self {
841 self.constraints.min_height = Some(value);
842 self
843 }
844
845 pub fn max_height(mut self, value: u32) -> Self {
847 self.constraints.max_height = Some(value);
848 self
849 }
850
851 pub fn constraints(mut self, constraints: Constraints) -> Self {
853 self.constraints = constraints;
854 self
855 }
856
857 pub fn gap(mut self, gap: u32) -> Self {
861 self.gap = gap;
862 self
863 }
864
865 pub fn grow(mut self, grow: u16) -> Self {
867 self.grow = grow;
868 self
869 }
870
871 pub fn align(mut self, align: Align) -> Self {
875 self.align = align;
876 self
877 }
878
879 pub fn center(self) -> Self {
881 self.align(Align::Center)
882 }
883
884 pub fn title(self, title: impl Into<String>) -> Self {
888 self.title_styled(title, Style::new())
889 }
890
891 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
893 self.title = Some((title.into(), style));
894 self
895 }
896
897 pub fn scroll_offset(mut self, offset: u32) -> Self {
901 self.scroll_offset = Some(offset);
902 self
903 }
904
905 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
910 self.finish(Direction::Column, f)
911 }
912
913 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
918 self.finish(Direction::Row, f)
919 }
920
921 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
922 let interaction_id = self.ctx.interaction_count;
923 self.ctx.interaction_count += 1;
924
925 if let Some(scroll_offset) = self.scroll_offset {
926 self.ctx.commands.push(Command::BeginScrollable {
927 grow: self.grow,
928 border: self.border,
929 border_style: self.border_style,
930 padding: self.padding,
931 margin: self.margin,
932 constraints: self.constraints,
933 title: self.title,
934 scroll_offset,
935 });
936 } else {
937 self.ctx.commands.push(Command::BeginContainer {
938 direction,
939 gap: self.gap,
940 align: self.align,
941 border: self.border,
942 border_style: self.border_style,
943 padding: self.padding,
944 margin: self.margin,
945 constraints: self.constraints,
946 title: self.title,
947 grow: self.grow,
948 });
949 }
950 f(self.ctx);
951 self.ctx.commands.push(Command::EndContainer);
952 self.ctx.last_text_idx = None;
953
954 self.ctx.response_for(interaction_id)
955 }
956}
957
958impl Context {
959 #[allow(clippy::too_many_arguments)]
960 pub(crate) fn new(
961 events: Vec<Event>,
962 width: u32,
963 height: u32,
964 tick: u64,
965 mut focus_index: usize,
966 prev_focus_count: usize,
967 prev_scroll_infos: Vec<(u32, u32)>,
968 prev_hit_map: Vec<Rect>,
969 prev_focus_rects: Vec<(usize, Rect)>,
970 debug: bool,
971 theme: Theme,
972 last_mouse_pos: Option<(u32, u32)>,
973 ) -> Self {
974 let consumed = vec![false; events.len()];
975
976 let mut mouse_pos = last_mouse_pos;
977 let mut click_pos = None;
978 for event in &events {
979 if let Event::Mouse(mouse) = event {
980 mouse_pos = Some((mouse.x, mouse.y));
981 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
982 click_pos = Some((mouse.x, mouse.y));
983 }
984 }
985 }
986
987 if let Some((mx, my)) = click_pos {
988 let mut best: Option<(usize, u64)> = None;
989 for &(fid, rect) in &prev_focus_rects {
990 if mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom() {
991 let area = rect.width as u64 * rect.height as u64;
992 if best.map_or(true, |(_, ba)| area < ba) {
993 best = Some((fid, area));
994 }
995 }
996 }
997 if let Some((fid, _)) = best {
998 focus_index = fid;
999 }
1000 }
1001
1002 Self {
1003 commands: Vec::new(),
1004 events,
1005 consumed,
1006 should_quit: false,
1007 area_width: width,
1008 area_height: height,
1009 tick,
1010 focus_index,
1011 focus_count: 0,
1012 prev_focus_count,
1013 scroll_count: 0,
1014 prev_scroll_infos,
1015 interaction_count: 0,
1016 prev_hit_map,
1017 _prev_focus_rects: prev_focus_rects,
1018 mouse_pos,
1019 click_pos,
1020 last_text_idx: None,
1021 debug,
1022 theme,
1023 }
1024 }
1025
1026 pub(crate) fn process_focus_keys(&mut self) {
1027 for (i, event) in self.events.iter().enumerate() {
1028 if let Event::Key(key) = event {
1029 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
1030 if self.prev_focus_count > 0 {
1031 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
1032 }
1033 self.consumed[i] = true;
1034 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
1035 || key.code == KeyCode::BackTab
1036 {
1037 if self.prev_focus_count > 0 {
1038 self.focus_index = if self.focus_index == 0 {
1039 self.prev_focus_count - 1
1040 } else {
1041 self.focus_index - 1
1042 };
1043 }
1044 self.consumed[i] = true;
1045 }
1046 }
1047 }
1048 }
1049
1050 pub fn widget<W: Widget>(&mut self, w: &mut W) -> W::Response {
1054 w.ui(self)
1055 }
1056
1057 pub fn error_boundary(&mut self, f: impl FnOnce(&mut Context)) {
1072 self.error_boundary_with(f, |ui, msg| {
1073 ui.styled(
1074 format!("⚠ Error: {msg}"),
1075 Style::new().fg(ui.theme.error).bold(),
1076 );
1077 });
1078 }
1079
1080 pub fn error_boundary_with(
1100 &mut self,
1101 f: impl FnOnce(&mut Context),
1102 fallback: impl FnOnce(&mut Context, String),
1103 ) {
1104 let cmd_count = self.commands.len();
1105 let last_text_idx = self.last_text_idx;
1106
1107 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1108 f(self);
1109 }));
1110
1111 match result {
1112 Ok(()) => {}
1113 Err(panic_info) => {
1114 self.commands.truncate(cmd_count);
1115 self.last_text_idx = last_text_idx;
1116
1117 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1118 (*s).to_string()
1119 } else if let Some(s) = panic_info.downcast_ref::<String>() {
1120 s.clone()
1121 } else {
1122 "widget panicked".to_string()
1123 };
1124
1125 fallback(self, msg);
1126 }
1127 }
1128 }
1129
1130 pub fn interaction(&mut self) -> Response {
1136 let id = self.interaction_count;
1137 self.interaction_count += 1;
1138 self.response_for(id)
1139 }
1140
1141 pub fn register_focusable(&mut self) -> bool {
1146 let id = self.focus_count;
1147 self.focus_count += 1;
1148 self.commands.push(Command::FocusMarker(id));
1149 if self.prev_focus_count == 0 {
1150 return true;
1151 }
1152 self.focus_index % self.prev_focus_count == id
1153 }
1154
1155 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
1168 let content = s.into();
1169 self.commands.push(Command::Text {
1170 content,
1171 style: Style::new(),
1172 grow: 0,
1173 align: Align::Start,
1174 wrap: false,
1175 margin: Margin::default(),
1176 constraints: Constraints::default(),
1177 });
1178 self.last_text_idx = Some(self.commands.len() - 1);
1179 self
1180 }
1181
1182 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
1187 let content = s.into();
1188 self.commands.push(Command::Text {
1189 content,
1190 style: Style::new(),
1191 grow: 0,
1192 align: Align::Start,
1193 wrap: true,
1194 margin: Margin::default(),
1195 constraints: Constraints::default(),
1196 });
1197 self.last_text_idx = Some(self.commands.len() - 1);
1198 self
1199 }
1200
1201 pub fn bold(&mut self) -> &mut Self {
1205 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
1206 self
1207 }
1208
1209 pub fn dim(&mut self) -> &mut Self {
1214 let text_dim = self.theme.text_dim;
1215 self.modify_last_style(|s| {
1216 s.modifiers |= Modifiers::DIM;
1217 if s.fg.is_none() {
1218 s.fg = Some(text_dim);
1219 }
1220 });
1221 self
1222 }
1223
1224 pub fn italic(&mut self) -> &mut Self {
1226 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
1227 self
1228 }
1229
1230 pub fn underline(&mut self) -> &mut Self {
1232 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
1233 self
1234 }
1235
1236 pub fn reversed(&mut self) -> &mut Self {
1238 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
1239 self
1240 }
1241
1242 pub fn strikethrough(&mut self) -> &mut Self {
1244 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
1245 self
1246 }
1247
1248 pub fn fg(&mut self, color: Color) -> &mut Self {
1250 self.modify_last_style(|s| s.fg = Some(color));
1251 self
1252 }
1253
1254 pub fn bg(&mut self, color: Color) -> &mut Self {
1256 self.modify_last_style(|s| s.bg = Some(color));
1257 self
1258 }
1259
1260 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
1265 self.commands.push(Command::Text {
1266 content: s.into(),
1267 style,
1268 grow: 0,
1269 align: Align::Start,
1270 wrap: false,
1271 margin: Margin::default(),
1272 constraints: Constraints::default(),
1273 });
1274 self.last_text_idx = Some(self.commands.len() - 1);
1275 self
1276 }
1277
1278 pub fn wrap(&mut self) -> &mut Self {
1280 if let Some(idx) = self.last_text_idx {
1281 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1282 *wrap = true;
1283 }
1284 }
1285 self
1286 }
1287
1288 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1289 if let Some(idx) = self.last_text_idx {
1290 if let Command::Text { style, .. } = &mut self.commands[idx] {
1291 f(style);
1292 }
1293 }
1294 }
1295
1296 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1314 self.push_container(Direction::Column, 0, f)
1315 }
1316
1317 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1321 self.push_container(Direction::Column, gap, f)
1322 }
1323
1324 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1341 self.push_container(Direction::Row, 0, f)
1342 }
1343
1344 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1348 self.push_container(Direction::Row, gap, f)
1349 }
1350
1351 pub fn container(&mut self) -> ContainerBuilder<'_> {
1372 let border = self.theme.border;
1373 ContainerBuilder {
1374 ctx: self,
1375 gap: 0,
1376 align: Align::Start,
1377 border: None,
1378 border_style: Style::new().fg(border),
1379 padding: Padding::default(),
1380 margin: Margin::default(),
1381 constraints: Constraints::default(),
1382 title: None,
1383 grow: 0,
1384 scroll_offset: None,
1385 }
1386 }
1387
1388 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1407 let index = self.scroll_count;
1408 self.scroll_count += 1;
1409 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1410 state.set_bounds(ch, vh);
1411 let max = ch.saturating_sub(vh) as usize;
1412 state.offset = state.offset.min(max);
1413 }
1414
1415 let next_id = self.interaction_count;
1416 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1417 self.auto_scroll(&rect, state);
1418 }
1419
1420 self.container().scroll_offset(state.offset as u32)
1421 }
1422
1423 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
1424 let mut to_consume: Vec<usize> = Vec::new();
1425
1426 for (i, event) in self.events.iter().enumerate() {
1427 if self.consumed[i] {
1428 continue;
1429 }
1430 if let Event::Mouse(mouse) = event {
1431 let in_bounds = mouse.x >= rect.x
1432 && mouse.x < rect.right()
1433 && mouse.y >= rect.y
1434 && mouse.y < rect.bottom();
1435 if !in_bounds {
1436 continue;
1437 }
1438 match mouse.kind {
1439 MouseKind::ScrollUp => {
1440 state.scroll_up(1);
1441 to_consume.push(i);
1442 }
1443 MouseKind::ScrollDown => {
1444 state.scroll_down(1);
1445 to_consume.push(i);
1446 }
1447 MouseKind::Drag(MouseButton::Left) => {
1448 }
1451 _ => {}
1452 }
1453 }
1454 }
1455
1456 for i in to_consume {
1457 self.consumed[i] = true;
1458 }
1459 }
1460
1461 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1465 self.container().border(border)
1466 }
1467
1468 fn push_container(
1469 &mut self,
1470 direction: Direction,
1471 gap: u32,
1472 f: impl FnOnce(&mut Context),
1473 ) -> Response {
1474 let interaction_id = self.interaction_count;
1475 self.interaction_count += 1;
1476 let border = self.theme.border;
1477
1478 self.commands.push(Command::BeginContainer {
1479 direction,
1480 gap,
1481 align: Align::Start,
1482 border: None,
1483 border_style: Style::new().fg(border),
1484 padding: Padding::default(),
1485 margin: Margin::default(),
1486 constraints: Constraints::default(),
1487 title: None,
1488 grow: 0,
1489 });
1490 f(self);
1491 self.commands.push(Command::EndContainer);
1492 self.last_text_idx = None;
1493
1494 self.response_for(interaction_id)
1495 }
1496
1497 fn response_for(&self, interaction_id: usize) -> Response {
1498 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1499 let clicked = self
1500 .click_pos
1501 .map(|(mx, my)| {
1502 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1503 })
1504 .unwrap_or(false);
1505 let hovered = self
1506 .mouse_pos
1507 .map(|(mx, my)| {
1508 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1509 })
1510 .unwrap_or(false);
1511 Response { clicked, hovered }
1512 } else {
1513 Response::default()
1514 }
1515 }
1516
1517 pub fn grow(&mut self, value: u16) -> &mut Self {
1522 if let Some(idx) = self.last_text_idx {
1523 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1524 *grow = value;
1525 }
1526 }
1527 self
1528 }
1529
1530 pub fn align(&mut self, align: Align) -> &mut Self {
1532 if let Some(idx) = self.last_text_idx {
1533 if let Command::Text {
1534 align: text_align, ..
1535 } = &mut self.commands[idx]
1536 {
1537 *text_align = align;
1538 }
1539 }
1540 self
1541 }
1542
1543 pub fn spacer(&mut self) -> &mut Self {
1547 self.commands.push(Command::Spacer { grow: 1 });
1548 self.last_text_idx = None;
1549 self
1550 }
1551
1552 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
1568 let focused = self.register_focusable();
1569 state.cursor = state.cursor.min(state.value.chars().count());
1570
1571 if focused {
1572 let mut consumed_indices = Vec::new();
1573 for (i, event) in self.events.iter().enumerate() {
1574 if let Event::Key(key) = event {
1575 match key.code {
1576 KeyCode::Char(ch) => {
1577 if let Some(max) = state.max_length {
1578 if state.value.chars().count() >= max {
1579 continue;
1580 }
1581 }
1582 let index = byte_index_for_char(&state.value, state.cursor);
1583 state.value.insert(index, ch);
1584 state.cursor += 1;
1585 consumed_indices.push(i);
1586 }
1587 KeyCode::Backspace => {
1588 if state.cursor > 0 {
1589 let start = byte_index_for_char(&state.value, state.cursor - 1);
1590 let end = byte_index_for_char(&state.value, state.cursor);
1591 state.value.replace_range(start..end, "");
1592 state.cursor -= 1;
1593 }
1594 consumed_indices.push(i);
1595 }
1596 KeyCode::Left => {
1597 state.cursor = state.cursor.saturating_sub(1);
1598 consumed_indices.push(i);
1599 }
1600 KeyCode::Right => {
1601 state.cursor = (state.cursor + 1).min(state.value.chars().count());
1602 consumed_indices.push(i);
1603 }
1604 KeyCode::Home => {
1605 state.cursor = 0;
1606 consumed_indices.push(i);
1607 }
1608 KeyCode::Delete => {
1609 let len = state.value.chars().count();
1610 if state.cursor < len {
1611 let start = byte_index_for_char(&state.value, state.cursor);
1612 let end = byte_index_for_char(&state.value, state.cursor + 1);
1613 state.value.replace_range(start..end, "");
1614 }
1615 consumed_indices.push(i);
1616 }
1617 KeyCode::End => {
1618 state.cursor = state.value.chars().count();
1619 consumed_indices.push(i);
1620 }
1621 _ => {}
1622 }
1623 }
1624 if let Event::Paste(ref text) = event {
1625 for ch in text.chars() {
1626 if let Some(max) = state.max_length {
1627 if state.value.chars().count() >= max {
1628 break;
1629 }
1630 }
1631 let index = byte_index_for_char(&state.value, state.cursor);
1632 state.value.insert(index, ch);
1633 state.cursor += 1;
1634 }
1635 consumed_indices.push(i);
1636 }
1637 }
1638
1639 for index in consumed_indices {
1640 self.consumed[index] = true;
1641 }
1642 }
1643
1644 if state.value.is_empty() {
1645 self.styled(
1646 state.placeholder.clone(),
1647 Style::new().dim().fg(self.theme.text_dim),
1648 )
1649 } else {
1650 let mut rendered = String::new();
1651 for (idx, ch) in state.value.chars().enumerate() {
1652 if focused && idx == state.cursor {
1653 rendered.push('▎');
1654 }
1655 rendered.push(ch);
1656 }
1657 if focused && state.cursor >= state.value.chars().count() {
1658 rendered.push('▎');
1659 }
1660 self.styled(rendered, Style::new().fg(self.theme.text))
1661 }
1662 }
1663
1664 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
1670 self.styled(
1671 state.frame(self.tick).to_string(),
1672 Style::new().fg(self.theme.primary),
1673 )
1674 }
1675
1676 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
1681 state.cleanup(self.tick);
1682 if state.messages.is_empty() {
1683 return self;
1684 }
1685
1686 self.interaction_count += 1;
1687 self.commands.push(Command::BeginContainer {
1688 direction: Direction::Column,
1689 gap: 0,
1690 align: Align::Start,
1691 border: None,
1692 border_style: Style::new().fg(self.theme.border),
1693 padding: Padding::default(),
1694 margin: Margin::default(),
1695 constraints: Constraints::default(),
1696 title: None,
1697 grow: 0,
1698 });
1699 for message in state.messages.iter().rev() {
1700 let color = match message.level {
1701 ToastLevel::Info => self.theme.primary,
1702 ToastLevel::Success => self.theme.success,
1703 ToastLevel::Warning => self.theme.warning,
1704 ToastLevel::Error => self.theme.error,
1705 };
1706 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
1707 }
1708 self.commands.push(Command::EndContainer);
1709 self.last_text_idx = None;
1710
1711 self
1712 }
1713
1714 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1719 if state.lines.is_empty() {
1720 state.lines.push(String::new());
1721 }
1722 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1723 state.cursor_col = state
1724 .cursor_col
1725 .min(state.lines[state.cursor_row].chars().count());
1726
1727 let focused = self.register_focusable();
1728
1729 if focused {
1730 let mut consumed_indices = Vec::new();
1731 for (i, event) in self.events.iter().enumerate() {
1732 if let Event::Key(key) = event {
1733 match key.code {
1734 KeyCode::Char(ch) => {
1735 if let Some(max) = state.max_length {
1736 let total: usize =
1737 state.lines.iter().map(|line| line.chars().count()).sum();
1738 if total >= max {
1739 continue;
1740 }
1741 }
1742 let index = byte_index_for_char(
1743 &state.lines[state.cursor_row],
1744 state.cursor_col,
1745 );
1746 state.lines[state.cursor_row].insert(index, ch);
1747 state.cursor_col += 1;
1748 consumed_indices.push(i);
1749 }
1750 KeyCode::Enter => {
1751 let split_index = byte_index_for_char(
1752 &state.lines[state.cursor_row],
1753 state.cursor_col,
1754 );
1755 let remainder = state.lines[state.cursor_row].split_off(split_index);
1756 state.cursor_row += 1;
1757 state.lines.insert(state.cursor_row, remainder);
1758 state.cursor_col = 0;
1759 consumed_indices.push(i);
1760 }
1761 KeyCode::Backspace => {
1762 if state.cursor_col > 0 {
1763 let start = byte_index_for_char(
1764 &state.lines[state.cursor_row],
1765 state.cursor_col - 1,
1766 );
1767 let end = byte_index_for_char(
1768 &state.lines[state.cursor_row],
1769 state.cursor_col,
1770 );
1771 state.lines[state.cursor_row].replace_range(start..end, "");
1772 state.cursor_col -= 1;
1773 } else if state.cursor_row > 0 {
1774 let current = state.lines.remove(state.cursor_row);
1775 state.cursor_row -= 1;
1776 state.cursor_col = state.lines[state.cursor_row].chars().count();
1777 state.lines[state.cursor_row].push_str(¤t);
1778 }
1779 consumed_indices.push(i);
1780 }
1781 KeyCode::Left => {
1782 if state.cursor_col > 0 {
1783 state.cursor_col -= 1;
1784 } else if state.cursor_row > 0 {
1785 state.cursor_row -= 1;
1786 state.cursor_col = state.lines[state.cursor_row].chars().count();
1787 }
1788 consumed_indices.push(i);
1789 }
1790 KeyCode::Right => {
1791 let line_len = state.lines[state.cursor_row].chars().count();
1792 if state.cursor_col < line_len {
1793 state.cursor_col += 1;
1794 } else if state.cursor_row + 1 < state.lines.len() {
1795 state.cursor_row += 1;
1796 state.cursor_col = 0;
1797 }
1798 consumed_indices.push(i);
1799 }
1800 KeyCode::Up => {
1801 if state.cursor_row > 0 {
1802 state.cursor_row -= 1;
1803 state.cursor_col = state
1804 .cursor_col
1805 .min(state.lines[state.cursor_row].chars().count());
1806 }
1807 consumed_indices.push(i);
1808 }
1809 KeyCode::Down => {
1810 if state.cursor_row + 1 < state.lines.len() {
1811 state.cursor_row += 1;
1812 state.cursor_col = state
1813 .cursor_col
1814 .min(state.lines[state.cursor_row].chars().count());
1815 }
1816 consumed_indices.push(i);
1817 }
1818 KeyCode::Home => {
1819 state.cursor_col = 0;
1820 consumed_indices.push(i);
1821 }
1822 KeyCode::Delete => {
1823 let line_len = state.lines[state.cursor_row].chars().count();
1824 if state.cursor_col < line_len {
1825 let start = byte_index_for_char(
1826 &state.lines[state.cursor_row],
1827 state.cursor_col,
1828 );
1829 let end = byte_index_for_char(
1830 &state.lines[state.cursor_row],
1831 state.cursor_col + 1,
1832 );
1833 state.lines[state.cursor_row].replace_range(start..end, "");
1834 } else if state.cursor_row + 1 < state.lines.len() {
1835 let next = state.lines.remove(state.cursor_row + 1);
1836 state.lines[state.cursor_row].push_str(&next);
1837 }
1838 consumed_indices.push(i);
1839 }
1840 KeyCode::End => {
1841 state.cursor_col = state.lines[state.cursor_row].chars().count();
1842 consumed_indices.push(i);
1843 }
1844 _ => {}
1845 }
1846 }
1847 if let Event::Paste(ref text) = event {
1848 for ch in text.chars() {
1849 if ch == '\n' || ch == '\r' {
1850 let split_index = byte_index_for_char(
1851 &state.lines[state.cursor_row],
1852 state.cursor_col,
1853 );
1854 let remainder = state.lines[state.cursor_row].split_off(split_index);
1855 state.cursor_row += 1;
1856 state.lines.insert(state.cursor_row, remainder);
1857 state.cursor_col = 0;
1858 } else {
1859 if let Some(max) = state.max_length {
1860 let total: usize =
1861 state.lines.iter().map(|l| l.chars().count()).sum();
1862 if total >= max {
1863 break;
1864 }
1865 }
1866 let index = byte_index_for_char(
1867 &state.lines[state.cursor_row],
1868 state.cursor_col,
1869 );
1870 state.lines[state.cursor_row].insert(index, ch);
1871 state.cursor_col += 1;
1872 }
1873 }
1874 consumed_indices.push(i);
1875 }
1876 }
1877
1878 for index in consumed_indices {
1879 self.consumed[index] = true;
1880 }
1881 }
1882
1883 self.interaction_count += 1;
1884 self.commands.push(Command::BeginContainer {
1885 direction: Direction::Column,
1886 gap: 0,
1887 align: Align::Start,
1888 border: None,
1889 border_style: Style::new().fg(self.theme.border),
1890 padding: Padding::default(),
1891 margin: Margin::default(),
1892 constraints: Constraints::default(),
1893 title: None,
1894 grow: 0,
1895 });
1896 for row in 0..visible_rows as usize {
1897 let line = state.lines.get(row).cloned().unwrap_or_default();
1898 let mut rendered = line.clone();
1899 let mut style = if line.is_empty() {
1900 Style::new().fg(self.theme.text_dim)
1901 } else {
1902 Style::new().fg(self.theme.text)
1903 };
1904
1905 if focused && row == state.cursor_row {
1906 rendered.clear();
1907 for (idx, ch) in line.chars().enumerate() {
1908 if idx == state.cursor_col {
1909 rendered.push('▎');
1910 }
1911 rendered.push(ch);
1912 }
1913 if state.cursor_col >= line.chars().count() {
1914 rendered.push('▎');
1915 }
1916 style = Style::new().fg(self.theme.text);
1917 }
1918
1919 self.styled(rendered, style);
1920 }
1921 self.commands.push(Command::EndContainer);
1922 self.last_text_idx = None;
1923
1924 self
1925 }
1926
1927 pub fn progress(&mut self, ratio: f64) -> &mut Self {
1932 self.progress_bar(ratio, 20)
1933 }
1934
1935 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1940 let clamped = ratio.clamp(0.0, 1.0);
1941 let filled = (clamped * width as f64).round() as u32;
1942 let empty = width.saturating_sub(filled);
1943 let mut bar = String::new();
1944 for _ in 0..filled {
1945 bar.push('█');
1946 }
1947 for _ in 0..empty {
1948 bar.push('░');
1949 }
1950 self.text(bar)
1951 }
1952
1953 pub fn bar_chart(&mut self, data: &[(&str, f64)], max_width: u32) -> &mut Self {
1974 if data.is_empty() {
1975 return self;
1976 }
1977
1978 let max_label_width = data
1979 .iter()
1980 .map(|(label, _)| UnicodeWidthStr::width(*label))
1981 .max()
1982 .unwrap_or(0);
1983 let max_value = data
1984 .iter()
1985 .map(|(_, value)| *value)
1986 .fold(f64::NEG_INFINITY, f64::max);
1987 let denom = if max_value > 0.0 { max_value } else { 1.0 };
1988
1989 self.interaction_count += 1;
1990 self.commands.push(Command::BeginContainer {
1991 direction: Direction::Column,
1992 gap: 0,
1993 align: Align::Start,
1994 border: None,
1995 border_style: Style::new().fg(self.theme.border),
1996 padding: Padding::default(),
1997 margin: Margin::default(),
1998 constraints: Constraints::default(),
1999 title: None,
2000 grow: 0,
2001 });
2002
2003 for (label, value) in data {
2004 let label_width = UnicodeWidthStr::width(*label);
2005 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2006 let normalized = (*value / denom).clamp(0.0, 1.0);
2007 let bar_len = (normalized * max_width as f64).round() as usize;
2008 let bar = "█".repeat(bar_len);
2009
2010 self.interaction_count += 1;
2011 self.commands.push(Command::BeginContainer {
2012 direction: Direction::Row,
2013 gap: 1,
2014 align: Align::Start,
2015 border: None,
2016 border_style: Style::new().fg(self.theme.border),
2017 padding: Padding::default(),
2018 margin: Margin::default(),
2019 constraints: Constraints::default(),
2020 title: None,
2021 grow: 0,
2022 });
2023 self.styled(
2024 format!("{label}{label_padding}"),
2025 Style::new().fg(self.theme.text),
2026 );
2027 self.styled(bar, Style::new().fg(self.theme.primary));
2028 self.styled(
2029 format_compact_number(*value),
2030 Style::new().fg(self.theme.text_dim),
2031 );
2032 self.commands.push(Command::EndContainer);
2033 self.last_text_idx = None;
2034 }
2035
2036 self.commands.push(Command::EndContainer);
2037 self.last_text_idx = None;
2038
2039 self
2040 }
2041
2042 pub fn bar_chart_styled(
2058 &mut self,
2059 bars: &[Bar],
2060 max_width: u32,
2061 direction: BarDirection,
2062 ) -> &mut Self {
2063 if bars.is_empty() {
2064 return self;
2065 }
2066
2067 let max_value = bars
2068 .iter()
2069 .map(|bar| bar.value)
2070 .fold(f64::NEG_INFINITY, f64::max);
2071 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2072
2073 match direction {
2074 BarDirection::Horizontal => {
2075 let max_label_width = bars
2076 .iter()
2077 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2078 .max()
2079 .unwrap_or(0);
2080
2081 self.interaction_count += 1;
2082 self.commands.push(Command::BeginContainer {
2083 direction: Direction::Column,
2084 gap: 0,
2085 align: Align::Start,
2086 border: None,
2087 border_style: Style::new().fg(self.theme.border),
2088 padding: Padding::default(),
2089 margin: Margin::default(),
2090 constraints: Constraints::default(),
2091 title: None,
2092 grow: 0,
2093 });
2094
2095 for bar in bars {
2096 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2097 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2098 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2099 let bar_len = (normalized * max_width as f64).round() as usize;
2100 let bar_text = "█".repeat(bar_len);
2101 let color = bar.color.unwrap_or(self.theme.primary);
2102
2103 self.interaction_count += 1;
2104 self.commands.push(Command::BeginContainer {
2105 direction: Direction::Row,
2106 gap: 1,
2107 align: Align::Start,
2108 border: None,
2109 border_style: Style::new().fg(self.theme.border),
2110 padding: Padding::default(),
2111 margin: Margin::default(),
2112 constraints: Constraints::default(),
2113 title: None,
2114 grow: 0,
2115 });
2116 self.styled(
2117 format!("{}{label_padding}", bar.label),
2118 Style::new().fg(self.theme.text),
2119 );
2120 self.styled(bar_text, Style::new().fg(color));
2121 self.styled(
2122 format_compact_number(bar.value),
2123 Style::new().fg(self.theme.text_dim),
2124 );
2125 self.commands.push(Command::EndContainer);
2126 self.last_text_idx = None;
2127 }
2128
2129 self.commands.push(Command::EndContainer);
2130 self.last_text_idx = None;
2131 }
2132 BarDirection::Vertical => {
2133 const FRACTION_BLOCKS: [char; 8] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇'];
2134
2135 let chart_height = max_width.max(1) as usize;
2136 let value_labels: Vec<String> = bars
2137 .iter()
2138 .map(|bar| format_compact_number(bar.value))
2139 .collect();
2140 let col_width = bars
2141 .iter()
2142 .zip(value_labels.iter())
2143 .map(|(bar, value)| {
2144 UnicodeWidthStr::width(bar.label.as_str())
2145 .max(UnicodeWidthStr::width(value.as_str()))
2146 .max(1)
2147 })
2148 .max()
2149 .unwrap_or(1);
2150
2151 let bar_units: Vec<usize> = bars
2152 .iter()
2153 .map(|bar| {
2154 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2155 (normalized * chart_height as f64 * 8.0).round() as usize
2156 })
2157 .collect();
2158
2159 self.interaction_count += 1;
2160 self.commands.push(Command::BeginContainer {
2161 direction: Direction::Column,
2162 gap: 0,
2163 align: Align::Start,
2164 border: None,
2165 border_style: Style::new().fg(self.theme.border),
2166 padding: Padding::default(),
2167 margin: Margin::default(),
2168 constraints: Constraints::default(),
2169 title: None,
2170 grow: 0,
2171 });
2172
2173 self.interaction_count += 1;
2174 self.commands.push(Command::BeginContainer {
2175 direction: Direction::Row,
2176 gap: 1,
2177 align: Align::Start,
2178 border: None,
2179 border_style: Style::new().fg(self.theme.border),
2180 padding: Padding::default(),
2181 margin: Margin::default(),
2182 constraints: Constraints::default(),
2183 title: None,
2184 grow: 0,
2185 });
2186 for value in &value_labels {
2187 self.styled(
2188 center_text(value, col_width),
2189 Style::new().fg(self.theme.text_dim),
2190 );
2191 }
2192 self.commands.push(Command::EndContainer);
2193 self.last_text_idx = None;
2194
2195 for row in (0..chart_height).rev() {
2196 self.interaction_count += 1;
2197 self.commands.push(Command::BeginContainer {
2198 direction: Direction::Row,
2199 gap: 1,
2200 align: Align::Start,
2201 border: None,
2202 border_style: Style::new().fg(self.theme.border),
2203 padding: Padding::default(),
2204 margin: Margin::default(),
2205 constraints: Constraints::default(),
2206 title: None,
2207 grow: 0,
2208 });
2209
2210 let row_base = row * 8;
2211 for (bar, units) in bars.iter().zip(bar_units.iter()) {
2212 let fill = if *units <= row_base {
2213 ' '
2214 } else {
2215 let delta = *units - row_base;
2216 if delta >= 8 {
2217 '█'
2218 } else {
2219 FRACTION_BLOCKS[delta]
2220 }
2221 };
2222
2223 self.styled(
2224 center_text(&fill.to_string(), col_width),
2225 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2226 );
2227 }
2228
2229 self.commands.push(Command::EndContainer);
2230 self.last_text_idx = None;
2231 }
2232
2233 self.interaction_count += 1;
2234 self.commands.push(Command::BeginContainer {
2235 direction: Direction::Row,
2236 gap: 1,
2237 align: Align::Start,
2238 border: None,
2239 border_style: Style::new().fg(self.theme.border),
2240 padding: Padding::default(),
2241 margin: Margin::default(),
2242 constraints: Constraints::default(),
2243 title: None,
2244 grow: 0,
2245 });
2246 for bar in bars {
2247 self.styled(
2248 center_text(&bar.label, col_width),
2249 Style::new().fg(self.theme.text),
2250 );
2251 }
2252 self.commands.push(Command::EndContainer);
2253 self.last_text_idx = None;
2254
2255 self.commands.push(Command::EndContainer);
2256 self.last_text_idx = None;
2257 }
2258 }
2259
2260 self
2261 }
2262
2263 pub fn bar_chart_grouped(&mut self, groups: &[BarGroup], max_width: u32) -> &mut Self {
2280 if groups.is_empty() {
2281 return self;
2282 }
2283
2284 let all_bars: Vec<&Bar> = groups.iter().flat_map(|group| group.bars.iter()).collect();
2285 if all_bars.is_empty() {
2286 return self;
2287 }
2288
2289 let max_label_width = all_bars
2290 .iter()
2291 .map(|bar| UnicodeWidthStr::width(bar.label.as_str()))
2292 .max()
2293 .unwrap_or(0);
2294 let max_value = all_bars
2295 .iter()
2296 .map(|bar| bar.value)
2297 .fold(f64::NEG_INFINITY, f64::max);
2298 let denom = if max_value > 0.0 { max_value } else { 1.0 };
2299
2300 self.interaction_count += 1;
2301 self.commands.push(Command::BeginContainer {
2302 direction: Direction::Column,
2303 gap: 1,
2304 align: Align::Start,
2305 border: None,
2306 border_style: Style::new().fg(self.theme.border),
2307 padding: Padding::default(),
2308 margin: Margin::default(),
2309 constraints: Constraints::default(),
2310 title: None,
2311 grow: 0,
2312 });
2313
2314 for group in groups {
2315 self.styled(group.label.clone(), Style::new().bold().fg(self.theme.text));
2316
2317 for bar in &group.bars {
2318 let label_width = UnicodeWidthStr::width(bar.label.as_str());
2319 let label_padding = " ".repeat(max_label_width.saturating_sub(label_width));
2320 let normalized = (bar.value / denom).clamp(0.0, 1.0);
2321 let bar_len = (normalized * max_width as f64).round() as usize;
2322 let bar_text = "█".repeat(bar_len);
2323
2324 self.interaction_count += 1;
2325 self.commands.push(Command::BeginContainer {
2326 direction: Direction::Row,
2327 gap: 1,
2328 align: Align::Start,
2329 border: None,
2330 border_style: Style::new().fg(self.theme.border),
2331 padding: Padding::default(),
2332 margin: Margin::default(),
2333 constraints: Constraints::default(),
2334 title: None,
2335 grow: 0,
2336 });
2337 self.styled(
2338 format!(" {}{label_padding}", bar.label),
2339 Style::new().fg(self.theme.text),
2340 );
2341 self.styled(
2342 bar_text,
2343 Style::new().fg(bar.color.unwrap_or(self.theme.primary)),
2344 );
2345 self.styled(
2346 format_compact_number(bar.value),
2347 Style::new().fg(self.theme.text_dim),
2348 );
2349 self.commands.push(Command::EndContainer);
2350 self.last_text_idx = None;
2351 }
2352 }
2353
2354 self.commands.push(Command::EndContainer);
2355 self.last_text_idx = None;
2356
2357 self
2358 }
2359
2360 pub fn sparkline(&mut self, data: &[f64], width: u32) -> &mut Self {
2376 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2377
2378 let w = width as usize;
2379 let window = if data.len() > w {
2380 &data[data.len() - w..]
2381 } else {
2382 data
2383 };
2384
2385 if window.is_empty() {
2386 return self;
2387 }
2388
2389 let min = window.iter().copied().fold(f64::INFINITY, f64::min);
2390 let max = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2391 let range = max - min;
2392
2393 let line: String = window
2394 .iter()
2395 .map(|&value| {
2396 let normalized = if range == 0.0 {
2397 0.5
2398 } else {
2399 (value - min) / range
2400 };
2401 let idx = (normalized * 7.0).round() as usize;
2402 BLOCKS[idx.min(7)]
2403 })
2404 .collect();
2405
2406 self.styled(line, Style::new().fg(self.theme.primary))
2407 }
2408
2409 pub fn sparkline_styled(&mut self, data: &[(f64, Option<Color>)], width: u32) -> &mut Self {
2429 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
2430
2431 let w = width as usize;
2432 let window = if data.len() > w {
2433 &data[data.len() - w..]
2434 } else {
2435 data
2436 };
2437
2438 if window.is_empty() {
2439 return self;
2440 }
2441
2442 let mut finite_values = window
2443 .iter()
2444 .map(|(value, _)| *value)
2445 .filter(|value| !value.is_nan());
2446 let Some(first) = finite_values.next() else {
2447 return self.styled(
2448 " ".repeat(window.len()),
2449 Style::new().fg(self.theme.text_dim),
2450 );
2451 };
2452
2453 let mut min = first;
2454 let mut max = first;
2455 for value in finite_values {
2456 min = f64::min(min, value);
2457 max = f64::max(max, value);
2458 }
2459 let range = max - min;
2460
2461 let mut cells: Vec<(char, Color)> = Vec::with_capacity(window.len());
2462 for (value, color) in window {
2463 if value.is_nan() {
2464 cells.push((' ', self.theme.text_dim));
2465 continue;
2466 }
2467
2468 let normalized = if range == 0.0 {
2469 0.5
2470 } else {
2471 ((*value - min) / range).clamp(0.0, 1.0)
2472 };
2473 let idx = (normalized * 7.0).round() as usize;
2474 cells.push((BLOCKS[idx.min(7)], color.unwrap_or(self.theme.primary)));
2475 }
2476
2477 self.interaction_count += 1;
2478 self.commands.push(Command::BeginContainer {
2479 direction: Direction::Row,
2480 gap: 0,
2481 align: Align::Start,
2482 border: None,
2483 border_style: Style::new().fg(self.theme.border),
2484 padding: Padding::default(),
2485 margin: Margin::default(),
2486 constraints: Constraints::default(),
2487 title: None,
2488 grow: 0,
2489 });
2490
2491 let mut seg = String::new();
2492 let mut seg_color = cells[0].1;
2493 for (ch, color) in cells {
2494 if color != seg_color {
2495 self.styled(seg, Style::new().fg(seg_color));
2496 seg = String::new();
2497 seg_color = color;
2498 }
2499 seg.push(ch);
2500 }
2501 if !seg.is_empty() {
2502 self.styled(seg, Style::new().fg(seg_color));
2503 }
2504
2505 self.commands.push(Command::EndContainer);
2506 self.last_text_idx = None;
2507
2508 self
2509 }
2510
2511 pub fn line_chart(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2525 if data.is_empty() || width == 0 || height == 0 {
2526 return self;
2527 }
2528
2529 let cols = width as usize;
2530 let rows = height as usize;
2531 let px_w = cols * 2;
2532 let px_h = rows * 4;
2533
2534 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
2535 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
2536 let range = if (max - min).abs() < f64::EPSILON {
2537 1.0
2538 } else {
2539 max - min
2540 };
2541
2542 let points: Vec<usize> = (0..px_w)
2543 .map(|px| {
2544 let data_idx = if px_w <= 1 {
2545 0.0
2546 } else {
2547 px as f64 * (data.len() - 1) as f64 / (px_w - 1) as f64
2548 };
2549 let idx = data_idx.floor() as usize;
2550 let frac = data_idx - idx as f64;
2551 let value = if idx + 1 < data.len() {
2552 data[idx] * (1.0 - frac) + data[idx + 1] * frac
2553 } else {
2554 data[idx.min(data.len() - 1)]
2555 };
2556
2557 let normalized = (value - min) / range;
2558 let py = ((1.0 - normalized) * (px_h - 1) as f64).round() as usize;
2559 py.min(px_h - 1)
2560 })
2561 .collect();
2562
2563 const LEFT_BITS: [u32; 4] = [0x01, 0x02, 0x04, 0x40];
2564 const RIGHT_BITS: [u32; 4] = [0x08, 0x10, 0x20, 0x80];
2565
2566 let mut grid = vec![vec![0u32; cols]; rows];
2567
2568 for i in 0..points.len() {
2569 let px = i;
2570 let py = points[i];
2571 let char_col = px / 2;
2572 let char_row = py / 4;
2573 let sub_col = px % 2;
2574 let sub_row = py % 4;
2575
2576 if char_col < cols && char_row < rows {
2577 grid[char_row][char_col] |= if sub_col == 0 {
2578 LEFT_BITS[sub_row]
2579 } else {
2580 RIGHT_BITS[sub_row]
2581 };
2582 }
2583
2584 if i + 1 < points.len() {
2585 let py_next = points[i + 1];
2586 let (y_start, y_end) = if py <= py_next {
2587 (py, py_next)
2588 } else {
2589 (py_next, py)
2590 };
2591 for y in y_start..=y_end {
2592 let cell_row = y / 4;
2593 let sub_y = y % 4;
2594 if char_col < cols && cell_row < rows {
2595 grid[cell_row][char_col] |= if sub_col == 0 {
2596 LEFT_BITS[sub_y]
2597 } else {
2598 RIGHT_BITS[sub_y]
2599 };
2600 }
2601 }
2602 }
2603 }
2604
2605 let style = Style::new().fg(self.theme.primary);
2606 for row in grid {
2607 let line: String = row
2608 .iter()
2609 .map(|&bits| char::from_u32(0x2800 + bits).unwrap_or(' '))
2610 .collect();
2611 self.styled(line, style);
2612 }
2613
2614 self
2615 }
2616
2617 pub fn canvas(
2634 &mut self,
2635 width: u32,
2636 height: u32,
2637 draw: impl FnOnce(&mut CanvasContext),
2638 ) -> &mut Self {
2639 if width == 0 || height == 0 {
2640 return self;
2641 }
2642
2643 let mut canvas = CanvasContext::new(width as usize, height as usize);
2644 draw(&mut canvas);
2645
2646 for segments in canvas.render() {
2647 self.interaction_count += 1;
2648 self.commands.push(Command::BeginContainer {
2649 direction: Direction::Row,
2650 gap: 0,
2651 align: Align::Start,
2652 border: None,
2653 border_style: Style::new(),
2654 padding: Padding::default(),
2655 margin: Margin::default(),
2656 constraints: Constraints::default(),
2657 title: None,
2658 grow: 0,
2659 });
2660 for (text, color) in segments {
2661 let c = if color == Color::Reset {
2662 self.theme.primary
2663 } else {
2664 color
2665 };
2666 self.styled(text, Style::new().fg(c));
2667 }
2668 self.commands.push(Command::EndContainer);
2669 self.last_text_idx = None;
2670 }
2671
2672 self
2673 }
2674
2675 pub fn chart(
2677 &mut self,
2678 configure: impl FnOnce(&mut ChartBuilder),
2679 width: u32,
2680 height: u32,
2681 ) -> &mut Self {
2682 if width == 0 || height == 0 {
2683 return self;
2684 }
2685
2686 let axis_style = Style::new().fg(self.theme.text_dim);
2687 let mut builder = ChartBuilder::new(width, height, axis_style, axis_style);
2688 configure(&mut builder);
2689
2690 let config = builder.build();
2691 let rows = render_chart(&config);
2692
2693 for row in rows {
2694 self.interaction_count += 1;
2695 self.commands.push(Command::BeginContainer {
2696 direction: Direction::Row,
2697 gap: 0,
2698 align: Align::Start,
2699 border: None,
2700 border_style: Style::new().fg(self.theme.border),
2701 padding: Padding::default(),
2702 margin: Margin::default(),
2703 constraints: Constraints::default(),
2704 title: None,
2705 grow: 0,
2706 });
2707 for (text, style) in row.segments {
2708 self.styled(text, style);
2709 }
2710 self.commands.push(Command::EndContainer);
2711 self.last_text_idx = None;
2712 }
2713
2714 self
2715 }
2716
2717 pub fn histogram(&mut self, data: &[f64], width: u32, height: u32) -> &mut Self {
2719 self.histogram_with(data, |_| {}, width, height)
2720 }
2721
2722 pub fn histogram_with(
2724 &mut self,
2725 data: &[f64],
2726 configure: impl FnOnce(&mut HistogramBuilder),
2727 width: u32,
2728 height: u32,
2729 ) -> &mut Self {
2730 if width == 0 || height == 0 {
2731 return self;
2732 }
2733
2734 let mut options = HistogramBuilder::default();
2735 configure(&mut options);
2736 let axis_style = Style::new().fg(self.theme.text_dim);
2737 let config = build_histogram_config(data, &options, width, height, axis_style);
2738 let rows = render_chart(&config);
2739
2740 for row in rows {
2741 self.interaction_count += 1;
2742 self.commands.push(Command::BeginContainer {
2743 direction: Direction::Row,
2744 gap: 0,
2745 align: Align::Start,
2746 border: None,
2747 border_style: Style::new().fg(self.theme.border),
2748 padding: Padding::default(),
2749 margin: Margin::default(),
2750 constraints: Constraints::default(),
2751 title: None,
2752 grow: 0,
2753 });
2754 for (text, style) in row.segments {
2755 self.styled(text, style);
2756 }
2757 self.commands.push(Command::EndContainer);
2758 self.last_text_idx = None;
2759 }
2760
2761 self
2762 }
2763
2764 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
2781 let interaction_id = self.interaction_count;
2782 self.interaction_count += 1;
2783 let border = self.theme.border;
2784
2785 self.commands.push(Command::BeginContainer {
2786 direction: Direction::Column,
2787 gap: 0,
2788 align: Align::Start,
2789 border: None,
2790 border_style: Style::new().fg(border),
2791 padding: Padding::default(),
2792 margin: Margin::default(),
2793 constraints: Constraints::default(),
2794 title: None,
2795 grow: 0,
2796 });
2797
2798 let children_start = self.commands.len();
2799 f(self);
2800 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
2801
2802 let mut elements: Vec<Vec<Command>> = Vec::new();
2803 let mut iter = child_commands.into_iter().peekable();
2804 while let Some(cmd) = iter.next() {
2805 match cmd {
2806 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2807 let mut depth = 1_u32;
2808 let mut element = vec![cmd];
2809 for next in iter.by_ref() {
2810 match next {
2811 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
2812 depth += 1;
2813 }
2814 Command::EndContainer => {
2815 depth = depth.saturating_sub(1);
2816 }
2817 _ => {}
2818 }
2819 let at_end = matches!(next, Command::EndContainer) && depth == 0;
2820 element.push(next);
2821 if at_end {
2822 break;
2823 }
2824 }
2825 elements.push(element);
2826 }
2827 Command::EndContainer => {}
2828 _ => elements.push(vec![cmd]),
2829 }
2830 }
2831
2832 let cols = cols.max(1) as usize;
2833 for row in elements.chunks(cols) {
2834 self.interaction_count += 1;
2835 self.commands.push(Command::BeginContainer {
2836 direction: Direction::Row,
2837 gap: 0,
2838 align: Align::Start,
2839 border: None,
2840 border_style: Style::new().fg(border),
2841 padding: Padding::default(),
2842 margin: Margin::default(),
2843 constraints: Constraints::default(),
2844 title: None,
2845 grow: 0,
2846 });
2847
2848 for element in row {
2849 self.interaction_count += 1;
2850 self.commands.push(Command::BeginContainer {
2851 direction: Direction::Column,
2852 gap: 0,
2853 align: Align::Start,
2854 border: None,
2855 border_style: Style::new().fg(border),
2856 padding: Padding::default(),
2857 margin: Margin::default(),
2858 constraints: Constraints::default(),
2859 title: None,
2860 grow: 1,
2861 });
2862 self.commands.extend(element.iter().cloned());
2863 self.commands.push(Command::EndContainer);
2864 }
2865
2866 self.commands.push(Command::EndContainer);
2867 }
2868
2869 self.commands.push(Command::EndContainer);
2870 self.last_text_idx = None;
2871
2872 self.response_for(interaction_id)
2873 }
2874
2875 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
2880 if state.items.is_empty() {
2881 state.selected = 0;
2882 return self;
2883 }
2884
2885 state.selected = state.selected.min(state.items.len().saturating_sub(1));
2886
2887 let focused = self.register_focusable();
2888
2889 if focused {
2890 let mut consumed_indices = Vec::new();
2891 for (i, event) in self.events.iter().enumerate() {
2892 if let Event::Key(key) = event {
2893 match key.code {
2894 KeyCode::Up | KeyCode::Char('k') => {
2895 state.selected = state.selected.saturating_sub(1);
2896 consumed_indices.push(i);
2897 }
2898 KeyCode::Down | KeyCode::Char('j') => {
2899 state.selected =
2900 (state.selected + 1).min(state.items.len().saturating_sub(1));
2901 consumed_indices.push(i);
2902 }
2903 _ => {}
2904 }
2905 }
2906 }
2907
2908 for index in consumed_indices {
2909 self.consumed[index] = true;
2910 }
2911 }
2912
2913 for (idx, item) in state.items.iter().enumerate() {
2914 if idx == state.selected {
2915 if focused {
2916 self.styled(
2917 format!("▸ {item}"),
2918 Style::new().bold().fg(self.theme.primary),
2919 );
2920 } else {
2921 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
2922 }
2923 } else {
2924 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
2925 }
2926 }
2927
2928 self
2929 }
2930
2931 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
2936 if state.is_dirty() {
2937 state.recompute_widths();
2938 }
2939
2940 let focused = self.register_focusable();
2941
2942 if focused && !state.rows.is_empty() {
2943 let mut consumed_indices = Vec::new();
2944 for (i, event) in self.events.iter().enumerate() {
2945 if let Event::Key(key) = event {
2946 match key.code {
2947 KeyCode::Up | KeyCode::Char('k') => {
2948 state.selected = state.selected.saturating_sub(1);
2949 consumed_indices.push(i);
2950 }
2951 KeyCode::Down | KeyCode::Char('j') => {
2952 state.selected =
2953 (state.selected + 1).min(state.rows.len().saturating_sub(1));
2954 consumed_indices.push(i);
2955 }
2956 _ => {}
2957 }
2958 }
2959 }
2960 for index in consumed_indices {
2961 self.consumed[index] = true;
2962 }
2963 }
2964
2965 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
2966
2967 let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
2968 self.styled(header_line, Style::new().bold().fg(self.theme.text));
2969
2970 let separator = state
2971 .column_widths()
2972 .iter()
2973 .map(|w| "─".repeat(*w as usize))
2974 .collect::<Vec<_>>()
2975 .join("─┼─");
2976 self.text(separator);
2977
2978 for (idx, row) in state.rows.iter().enumerate() {
2979 let line = format_table_row(row, state.column_widths(), " │ ");
2980 if idx == state.selected {
2981 let mut style = Style::new()
2982 .bg(self.theme.selected_bg)
2983 .fg(self.theme.selected_fg);
2984 if focused {
2985 style = style.bold();
2986 }
2987 self.styled(line, style);
2988 } else {
2989 self.styled(line, Style::new().fg(self.theme.text));
2990 }
2991 }
2992
2993 self
2994 }
2995
2996 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
3001 if state.labels.is_empty() {
3002 state.selected = 0;
3003 return self;
3004 }
3005
3006 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
3007 let focused = self.register_focusable();
3008
3009 if focused {
3010 let mut consumed_indices = Vec::new();
3011 for (i, event) in self.events.iter().enumerate() {
3012 if let Event::Key(key) = event {
3013 match key.code {
3014 KeyCode::Left => {
3015 state.selected = if state.selected == 0 {
3016 state.labels.len().saturating_sub(1)
3017 } else {
3018 state.selected - 1
3019 };
3020 consumed_indices.push(i);
3021 }
3022 KeyCode::Right => {
3023 state.selected = (state.selected + 1) % state.labels.len();
3024 consumed_indices.push(i);
3025 }
3026 _ => {}
3027 }
3028 }
3029 }
3030
3031 for index in consumed_indices {
3032 self.consumed[index] = true;
3033 }
3034 }
3035
3036 self.interaction_count += 1;
3037 self.commands.push(Command::BeginContainer {
3038 direction: Direction::Row,
3039 gap: 1,
3040 align: Align::Start,
3041 border: None,
3042 border_style: Style::new().fg(self.theme.border),
3043 padding: Padding::default(),
3044 margin: Margin::default(),
3045 constraints: Constraints::default(),
3046 title: None,
3047 grow: 0,
3048 });
3049 for (idx, label) in state.labels.iter().enumerate() {
3050 let style = if idx == state.selected {
3051 let s = Style::new().fg(self.theme.primary).bold();
3052 if focused {
3053 s.underline()
3054 } else {
3055 s
3056 }
3057 } else {
3058 Style::new().fg(self.theme.text_dim)
3059 };
3060 self.styled(format!("[ {label} ]"), style);
3061 }
3062 self.commands.push(Command::EndContainer);
3063 self.last_text_idx = None;
3064
3065 self
3066 }
3067
3068 pub fn button(&mut self, label: impl Into<String>) -> bool {
3073 let focused = self.register_focusable();
3074 let interaction_id = self.interaction_count;
3075 self.interaction_count += 1;
3076 let response = self.response_for(interaction_id);
3077
3078 let mut activated = response.clicked;
3079 if focused {
3080 let mut consumed_indices = Vec::new();
3081 for (i, event) in self.events.iter().enumerate() {
3082 if let Event::Key(key) = event {
3083 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3084 activated = true;
3085 consumed_indices.push(i);
3086 }
3087 }
3088 }
3089
3090 for index in consumed_indices {
3091 self.consumed[index] = true;
3092 }
3093 }
3094
3095 let style = if focused {
3096 Style::new().fg(self.theme.primary).bold()
3097 } else if response.hovered {
3098 Style::new().fg(self.theme.accent)
3099 } else {
3100 Style::new().fg(self.theme.text)
3101 };
3102
3103 self.commands.push(Command::BeginContainer {
3104 direction: Direction::Row,
3105 gap: 0,
3106 align: Align::Start,
3107 border: None,
3108 border_style: Style::new().fg(self.theme.border),
3109 padding: Padding::default(),
3110 margin: Margin::default(),
3111 constraints: Constraints::default(),
3112 title: None,
3113 grow: 0,
3114 });
3115 self.styled(format!("[ {} ]", label.into()), style);
3116 self.commands.push(Command::EndContainer);
3117 self.last_text_idx = None;
3118
3119 activated
3120 }
3121
3122 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
3127 let focused = self.register_focusable();
3128 let interaction_id = self.interaction_count;
3129 self.interaction_count += 1;
3130 let response = self.response_for(interaction_id);
3131 let mut should_toggle = response.clicked;
3132
3133 if focused {
3134 let mut consumed_indices = Vec::new();
3135 for (i, event) in self.events.iter().enumerate() {
3136 if let Event::Key(key) = event {
3137 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3138 should_toggle = true;
3139 consumed_indices.push(i);
3140 }
3141 }
3142 }
3143
3144 for index in consumed_indices {
3145 self.consumed[index] = true;
3146 }
3147 }
3148
3149 if should_toggle {
3150 *checked = !*checked;
3151 }
3152
3153 self.commands.push(Command::BeginContainer {
3154 direction: Direction::Row,
3155 gap: 1,
3156 align: Align::Start,
3157 border: None,
3158 border_style: Style::new().fg(self.theme.border),
3159 padding: Padding::default(),
3160 margin: Margin::default(),
3161 constraints: Constraints::default(),
3162 title: None,
3163 grow: 0,
3164 });
3165 let marker_style = if *checked {
3166 Style::new().fg(self.theme.success)
3167 } else {
3168 Style::new().fg(self.theme.text_dim)
3169 };
3170 let marker = if *checked { "[x]" } else { "[ ]" };
3171 let label_text = label.into();
3172 if focused {
3173 self.styled(format!("▸ {marker}"), marker_style.bold());
3174 self.styled(label_text, Style::new().fg(self.theme.text).bold());
3175 } else {
3176 self.styled(marker, marker_style);
3177 self.styled(label_text, Style::new().fg(self.theme.text));
3178 }
3179 self.commands.push(Command::EndContainer);
3180 self.last_text_idx = None;
3181
3182 self
3183 }
3184
3185 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
3191 let focused = self.register_focusable();
3192 let interaction_id = self.interaction_count;
3193 self.interaction_count += 1;
3194 let response = self.response_for(interaction_id);
3195 let mut should_toggle = response.clicked;
3196
3197 if focused {
3198 let mut consumed_indices = Vec::new();
3199 for (i, event) in self.events.iter().enumerate() {
3200 if let Event::Key(key) = event {
3201 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
3202 should_toggle = true;
3203 consumed_indices.push(i);
3204 }
3205 }
3206 }
3207
3208 for index in consumed_indices {
3209 self.consumed[index] = true;
3210 }
3211 }
3212
3213 if should_toggle {
3214 *on = !*on;
3215 }
3216
3217 self.commands.push(Command::BeginContainer {
3218 direction: Direction::Row,
3219 gap: 2,
3220 align: Align::Start,
3221 border: None,
3222 border_style: Style::new().fg(self.theme.border),
3223 padding: Padding::default(),
3224 margin: Margin::default(),
3225 constraints: Constraints::default(),
3226 title: None,
3227 grow: 0,
3228 });
3229 let label_text = label.into();
3230 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
3231 let switch_style = if *on {
3232 Style::new().fg(self.theme.success)
3233 } else {
3234 Style::new().fg(self.theme.text_dim)
3235 };
3236 if focused {
3237 self.styled(
3238 format!("▸ {label_text}"),
3239 Style::new().fg(self.theme.text).bold(),
3240 );
3241 self.styled(switch, switch_style.bold());
3242 } else {
3243 self.styled(label_text, Style::new().fg(self.theme.text));
3244 self.styled(switch, switch_style);
3245 }
3246 self.commands.push(Command::EndContainer);
3247 self.last_text_idx = None;
3248
3249 self
3250 }
3251
3252 pub fn separator(&mut self) -> &mut Self {
3257 self.commands.push(Command::Text {
3258 content: "─".repeat(200),
3259 style: Style::new().fg(self.theme.border).dim(),
3260 grow: 0,
3261 align: Align::Start,
3262 wrap: false,
3263 margin: Margin::default(),
3264 constraints: Constraints::default(),
3265 });
3266 self.last_text_idx = Some(self.commands.len() - 1);
3267 self
3268 }
3269
3270 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
3276 if bindings.is_empty() {
3277 return self;
3278 }
3279
3280 self.interaction_count += 1;
3281 self.commands.push(Command::BeginContainer {
3282 direction: Direction::Row,
3283 gap: 2,
3284 align: Align::Start,
3285 border: None,
3286 border_style: Style::new().fg(self.theme.border),
3287 padding: Padding::default(),
3288 margin: Margin::default(),
3289 constraints: Constraints::default(),
3290 title: None,
3291 grow: 0,
3292 });
3293 for (idx, (key, action)) in bindings.iter().enumerate() {
3294 if idx > 0 {
3295 self.styled("·", Style::new().fg(self.theme.text_dim));
3296 }
3297 self.styled(*key, Style::new().bold().fg(self.theme.primary));
3298 self.styled(*action, Style::new().fg(self.theme.text_dim));
3299 }
3300 self.commands.push(Command::EndContainer);
3301 self.last_text_idx = None;
3302
3303 self
3304 }
3305
3306 pub fn key(&self, c: char) -> bool {
3312 self.events.iter().enumerate().any(|(i, e)| {
3313 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
3314 })
3315 }
3316
3317 pub fn key_code(&self, code: KeyCode) -> bool {
3321 self.events
3322 .iter()
3323 .enumerate()
3324 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
3325 }
3326
3327 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3331 self.events.iter().enumerate().any(|(i, e)| {
3332 !self.consumed[i]
3333 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3334 })
3335 }
3336
3337 pub fn mouse_down(&self) -> Option<(u32, u32)> {
3341 self.events.iter().enumerate().find_map(|(i, event)| {
3342 if self.consumed[i] {
3343 return None;
3344 }
3345 if let Event::Mouse(mouse) = event {
3346 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3347 return Some((mouse.x, mouse.y));
3348 }
3349 }
3350 None
3351 })
3352 }
3353
3354 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3359 self.mouse_pos
3360 }
3361
3362 pub fn paste(&self) -> Option<&str> {
3364 self.events.iter().enumerate().find_map(|(i, event)| {
3365 if self.consumed[i] {
3366 return None;
3367 }
3368 if let Event::Paste(ref text) = event {
3369 return Some(text.as_str());
3370 }
3371 None
3372 })
3373 }
3374
3375 pub fn scroll_up(&self) -> bool {
3377 self.events.iter().enumerate().any(|(i, event)| {
3378 !self.consumed[i]
3379 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3380 })
3381 }
3382
3383 pub fn scroll_down(&self) -> bool {
3385 self.events.iter().enumerate().any(|(i, event)| {
3386 !self.consumed[i]
3387 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3388 })
3389 }
3390
3391 pub fn quit(&mut self) {
3393 self.should_quit = true;
3394 }
3395
3396 pub fn theme(&self) -> &Theme {
3398 &self.theme
3399 }
3400
3401 pub fn set_theme(&mut self, theme: Theme) {
3405 self.theme = theme;
3406 }
3407
3408 pub fn width(&self) -> u32 {
3412 self.area_width
3413 }
3414
3415 pub fn height(&self) -> u32 {
3417 self.area_height
3418 }
3419
3420 pub fn tick(&self) -> u64 {
3425 self.tick
3426 }
3427
3428 pub fn debug_enabled(&self) -> bool {
3432 self.debug
3433 }
3434}
3435
3436#[inline]
3437fn byte_index_for_char(value: &str, char_index: usize) -> usize {
3438 if char_index == 0 {
3439 return 0;
3440 }
3441 value
3442 .char_indices()
3443 .nth(char_index)
3444 .map_or(value.len(), |(idx, _)| idx)
3445}
3446
3447fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
3448 let mut parts: Vec<String> = Vec::new();
3449 for (i, width) in widths.iter().enumerate() {
3450 let cell = cells.get(i).map(String::as_str).unwrap_or("");
3451 let cell_width = UnicodeWidthStr::width(cell) as u32;
3452 let padding = (*width).saturating_sub(cell_width) as usize;
3453 parts.push(format!("{cell}{}", " ".repeat(padding)));
3454 }
3455 parts.join(separator)
3456}
3457
3458fn format_compact_number(value: f64) -> String {
3459 if value.fract().abs() < f64::EPSILON {
3460 return format!("{value:.0}");
3461 }
3462
3463 let mut s = format!("{value:.2}");
3464 while s.contains('.') && s.ends_with('0') {
3465 s.pop();
3466 }
3467 if s.ends_with('.') {
3468 s.pop();
3469 }
3470 s
3471}
3472
3473fn center_text(text: &str, width: usize) -> String {
3474 let text_width = UnicodeWidthStr::width(text);
3475 if text_width >= width {
3476 return text.to_string();
3477 }
3478
3479 let total = width - text_width;
3480 let left = total / 2;
3481 let right = total - left;
3482 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
3483}