1use crate::event::{Event, KeyCode, KeyModifiers, MouseButton, MouseKind};
2use crate::layout::{Command, Direction};
3use crate::rect::Rect;
4use crate::style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
5use crate::widgets::{
6 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
7 ToastLevel, ToastState,
8};
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, Copy, Default)]
17pub struct Response {
18 pub clicked: bool,
20 pub hovered: bool,
22}
23
24pub struct Context {
40 pub(crate) commands: Vec<Command>,
41 pub(crate) events: Vec<Event>,
42 pub(crate) consumed: Vec<bool>,
43 pub(crate) should_quit: bool,
44 pub(crate) area_width: u32,
45 pub(crate) area_height: u32,
46 pub(crate) tick: u64,
47 pub(crate) focus_index: usize,
48 pub(crate) focus_count: usize,
49 prev_focus_count: usize,
50 scroll_count: usize,
51 prev_scroll_infos: Vec<(u32, u32)>,
52 interaction_count: usize,
53 prev_hit_map: Vec<Rect>,
54 mouse_pos: Option<(u32, u32)>,
55 click_pos: Option<(u32, u32)>,
56 last_mouse_pos: Option<(u32, u32)>,
57 last_text_idx: Option<usize>,
58 debug: bool,
59 theme: Theme,
60}
61
62pub struct ContainerBuilder<'a> {
83 ctx: &'a mut Context,
84 gap: u32,
85 align: Align,
86 border: Option<Border>,
87 border_style: Style,
88 padding: Padding,
89 margin: Margin,
90 constraints: Constraints,
91 title: Option<(String, Style)>,
92 grow: u16,
93 scroll_offset: Option<u32>,
94}
95
96impl<'a> ContainerBuilder<'a> {
97 pub fn border(mut self, border: Border) -> Self {
101 self.border = Some(border);
102 self
103 }
104
105 pub fn rounded(self) -> Self {
107 self.border(Border::Rounded)
108 }
109
110 pub fn border_style(mut self, style: Style) -> Self {
112 self.border_style = style;
113 self
114 }
115
116 pub fn p(self, value: u32) -> Self {
120 self.pad(value)
121 }
122
123 pub fn pad(mut self, value: u32) -> Self {
125 self.padding = Padding::all(value);
126 self
127 }
128
129 pub fn px(mut self, value: u32) -> Self {
131 self.padding.left = value;
132 self.padding.right = value;
133 self
134 }
135
136 pub fn py(mut self, value: u32) -> Self {
138 self.padding.top = value;
139 self.padding.bottom = value;
140 self
141 }
142
143 pub fn pt(mut self, value: u32) -> Self {
145 self.padding.top = value;
146 self
147 }
148
149 pub fn pr(mut self, value: u32) -> Self {
151 self.padding.right = value;
152 self
153 }
154
155 pub fn pb(mut self, value: u32) -> Self {
157 self.padding.bottom = value;
158 self
159 }
160
161 pub fn pl(mut self, value: u32) -> Self {
163 self.padding.left = value;
164 self
165 }
166
167 pub fn padding(mut self, padding: Padding) -> Self {
169 self.padding = padding;
170 self
171 }
172
173 pub fn m(mut self, value: u32) -> Self {
177 self.margin = Margin::all(value);
178 self
179 }
180
181 pub fn mx(mut self, value: u32) -> Self {
183 self.margin.left = value;
184 self.margin.right = value;
185 self
186 }
187
188 pub fn my(mut self, value: u32) -> Self {
190 self.margin.top = value;
191 self.margin.bottom = value;
192 self
193 }
194
195 pub fn mt(mut self, value: u32) -> Self {
197 self.margin.top = value;
198 self
199 }
200
201 pub fn mr(mut self, value: u32) -> Self {
203 self.margin.right = value;
204 self
205 }
206
207 pub fn mb(mut self, value: u32) -> Self {
209 self.margin.bottom = value;
210 self
211 }
212
213 pub fn ml(mut self, value: u32) -> Self {
215 self.margin.left = value;
216 self
217 }
218
219 pub fn margin(mut self, margin: Margin) -> Self {
221 self.margin = margin;
222 self
223 }
224
225 pub fn w(mut self, value: u32) -> Self {
229 self.constraints.min_width = Some(value);
230 self.constraints.max_width = Some(value);
231 self
232 }
233
234 pub fn h(mut self, value: u32) -> Self {
236 self.constraints.min_height = Some(value);
237 self.constraints.max_height = Some(value);
238 self
239 }
240
241 pub fn min_w(mut self, value: u32) -> Self {
243 self.constraints.min_width = Some(value);
244 self
245 }
246
247 pub fn max_w(mut self, value: u32) -> Self {
249 self.constraints.max_width = Some(value);
250 self
251 }
252
253 pub fn min_h(mut self, value: u32) -> Self {
255 self.constraints.min_height = Some(value);
256 self
257 }
258
259 pub fn max_h(mut self, value: u32) -> Self {
261 self.constraints.max_height = Some(value);
262 self
263 }
264
265 pub fn min_width(mut self, value: u32) -> Self {
267 self.constraints.min_width = Some(value);
268 self
269 }
270
271 pub fn max_width(mut self, value: u32) -> Self {
273 self.constraints.max_width = Some(value);
274 self
275 }
276
277 pub fn min_height(mut self, value: u32) -> Self {
279 self.constraints.min_height = Some(value);
280 self
281 }
282
283 pub fn max_height(mut self, value: u32) -> Self {
285 self.constraints.max_height = Some(value);
286 self
287 }
288
289 pub fn constraints(mut self, constraints: Constraints) -> Self {
291 self.constraints = constraints;
292 self
293 }
294
295 pub fn gap(mut self, gap: u32) -> Self {
299 self.gap = gap;
300 self
301 }
302
303 pub fn grow(mut self, grow: u16) -> Self {
305 self.grow = grow;
306 self
307 }
308
309 pub fn align(mut self, align: Align) -> Self {
313 self.align = align;
314 self
315 }
316
317 pub fn center(self) -> Self {
319 self.align(Align::Center)
320 }
321
322 pub fn title(self, title: impl Into<String>) -> Self {
326 self.title_styled(title, Style::new())
327 }
328
329 pub fn title_styled(mut self, title: impl Into<String>, style: Style) -> Self {
331 self.title = Some((title.into(), style));
332 self
333 }
334
335 pub fn scroll_offset(mut self, offset: u32) -> Self {
339 self.scroll_offset = Some(offset);
340 self
341 }
342
343 pub fn col(self, f: impl FnOnce(&mut Context)) -> Response {
348 self.finish(Direction::Column, f)
349 }
350
351 pub fn row(self, f: impl FnOnce(&mut Context)) -> Response {
356 self.finish(Direction::Row, f)
357 }
358
359 fn finish(self, direction: Direction, f: impl FnOnce(&mut Context)) -> Response {
360 let interaction_id = self.ctx.interaction_count;
361 self.ctx.interaction_count += 1;
362
363 if let Some(scroll_offset) = self.scroll_offset {
364 self.ctx.commands.push(Command::BeginScrollable {
365 grow: self.grow,
366 border: self.border,
367 border_style: self.border_style,
368 padding: self.padding,
369 margin: self.margin,
370 constraints: self.constraints,
371 title: self.title,
372 scroll_offset,
373 });
374 } else {
375 self.ctx.commands.push(Command::BeginContainer {
376 direction,
377 gap: self.gap,
378 align: self.align,
379 border: self.border,
380 border_style: self.border_style,
381 padding: self.padding,
382 margin: self.margin,
383 constraints: self.constraints,
384 title: self.title,
385 grow: self.grow,
386 });
387 }
388 f(self.ctx);
389 self.ctx.commands.push(Command::EndContainer);
390 self.ctx.last_text_idx = None;
391
392 self.ctx.response_for(interaction_id)
393 }
394}
395
396impl Context {
397 #[allow(clippy::too_many_arguments)]
398 pub(crate) fn new(
399 events: Vec<Event>,
400 width: u32,
401 height: u32,
402 tick: u64,
403 focus_index: usize,
404 prev_focus_count: usize,
405 prev_scroll_infos: Vec<(u32, u32)>,
406 prev_hit_map: Vec<Rect>,
407 debug: bool,
408 theme: Theme,
409 last_mouse_pos: Option<(u32, u32)>,
410 ) -> Self {
411 let consumed = vec![false; events.len()];
412
413 let mut mouse_pos = last_mouse_pos;
414 let mut click_pos = None;
415 for event in &events {
416 if let Event::Mouse(mouse) = event {
417 mouse_pos = Some((mouse.x, mouse.y));
418 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
419 click_pos = Some((mouse.x, mouse.y));
420 }
421 }
422 }
423
424 Self {
425 commands: Vec::new(),
426 events,
427 consumed,
428 should_quit: false,
429 area_width: width,
430 area_height: height,
431 tick,
432 focus_index,
433 focus_count: 0,
434 prev_focus_count,
435 scroll_count: 0,
436 prev_scroll_infos,
437 interaction_count: 0,
438 prev_hit_map,
439 mouse_pos,
440 click_pos,
441 last_mouse_pos,
442 last_text_idx: None,
443 debug,
444 theme,
445 }
446 }
447
448 pub(crate) fn process_focus_keys(&mut self) {
449 for (i, event) in self.events.iter().enumerate() {
450 if let Event::Key(key) = event {
451 if key.code == KeyCode::Tab && !key.modifiers.contains(KeyModifiers::SHIFT) {
452 if self.prev_focus_count > 0 {
453 self.focus_index = (self.focus_index + 1) % self.prev_focus_count;
454 }
455 self.consumed[i] = true;
456 } else if (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
457 || key.code == KeyCode::BackTab
458 {
459 if self.prev_focus_count > 0 {
460 self.focus_index = if self.focus_index == 0 {
461 self.prev_focus_count - 1
462 } else {
463 self.focus_index - 1
464 };
465 }
466 self.consumed[i] = true;
467 }
468 }
469 }
470 }
471
472 pub fn register_focusable(&mut self) -> bool {
478 let id = self.focus_count;
479 self.focus_count += 1;
480 if self.prev_focus_count == 0 {
481 return true;
482 }
483 self.focus_index % self.prev_focus_count == id
484 }
485
486 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
499 let content = s.into();
500 self.commands.push(Command::Text {
501 content,
502 style: Style::new(),
503 grow: 0,
504 align: Align::Start,
505 wrap: false,
506 margin: Margin::default(),
507 constraints: Constraints::default(),
508 });
509 self.last_text_idx = Some(self.commands.len() - 1);
510 self
511 }
512
513 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
518 let content = s.into();
519 self.commands.push(Command::Text {
520 content,
521 style: Style::new(),
522 grow: 0,
523 align: Align::Start,
524 wrap: true,
525 margin: Margin::default(),
526 constraints: Constraints::default(),
527 });
528 self.last_text_idx = Some(self.commands.len() - 1);
529 self
530 }
531
532 pub fn bold(&mut self) -> &mut Self {
536 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
537 self
538 }
539
540 pub fn dim(&mut self) -> &mut Self {
545 let text_dim = self.theme.text_dim;
546 self.modify_last_style(|s| {
547 s.modifiers |= Modifiers::DIM;
548 if s.fg.is_none() {
549 s.fg = Some(text_dim);
550 }
551 });
552 self
553 }
554
555 pub fn italic(&mut self) -> &mut Self {
557 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
558 self
559 }
560
561 pub fn underline(&mut self) -> &mut Self {
563 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
564 self
565 }
566
567 pub fn reversed(&mut self) -> &mut Self {
569 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
570 self
571 }
572
573 pub fn strikethrough(&mut self) -> &mut Self {
575 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
576 self
577 }
578
579 pub fn fg(&mut self, color: Color) -> &mut Self {
581 self.modify_last_style(|s| s.fg = Some(color));
582 self
583 }
584
585 pub fn bg(&mut self, color: Color) -> &mut Self {
587 self.modify_last_style(|s| s.bg = Some(color));
588 self
589 }
590
591 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
596 self.commands.push(Command::Text {
597 content: s.into(),
598 style,
599 grow: 0,
600 align: Align::Start,
601 wrap: false,
602 margin: Margin::default(),
603 constraints: Constraints::default(),
604 });
605 self.last_text_idx = Some(self.commands.len() - 1);
606 self
607 }
608
609 pub fn wrap(&mut self) -> &mut Self {
611 if let Some(idx) = self.last_text_idx {
612 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
613 *wrap = true;
614 }
615 }
616 self
617 }
618
619 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
620 if let Some(idx) = self.last_text_idx {
621 if let Command::Text { style, .. } = &mut self.commands[idx] {
622 f(style);
623 }
624 }
625 }
626
627 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
645 self.push_container(Direction::Column, 0, f)
646 }
647
648 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
652 self.push_container(Direction::Column, gap, f)
653 }
654
655 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
672 self.push_container(Direction::Row, 0, f)
673 }
674
675 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
679 self.push_container(Direction::Row, gap, f)
680 }
681
682 pub fn container(&mut self) -> ContainerBuilder<'_> {
703 let border = self.theme.border;
704 ContainerBuilder {
705 ctx: self,
706 gap: 0,
707 align: Align::Start,
708 border: None,
709 border_style: Style::new().fg(border),
710 padding: Padding::default(),
711 margin: Margin::default(),
712 constraints: Constraints::default(),
713 title: None,
714 grow: 0,
715 scroll_offset: None,
716 }
717 }
718
719 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
738 let index = self.scroll_count;
739 self.scroll_count += 1;
740 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
741 state.set_bounds(ch, vh);
742 let max = ch.saturating_sub(vh) as usize;
743 state.offset = state.offset.min(max);
744 }
745
746 let next_id = self.interaction_count;
747 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
748 self.auto_scroll(&rect, state);
749 }
750
751 self.container().scroll_offset(state.offset as u32)
752 }
753
754 fn auto_scroll(&mut self, rect: &Rect, state: &mut ScrollState) {
755 let last_y = self.last_mouse_pos.map(|(_, y)| y);
756 let mut to_consume: Vec<usize> = Vec::new();
757
758 for (i, event) in self.events.iter().enumerate() {
759 if self.consumed[i] {
760 continue;
761 }
762 if let Event::Mouse(mouse) = event {
763 let in_bounds = mouse.x >= rect.x
764 && mouse.x < rect.right()
765 && mouse.y >= rect.y
766 && mouse.y < rect.bottom();
767 if !in_bounds {
768 continue;
769 }
770 match mouse.kind {
771 MouseKind::ScrollUp => {
772 state.scroll_up(1);
773 to_consume.push(i);
774 }
775 MouseKind::ScrollDown => {
776 state.scroll_down(1);
777 to_consume.push(i);
778 }
779 MouseKind::Drag(MouseButton::Left) => {
780 if let Some(prev_y) = last_y {
781 let delta = mouse.y as i32 - prev_y as i32;
782 if delta < 0 {
783 state.scroll_down((-delta) as usize);
784 } else if delta > 0 {
785 state.scroll_up(delta as usize);
786 }
787 }
788 to_consume.push(i);
789 }
790 _ => {}
791 }
792 }
793 }
794
795 for i in to_consume {
796 self.consumed[i] = true;
797 }
798 }
799
800 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
804 self.container().border(border)
805 }
806
807 fn push_container(
808 &mut self,
809 direction: Direction,
810 gap: u32,
811 f: impl FnOnce(&mut Context),
812 ) -> Response {
813 let interaction_id = self.interaction_count;
814 self.interaction_count += 1;
815 let border = self.theme.border;
816
817 self.commands.push(Command::BeginContainer {
818 direction,
819 gap,
820 align: Align::Start,
821 border: None,
822 border_style: Style::new().fg(border),
823 padding: Padding::default(),
824 margin: Margin::default(),
825 constraints: Constraints::default(),
826 title: None,
827 grow: 0,
828 });
829 f(self);
830 self.commands.push(Command::EndContainer);
831 self.last_text_idx = None;
832
833 self.response_for(interaction_id)
834 }
835
836 fn response_for(&self, interaction_id: usize) -> Response {
837 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
838 let clicked = self
839 .click_pos
840 .map(|(mx, my)| {
841 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
842 })
843 .unwrap_or(false);
844 let hovered = self
845 .mouse_pos
846 .map(|(mx, my)| {
847 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
848 })
849 .unwrap_or(false);
850 Response { clicked, hovered }
851 } else {
852 Response::default()
853 }
854 }
855
856 pub fn grow(&mut self, value: u16) -> &mut Self {
861 if let Some(idx) = self.last_text_idx {
862 if let Command::Text { grow, .. } = &mut self.commands[idx] {
863 *grow = value;
864 }
865 }
866 self
867 }
868
869 pub fn align(&mut self, align: Align) -> &mut Self {
871 if let Some(idx) = self.last_text_idx {
872 if let Command::Text {
873 align: text_align, ..
874 } = &mut self.commands[idx]
875 {
876 *text_align = align;
877 }
878 }
879 self
880 }
881
882 pub fn spacer(&mut self) -> &mut Self {
886 self.commands.push(Command::Spacer { grow: 1 });
887 self.last_text_idx = None;
888 self
889 }
890
891 pub fn text_input(&mut self, state: &mut TextInputState) -> &mut Self {
907 let focused = self.register_focusable();
908
909 if focused {
910 let mut consumed_indices = Vec::new();
911 for (i, event) in self.events.iter().enumerate() {
912 if let Event::Key(key) = event {
913 match key.code {
914 KeyCode::Char(ch) => {
915 let index = byte_index_for_char(&state.value, state.cursor);
916 state.value.insert(index, ch);
917 state.cursor += 1;
918 consumed_indices.push(i);
919 }
920 KeyCode::Backspace => {
921 if state.cursor > 0 {
922 let start = byte_index_for_char(&state.value, state.cursor - 1);
923 let end = byte_index_for_char(&state.value, state.cursor);
924 state.value.replace_range(start..end, "");
925 state.cursor -= 1;
926 }
927 consumed_indices.push(i);
928 }
929 KeyCode::Left => {
930 state.cursor = state.cursor.saturating_sub(1);
931 consumed_indices.push(i);
932 }
933 KeyCode::Right => {
934 state.cursor = (state.cursor + 1).min(state.value.chars().count());
935 consumed_indices.push(i);
936 }
937 KeyCode::Home => {
938 state.cursor = 0;
939 consumed_indices.push(i);
940 }
941 KeyCode::End => {
942 state.cursor = state.value.chars().count();
943 consumed_indices.push(i);
944 }
945 _ => {}
946 }
947 }
948 }
949
950 for index in consumed_indices {
951 self.consumed[index] = true;
952 }
953 }
954
955 if state.value.is_empty() {
956 self.styled(
957 state.placeholder.clone(),
958 Style::new().dim().fg(self.theme.text_dim),
959 )
960 } else {
961 let mut rendered = String::new();
962 for (idx, ch) in state.value.chars().enumerate() {
963 if focused && idx == state.cursor {
964 rendered.push('▎');
965 }
966 rendered.push(ch);
967 }
968 if focused && state.cursor >= state.value.chars().count() {
969 rendered.push('▎');
970 }
971 self.styled(rendered, Style::new().fg(self.theme.text))
972 }
973 }
974
975 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
981 self.styled(
982 state.frame(self.tick).to_string(),
983 Style::new().fg(self.theme.primary),
984 )
985 }
986
987 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
992 state.cleanup(self.tick);
993 if state.messages.is_empty() {
994 return self;
995 }
996
997 self.interaction_count += 1;
998 self.commands.push(Command::BeginContainer {
999 direction: Direction::Column,
1000 gap: 0,
1001 align: Align::Start,
1002 border: None,
1003 border_style: Style::new().fg(self.theme.border),
1004 padding: Padding::default(),
1005 margin: Margin::default(),
1006 constraints: Constraints::default(),
1007 title: None,
1008 grow: 0,
1009 });
1010 for message in state.messages.iter().rev() {
1011 let color = match message.level {
1012 ToastLevel::Info => self.theme.primary,
1013 ToastLevel::Success => self.theme.success,
1014 ToastLevel::Warning => self.theme.warning,
1015 ToastLevel::Error => self.theme.error,
1016 };
1017 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
1018 }
1019 self.commands.push(Command::EndContainer);
1020 self.last_text_idx = None;
1021
1022 self
1023 }
1024
1025 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> &mut Self {
1030 if state.lines.is_empty() {
1031 state.lines.push(String::new());
1032 }
1033 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
1034 state.cursor_col = state
1035 .cursor_col
1036 .min(state.lines[state.cursor_row].chars().count());
1037
1038 let focused = self.register_focusable();
1039
1040 if focused {
1041 let mut consumed_indices = Vec::new();
1042 for (i, event) in self.events.iter().enumerate() {
1043 if let Event::Key(key) = event {
1044 match key.code {
1045 KeyCode::Char(ch) => {
1046 let index = byte_index_for_char(
1047 &state.lines[state.cursor_row],
1048 state.cursor_col,
1049 );
1050 state.lines[state.cursor_row].insert(index, ch);
1051 state.cursor_col += 1;
1052 consumed_indices.push(i);
1053 }
1054 KeyCode::Enter => {
1055 let split_index = byte_index_for_char(
1056 &state.lines[state.cursor_row],
1057 state.cursor_col,
1058 );
1059 let remainder = state.lines[state.cursor_row].split_off(split_index);
1060 state.cursor_row += 1;
1061 state.lines.insert(state.cursor_row, remainder);
1062 state.cursor_col = 0;
1063 consumed_indices.push(i);
1064 }
1065 KeyCode::Backspace => {
1066 if state.cursor_col > 0 {
1067 let start = byte_index_for_char(
1068 &state.lines[state.cursor_row],
1069 state.cursor_col - 1,
1070 );
1071 let end = byte_index_for_char(
1072 &state.lines[state.cursor_row],
1073 state.cursor_col,
1074 );
1075 state.lines[state.cursor_row].replace_range(start..end, "");
1076 state.cursor_col -= 1;
1077 } else if state.cursor_row > 0 {
1078 let current = state.lines.remove(state.cursor_row);
1079 state.cursor_row -= 1;
1080 state.cursor_col = state.lines[state.cursor_row].chars().count();
1081 state.lines[state.cursor_row].push_str(¤t);
1082 }
1083 consumed_indices.push(i);
1084 }
1085 KeyCode::Left => {
1086 if state.cursor_col > 0 {
1087 state.cursor_col -= 1;
1088 } else if state.cursor_row > 0 {
1089 state.cursor_row -= 1;
1090 state.cursor_col = state.lines[state.cursor_row].chars().count();
1091 }
1092 consumed_indices.push(i);
1093 }
1094 KeyCode::Right => {
1095 let line_len = state.lines[state.cursor_row].chars().count();
1096 if state.cursor_col < line_len {
1097 state.cursor_col += 1;
1098 } else if state.cursor_row + 1 < state.lines.len() {
1099 state.cursor_row += 1;
1100 state.cursor_col = 0;
1101 }
1102 consumed_indices.push(i);
1103 }
1104 KeyCode::Up => {
1105 if state.cursor_row > 0 {
1106 state.cursor_row -= 1;
1107 state.cursor_col = state
1108 .cursor_col
1109 .min(state.lines[state.cursor_row].chars().count());
1110 }
1111 consumed_indices.push(i);
1112 }
1113 KeyCode::Down => {
1114 if state.cursor_row + 1 < state.lines.len() {
1115 state.cursor_row += 1;
1116 state.cursor_col = state
1117 .cursor_col
1118 .min(state.lines[state.cursor_row].chars().count());
1119 }
1120 consumed_indices.push(i);
1121 }
1122 KeyCode::Home => {
1123 state.cursor_col = 0;
1124 consumed_indices.push(i);
1125 }
1126 KeyCode::End => {
1127 state.cursor_col = state.lines[state.cursor_row].chars().count();
1128 consumed_indices.push(i);
1129 }
1130 _ => {}
1131 }
1132 }
1133 }
1134
1135 for index in consumed_indices {
1136 self.consumed[index] = true;
1137 }
1138 }
1139
1140 self.interaction_count += 1;
1141 self.commands.push(Command::BeginContainer {
1142 direction: Direction::Column,
1143 gap: 0,
1144 align: Align::Start,
1145 border: None,
1146 border_style: Style::new().fg(self.theme.border),
1147 padding: Padding::default(),
1148 margin: Margin::default(),
1149 constraints: Constraints::default(),
1150 title: None,
1151 grow: 0,
1152 });
1153 for row in 0..visible_rows as usize {
1154 let line = state.lines.get(row).cloned().unwrap_or_default();
1155 let mut rendered = line.clone();
1156 let mut style = if line.is_empty() {
1157 Style::new().fg(self.theme.text_dim)
1158 } else {
1159 Style::new().fg(self.theme.text)
1160 };
1161
1162 if focused && row == state.cursor_row {
1163 rendered.clear();
1164 for (idx, ch) in line.chars().enumerate() {
1165 if idx == state.cursor_col {
1166 rendered.push('▎');
1167 }
1168 rendered.push(ch);
1169 }
1170 if state.cursor_col >= line.chars().count() {
1171 rendered.push('▎');
1172 }
1173 style = Style::new().fg(self.theme.text);
1174 }
1175
1176 self.styled(rendered, style);
1177 }
1178 self.commands.push(Command::EndContainer);
1179 self.last_text_idx = None;
1180
1181 self
1182 }
1183
1184 pub fn progress(&mut self, ratio: f64) -> &mut Self {
1189 self.progress_bar(ratio, 20)
1190 }
1191
1192 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
1197 let clamped = ratio.clamp(0.0, 1.0);
1198 let filled = (clamped * width as f64).round() as u32;
1199 let empty = width.saturating_sub(filled);
1200 let mut bar = String::new();
1201 for _ in 0..filled {
1202 bar.push('█');
1203 }
1204 for _ in 0..empty {
1205 bar.push('░');
1206 }
1207 self.text(bar)
1208 }
1209
1210 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
1215 if state.items.is_empty() {
1216 state.selected = 0;
1217 return self;
1218 }
1219
1220 let focused = self.register_focusable();
1221
1222 if focused {
1223 let mut consumed_indices = Vec::new();
1224 for (i, event) in self.events.iter().enumerate() {
1225 if let Event::Key(key) = event {
1226 match key.code {
1227 KeyCode::Up | KeyCode::Char('k') => {
1228 state.selected = state.selected.saturating_sub(1);
1229 consumed_indices.push(i);
1230 }
1231 KeyCode::Down | KeyCode::Char('j') => {
1232 state.selected = (state.selected + 1).min(state.items.len() - 1);
1233 consumed_indices.push(i);
1234 }
1235 _ => {}
1236 }
1237 }
1238 }
1239
1240 for index in consumed_indices {
1241 self.consumed[index] = true;
1242 }
1243 }
1244
1245 for (idx, item) in state.items.iter().enumerate() {
1246 if idx == state.selected {
1247 if focused {
1248 self.styled(
1249 format!("▸ {item}"),
1250 Style::new().bold().fg(self.theme.primary),
1251 );
1252 } else {
1253 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
1254 }
1255 } else {
1256 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
1257 }
1258 }
1259
1260 self
1261 }
1262
1263 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
1268 if state.is_dirty() {
1269 state.recompute_widths();
1270 }
1271
1272 let focused = self.register_focusable();
1273
1274 if focused && !state.rows.is_empty() {
1275 let mut consumed_indices = Vec::new();
1276 for (i, event) in self.events.iter().enumerate() {
1277 if let Event::Key(key) = event {
1278 match key.code {
1279 KeyCode::Up | KeyCode::Char('k') => {
1280 state.selected = state.selected.saturating_sub(1);
1281 consumed_indices.push(i);
1282 }
1283 KeyCode::Down | KeyCode::Char('j') => {
1284 state.selected = (state.selected + 1).min(state.rows.len() - 1);
1285 consumed_indices.push(i);
1286 }
1287 _ => {}
1288 }
1289 }
1290 }
1291 for index in consumed_indices {
1292 self.consumed[index] = true;
1293 }
1294 }
1295
1296 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
1297
1298 let header_line = format_table_row(&state.headers, state.column_widths(), " │ ");
1299 self.styled(header_line, Style::new().bold().fg(self.theme.text));
1300
1301 let separator = state
1302 .column_widths()
1303 .iter()
1304 .map(|w| "─".repeat(*w as usize))
1305 .collect::<Vec<_>>()
1306 .join("─┼─");
1307 self.text(separator);
1308
1309 for (idx, row) in state.rows.iter().enumerate() {
1310 let line = format_table_row(row, state.column_widths(), " │ ");
1311 if idx == state.selected {
1312 let mut style = Style::new()
1313 .bg(self.theme.selected_bg)
1314 .fg(self.theme.selected_fg);
1315 if focused {
1316 style = style.bold();
1317 }
1318 self.styled(line, style);
1319 } else {
1320 self.styled(line, Style::new().fg(self.theme.text));
1321 }
1322 }
1323
1324 self
1325 }
1326
1327 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
1332 if state.labels.is_empty() {
1333 state.selected = 0;
1334 return self;
1335 }
1336
1337 state.selected = state.selected.min(state.labels.len() - 1);
1338 let focused = self.register_focusable();
1339
1340 if focused {
1341 let mut consumed_indices = Vec::new();
1342 for (i, event) in self.events.iter().enumerate() {
1343 if let Event::Key(key) = event {
1344 match key.code {
1345 KeyCode::Left => {
1346 state.selected = if state.selected == 0 {
1347 state.labels.len() - 1
1348 } else {
1349 state.selected - 1
1350 };
1351 consumed_indices.push(i);
1352 }
1353 KeyCode::Right => {
1354 state.selected = (state.selected + 1) % state.labels.len();
1355 consumed_indices.push(i);
1356 }
1357 _ => {}
1358 }
1359 }
1360 }
1361
1362 for index in consumed_indices {
1363 self.consumed[index] = true;
1364 }
1365 }
1366
1367 self.interaction_count += 1;
1368 self.commands.push(Command::BeginContainer {
1369 direction: Direction::Row,
1370 gap: 1,
1371 align: Align::Start,
1372 border: None,
1373 border_style: Style::new().fg(self.theme.border),
1374 padding: Padding::default(),
1375 margin: Margin::default(),
1376 constraints: Constraints::default(),
1377 title: None,
1378 grow: 0,
1379 });
1380 for (idx, label) in state.labels.iter().enumerate() {
1381 let style = if idx == state.selected {
1382 let s = Style::new().fg(self.theme.primary).bold();
1383 if focused {
1384 s.underline()
1385 } else {
1386 s
1387 }
1388 } else {
1389 Style::new().fg(self.theme.text_dim)
1390 };
1391 self.styled(format!("[ {label} ]"), style);
1392 }
1393 self.commands.push(Command::EndContainer);
1394 self.last_text_idx = None;
1395
1396 self
1397 }
1398
1399 pub fn button(&mut self, label: impl Into<String>) -> bool {
1404 let focused = self.register_focusable();
1405 let interaction_id = self.interaction_count;
1406 self.interaction_count += 1;
1407 let response = self.response_for(interaction_id);
1408
1409 let mut activated = response.clicked;
1410 if focused {
1411 let mut consumed_indices = Vec::new();
1412 for (i, event) in self.events.iter().enumerate() {
1413 if let Event::Key(key) = event {
1414 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1415 activated = true;
1416 consumed_indices.push(i);
1417 }
1418 }
1419 }
1420
1421 for index in consumed_indices {
1422 self.consumed[index] = true;
1423 }
1424 }
1425
1426 let style = if focused {
1427 Style::new().fg(self.theme.primary).bold()
1428 } else if response.hovered {
1429 Style::new().fg(self.theme.accent)
1430 } else {
1431 Style::new().fg(self.theme.text)
1432 };
1433
1434 self.commands.push(Command::BeginContainer {
1435 direction: Direction::Row,
1436 gap: 0,
1437 align: Align::Start,
1438 border: None,
1439 border_style: Style::new().fg(self.theme.border),
1440 padding: Padding::default(),
1441 margin: Margin::default(),
1442 constraints: Constraints::default(),
1443 title: None,
1444 grow: 0,
1445 });
1446 self.styled(format!("[ {} ]", label.into()), style);
1447 self.commands.push(Command::EndContainer);
1448 self.last_text_idx = None;
1449
1450 activated
1451 }
1452
1453 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
1458 let focused = self.register_focusable();
1459 let interaction_id = self.interaction_count;
1460 self.interaction_count += 1;
1461 let response = self.response_for(interaction_id);
1462 let mut should_toggle = response.clicked;
1463
1464 if focused {
1465 let mut consumed_indices = Vec::new();
1466 for (i, event) in self.events.iter().enumerate() {
1467 if let Event::Key(key) = event {
1468 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1469 should_toggle = true;
1470 consumed_indices.push(i);
1471 }
1472 }
1473 }
1474
1475 for index in consumed_indices {
1476 self.consumed[index] = true;
1477 }
1478 }
1479
1480 if should_toggle {
1481 *checked = !*checked;
1482 }
1483
1484 self.commands.push(Command::BeginContainer {
1485 direction: Direction::Row,
1486 gap: 1,
1487 align: Align::Start,
1488 border: None,
1489 border_style: Style::new().fg(self.theme.border),
1490 padding: Padding::default(),
1491 margin: Margin::default(),
1492 constraints: Constraints::default(),
1493 title: None,
1494 grow: 0,
1495 });
1496 let marker_style = if *checked {
1497 Style::new().fg(self.theme.success)
1498 } else {
1499 Style::new().fg(self.theme.text_dim)
1500 };
1501 let marker = if *checked { "[x]" } else { "[ ]" };
1502 let label_text = label.into();
1503 if focused {
1504 self.styled(format!("▸ {marker}"), marker_style.bold());
1505 self.styled(label_text, Style::new().fg(self.theme.text).bold());
1506 } else {
1507 self.styled(marker, marker_style);
1508 self.styled(label_text, Style::new().fg(self.theme.text));
1509 }
1510 self.commands.push(Command::EndContainer);
1511 self.last_text_idx = None;
1512
1513 self
1514 }
1515
1516 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
1522 let focused = self.register_focusable();
1523 let interaction_id = self.interaction_count;
1524 self.interaction_count += 1;
1525 let response = self.response_for(interaction_id);
1526 let mut should_toggle = response.clicked;
1527
1528 if focused {
1529 let mut consumed_indices = Vec::new();
1530 for (i, event) in self.events.iter().enumerate() {
1531 if let Event::Key(key) = event {
1532 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1533 should_toggle = true;
1534 consumed_indices.push(i);
1535 }
1536 }
1537 }
1538
1539 for index in consumed_indices {
1540 self.consumed[index] = true;
1541 }
1542 }
1543
1544 if should_toggle {
1545 *on = !*on;
1546 }
1547
1548 self.commands.push(Command::BeginContainer {
1549 direction: Direction::Row,
1550 gap: 2,
1551 align: Align::Start,
1552 border: None,
1553 border_style: Style::new().fg(self.theme.border),
1554 padding: Padding::default(),
1555 margin: Margin::default(),
1556 constraints: Constraints::default(),
1557 title: None,
1558 grow: 0,
1559 });
1560 let label_text = label.into();
1561 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1562 let switch_style = if *on {
1563 Style::new().fg(self.theme.success)
1564 } else {
1565 Style::new().fg(self.theme.text_dim)
1566 };
1567 if focused {
1568 self.styled(
1569 format!("▸ {label_text}"),
1570 Style::new().fg(self.theme.text).bold(),
1571 );
1572 self.styled(switch, switch_style.bold());
1573 } else {
1574 self.styled(label_text, Style::new().fg(self.theme.text));
1575 self.styled(switch, switch_style);
1576 }
1577 self.commands.push(Command::EndContainer);
1578 self.last_text_idx = None;
1579
1580 self
1581 }
1582
1583 pub fn separator(&mut self) -> &mut Self {
1588 self.commands.push(Command::Text {
1589 content: "─".repeat(200),
1590 style: Style::new().fg(self.theme.border).dim(),
1591 grow: 0,
1592 align: Align::Start,
1593 wrap: false,
1594 margin: Margin::default(),
1595 constraints: Constraints::default(),
1596 });
1597 self.last_text_idx = Some(self.commands.len() - 1);
1598 self
1599 }
1600
1601 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
1607 if bindings.is_empty() {
1608 return self;
1609 }
1610
1611 self.interaction_count += 1;
1612 self.commands.push(Command::BeginContainer {
1613 direction: Direction::Row,
1614 gap: 2,
1615 align: Align::Start,
1616 border: None,
1617 border_style: Style::new().fg(self.theme.border),
1618 padding: Padding::default(),
1619 margin: Margin::default(),
1620 constraints: Constraints::default(),
1621 title: None,
1622 grow: 0,
1623 });
1624 for (idx, (key, action)) in bindings.iter().enumerate() {
1625 if idx > 0 {
1626 self.styled("·", Style::new().fg(self.theme.text_dim));
1627 }
1628 self.styled(*key, Style::new().bold().fg(self.theme.primary));
1629 self.styled(*action, Style::new().fg(self.theme.text_dim));
1630 }
1631 self.commands.push(Command::EndContainer);
1632 self.last_text_idx = None;
1633
1634 self
1635 }
1636
1637 pub fn key(&self, c: char) -> bool {
1643 self.events.iter().enumerate().any(|(i, e)| {
1644 !self.consumed[i] && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c))
1645 })
1646 }
1647
1648 pub fn key_code(&self, code: KeyCode) -> bool {
1652 self.events
1653 .iter()
1654 .enumerate()
1655 .any(|(i, e)| !self.consumed[i] && matches!(e, Event::Key(k) if k.code == code))
1656 }
1657
1658 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
1662 self.events.iter().enumerate().any(|(i, e)| {
1663 !self.consumed[i]
1664 && matches!(e, Event::Key(k) if k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
1665 })
1666 }
1667
1668 pub fn mouse_down(&self) -> Option<(u32, u32)> {
1672 self.events.iter().enumerate().find_map(|(i, event)| {
1673 if self.consumed[i] {
1674 return None;
1675 }
1676 if let Event::Mouse(mouse) = event {
1677 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1678 return Some((mouse.x, mouse.y));
1679 }
1680 }
1681 None
1682 })
1683 }
1684
1685 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
1690 self.mouse_pos
1691 }
1692
1693 pub fn scroll_up(&self) -> bool {
1695 self.events.iter().enumerate().any(|(i, event)| {
1696 !self.consumed[i]
1697 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
1698 })
1699 }
1700
1701 pub fn scroll_down(&self) -> bool {
1703 self.events.iter().enumerate().any(|(i, event)| {
1704 !self.consumed[i]
1705 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
1706 })
1707 }
1708
1709 pub fn quit(&mut self) {
1711 self.should_quit = true;
1712 }
1713
1714 pub fn theme(&self) -> &Theme {
1716 &self.theme
1717 }
1718
1719 pub fn set_theme(&mut self, theme: Theme) {
1723 self.theme = theme;
1724 }
1725
1726 pub fn width(&self) -> u32 {
1730 self.area_width
1731 }
1732
1733 pub fn height(&self) -> u32 {
1735 self.area_height
1736 }
1737
1738 pub fn tick(&self) -> u64 {
1743 self.tick
1744 }
1745
1746 pub fn debug_enabled(&self) -> bool {
1750 self.debug
1751 }
1752}
1753
1754fn byte_index_for_char(value: &str, char_index: usize) -> usize {
1755 if char_index == 0 {
1756 return 0;
1757 }
1758 value
1759 .char_indices()
1760 .nth(char_index)
1761 .map_or(value.len(), |(idx, _)| idx)
1762}
1763
1764fn format_table_row(cells: &[String], widths: &[u32], separator: &str) -> String {
1765 let mut parts: Vec<String> = Vec::new();
1766 for (i, width) in widths.iter().enumerate() {
1767 let cell = cells.get(i).map(String::as_str).unwrap_or("");
1768 let cell_width = UnicodeWidthStr::width(cell) as u32;
1769 let padding = (*width).saturating_sub(cell_width) as usize;
1770 parts.push(format!("{cell}{}", " ".repeat(padding)));
1771 }
1772 parts.join(separator)
1773}