1use super::*;
2
3impl Context {
4 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
21 slt_assert(cols > 0, "grid() requires at least 1 column");
22 let interaction_id = self.interaction_count;
23 self.interaction_count += 1;
24 let border = self.theme.border;
25
26 self.commands.push(Command::BeginContainer {
27 direction: Direction::Column,
28 gap: 0,
29 align: Align::Start,
30 justify: Justify::Start,
31 border: None,
32 border_sides: BorderSides::all(),
33 border_style: Style::new().fg(border),
34 bg_color: None,
35 padding: Padding::default(),
36 margin: Margin::default(),
37 constraints: Constraints::default(),
38 title: None,
39 grow: 0,
40 group_name: None,
41 });
42
43 let children_start = self.commands.len();
44 f(self);
45 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
46
47 let mut elements: Vec<Vec<Command>> = Vec::new();
48 let mut iter = child_commands.into_iter().peekable();
49 while let Some(cmd) = iter.next() {
50 match cmd {
51 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
52 let mut depth = 1_u32;
53 let mut element = vec![cmd];
54 for next in iter.by_ref() {
55 match next {
56 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
57 depth += 1;
58 }
59 Command::EndContainer => {
60 depth = depth.saturating_sub(1);
61 }
62 _ => {}
63 }
64 let at_end = matches!(next, Command::EndContainer) && depth == 0;
65 element.push(next);
66 if at_end {
67 break;
68 }
69 }
70 elements.push(element);
71 }
72 Command::EndContainer => {}
73 _ => elements.push(vec![cmd]),
74 }
75 }
76
77 let cols = cols.max(1) as usize;
78 for row in elements.chunks(cols) {
79 self.interaction_count += 1;
80 self.commands.push(Command::BeginContainer {
81 direction: Direction::Row,
82 gap: 0,
83 align: Align::Start,
84 justify: Justify::Start,
85 border: None,
86 border_sides: BorderSides::all(),
87 border_style: Style::new().fg(border),
88 bg_color: None,
89 padding: Padding::default(),
90 margin: Margin::default(),
91 constraints: Constraints::default(),
92 title: None,
93 grow: 0,
94 group_name: None,
95 });
96
97 for element in row {
98 self.interaction_count += 1;
99 self.commands.push(Command::BeginContainer {
100 direction: Direction::Column,
101 gap: 0,
102 align: Align::Start,
103 justify: Justify::Start,
104 border: None,
105 border_sides: BorderSides::all(),
106 border_style: Style::new().fg(border),
107 bg_color: None,
108 padding: Padding::default(),
109 margin: Margin::default(),
110 constraints: Constraints::default(),
111 title: None,
112 grow: 1,
113 group_name: None,
114 });
115 self.commands.extend(element.iter().cloned());
116 self.commands.push(Command::EndContainer);
117 }
118
119 self.commands.push(Command::EndContainer);
120 }
121
122 self.commands.push(Command::EndContainer);
123 self.last_text_idx = None;
124
125 self.response_for(interaction_id)
126 }
127
128 pub fn list(&mut self, state: &mut ListState) -> &mut Self {
133 let visible = state.visible_indices().to_vec();
134 if visible.is_empty() && state.items.is_empty() {
135 state.selected = 0;
136 return self;
137 }
138
139 if !visible.is_empty() {
140 state.selected = state.selected.min(visible.len().saturating_sub(1));
141 }
142
143 let focused = self.register_focusable();
144 let interaction_id = self.interaction_count;
145 self.interaction_count += 1;
146
147 if focused {
148 let mut consumed_indices = Vec::new();
149 for (i, event) in self.events.iter().enumerate() {
150 if let Event::Key(key) = event {
151 if key.kind != KeyEventKind::Press {
152 continue;
153 }
154 match key.code {
155 KeyCode::Up | KeyCode::Char('k') => {
156 state.selected = state.selected.saturating_sub(1);
157 consumed_indices.push(i);
158 }
159 KeyCode::Down | KeyCode::Char('j') => {
160 state.selected =
161 (state.selected + 1).min(visible.len().saturating_sub(1));
162 consumed_indices.push(i);
163 }
164 _ => {}
165 }
166 }
167 }
168
169 for index in consumed_indices {
170 self.consumed[index] = true;
171 }
172 }
173
174 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
175 for (i, event) in self.events.iter().enumerate() {
176 if self.consumed[i] {
177 continue;
178 }
179 if let Event::Mouse(mouse) = event {
180 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
181 continue;
182 }
183 let in_bounds = mouse.x >= rect.x
184 && mouse.x < rect.right()
185 && mouse.y >= rect.y
186 && mouse.y < rect.bottom();
187 if !in_bounds {
188 continue;
189 }
190 let clicked_idx = (mouse.y - rect.y) as usize;
191 if clicked_idx < visible.len() {
192 state.selected = clicked_idx;
193 self.consumed[i] = true;
194 }
195 }
196 }
197 }
198
199 self.commands.push(Command::BeginContainer {
200 direction: Direction::Column,
201 gap: 0,
202 align: Align::Start,
203 justify: Justify::Start,
204 border: None,
205 border_sides: BorderSides::all(),
206 border_style: Style::new().fg(self.theme.border),
207 bg_color: None,
208 padding: Padding::default(),
209 margin: Margin::default(),
210 constraints: Constraints::default(),
211 title: None,
212 grow: 0,
213 group_name: None,
214 });
215
216 for (view_idx, &item_idx) in visible.iter().enumerate() {
217 let item = &state.items[item_idx];
218 if view_idx == state.selected {
219 if focused {
220 self.styled(
221 format!("▸ {item}"),
222 Style::new().bold().fg(self.theme.primary),
223 );
224 } else {
225 self.styled(format!("▸ {item}"), Style::new().fg(self.theme.primary));
226 }
227 } else {
228 self.styled(format!(" {item}"), Style::new().fg(self.theme.text));
229 }
230 }
231
232 self.commands.push(Command::EndContainer);
233 self.last_text_idx = None;
234
235 self
236 }
237
238 pub fn table(&mut self, state: &mut TableState) -> &mut Self {
243 if state.is_dirty() {
244 state.recompute_widths();
245 }
246
247 let focused = self.register_focusable();
248 let interaction_id = self.interaction_count;
249 self.interaction_count += 1;
250
251 if focused && !state.visible_indices().is_empty() {
252 let mut consumed_indices = Vec::new();
253 for (i, event) in self.events.iter().enumerate() {
254 if let Event::Key(key) = event {
255 if key.kind != KeyEventKind::Press {
256 continue;
257 }
258 match key.code {
259 KeyCode::Up | KeyCode::Char('k') => {
260 let visible_len = if state.page_size > 0 {
261 let start = state
262 .page
263 .saturating_mul(state.page_size)
264 .min(state.visible_indices().len());
265 let end =
266 (start + state.page_size).min(state.visible_indices().len());
267 end.saturating_sub(start)
268 } else {
269 state.visible_indices().len()
270 };
271 state.selected = state.selected.min(visible_len.saturating_sub(1));
272 state.selected = state.selected.saturating_sub(1);
273 consumed_indices.push(i);
274 }
275 KeyCode::Down | KeyCode::Char('j') => {
276 let visible_len = if state.page_size > 0 {
277 let start = state
278 .page
279 .saturating_mul(state.page_size)
280 .min(state.visible_indices().len());
281 let end =
282 (start + state.page_size).min(state.visible_indices().len());
283 end.saturating_sub(start)
284 } else {
285 state.visible_indices().len()
286 };
287 state.selected =
288 (state.selected + 1).min(visible_len.saturating_sub(1));
289 consumed_indices.push(i);
290 }
291 KeyCode::PageUp => {
292 let old_page = state.page;
293 state.prev_page();
294 if state.page != old_page {
295 state.selected = 0;
296 }
297 consumed_indices.push(i);
298 }
299 KeyCode::PageDown => {
300 let old_page = state.page;
301 state.next_page();
302 if state.page != old_page {
303 state.selected = 0;
304 }
305 consumed_indices.push(i);
306 }
307 _ => {}
308 }
309 }
310 }
311 for index in consumed_indices {
312 self.consumed[index] = true;
313 }
314 }
315
316 if !state.visible_indices().is_empty() || !state.headers.is_empty() {
317 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
318 for (i, event) in self.events.iter().enumerate() {
319 if self.consumed[i] {
320 continue;
321 }
322 if let Event::Mouse(mouse) = event {
323 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
324 continue;
325 }
326 let in_bounds = mouse.x >= rect.x
327 && mouse.x < rect.right()
328 && mouse.y >= rect.y
329 && mouse.y < rect.bottom();
330 if !in_bounds {
331 continue;
332 }
333
334 if mouse.y == rect.y {
335 let rel_x = mouse.x.saturating_sub(rect.x);
336 let mut x_offset = 0u32;
337 for (col_idx, width) in state.column_widths().iter().enumerate() {
338 if rel_x >= x_offset && rel_x < x_offset + *width {
339 state.toggle_sort(col_idx);
340 state.selected = 0;
341 self.consumed[i] = true;
342 break;
343 }
344 x_offset += *width;
345 if col_idx + 1 < state.column_widths().len() {
346 x_offset += 3;
347 }
348 }
349 continue;
350 }
351
352 if mouse.y < rect.y + 2 {
353 continue;
354 }
355
356 let visible_len = if state.page_size > 0 {
357 let start = state
358 .page
359 .saturating_mul(state.page_size)
360 .min(state.visible_indices().len());
361 let end = (start + state.page_size).min(state.visible_indices().len());
362 end.saturating_sub(start)
363 } else {
364 state.visible_indices().len()
365 };
366 let clicked_idx = (mouse.y - rect.y - 2) as usize;
367 if clicked_idx < visible_len {
368 state.selected = clicked_idx;
369 self.consumed[i] = true;
370 }
371 }
372 }
373 }
374 }
375
376 if state.is_dirty() {
377 state.recompute_widths();
378 }
379
380 let total_visible = state.visible_indices().len();
381 let page_start = if state.page_size > 0 {
382 state
383 .page
384 .saturating_mul(state.page_size)
385 .min(total_visible)
386 } else {
387 0
388 };
389 let page_end = if state.page_size > 0 {
390 (page_start + state.page_size).min(total_visible)
391 } else {
392 total_visible
393 };
394 let visible_len = page_end.saturating_sub(page_start);
395 state.selected = state.selected.min(visible_len.saturating_sub(1));
396
397 self.commands.push(Command::BeginContainer {
398 direction: Direction::Column,
399 gap: 0,
400 align: Align::Start,
401 justify: Justify::Start,
402 border: None,
403 border_sides: BorderSides::all(),
404 border_style: Style::new().fg(self.theme.border),
405 bg_color: None,
406 padding: Padding::default(),
407 margin: Margin::default(),
408 constraints: Constraints::default(),
409 title: None,
410 grow: 0,
411 group_name: None,
412 });
413
414 let header_cells = state
415 .headers
416 .iter()
417 .enumerate()
418 .map(|(i, header)| {
419 if state.sort_column == Some(i) {
420 if state.sort_ascending {
421 format!("{header} ▲")
422 } else {
423 format!("{header} ▼")
424 }
425 } else {
426 header.clone()
427 }
428 })
429 .collect::<Vec<_>>();
430 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
431 self.styled(header_line, Style::new().bold().fg(self.theme.text));
432
433 let separator = state
434 .column_widths()
435 .iter()
436 .map(|w| "─".repeat(*w as usize))
437 .collect::<Vec<_>>()
438 .join("─┼─");
439 self.text(separator);
440
441 for idx in 0..visible_len {
442 let data_idx = state.visible_indices()[page_start + idx];
443 let Some(row) = state.rows.get(data_idx) else {
444 continue;
445 };
446 let line = format_table_row(row, state.column_widths(), " │ ");
447 if idx == state.selected {
448 let mut style = Style::new()
449 .bg(self.theme.selected_bg)
450 .fg(self.theme.selected_fg);
451 if focused {
452 style = style.bold();
453 }
454 self.styled(line, style);
455 } else {
456 self.styled(line, Style::new().fg(self.theme.text));
457 }
458 }
459
460 if state.page_size > 0 && state.total_pages() > 1 {
461 self.styled(
462 format!("Page {}/{}", state.page + 1, state.total_pages()),
463 Style::new().dim().fg(self.theme.text_dim),
464 );
465 }
466
467 self.commands.push(Command::EndContainer);
468 self.last_text_idx = None;
469
470 self
471 }
472
473 pub fn tabs(&mut self, state: &mut TabsState) -> &mut Self {
478 if state.labels.is_empty() {
479 state.selected = 0;
480 return self;
481 }
482
483 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
484 let focused = self.register_focusable();
485 let interaction_id = self.interaction_count;
486
487 if focused {
488 let mut consumed_indices = Vec::new();
489 for (i, event) in self.events.iter().enumerate() {
490 if let Event::Key(key) = event {
491 if key.kind != KeyEventKind::Press {
492 continue;
493 }
494 match key.code {
495 KeyCode::Left => {
496 state.selected = if state.selected == 0 {
497 state.labels.len().saturating_sub(1)
498 } else {
499 state.selected - 1
500 };
501 consumed_indices.push(i);
502 }
503 KeyCode::Right => {
504 if !state.labels.is_empty() {
505 state.selected = (state.selected + 1) % state.labels.len();
506 }
507 consumed_indices.push(i);
508 }
509 _ => {}
510 }
511 }
512 }
513
514 for index in consumed_indices {
515 self.consumed[index] = true;
516 }
517 }
518
519 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
520 for (i, event) in self.events.iter().enumerate() {
521 if self.consumed[i] {
522 continue;
523 }
524 if let Event::Mouse(mouse) = event {
525 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
526 continue;
527 }
528 let in_bounds = mouse.x >= rect.x
529 && mouse.x < rect.right()
530 && mouse.y >= rect.y
531 && mouse.y < rect.bottom();
532 if !in_bounds {
533 continue;
534 }
535
536 let mut x_offset = 0u32;
537 let rel_x = mouse.x - rect.x;
538 for (idx, label) in state.labels.iter().enumerate() {
539 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
540 if rel_x >= x_offset && rel_x < x_offset + tab_width {
541 state.selected = idx;
542 self.consumed[i] = true;
543 break;
544 }
545 x_offset += tab_width + 1;
546 }
547 }
548 }
549 }
550
551 self.interaction_count += 1;
552 self.commands.push(Command::BeginContainer {
553 direction: Direction::Row,
554 gap: 1,
555 align: Align::Start,
556 justify: Justify::Start,
557 border: None,
558 border_sides: BorderSides::all(),
559 border_style: Style::new().fg(self.theme.border),
560 bg_color: None,
561 padding: Padding::default(),
562 margin: Margin::default(),
563 constraints: Constraints::default(),
564 title: None,
565 grow: 0,
566 group_name: None,
567 });
568 for (idx, label) in state.labels.iter().enumerate() {
569 let style = if idx == state.selected {
570 let s = Style::new().fg(self.theme.primary).bold();
571 if focused {
572 s.underline()
573 } else {
574 s
575 }
576 } else {
577 Style::new().fg(self.theme.text_dim)
578 };
579 self.styled(format!("[ {label} ]"), style);
580 }
581 self.commands.push(Command::EndContainer);
582 self.last_text_idx = None;
583
584 self
585 }
586
587 pub fn button(&mut self, label: impl Into<String>) -> bool {
592 let focused = self.register_focusable();
593 let interaction_id = self.interaction_count;
594 self.interaction_count += 1;
595 let response = self.response_for(interaction_id);
596
597 let mut activated = response.clicked;
598 if focused {
599 let mut consumed_indices = Vec::new();
600 for (i, event) in self.events.iter().enumerate() {
601 if let Event::Key(key) = event {
602 if key.kind != KeyEventKind::Press {
603 continue;
604 }
605 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
606 activated = true;
607 consumed_indices.push(i);
608 }
609 }
610 }
611
612 for index in consumed_indices {
613 self.consumed[index] = true;
614 }
615 }
616
617 let hovered = response.hovered;
618 let style = if focused {
619 Style::new().fg(self.theme.primary).bold()
620 } else if hovered {
621 Style::new().fg(self.theme.accent)
622 } else {
623 Style::new().fg(self.theme.text)
624 };
625 let hover_bg = if hovered || focused {
626 Some(self.theme.surface_hover)
627 } else {
628 None
629 };
630
631 self.commands.push(Command::BeginContainer {
632 direction: Direction::Row,
633 gap: 0,
634 align: Align::Start,
635 justify: Justify::Start,
636 border: None,
637 border_sides: BorderSides::all(),
638 border_style: Style::new().fg(self.theme.border),
639 bg_color: hover_bg,
640 padding: Padding::default(),
641 margin: Margin::default(),
642 constraints: Constraints::default(),
643 title: None,
644 grow: 0,
645 group_name: None,
646 });
647 self.styled(format!("[ {} ]", label.into()), style);
648 self.commands.push(Command::EndContainer);
649 self.last_text_idx = None;
650
651 activated
652 }
653
654 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> bool {
659 let focused = self.register_focusable();
660 let interaction_id = self.interaction_count;
661 self.interaction_count += 1;
662 let response = self.response_for(interaction_id);
663
664 let mut activated = response.clicked;
665 if focused {
666 let mut consumed_indices = Vec::new();
667 for (i, event) in self.events.iter().enumerate() {
668 if let Event::Key(key) = event {
669 if key.kind != KeyEventKind::Press {
670 continue;
671 }
672 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
673 activated = true;
674 consumed_indices.push(i);
675 }
676 }
677 }
678 for index in consumed_indices {
679 self.consumed[index] = true;
680 }
681 }
682
683 let label = label.into();
684 let hover_bg = if response.hovered || focused {
685 Some(self.theme.surface_hover)
686 } else {
687 None
688 };
689 let (text, style, bg_color, border) = match variant {
690 ButtonVariant::Default => {
691 let style = if focused {
692 Style::new().fg(self.theme.primary).bold()
693 } else if response.hovered {
694 Style::new().fg(self.theme.accent)
695 } else {
696 Style::new().fg(self.theme.text)
697 };
698 (format!("[ {label} ]"), style, hover_bg, None)
699 }
700 ButtonVariant::Primary => {
701 let style = if focused {
702 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
703 } else if response.hovered {
704 Style::new().fg(self.theme.bg).bg(self.theme.accent)
705 } else {
706 Style::new().fg(self.theme.bg).bg(self.theme.primary)
707 };
708 (format!(" {label} "), style, hover_bg, None)
709 }
710 ButtonVariant::Danger => {
711 let style = if focused {
712 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
713 } else if response.hovered {
714 Style::new().fg(self.theme.bg).bg(self.theme.warning)
715 } else {
716 Style::new().fg(self.theme.bg).bg(self.theme.error)
717 };
718 (format!(" {label} "), style, hover_bg, None)
719 }
720 ButtonVariant::Outline => {
721 let border_color = if focused {
722 self.theme.primary
723 } else if response.hovered {
724 self.theme.accent
725 } else {
726 self.theme.border
727 };
728 let style = if focused {
729 Style::new().fg(self.theme.primary).bold()
730 } else if response.hovered {
731 Style::new().fg(self.theme.accent)
732 } else {
733 Style::new().fg(self.theme.text)
734 };
735 (
736 format!(" {label} "),
737 style,
738 hover_bg,
739 Some((Border::Rounded, Style::new().fg(border_color))),
740 )
741 }
742 };
743
744 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
745 self.commands.push(Command::BeginContainer {
746 direction: Direction::Row,
747 gap: 0,
748 align: Align::Center,
749 justify: Justify::Center,
750 border: if border.is_some() {
751 Some(btn_border)
752 } else {
753 None
754 },
755 border_sides: BorderSides::all(),
756 border_style: btn_border_style,
757 bg_color,
758 padding: Padding::default(),
759 margin: Margin::default(),
760 constraints: Constraints::default(),
761 title: None,
762 grow: 0,
763 group_name: None,
764 });
765 self.styled(text, style);
766 self.commands.push(Command::EndContainer);
767 self.last_text_idx = None;
768
769 activated
770 }
771
772 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> &mut Self {
777 let focused = self.register_focusable();
778 let interaction_id = self.interaction_count;
779 self.interaction_count += 1;
780 let response = self.response_for(interaction_id);
781 let mut should_toggle = response.clicked;
782
783 if focused {
784 let mut consumed_indices = Vec::new();
785 for (i, event) in self.events.iter().enumerate() {
786 if let Event::Key(key) = event {
787 if key.kind != KeyEventKind::Press {
788 continue;
789 }
790 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
791 should_toggle = true;
792 consumed_indices.push(i);
793 }
794 }
795 }
796
797 for index in consumed_indices {
798 self.consumed[index] = true;
799 }
800 }
801
802 if should_toggle {
803 *checked = !*checked;
804 }
805
806 let hover_bg = if response.hovered || focused {
807 Some(self.theme.surface_hover)
808 } else {
809 None
810 };
811 self.commands.push(Command::BeginContainer {
812 direction: Direction::Row,
813 gap: 1,
814 align: Align::Start,
815 justify: Justify::Start,
816 border: None,
817 border_sides: BorderSides::all(),
818 border_style: Style::new().fg(self.theme.border),
819 bg_color: hover_bg,
820 padding: Padding::default(),
821 margin: Margin::default(),
822 constraints: Constraints::default(),
823 title: None,
824 grow: 0,
825 group_name: None,
826 });
827 let marker_style = if *checked {
828 Style::new().fg(self.theme.success)
829 } else {
830 Style::new().fg(self.theme.text_dim)
831 };
832 let marker = if *checked { "[x]" } else { "[ ]" };
833 let label_text = label.into();
834 if focused {
835 self.styled(format!("▸ {marker}"), marker_style.bold());
836 self.styled(label_text, Style::new().fg(self.theme.text).bold());
837 } else {
838 self.styled(marker, marker_style);
839 self.styled(label_text, Style::new().fg(self.theme.text));
840 }
841 self.commands.push(Command::EndContainer);
842 self.last_text_idx = None;
843
844 self
845 }
846
847 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> &mut Self {
853 let focused = self.register_focusable();
854 let interaction_id = self.interaction_count;
855 self.interaction_count += 1;
856 let response = self.response_for(interaction_id);
857 let mut should_toggle = response.clicked;
858
859 if focused {
860 let mut consumed_indices = Vec::new();
861 for (i, event) in self.events.iter().enumerate() {
862 if let Event::Key(key) = event {
863 if key.kind != KeyEventKind::Press {
864 continue;
865 }
866 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
867 should_toggle = true;
868 consumed_indices.push(i);
869 }
870 }
871 }
872
873 for index in consumed_indices {
874 self.consumed[index] = true;
875 }
876 }
877
878 if should_toggle {
879 *on = !*on;
880 }
881
882 let hover_bg = if response.hovered || focused {
883 Some(self.theme.surface_hover)
884 } else {
885 None
886 };
887 self.commands.push(Command::BeginContainer {
888 direction: Direction::Row,
889 gap: 2,
890 align: Align::Start,
891 justify: Justify::Start,
892 border: None,
893 border_sides: BorderSides::all(),
894 border_style: Style::new().fg(self.theme.border),
895 bg_color: hover_bg,
896 padding: Padding::default(),
897 margin: Margin::default(),
898 constraints: Constraints::default(),
899 title: None,
900 grow: 0,
901 group_name: None,
902 });
903 let label_text = label.into();
904 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
905 let switch_style = if *on {
906 Style::new().fg(self.theme.success)
907 } else {
908 Style::new().fg(self.theme.text_dim)
909 };
910 if focused {
911 self.styled(
912 format!("▸ {label_text}"),
913 Style::new().fg(self.theme.text).bold(),
914 );
915 self.styled(switch, switch_style.bold());
916 } else {
917 self.styled(label_text, Style::new().fg(self.theme.text));
918 self.styled(switch, switch_style);
919 }
920 self.commands.push(Command::EndContainer);
921 self.last_text_idx = None;
922
923 self
924 }
925
926 pub fn select(&mut self, state: &mut SelectState) -> bool {
932 if state.items.is_empty() {
933 return false;
934 }
935 state.selected = state.selected.min(state.items.len().saturating_sub(1));
936
937 let focused = self.register_focusable();
938 let interaction_id = self.interaction_count;
939 self.interaction_count += 1;
940 let response = self.response_for(interaction_id);
941 let old_selected = state.selected;
942
943 if response.clicked {
944 state.open = !state.open;
945 if state.open {
946 state.set_cursor(state.selected);
947 }
948 }
949
950 if focused {
951 let mut consumed_indices = Vec::new();
952 for (i, event) in self.events.iter().enumerate() {
953 if self.consumed[i] {
954 continue;
955 }
956 if let Event::Key(key) = event {
957 if key.kind != KeyEventKind::Press {
958 continue;
959 }
960 if state.open {
961 match key.code {
962 KeyCode::Up | KeyCode::Char('k') => {
963 let c = state.cursor();
964 state.set_cursor(c.saturating_sub(1));
965 consumed_indices.push(i);
966 }
967 KeyCode::Down | KeyCode::Char('j') => {
968 let c = state.cursor();
969 state.set_cursor((c + 1).min(state.items.len().saturating_sub(1)));
970 consumed_indices.push(i);
971 }
972 KeyCode::Enter | KeyCode::Char(' ') => {
973 state.selected = state.cursor();
974 state.open = false;
975 consumed_indices.push(i);
976 }
977 KeyCode::Esc => {
978 state.open = false;
979 consumed_indices.push(i);
980 }
981 _ => {}
982 }
983 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
984 state.open = true;
985 state.set_cursor(state.selected);
986 consumed_indices.push(i);
987 }
988 }
989 }
990 for idx in consumed_indices {
991 self.consumed[idx] = true;
992 }
993 }
994
995 let changed = state.selected != old_selected;
996
997 let border_color = if focused {
998 self.theme.primary
999 } else {
1000 self.theme.border
1001 };
1002 let display_text = state
1003 .items
1004 .get(state.selected)
1005 .cloned()
1006 .unwrap_or_else(|| state.placeholder.clone());
1007 let arrow = if state.open { "▲" } else { "▼" };
1008
1009 self.commands.push(Command::BeginContainer {
1010 direction: Direction::Column,
1011 gap: 0,
1012 align: Align::Start,
1013 justify: Justify::Start,
1014 border: None,
1015 border_sides: BorderSides::all(),
1016 border_style: Style::new().fg(self.theme.border),
1017 bg_color: None,
1018 padding: Padding::default(),
1019 margin: Margin::default(),
1020 constraints: Constraints::default(),
1021 title: None,
1022 grow: 0,
1023 group_name: None,
1024 });
1025
1026 self.commands.push(Command::BeginContainer {
1027 direction: Direction::Row,
1028 gap: 1,
1029 align: Align::Start,
1030 justify: Justify::Start,
1031 border: Some(Border::Rounded),
1032 border_sides: BorderSides::all(),
1033 border_style: Style::new().fg(border_color),
1034 bg_color: None,
1035 padding: Padding {
1036 left: 1,
1037 right: 1,
1038 top: 0,
1039 bottom: 0,
1040 },
1041 margin: Margin::default(),
1042 constraints: Constraints::default(),
1043 title: None,
1044 grow: 0,
1045 group_name: None,
1046 });
1047 self.interaction_count += 1;
1048 self.styled(&display_text, Style::new().fg(self.theme.text));
1049 self.styled(arrow, Style::new().fg(self.theme.text_dim));
1050 self.commands.push(Command::EndContainer);
1051 self.last_text_idx = None;
1052
1053 if state.open {
1054 for (idx, item) in state.items.iter().enumerate() {
1055 let is_cursor = idx == state.cursor();
1056 let style = if is_cursor {
1057 Style::new().bold().fg(self.theme.primary)
1058 } else {
1059 Style::new().fg(self.theme.text)
1060 };
1061 let prefix = if is_cursor { "▸ " } else { " " };
1062 self.styled(format!("{prefix}{item}"), style);
1063 }
1064 }
1065
1066 self.commands.push(Command::EndContainer);
1067 self.last_text_idx = None;
1068 changed
1069 }
1070
1071 pub fn radio(&mut self, state: &mut RadioState) -> bool {
1075 if state.items.is_empty() {
1076 return false;
1077 }
1078 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1079 let focused = self.register_focusable();
1080 let old_selected = state.selected;
1081
1082 if focused {
1083 let mut consumed_indices = Vec::new();
1084 for (i, event) in self.events.iter().enumerate() {
1085 if self.consumed[i] {
1086 continue;
1087 }
1088 if let Event::Key(key) = event {
1089 if key.kind != KeyEventKind::Press {
1090 continue;
1091 }
1092 match key.code {
1093 KeyCode::Up | KeyCode::Char('k') => {
1094 state.selected = state.selected.saturating_sub(1);
1095 consumed_indices.push(i);
1096 }
1097 KeyCode::Down | KeyCode::Char('j') => {
1098 state.selected =
1099 (state.selected + 1).min(state.items.len().saturating_sub(1));
1100 consumed_indices.push(i);
1101 }
1102 KeyCode::Enter | KeyCode::Char(' ') => {
1103 consumed_indices.push(i);
1104 }
1105 _ => {}
1106 }
1107 }
1108 }
1109 for idx in consumed_indices {
1110 self.consumed[idx] = true;
1111 }
1112 }
1113
1114 let interaction_id = self.interaction_count;
1115 self.interaction_count += 1;
1116
1117 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1118 for (i, event) in self.events.iter().enumerate() {
1119 if self.consumed[i] {
1120 continue;
1121 }
1122 if let Event::Mouse(mouse) = event {
1123 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1124 continue;
1125 }
1126 let in_bounds = mouse.x >= rect.x
1127 && mouse.x < rect.right()
1128 && mouse.y >= rect.y
1129 && mouse.y < rect.bottom();
1130 if !in_bounds {
1131 continue;
1132 }
1133 let clicked_idx = (mouse.y - rect.y) as usize;
1134 if clicked_idx < state.items.len() {
1135 state.selected = clicked_idx;
1136 self.consumed[i] = true;
1137 }
1138 }
1139 }
1140 }
1141
1142 self.commands.push(Command::BeginContainer {
1143 direction: Direction::Column,
1144 gap: 0,
1145 align: Align::Start,
1146 justify: Justify::Start,
1147 border: None,
1148 border_sides: BorderSides::all(),
1149 border_style: Style::new().fg(self.theme.border),
1150 bg_color: None,
1151 padding: Padding::default(),
1152 margin: Margin::default(),
1153 constraints: Constraints::default(),
1154 title: None,
1155 grow: 0,
1156 group_name: None,
1157 });
1158
1159 for (idx, item) in state.items.iter().enumerate() {
1160 let is_selected = idx == state.selected;
1161 let marker = if is_selected { "●" } else { "○" };
1162 let style = if is_selected {
1163 if focused {
1164 Style::new().bold().fg(self.theme.primary)
1165 } else {
1166 Style::new().fg(self.theme.primary)
1167 }
1168 } else {
1169 Style::new().fg(self.theme.text)
1170 };
1171 let prefix = if focused && idx == state.selected {
1172 "▸ "
1173 } else {
1174 " "
1175 };
1176 self.styled(format!("{prefix}{marker} {item}"), style);
1177 }
1178
1179 self.commands.push(Command::EndContainer);
1180 self.last_text_idx = None;
1181 state.selected != old_selected
1182 }
1183
1184 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> &mut Self {
1188 if state.items.is_empty() {
1189 return self;
1190 }
1191 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
1192 let focused = self.register_focusable();
1193
1194 if focused {
1195 let mut consumed_indices = Vec::new();
1196 for (i, event) in self.events.iter().enumerate() {
1197 if self.consumed[i] {
1198 continue;
1199 }
1200 if let Event::Key(key) = event {
1201 if key.kind != KeyEventKind::Press {
1202 continue;
1203 }
1204 match key.code {
1205 KeyCode::Up | KeyCode::Char('k') => {
1206 state.cursor = state.cursor.saturating_sub(1);
1207 consumed_indices.push(i);
1208 }
1209 KeyCode::Down | KeyCode::Char('j') => {
1210 state.cursor =
1211 (state.cursor + 1).min(state.items.len().saturating_sub(1));
1212 consumed_indices.push(i);
1213 }
1214 KeyCode::Char(' ') | KeyCode::Enter => {
1215 state.toggle(state.cursor);
1216 consumed_indices.push(i);
1217 }
1218 _ => {}
1219 }
1220 }
1221 }
1222 for idx in consumed_indices {
1223 self.consumed[idx] = true;
1224 }
1225 }
1226
1227 let interaction_id = self.interaction_count;
1228 self.interaction_count += 1;
1229
1230 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1231 for (i, event) in self.events.iter().enumerate() {
1232 if self.consumed[i] {
1233 continue;
1234 }
1235 if let Event::Mouse(mouse) = event {
1236 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1237 continue;
1238 }
1239 let in_bounds = mouse.x >= rect.x
1240 && mouse.x < rect.right()
1241 && mouse.y >= rect.y
1242 && mouse.y < rect.bottom();
1243 if !in_bounds {
1244 continue;
1245 }
1246 let clicked_idx = (mouse.y - rect.y) as usize;
1247 if clicked_idx < state.items.len() {
1248 state.toggle(clicked_idx);
1249 state.cursor = clicked_idx;
1250 self.consumed[i] = true;
1251 }
1252 }
1253 }
1254 }
1255
1256 self.commands.push(Command::BeginContainer {
1257 direction: Direction::Column,
1258 gap: 0,
1259 align: Align::Start,
1260 justify: Justify::Start,
1261 border: None,
1262 border_sides: BorderSides::all(),
1263 border_style: Style::new().fg(self.theme.border),
1264 bg_color: None,
1265 padding: Padding::default(),
1266 margin: Margin::default(),
1267 constraints: Constraints::default(),
1268 title: None,
1269 grow: 0,
1270 group_name: None,
1271 });
1272
1273 for (idx, item) in state.items.iter().enumerate() {
1274 let checked = state.selected.contains(&idx);
1275 let marker = if checked { "[x]" } else { "[ ]" };
1276 let is_cursor = idx == state.cursor;
1277 let style = if is_cursor && focused {
1278 Style::new().bold().fg(self.theme.primary)
1279 } else if checked {
1280 Style::new().fg(self.theme.success)
1281 } else {
1282 Style::new().fg(self.theme.text)
1283 };
1284 let prefix = if is_cursor && focused { "▸ " } else { " " };
1285 self.styled(format!("{prefix}{marker} {item}"), style);
1286 }
1287
1288 self.commands.push(Command::EndContainer);
1289 self.last_text_idx = None;
1290 self
1291 }
1292
1293 pub fn tree(&mut self, state: &mut TreeState) -> &mut Self {
1297 let entries = state.flatten();
1298 if entries.is_empty() {
1299 return self;
1300 }
1301 state.selected = state.selected.min(entries.len().saturating_sub(1));
1302 let focused = self.register_focusable();
1303
1304 if focused {
1305 let mut consumed_indices = Vec::new();
1306 for (i, event) in self.events.iter().enumerate() {
1307 if self.consumed[i] {
1308 continue;
1309 }
1310 if let Event::Key(key) = event {
1311 if key.kind != KeyEventKind::Press {
1312 continue;
1313 }
1314 match key.code {
1315 KeyCode::Up | KeyCode::Char('k') => {
1316 state.selected = state.selected.saturating_sub(1);
1317 consumed_indices.push(i);
1318 }
1319 KeyCode::Down | KeyCode::Char('j') => {
1320 let max = state.flatten().len().saturating_sub(1);
1321 state.selected = (state.selected + 1).min(max);
1322 consumed_indices.push(i);
1323 }
1324 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
1325 state.toggle_at(state.selected);
1326 consumed_indices.push(i);
1327 }
1328 KeyCode::Left => {
1329 let entry = &entries[state.selected.min(entries.len() - 1)];
1330 if entry.expanded {
1331 state.toggle_at(state.selected);
1332 }
1333 consumed_indices.push(i);
1334 }
1335 _ => {}
1336 }
1337 }
1338 }
1339 for idx in consumed_indices {
1340 self.consumed[idx] = true;
1341 }
1342 }
1343
1344 self.interaction_count += 1;
1345 self.commands.push(Command::BeginContainer {
1346 direction: Direction::Column,
1347 gap: 0,
1348 align: Align::Start,
1349 justify: Justify::Start,
1350 border: None,
1351 border_sides: BorderSides::all(),
1352 border_style: Style::new().fg(self.theme.border),
1353 bg_color: None,
1354 padding: Padding::default(),
1355 margin: Margin::default(),
1356 constraints: Constraints::default(),
1357 title: None,
1358 grow: 0,
1359 group_name: None,
1360 });
1361
1362 let entries = state.flatten();
1363 for (idx, entry) in entries.iter().enumerate() {
1364 let indent = " ".repeat(entry.depth);
1365 let icon = if entry.is_leaf {
1366 " "
1367 } else if entry.expanded {
1368 "▾ "
1369 } else {
1370 "▸ "
1371 };
1372 let is_selected = idx == state.selected;
1373 let style = if is_selected && focused {
1374 Style::new().bold().fg(self.theme.primary)
1375 } else if is_selected {
1376 Style::new().fg(self.theme.primary)
1377 } else {
1378 Style::new().fg(self.theme.text)
1379 };
1380 let cursor = if is_selected && focused { "▸" } else { " " };
1381 self.styled(format!("{cursor}{indent}{icon}{}", entry.label), style);
1382 }
1383
1384 self.commands.push(Command::EndContainer);
1385 self.last_text_idx = None;
1386 self
1387 }
1388
1389 pub fn virtual_list(
1396 &mut self,
1397 state: &mut ListState,
1398 visible_height: usize,
1399 f: impl Fn(&mut Context, usize),
1400 ) -> &mut Self {
1401 if state.items.is_empty() {
1402 return self;
1403 }
1404 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1405 let focused = self.register_focusable();
1406
1407 if focused {
1408 let mut consumed_indices = Vec::new();
1409 for (i, event) in self.events.iter().enumerate() {
1410 if self.consumed[i] {
1411 continue;
1412 }
1413 if let Event::Key(key) = event {
1414 if key.kind != KeyEventKind::Press {
1415 continue;
1416 }
1417 match key.code {
1418 KeyCode::Up | KeyCode::Char('k') => {
1419 state.selected = state.selected.saturating_sub(1);
1420 consumed_indices.push(i);
1421 }
1422 KeyCode::Down | KeyCode::Char('j') => {
1423 state.selected =
1424 (state.selected + 1).min(state.items.len().saturating_sub(1));
1425 consumed_indices.push(i);
1426 }
1427 KeyCode::PageUp => {
1428 state.selected = state.selected.saturating_sub(visible_height);
1429 consumed_indices.push(i);
1430 }
1431 KeyCode::PageDown => {
1432 state.selected = (state.selected + visible_height)
1433 .min(state.items.len().saturating_sub(1));
1434 consumed_indices.push(i);
1435 }
1436 KeyCode::Home => {
1437 state.selected = 0;
1438 consumed_indices.push(i);
1439 }
1440 KeyCode::End => {
1441 state.selected = state.items.len().saturating_sub(1);
1442 consumed_indices.push(i);
1443 }
1444 _ => {}
1445 }
1446 }
1447 }
1448 for idx in consumed_indices {
1449 self.consumed[idx] = true;
1450 }
1451 }
1452
1453 let start = if state.selected >= visible_height {
1454 state.selected - visible_height + 1
1455 } else {
1456 0
1457 };
1458 let end = (start + visible_height).min(state.items.len());
1459
1460 self.interaction_count += 1;
1461 self.commands.push(Command::BeginContainer {
1462 direction: Direction::Column,
1463 gap: 0,
1464 align: Align::Start,
1465 justify: Justify::Start,
1466 border: None,
1467 border_sides: BorderSides::all(),
1468 border_style: Style::new().fg(self.theme.border),
1469 bg_color: None,
1470 padding: Padding::default(),
1471 margin: Margin::default(),
1472 constraints: Constraints::default(),
1473 title: None,
1474 grow: 0,
1475 group_name: None,
1476 });
1477
1478 if start > 0 {
1479 self.styled(
1480 format!(" ↑ {} more", start),
1481 Style::new().fg(self.theme.text_dim).dim(),
1482 );
1483 }
1484
1485 for idx in start..end {
1486 f(self, idx);
1487 }
1488
1489 let remaining = state.items.len().saturating_sub(end);
1490 if remaining > 0 {
1491 self.styled(
1492 format!(" ↓ {} more", remaining),
1493 Style::new().fg(self.theme.text_dim).dim(),
1494 );
1495 }
1496
1497 self.commands.push(Command::EndContainer);
1498 self.last_text_idx = None;
1499 self
1500 }
1501
1502 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Option<usize> {
1506 if !state.open {
1507 return None;
1508 }
1509
1510 let filtered = state.filtered_indices();
1511 let sel = state.selected().min(filtered.len().saturating_sub(1));
1512 state.set_selected(sel);
1513
1514 let mut consumed_indices = Vec::new();
1515 let mut result: Option<usize> = None;
1516
1517 for (i, event) in self.events.iter().enumerate() {
1518 if self.consumed[i] {
1519 continue;
1520 }
1521 if let Event::Key(key) = event {
1522 if key.kind != KeyEventKind::Press {
1523 continue;
1524 }
1525 match key.code {
1526 KeyCode::Esc => {
1527 state.open = false;
1528 consumed_indices.push(i);
1529 }
1530 KeyCode::Up => {
1531 let s = state.selected();
1532 state.set_selected(s.saturating_sub(1));
1533 consumed_indices.push(i);
1534 }
1535 KeyCode::Down => {
1536 let s = state.selected();
1537 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
1538 consumed_indices.push(i);
1539 }
1540 KeyCode::Enter => {
1541 if let Some(&cmd_idx) = filtered.get(state.selected()) {
1542 result = Some(cmd_idx);
1543 state.open = false;
1544 }
1545 consumed_indices.push(i);
1546 }
1547 KeyCode::Backspace => {
1548 if state.cursor > 0 {
1549 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
1550 let end_idx = byte_index_for_char(&state.input, state.cursor);
1551 state.input.replace_range(byte_idx..end_idx, "");
1552 state.cursor -= 1;
1553 state.set_selected(0);
1554 }
1555 consumed_indices.push(i);
1556 }
1557 KeyCode::Char(ch) => {
1558 let byte_idx = byte_index_for_char(&state.input, state.cursor);
1559 state.input.insert(byte_idx, ch);
1560 state.cursor += 1;
1561 state.set_selected(0);
1562 consumed_indices.push(i);
1563 }
1564 _ => {}
1565 }
1566 }
1567 }
1568 for idx in consumed_indices {
1569 self.consumed[idx] = true;
1570 }
1571
1572 let filtered = state.filtered_indices();
1573
1574 self.modal(|ui| {
1575 let primary = ui.theme.primary;
1576 ui.container()
1577 .border(Border::Rounded)
1578 .border_style(Style::new().fg(primary))
1579 .pad(1)
1580 .max_w(60)
1581 .col(|ui| {
1582 let border_color = ui.theme.primary;
1583 ui.bordered(Border::Rounded)
1584 .border_style(Style::new().fg(border_color))
1585 .px(1)
1586 .col(|ui| {
1587 let display = if state.input.is_empty() {
1588 "Type to search...".to_string()
1589 } else {
1590 state.input.clone()
1591 };
1592 let style = if state.input.is_empty() {
1593 Style::new().dim().fg(ui.theme.text_dim)
1594 } else {
1595 Style::new().fg(ui.theme.text)
1596 };
1597 ui.styled(display, style);
1598 });
1599
1600 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
1601 let cmd = &state.commands[cmd_idx];
1602 let is_selected = list_idx == state.selected();
1603 let style = if is_selected {
1604 Style::new().bold().fg(ui.theme.primary)
1605 } else {
1606 Style::new().fg(ui.theme.text)
1607 };
1608 let prefix = if is_selected { "▸ " } else { " " };
1609 let shortcut_text = cmd
1610 .shortcut
1611 .as_deref()
1612 .map(|s| format!(" ({s})"))
1613 .unwrap_or_default();
1614 ui.styled(format!("{prefix}{}{shortcut_text}", cmd.label), style);
1615 if is_selected && !cmd.description.is_empty() {
1616 ui.styled(
1617 format!(" {}", cmd.description),
1618 Style::new().dim().fg(ui.theme.text_dim),
1619 );
1620 }
1621 }
1622
1623 if filtered.is_empty() {
1624 ui.styled(
1625 " No matching commands",
1626 Style::new().dim().fg(ui.theme.text_dim),
1627 );
1628 }
1629 });
1630 });
1631
1632 result
1633 }
1634
1635 pub fn markdown(&mut self, text: &str) -> &mut Self {
1642 self.commands.push(Command::BeginContainer {
1643 direction: Direction::Column,
1644 gap: 0,
1645 align: Align::Start,
1646 justify: Justify::Start,
1647 border: None,
1648 border_sides: BorderSides::all(),
1649 border_style: Style::new().fg(self.theme.border),
1650 bg_color: None,
1651 padding: Padding::default(),
1652 margin: Margin::default(),
1653 constraints: Constraints::default(),
1654 title: None,
1655 grow: 0,
1656 group_name: None,
1657 });
1658 self.interaction_count += 1;
1659
1660 let text_style = Style::new().fg(self.theme.text);
1661 let bold_style = Style::new().fg(self.theme.text).bold();
1662 let code_style = Style::new().fg(self.theme.accent);
1663
1664 for line in text.lines() {
1665 let trimmed = line.trim();
1666 if trimmed.is_empty() {
1667 self.text(" ");
1668 continue;
1669 }
1670 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
1671 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
1672 continue;
1673 }
1674 if let Some(heading) = trimmed.strip_prefix("### ") {
1675 self.styled(heading, Style::new().bold().fg(self.theme.accent));
1676 } else if let Some(heading) = trimmed.strip_prefix("## ") {
1677 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
1678 } else if let Some(heading) = trimmed.strip_prefix("# ") {
1679 self.styled(heading, Style::new().bold().fg(self.theme.primary));
1680 } else if let Some(item) = trimmed
1681 .strip_prefix("- ")
1682 .or_else(|| trimmed.strip_prefix("* "))
1683 {
1684 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
1685 if segs.len() <= 1 {
1686 self.styled(format!(" • {item}"), text_style);
1687 } else {
1688 self.line(|ui| {
1689 ui.styled(" • ", text_style);
1690 for (s, st) in segs {
1691 ui.styled(s, st);
1692 }
1693 });
1694 }
1695 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
1696 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
1697 if parts.len() == 2 {
1698 let segs =
1699 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
1700 if segs.len() <= 1 {
1701 self.styled(format!(" {}. {}", parts[0], parts[1]), text_style);
1702 } else {
1703 self.line(|ui| {
1704 ui.styled(format!(" {}. ", parts[0]), text_style);
1705 for (s, st) in segs {
1706 ui.styled(s, st);
1707 }
1708 });
1709 }
1710 } else {
1711 self.text(trimmed);
1712 }
1713 } else if let Some(code) = trimmed.strip_prefix("```") {
1714 let _ = code;
1715 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
1716 } else {
1717 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
1718 if segs.len() <= 1 {
1719 self.styled(trimmed, text_style);
1720 } else {
1721 self.line(|ui| {
1722 for (s, st) in segs {
1723 ui.styled(s, st);
1724 }
1725 });
1726 }
1727 }
1728 }
1729
1730 self.commands.push(Command::EndContainer);
1731 self.last_text_idx = None;
1732 self
1733 }
1734
1735 fn parse_inline_segments(
1736 text: &str,
1737 base: Style,
1738 bold: Style,
1739 code: Style,
1740 ) -> Vec<(String, Style)> {
1741 let mut segments: Vec<(String, Style)> = Vec::new();
1742 let mut current = String::new();
1743 let chars: Vec<char> = text.chars().collect();
1744 let mut i = 0;
1745 while i < chars.len() {
1746 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
1747 let rest: String = chars[i + 2..].iter().collect();
1748 if let Some(end) = rest.find("**") {
1749 if !current.is_empty() {
1750 segments.push((std::mem::take(&mut current), base));
1751 }
1752 let inner: String = rest[..end].to_string();
1753 let char_count = inner.chars().count();
1754 segments.push((inner, bold));
1755 i += 2 + char_count + 2;
1756 continue;
1757 }
1758 }
1759 if chars[i] == '*'
1760 && (i + 1 >= chars.len() || chars[i + 1] != '*')
1761 && (i == 0 || chars[i - 1] != '*')
1762 {
1763 let rest: String = chars[i + 1..].iter().collect();
1764 if let Some(end) = rest.find('*') {
1765 if !current.is_empty() {
1766 segments.push((std::mem::take(&mut current), base));
1767 }
1768 let inner: String = rest[..end].to_string();
1769 let char_count = inner.chars().count();
1770 segments.push((inner, base.italic()));
1771 i += 1 + char_count + 1;
1772 continue;
1773 }
1774 }
1775 if chars[i] == '`' {
1776 let rest: String = chars[i + 1..].iter().collect();
1777 if let Some(end) = rest.find('`') {
1778 if !current.is_empty() {
1779 segments.push((std::mem::take(&mut current), base));
1780 }
1781 let inner: String = rest[..end].to_string();
1782 let char_count = inner.chars().count();
1783 segments.push((inner, code));
1784 i += 1 + char_count + 1;
1785 continue;
1786 }
1787 }
1788 current.push(chars[i]);
1789 i += 1;
1790 }
1791 if !current.is_empty() {
1792 segments.push((current, base));
1793 }
1794 segments
1795 }
1796
1797 pub fn key_seq(&self, seq: &str) -> bool {
1804 if seq.is_empty() {
1805 return false;
1806 }
1807 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1808 return false;
1809 }
1810 let target: Vec<char> = seq.chars().collect();
1811 let mut matched = 0;
1812 for (i, event) in self.events.iter().enumerate() {
1813 if self.consumed[i] {
1814 continue;
1815 }
1816 if let Event::Key(key) = event {
1817 if key.kind != KeyEventKind::Press {
1818 continue;
1819 }
1820 if let KeyCode::Char(c) = key.code {
1821 if c == target[matched] {
1822 matched += 1;
1823 if matched == target.len() {
1824 return true;
1825 }
1826 } else {
1827 matched = 0;
1828 if c == target[0] {
1829 matched = 1;
1830 }
1831 }
1832 }
1833 }
1834 }
1835 false
1836 }
1837
1838 pub fn separator(&mut self) -> &mut Self {
1843 self.commands.push(Command::Text {
1844 content: "─".repeat(200),
1845 style: Style::new().fg(self.theme.border).dim(),
1846 grow: 0,
1847 align: Align::Start,
1848 wrap: false,
1849 margin: Margin::default(),
1850 constraints: Constraints::default(),
1851 });
1852 self.last_text_idx = Some(self.commands.len() - 1);
1853 self
1854 }
1855
1856 pub fn help(&mut self, bindings: &[(&str, &str)]) -> &mut Self {
1862 if bindings.is_empty() {
1863 return self;
1864 }
1865
1866 self.interaction_count += 1;
1867 self.commands.push(Command::BeginContainer {
1868 direction: Direction::Row,
1869 gap: 2,
1870 align: Align::Start,
1871 justify: Justify::Start,
1872 border: None,
1873 border_sides: BorderSides::all(),
1874 border_style: Style::new().fg(self.theme.border),
1875 bg_color: None,
1876 padding: Padding::default(),
1877 margin: Margin::default(),
1878 constraints: Constraints::default(),
1879 title: None,
1880 grow: 0,
1881 group_name: None,
1882 });
1883 for (idx, (key, action)) in bindings.iter().enumerate() {
1884 if idx > 0 {
1885 self.styled("·", Style::new().fg(self.theme.text_dim));
1886 }
1887 self.styled(*key, Style::new().bold().fg(self.theme.primary));
1888 self.styled(*action, Style::new().fg(self.theme.text_dim));
1889 }
1890 self.commands.push(Command::EndContainer);
1891 self.last_text_idx = None;
1892
1893 self
1894 }
1895
1896 pub fn key(&self, c: char) -> bool {
1902 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1903 return false;
1904 }
1905 self.events.iter().enumerate().any(|(i, e)| {
1906 !self.consumed[i]
1907 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
1908 })
1909 }
1910
1911 pub fn key_code(&self, code: KeyCode) -> bool {
1915 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1916 return false;
1917 }
1918 self.events.iter().enumerate().any(|(i, e)| {
1919 !self.consumed[i]
1920 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
1921 })
1922 }
1923
1924 pub fn key_release(&self, c: char) -> bool {
1928 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1929 return false;
1930 }
1931 self.events.iter().enumerate().any(|(i, e)| {
1932 !self.consumed[i]
1933 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
1934 })
1935 }
1936
1937 pub fn key_code_release(&self, code: KeyCode) -> bool {
1941 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1942 return false;
1943 }
1944 self.events.iter().enumerate().any(|(i, e)| {
1945 !self.consumed[i]
1946 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
1947 })
1948 }
1949
1950 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
1954 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1955 return false;
1956 }
1957 self.events.iter().enumerate().any(|(i, e)| {
1958 !self.consumed[i]
1959 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
1960 })
1961 }
1962
1963 pub fn mouse_down(&self) -> Option<(u32, u32)> {
1967 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1968 return None;
1969 }
1970 self.events.iter().enumerate().find_map(|(i, event)| {
1971 if self.consumed[i] {
1972 return None;
1973 }
1974 if let Event::Mouse(mouse) = event {
1975 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1976 return Some((mouse.x, mouse.y));
1977 }
1978 }
1979 None
1980 })
1981 }
1982
1983 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
1988 self.mouse_pos
1989 }
1990
1991 pub fn paste(&self) -> Option<&str> {
1993 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1994 return None;
1995 }
1996 self.events.iter().enumerate().find_map(|(i, event)| {
1997 if self.consumed[i] {
1998 return None;
1999 }
2000 if let Event::Paste(ref text) = event {
2001 return Some(text.as_str());
2002 }
2003 None
2004 })
2005 }
2006
2007 pub fn scroll_up(&self) -> bool {
2009 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2010 return false;
2011 }
2012 self.events.iter().enumerate().any(|(i, event)| {
2013 !self.consumed[i]
2014 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
2015 })
2016 }
2017
2018 pub fn scroll_down(&self) -> bool {
2020 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2021 return false;
2022 }
2023 self.events.iter().enumerate().any(|(i, event)| {
2024 !self.consumed[i]
2025 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
2026 })
2027 }
2028
2029 pub fn quit(&mut self) {
2031 self.should_quit = true;
2032 }
2033
2034 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
2042 self.clipboard_text = Some(text.into());
2043 }
2044
2045 pub fn theme(&self) -> &Theme {
2047 &self.theme
2048 }
2049
2050 pub fn set_theme(&mut self, theme: Theme) {
2054 self.theme = theme;
2055 }
2056
2057 pub fn is_dark_mode(&self) -> bool {
2059 self.dark_mode
2060 }
2061
2062 pub fn set_dark_mode(&mut self, dark: bool) {
2064 self.dark_mode = dark;
2065 }
2066
2067 pub fn width(&self) -> u32 {
2071 self.area_width
2072 }
2073
2074 pub fn breakpoint(&self) -> Breakpoint {
2098 let w = self.area_width;
2099 if w < 40 {
2100 Breakpoint::Xs
2101 } else if w < 80 {
2102 Breakpoint::Sm
2103 } else if w < 120 {
2104 Breakpoint::Md
2105 } else if w < 160 {
2106 Breakpoint::Lg
2107 } else {
2108 Breakpoint::Xl
2109 }
2110 }
2111
2112 pub fn height(&self) -> u32 {
2114 self.area_height
2115 }
2116
2117 pub fn tick(&self) -> u64 {
2122 self.tick
2123 }
2124
2125 pub fn debug_enabled(&self) -> bool {
2129 self.debug
2130 }
2131}