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 align_self: None,
31 justify: Justify::Start,
32 border: None,
33 border_sides: BorderSides::all(),
34 border_style: Style::new().fg(border),
35 bg_color: None,
36 padding: Padding::default(),
37 margin: Margin::default(),
38 constraints: Constraints::default(),
39 title: None,
40 grow: 0,
41 group_name: None,
42 });
43
44 let children_start = self.commands.len();
45 f(self);
46 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
47
48 let mut elements: Vec<Vec<Command>> = Vec::new();
49 let mut iter = child_commands.into_iter().peekable();
50 while let Some(cmd) = iter.next() {
51 match cmd {
52 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
53 let mut depth = 1_u32;
54 let mut element = vec![cmd];
55 for next in iter.by_ref() {
56 match next {
57 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
58 depth += 1;
59 }
60 Command::EndContainer => {
61 depth = depth.saturating_sub(1);
62 }
63 _ => {}
64 }
65 let at_end = matches!(next, Command::EndContainer) && depth == 0;
66 element.push(next);
67 if at_end {
68 break;
69 }
70 }
71 elements.push(element);
72 }
73 Command::EndContainer => {}
74 _ => elements.push(vec![cmd]),
75 }
76 }
77
78 let cols = cols.max(1) as usize;
79 for row in elements.chunks(cols) {
80 self.interaction_count += 1;
81 self.commands.push(Command::BeginContainer {
82 direction: Direction::Row,
83 gap: 0,
84 align: Align::Start,
85 align_self: None,
86 justify: Justify::Start,
87 border: None,
88 border_sides: BorderSides::all(),
89 border_style: Style::new().fg(border),
90 bg_color: None,
91 padding: Padding::default(),
92 margin: Margin::default(),
93 constraints: Constraints::default(),
94 title: None,
95 grow: 0,
96 group_name: None,
97 });
98
99 for element in row {
100 self.interaction_count += 1;
101 self.commands.push(Command::BeginContainer {
102 direction: Direction::Column,
103 gap: 0,
104 align: Align::Start,
105 align_self: None,
106 justify: Justify::Start,
107 border: None,
108 border_sides: BorderSides::all(),
109 border_style: Style::new().fg(border),
110 bg_color: None,
111 padding: Padding::default(),
112 margin: Margin::default(),
113 constraints: Constraints::default(),
114 title: None,
115 grow: 1,
116 group_name: None,
117 });
118 self.commands.extend(element.iter().cloned());
119 self.commands.push(Command::EndContainer);
120 }
121
122 self.commands.push(Command::EndContainer);
123 }
124
125 self.commands.push(Command::EndContainer);
126 self.last_text_idx = None;
127
128 self.response_for(interaction_id)
129 }
130
131 pub fn list(&mut self, state: &mut ListState) -> Response {
136 self.list_colored(state, &WidgetColors::new())
137 }
138
139 pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
140 let visible = state.visible_indices().to_vec();
141 if visible.is_empty() && state.items.is_empty() {
142 state.selected = 0;
143 return Response::none();
144 }
145
146 if !visible.is_empty() {
147 state.selected = state.selected.min(visible.len().saturating_sub(1));
148 }
149
150 let old_selected = state.selected;
151 let focused = self.register_focusable();
152 let interaction_id = self.interaction_count;
153 self.interaction_count += 1;
154 let mut response = self.response_for(interaction_id);
155 response.focused = focused;
156
157 if focused {
158 let mut consumed_indices = Vec::new();
159 for (i, event) in self.events.iter().enumerate() {
160 if let Event::Key(key) = event {
161 if key.kind != KeyEventKind::Press {
162 continue;
163 }
164 match key.code {
165 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
166 let _ = handle_vertical_nav(
167 &mut state.selected,
168 visible.len().saturating_sub(1),
169 key.code.clone(),
170 );
171 consumed_indices.push(i);
172 }
173 _ => {}
174 }
175 }
176 }
177
178 for index in consumed_indices {
179 self.consumed[index] = true;
180 }
181 }
182
183 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
184 for (i, event) in self.events.iter().enumerate() {
185 if self.consumed[i] {
186 continue;
187 }
188 if let Event::Mouse(mouse) = event {
189 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
190 continue;
191 }
192 let in_bounds = mouse.x >= rect.x
193 && mouse.x < rect.right()
194 && mouse.y >= rect.y
195 && mouse.y < rect.bottom();
196 if !in_bounds {
197 continue;
198 }
199 let clicked_idx = (mouse.y - rect.y) as usize;
200 if clicked_idx < visible.len() {
201 state.selected = clicked_idx;
202 self.consumed[i] = true;
203 }
204 }
205 }
206 }
207
208 self.commands.push(Command::BeginContainer {
209 direction: Direction::Column,
210 gap: 0,
211 align: Align::Start,
212 align_self: None,
213 justify: Justify::Start,
214 border: None,
215 border_sides: BorderSides::all(),
216 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
217 bg_color: None,
218 padding: Padding::default(),
219 margin: Margin::default(),
220 constraints: Constraints::default(),
221 title: None,
222 grow: 0,
223 group_name: None,
224 });
225
226 for (view_idx, &item_idx) in visible.iter().enumerate() {
227 let item = &state.items[item_idx];
228 if view_idx == state.selected {
229 let mut selected_style = Style::new()
230 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
231 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
232 if focused {
233 selected_style = selected_style.bold();
234 }
235 let mut row = String::with_capacity(2 + item.len());
236 row.push_str("▸ ");
237 row.push_str(item);
238 self.styled(row, selected_style);
239 } else {
240 let mut row = String::with_capacity(2 + item.len());
241 row.push_str(" ");
242 row.push_str(item);
243 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
244 }
245 }
246
247 self.commands.push(Command::EndContainer);
248 self.last_text_idx = None;
249
250 response.changed = state.selected != old_selected;
251 response
252 }
253
254 pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
255 if state.dirty {
256 state.refresh();
257 }
258 if !state.entries.is_empty() {
259 state.selected = state.selected.min(state.entries.len().saturating_sub(1));
260 }
261
262 let focused = self.register_focusable();
263 let interaction_id = self.interaction_count;
264 self.interaction_count += 1;
265 let mut response = self.response_for(interaction_id);
266 response.focused = focused;
267 let mut file_selected = false;
268
269 if focused {
270 let mut consumed_indices = Vec::new();
271 for (i, event) in self.events.iter().enumerate() {
272 if self.consumed[i] {
273 continue;
274 }
275 if let Event::Key(key) = event {
276 if key.kind != KeyEventKind::Press {
277 continue;
278 }
279 match key.code {
280 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
281 if !state.entries.is_empty() {
282 let _ = handle_vertical_nav(
283 &mut state.selected,
284 state.entries.len().saturating_sub(1),
285 key.code.clone(),
286 );
287 }
288 consumed_indices.push(i);
289 }
290 KeyCode::Enter => {
291 if let Some(entry) = state.entries.get(state.selected).cloned() {
292 if entry.is_dir {
293 state.current_dir = entry.path;
294 state.selected = 0;
295 state.selected_file = None;
296 state.dirty = true;
297 } else {
298 state.selected_file = Some(entry.path);
299 file_selected = true;
300 }
301 }
302 consumed_indices.push(i);
303 }
304 KeyCode::Backspace => {
305 if let Some(parent) =
306 state.current_dir.parent().map(|p| p.to_path_buf())
307 {
308 state.current_dir = parent;
309 state.selected = 0;
310 state.selected_file = None;
311 state.dirty = true;
312 }
313 consumed_indices.push(i);
314 }
315 KeyCode::Char('h') => {
316 state.show_hidden = !state.show_hidden;
317 state.selected = 0;
318 state.dirty = true;
319 consumed_indices.push(i);
320 }
321 KeyCode::Esc => {
322 state.selected_file = None;
323 consumed_indices.push(i);
324 }
325 _ => {}
326 }
327 }
328 }
329
330 for index in consumed_indices {
331 self.consumed[index] = true;
332 }
333 }
334
335 if state.dirty {
336 state.refresh();
337 }
338
339 self.commands.push(Command::BeginContainer {
340 direction: Direction::Column,
341 gap: 0,
342 align: Align::Start,
343 align_self: None,
344 justify: Justify::Start,
345 border: None,
346 border_sides: BorderSides::all(),
347 border_style: Style::new().fg(self.theme.border),
348 bg_color: None,
349 padding: Padding::default(),
350 margin: Margin::default(),
351 constraints: Constraints::default(),
352 title: None,
353 grow: 0,
354 group_name: None,
355 });
356
357 let dir_text = {
358 let dir = state.current_dir.display().to_string();
359 let mut text = String::with_capacity(5 + dir.len());
360 text.push_str("Dir: ");
361 text.push_str(&dir);
362 text
363 };
364 self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
365
366 if state.entries.is_empty() {
367 self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
368 } else {
369 for (idx, entry) in state.entries.iter().enumerate() {
370 let icon = if entry.is_dir { "▸ " } else { " " };
371 let row = if entry.is_dir {
372 let mut row = String::with_capacity(icon.len() + entry.name.len());
373 row.push_str(icon);
374 row.push_str(&entry.name);
375 row
376 } else {
377 let size_text = entry.size.to_string();
378 let mut row =
379 String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
380 row.push_str(icon);
381 row.push_str(&entry.name);
382 row.push_str(" ");
383 row.push_str(&size_text);
384 row.push_str(" B");
385 row
386 };
387
388 let style = if idx == state.selected {
389 if focused {
390 Style::new().bold().fg(self.theme.primary)
391 } else {
392 Style::new().fg(self.theme.primary)
393 }
394 } else {
395 Style::new().fg(self.theme.text)
396 };
397 self.styled(row, style);
398 }
399 }
400
401 self.commands.push(Command::EndContainer);
402 self.last_text_idx = None;
403
404 response.changed = file_selected;
405 response
406 }
407
408 pub fn table(&mut self, state: &mut TableState) -> Response {
413 self.table_colored(state, &WidgetColors::new())
414 }
415
416 pub fn table_colored(&mut self, state: &mut TableState, colors: &WidgetColors) -> Response {
417 if state.is_dirty() {
418 state.recompute_widths();
419 }
420
421 let old_selected = state.selected;
422 let old_sort_column = state.sort_column;
423 let old_sort_ascending = state.sort_ascending;
424 let old_page = state.page;
425 let old_filter = state.filter.clone();
426
427 let focused = self.register_focusable();
428 let interaction_id = self.interaction_count;
429 self.interaction_count += 1;
430 let mut response = self.response_for(interaction_id);
431 response.focused = focused;
432
433 self.table_handle_events(state, focused, interaction_id);
434
435 if state.is_dirty() {
436 state.recompute_widths();
437 }
438
439 self.table_render(state, focused, colors);
440
441 response.changed = state.selected != old_selected
442 || state.sort_column != old_sort_column
443 || state.sort_ascending != old_sort_ascending
444 || state.page != old_page
445 || state.filter != old_filter;
446 response
447 }
448
449 fn table_handle_events(
450 &mut self,
451 state: &mut TableState,
452 focused: bool,
453 interaction_id: usize,
454 ) {
455 self.handle_table_keys(state, focused);
456
457 if state.visible_indices().is_empty() && state.headers.is_empty() {
458 return;
459 }
460
461 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
462 for (i, event) in self.events.iter().enumerate() {
463 if self.consumed[i] {
464 continue;
465 }
466 if let Event::Mouse(mouse) = event {
467 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
468 continue;
469 }
470 let in_bounds = mouse.x >= rect.x
471 && mouse.x < rect.right()
472 && mouse.y >= rect.y
473 && mouse.y < rect.bottom();
474 if !in_bounds {
475 continue;
476 }
477
478 if mouse.y == rect.y {
479 let rel_x = mouse.x.saturating_sub(rect.x);
480 let mut x_offset = 0u32;
481 for (col_idx, width) in state.column_widths().iter().enumerate() {
482 if rel_x >= x_offset && rel_x < x_offset + *width {
483 state.toggle_sort(col_idx);
484 state.selected = 0;
485 self.consumed[i] = true;
486 break;
487 }
488 x_offset += *width;
489 if col_idx + 1 < state.column_widths().len() {
490 x_offset += 3;
491 }
492 }
493 continue;
494 }
495
496 if mouse.y < rect.y + 2 {
497 continue;
498 }
499
500 let visible_len = if state.page_size > 0 {
501 let start = state
502 .page
503 .saturating_mul(state.page_size)
504 .min(state.visible_indices().len());
505 let end = (start + state.page_size).min(state.visible_indices().len());
506 end.saturating_sub(start)
507 } else {
508 state.visible_indices().len()
509 };
510 let clicked_idx = (mouse.y - rect.y - 2) as usize;
511 if clicked_idx < visible_len {
512 state.selected = clicked_idx;
513 self.consumed[i] = true;
514 }
515 }
516 }
517 }
518 }
519
520 fn table_render(&mut self, state: &mut TableState, focused: bool, colors: &WidgetColors) {
521 let total_visible = state.visible_indices().len();
522 let page_start = if state.page_size > 0 {
523 state
524 .page
525 .saturating_mul(state.page_size)
526 .min(total_visible)
527 } else {
528 0
529 };
530 let page_end = if state.page_size > 0 {
531 (page_start + state.page_size).min(total_visible)
532 } else {
533 total_visible
534 };
535 let visible_len = page_end.saturating_sub(page_start);
536 state.selected = state.selected.min(visible_len.saturating_sub(1));
537
538 self.commands.push(Command::BeginContainer {
539 direction: Direction::Column,
540 gap: 0,
541 align: Align::Start,
542 align_self: None,
543 justify: Justify::Start,
544 border: None,
545 border_sides: BorderSides::all(),
546 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
547 bg_color: None,
548 padding: Padding::default(),
549 margin: Margin::default(),
550 constraints: Constraints::default(),
551 title: None,
552 grow: 0,
553 group_name: None,
554 });
555
556 self.render_table_header(state, colors);
557 self.render_table_rows(state, focused, page_start, visible_len, colors);
558
559 if state.page_size > 0 && state.total_pages() > 1 {
560 let current_page = (state.page + 1).to_string();
561 let total_pages = state.total_pages().to_string();
562 let mut page_text = String::with_capacity(current_page.len() + total_pages.len() + 6);
563 page_text.push_str("Page ");
564 page_text.push_str(¤t_page);
565 page_text.push('/');
566 page_text.push_str(&total_pages);
567 self.styled(
568 page_text,
569 Style::new()
570 .dim()
571 .fg(colors.fg.unwrap_or(self.theme.text_dim)),
572 );
573 }
574
575 self.commands.push(Command::EndContainer);
576 self.last_text_idx = None;
577 }
578
579 fn handle_table_keys(&mut self, state: &mut TableState, focused: bool) {
580 if !focused || state.visible_indices().is_empty() {
581 return;
582 }
583
584 let mut consumed_indices = Vec::new();
585 for (i, event) in self.events.iter().enumerate() {
586 if let Event::Key(key) = event {
587 if key.kind != KeyEventKind::Press {
588 continue;
589 }
590 match key.code {
591 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
592 let visible_len = table_visible_len(state);
593 state.selected = state.selected.min(visible_len.saturating_sub(1));
594 let _ = handle_vertical_nav(
595 &mut state.selected,
596 visible_len.saturating_sub(1),
597 key.code.clone(),
598 );
599 consumed_indices.push(i);
600 }
601 KeyCode::PageUp => {
602 let old_page = state.page;
603 state.prev_page();
604 if state.page != old_page {
605 state.selected = 0;
606 }
607 consumed_indices.push(i);
608 }
609 KeyCode::PageDown => {
610 let old_page = state.page;
611 state.next_page();
612 if state.page != old_page {
613 state.selected = 0;
614 }
615 consumed_indices.push(i);
616 }
617 _ => {}
618 }
619 }
620 }
621 for index in consumed_indices {
622 self.consumed[index] = true;
623 }
624 }
625
626 fn render_table_header(&mut self, state: &TableState, colors: &WidgetColors) {
627 let header_cells = state
628 .headers
629 .iter()
630 .enumerate()
631 .map(|(i, header)| {
632 if state.sort_column == Some(i) {
633 if state.sort_ascending {
634 let mut sorted_header = String::with_capacity(header.len() + 2);
635 sorted_header.push_str(header);
636 sorted_header.push_str(" ▲");
637 sorted_header
638 } else {
639 let mut sorted_header = String::with_capacity(header.len() + 2);
640 sorted_header.push_str(header);
641 sorted_header.push_str(" ▼");
642 sorted_header
643 }
644 } else {
645 header.clone()
646 }
647 })
648 .collect::<Vec<_>>();
649 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
650 self.styled(
651 header_line,
652 Style::new().bold().fg(colors.fg.unwrap_or(self.theme.text)),
653 );
654
655 let separator = state
656 .column_widths()
657 .iter()
658 .map(|w| "─".repeat(*w as usize))
659 .collect::<Vec<_>>()
660 .join("─┼─");
661 self.text(separator);
662 }
663
664 fn render_table_rows(
665 &mut self,
666 state: &TableState,
667 focused: bool,
668 page_start: usize,
669 visible_len: usize,
670 colors: &WidgetColors,
671 ) {
672 for idx in 0..visible_len {
673 let data_idx = state.visible_indices()[page_start + idx];
674 let Some(row) = state.rows.get(data_idx) else {
675 continue;
676 };
677 let line = format_table_row(row, state.column_widths(), " │ ");
678 if idx == state.selected {
679 let mut style = Style::new()
680 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
681 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
682 if focused {
683 style = style.bold();
684 }
685 self.styled(line, style);
686 } else {
687 self.styled(line, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
688 }
689 }
690 }
691
692 pub fn tabs(&mut self, state: &mut TabsState) -> Response {
697 self.tabs_colored(state, &WidgetColors::new())
698 }
699
700 pub fn tabs_colored(&mut self, state: &mut TabsState, colors: &WidgetColors) -> Response {
701 if state.labels.is_empty() {
702 state.selected = 0;
703 return Response::none();
704 }
705
706 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
707 let old_selected = state.selected;
708 let focused = self.register_focusable();
709 let interaction_id = self.interaction_count;
710 self.interaction_count += 1;
711 let mut response = self.response_for(interaction_id);
712 response.focused = focused;
713
714 if focused {
715 let mut consumed_indices = Vec::new();
716 for (i, event) in self.events.iter().enumerate() {
717 if let Event::Key(key) = event {
718 if key.kind != KeyEventKind::Press {
719 continue;
720 }
721 match key.code {
722 KeyCode::Left => {
723 state.selected = if state.selected == 0 {
724 state.labels.len().saturating_sub(1)
725 } else {
726 state.selected - 1
727 };
728 consumed_indices.push(i);
729 }
730 KeyCode::Right => {
731 if !state.labels.is_empty() {
732 state.selected = (state.selected + 1) % state.labels.len();
733 }
734 consumed_indices.push(i);
735 }
736 _ => {}
737 }
738 }
739 }
740
741 for index in consumed_indices {
742 self.consumed[index] = true;
743 }
744 }
745
746 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
747 for (i, event) in self.events.iter().enumerate() {
748 if self.consumed[i] {
749 continue;
750 }
751 if let Event::Mouse(mouse) = event {
752 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
753 continue;
754 }
755 let in_bounds = mouse.x >= rect.x
756 && mouse.x < rect.right()
757 && mouse.y >= rect.y
758 && mouse.y < rect.bottom();
759 if !in_bounds {
760 continue;
761 }
762
763 let mut x_offset = 0u32;
764 let rel_x = mouse.x - rect.x;
765 for (idx, label) in state.labels.iter().enumerate() {
766 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
767 if rel_x >= x_offset && rel_x < x_offset + tab_width {
768 state.selected = idx;
769 self.consumed[i] = true;
770 break;
771 }
772 x_offset += tab_width + 1;
773 }
774 }
775 }
776 }
777
778 self.commands.push(Command::BeginContainer {
779 direction: Direction::Row,
780 gap: 1,
781 align: Align::Start,
782 align_self: None,
783 justify: Justify::Start,
784 border: None,
785 border_sides: BorderSides::all(),
786 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
787 bg_color: None,
788 padding: Padding::default(),
789 margin: Margin::default(),
790 constraints: Constraints::default(),
791 title: None,
792 grow: 0,
793 group_name: None,
794 });
795 for (idx, label) in state.labels.iter().enumerate() {
796 let style = if idx == state.selected {
797 let s = Style::new()
798 .fg(colors.accent.unwrap_or(self.theme.primary))
799 .bold();
800 if focused {
801 s.underline()
802 } else {
803 s
804 }
805 } else {
806 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
807 };
808 let mut tab = String::with_capacity(label.len() + 4);
809 tab.push_str("[ ");
810 tab.push_str(label);
811 tab.push_str(" ]");
812 self.styled(tab, style);
813 }
814 self.commands.push(Command::EndContainer);
815 self.last_text_idx = None;
816
817 response.changed = state.selected != old_selected;
818 response
819 }
820
821 pub fn button(&mut self, label: impl Into<String>) -> Response {
826 self.button_colored(label, &WidgetColors::new())
827 }
828
829 pub fn button_colored(&mut self, label: impl Into<String>, colors: &WidgetColors) -> Response {
830 let focused = self.register_focusable();
831 let interaction_id = self.interaction_count;
832 self.interaction_count += 1;
833 let mut response = self.response_for(interaction_id);
834 response.focused = focused;
835
836 let mut activated = response.clicked;
837 if focused {
838 let mut consumed_indices = Vec::new();
839 for (i, event) in self.events.iter().enumerate() {
840 if let Event::Key(key) = event {
841 if key.kind != KeyEventKind::Press {
842 continue;
843 }
844 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
845 activated = true;
846 consumed_indices.push(i);
847 }
848 }
849 }
850
851 for index in consumed_indices {
852 self.consumed[index] = true;
853 }
854 }
855
856 let hovered = response.hovered;
857 let base_fg = colors.fg.unwrap_or(self.theme.text);
858 let accent = colors.accent.unwrap_or(self.theme.accent);
859 let base_bg = colors.bg.unwrap_or(self.theme.surface_hover);
860 let style = if focused {
861 Style::new().fg(accent).bold()
862 } else if hovered {
863 Style::new().fg(accent)
864 } else {
865 Style::new().fg(base_fg)
866 };
867 let has_custom_bg = colors.bg.is_some();
868 let bg_color = if has_custom_bg || hovered || focused {
869 Some(base_bg)
870 } else {
871 None
872 };
873
874 self.commands.push(Command::BeginContainer {
875 direction: Direction::Row,
876 gap: 0,
877 align: Align::Start,
878 align_self: None,
879 justify: Justify::Start,
880 border: None,
881 border_sides: BorderSides::all(),
882 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
883 bg_color,
884 padding: Padding::default(),
885 margin: Margin::default(),
886 constraints: Constraints::default(),
887 title: None,
888 grow: 0,
889 group_name: None,
890 });
891 let raw_label = label.into();
892 let mut label_text = String::with_capacity(raw_label.len() + 4);
893 label_text.push_str("[ ");
894 label_text.push_str(&raw_label);
895 label_text.push_str(" ]");
896 self.styled(label_text, style);
897 self.commands.push(Command::EndContainer);
898 self.last_text_idx = None;
899
900 response.clicked = activated;
901 response
902 }
903
904 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> Response {
909 let focused = self.register_focusable();
910 let interaction_id = self.interaction_count;
911 self.interaction_count += 1;
912 let mut response = self.response_for(interaction_id);
913 response.focused = focused;
914
915 let mut activated = response.clicked;
916 if focused {
917 let mut consumed_indices = Vec::new();
918 for (i, event) in self.events.iter().enumerate() {
919 if let Event::Key(key) = event {
920 if key.kind != KeyEventKind::Press {
921 continue;
922 }
923 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
924 activated = true;
925 consumed_indices.push(i);
926 }
927 }
928 }
929 for index in consumed_indices {
930 self.consumed[index] = true;
931 }
932 }
933
934 let label = label.into();
935 let hover_bg = if response.hovered || focused {
936 Some(self.theme.surface_hover)
937 } else {
938 None
939 };
940 let (text, style, bg_color, border) = match variant {
941 ButtonVariant::Default => {
942 let style = if focused {
943 Style::new().fg(self.theme.primary).bold()
944 } else if response.hovered {
945 Style::new().fg(self.theme.accent)
946 } else {
947 Style::new().fg(self.theme.text)
948 };
949 let mut text = String::with_capacity(label.len() + 4);
950 text.push_str("[ ");
951 text.push_str(&label);
952 text.push_str(" ]");
953 (text, style, hover_bg, None)
954 }
955 ButtonVariant::Primary => {
956 let style = if focused {
957 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
958 } else if response.hovered {
959 Style::new().fg(self.theme.bg).bg(self.theme.accent)
960 } else {
961 Style::new().fg(self.theme.bg).bg(self.theme.primary)
962 };
963 let mut text = String::with_capacity(label.len() + 2);
964 text.push(' ');
965 text.push_str(&label);
966 text.push(' ');
967 (text, style, hover_bg, None)
968 }
969 ButtonVariant::Danger => {
970 let style = if focused {
971 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
972 } else if response.hovered {
973 Style::new().fg(self.theme.bg).bg(self.theme.warning)
974 } else {
975 Style::new().fg(self.theme.bg).bg(self.theme.error)
976 };
977 let mut text = String::with_capacity(label.len() + 2);
978 text.push(' ');
979 text.push_str(&label);
980 text.push(' ');
981 (text, style, hover_bg, None)
982 }
983 ButtonVariant::Outline => {
984 let border_color = if focused {
985 self.theme.primary
986 } else if response.hovered {
987 self.theme.accent
988 } else {
989 self.theme.border
990 };
991 let style = if focused {
992 Style::new().fg(self.theme.primary).bold()
993 } else if response.hovered {
994 Style::new().fg(self.theme.accent)
995 } else {
996 Style::new().fg(self.theme.text)
997 };
998 (
999 {
1000 let mut text = String::with_capacity(label.len() + 2);
1001 text.push(' ');
1002 text.push_str(&label);
1003 text.push(' ');
1004 text
1005 },
1006 style,
1007 hover_bg,
1008 Some((Border::Rounded, Style::new().fg(border_color))),
1009 )
1010 }
1011 };
1012
1013 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
1014 self.commands.push(Command::BeginContainer {
1015 direction: Direction::Row,
1016 gap: 0,
1017 align: Align::Center,
1018 align_self: None,
1019 justify: Justify::Center,
1020 border: if border.is_some() {
1021 Some(btn_border)
1022 } else {
1023 None
1024 },
1025 border_sides: BorderSides::all(),
1026 border_style: btn_border_style,
1027 bg_color,
1028 padding: Padding::default(),
1029 margin: Margin::default(),
1030 constraints: Constraints::default(),
1031 title: None,
1032 grow: 0,
1033 group_name: None,
1034 });
1035 self.styled(text, style);
1036 self.commands.push(Command::EndContainer);
1037 self.last_text_idx = None;
1038
1039 response.clicked = activated;
1040 response
1041 }
1042
1043 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> Response {
1048 self.checkbox_colored(label, checked, &WidgetColors::new())
1049 }
1050
1051 pub fn checkbox_colored(
1052 &mut self,
1053 label: impl Into<String>,
1054 checked: &mut bool,
1055 colors: &WidgetColors,
1056 ) -> Response {
1057 let focused = self.register_focusable();
1058 let interaction_id = self.interaction_count;
1059 self.interaction_count += 1;
1060 let mut response = self.response_for(interaction_id);
1061 response.focused = focused;
1062 let mut should_toggle = response.clicked;
1063 let old_checked = *checked;
1064
1065 if focused {
1066 let mut consumed_indices = Vec::new();
1067 for (i, event) in self.events.iter().enumerate() {
1068 if let Event::Key(key) = event {
1069 if key.kind != KeyEventKind::Press {
1070 continue;
1071 }
1072 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1073 should_toggle = true;
1074 consumed_indices.push(i);
1075 }
1076 }
1077 }
1078
1079 for index in consumed_indices {
1080 self.consumed[index] = true;
1081 }
1082 }
1083
1084 if should_toggle {
1085 *checked = !*checked;
1086 }
1087
1088 let hover_bg = if response.hovered || focused {
1089 Some(self.theme.surface_hover)
1090 } else {
1091 None
1092 };
1093 self.commands.push(Command::BeginContainer {
1094 direction: Direction::Row,
1095 gap: 1,
1096 align: Align::Start,
1097 align_self: None,
1098 justify: Justify::Start,
1099 border: None,
1100 border_sides: BorderSides::all(),
1101 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1102 bg_color: hover_bg,
1103 padding: Padding::default(),
1104 margin: Margin::default(),
1105 constraints: Constraints::default(),
1106 title: None,
1107 grow: 0,
1108 group_name: None,
1109 });
1110 let marker_style = if *checked {
1111 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1112 } else {
1113 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1114 };
1115 let marker = if *checked { "[x]" } else { "[ ]" };
1116 let label_text = label.into();
1117 if focused {
1118 let mut marker_text = String::with_capacity(2 + marker.len());
1119 marker_text.push_str("▸ ");
1120 marker_text.push_str(marker);
1121 self.styled(marker_text, marker_style.bold());
1122 self.styled(
1123 label_text,
1124 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1125 );
1126 } else {
1127 self.styled(marker, marker_style);
1128 self.styled(
1129 label_text,
1130 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1131 );
1132 }
1133 self.commands.push(Command::EndContainer);
1134 self.last_text_idx = None;
1135
1136 response.changed = *checked != old_checked;
1137 response
1138 }
1139
1140 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> Response {
1146 self.toggle_colored(label, on, &WidgetColors::new())
1147 }
1148
1149 pub fn toggle_colored(
1150 &mut self,
1151 label: impl Into<String>,
1152 on: &mut bool,
1153 colors: &WidgetColors,
1154 ) -> Response {
1155 let focused = self.register_focusable();
1156 let interaction_id = self.interaction_count;
1157 self.interaction_count += 1;
1158 let mut response = self.response_for(interaction_id);
1159 response.focused = focused;
1160 let mut should_toggle = response.clicked;
1161 let old_on = *on;
1162
1163 if focused {
1164 let mut consumed_indices = Vec::new();
1165 for (i, event) in self.events.iter().enumerate() {
1166 if let Event::Key(key) = event {
1167 if key.kind != KeyEventKind::Press {
1168 continue;
1169 }
1170 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1171 should_toggle = true;
1172 consumed_indices.push(i);
1173 }
1174 }
1175 }
1176
1177 for index in consumed_indices {
1178 self.consumed[index] = true;
1179 }
1180 }
1181
1182 if should_toggle {
1183 *on = !*on;
1184 }
1185
1186 let hover_bg = if response.hovered || focused {
1187 Some(self.theme.surface_hover)
1188 } else {
1189 None
1190 };
1191 self.commands.push(Command::BeginContainer {
1192 direction: Direction::Row,
1193 gap: 2,
1194 align: Align::Start,
1195 align_self: None,
1196 justify: Justify::Start,
1197 border: None,
1198 border_sides: BorderSides::all(),
1199 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1200 bg_color: hover_bg,
1201 padding: Padding::default(),
1202 margin: Margin::default(),
1203 constraints: Constraints::default(),
1204 title: None,
1205 grow: 0,
1206 group_name: None,
1207 });
1208 let label_text = label.into();
1209 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1210 let switch_style = if *on {
1211 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1212 } else {
1213 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1214 };
1215 if focused {
1216 let mut focused_label = String::with_capacity(2 + label_text.len());
1217 focused_label.push_str("▸ ");
1218 focused_label.push_str(&label_text);
1219 self.styled(
1220 focused_label,
1221 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1222 );
1223 self.styled(switch, switch_style.bold());
1224 } else {
1225 self.styled(
1226 label_text,
1227 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1228 );
1229 self.styled(switch, switch_style);
1230 }
1231 self.commands.push(Command::EndContainer);
1232 self.last_text_idx = None;
1233
1234 response.changed = *on != old_on;
1235 response
1236 }
1237
1238 pub fn select(&mut self, state: &mut SelectState) -> Response {
1244 self.select_colored(state, &WidgetColors::new())
1245 }
1246
1247 pub fn select_colored(&mut self, state: &mut SelectState, colors: &WidgetColors) -> Response {
1248 if state.items.is_empty() {
1249 return Response::none();
1250 }
1251 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1252
1253 let focused = self.register_focusable();
1254 let interaction_id = self.interaction_count;
1255 self.interaction_count += 1;
1256 let mut response = self.response_for(interaction_id);
1257 response.focused = focused;
1258 let old_selected = state.selected;
1259
1260 self.select_handle_events(state, focused, response.clicked);
1261 self.select_render(state, focused, colors);
1262 response.changed = state.selected != old_selected;
1263 response
1264 }
1265
1266 fn select_handle_events(&mut self, state: &mut SelectState, focused: bool, clicked: bool) {
1267 if clicked {
1268 state.open = !state.open;
1269 if state.open {
1270 state.set_cursor(state.selected);
1271 }
1272 }
1273
1274 if !focused {
1275 return;
1276 }
1277
1278 let mut consumed_indices = Vec::new();
1279 for (i, event) in self.events.iter().enumerate() {
1280 if self.consumed[i] {
1281 continue;
1282 }
1283 if let Event::Key(key) = event {
1284 if key.kind != KeyEventKind::Press {
1285 continue;
1286 }
1287 if state.open {
1288 match key.code {
1289 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1290 let mut cursor = state.cursor();
1291 let _ = handle_vertical_nav(
1292 &mut cursor,
1293 state.items.len().saturating_sub(1),
1294 key.code.clone(),
1295 );
1296 state.set_cursor(cursor);
1297 consumed_indices.push(i);
1298 }
1299 KeyCode::Enter | KeyCode::Char(' ') => {
1300 state.selected = state.cursor();
1301 state.open = false;
1302 consumed_indices.push(i);
1303 }
1304 KeyCode::Esc => {
1305 state.open = false;
1306 consumed_indices.push(i);
1307 }
1308 _ => {}
1309 }
1310 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1311 state.open = true;
1312 state.set_cursor(state.selected);
1313 consumed_indices.push(i);
1314 }
1315 }
1316 }
1317 for idx in consumed_indices {
1318 self.consumed[idx] = true;
1319 }
1320 }
1321
1322 fn select_render(&mut self, state: &SelectState, focused: bool, colors: &WidgetColors) {
1323 let border_color = if focused {
1324 colors.accent.unwrap_or(self.theme.primary)
1325 } else {
1326 colors.border.unwrap_or(self.theme.border)
1327 };
1328 let display_text = state
1329 .items
1330 .get(state.selected)
1331 .cloned()
1332 .unwrap_or_else(|| state.placeholder.clone());
1333 let arrow = if state.open { "▲" } else { "▼" };
1334
1335 self.commands.push(Command::BeginContainer {
1336 direction: Direction::Column,
1337 gap: 0,
1338 align: Align::Start,
1339 align_self: None,
1340 justify: Justify::Start,
1341 border: None,
1342 border_sides: BorderSides::all(),
1343 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1344 bg_color: None,
1345 padding: Padding::default(),
1346 margin: Margin::default(),
1347 constraints: Constraints::default(),
1348 title: None,
1349 grow: 0,
1350 group_name: None,
1351 });
1352
1353 self.render_select_trigger(&display_text, arrow, border_color, colors);
1354
1355 if state.open {
1356 self.render_select_dropdown(state, colors);
1357 }
1358
1359 self.commands.push(Command::EndContainer);
1360 self.last_text_idx = None;
1361 }
1362
1363 fn render_select_trigger(
1364 &mut self,
1365 display_text: &str,
1366 arrow: &str,
1367 border_color: Color,
1368 colors: &WidgetColors,
1369 ) {
1370 self.commands.push(Command::BeginContainer {
1371 direction: Direction::Row,
1372 gap: 1,
1373 align: Align::Start,
1374 align_self: None,
1375 justify: Justify::Start,
1376 border: Some(Border::Rounded),
1377 border_sides: BorderSides::all(),
1378 border_style: Style::new().fg(border_color),
1379 bg_color: None,
1380 padding: Padding {
1381 left: 1,
1382 right: 1,
1383 top: 0,
1384 bottom: 0,
1385 },
1386 margin: Margin::default(),
1387 constraints: Constraints::default(),
1388 title: None,
1389 grow: 0,
1390 group_name: None,
1391 });
1392 self.interaction_count += 1;
1393 self.styled(
1394 display_text,
1395 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1396 );
1397 self.styled(
1398 arrow,
1399 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim)),
1400 );
1401 self.commands.push(Command::EndContainer);
1402 self.last_text_idx = None;
1403 }
1404
1405 fn render_select_dropdown(&mut self, state: &SelectState, colors: &WidgetColors) {
1406 for (idx, item) in state.items.iter().enumerate() {
1407 let is_cursor = idx == state.cursor();
1408 let style = if is_cursor {
1409 Style::new()
1410 .bold()
1411 .fg(colors.accent.unwrap_or(self.theme.primary))
1412 } else {
1413 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1414 };
1415 let prefix = if is_cursor { "▸ " } else { " " };
1416 let mut row = String::with_capacity(prefix.len() + item.len());
1417 row.push_str(prefix);
1418 row.push_str(item);
1419 self.styled(row, style);
1420 }
1421 }
1422
1423 pub fn radio(&mut self, state: &mut RadioState) -> Response {
1427 self.radio_colored(state, &WidgetColors::new())
1428 }
1429
1430 pub fn radio_colored(&mut self, state: &mut RadioState, colors: &WidgetColors) -> Response {
1431 if state.items.is_empty() {
1432 return Response::none();
1433 }
1434 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1435 let focused = self.register_focusable();
1436 let old_selected = state.selected;
1437
1438 if focused {
1439 let mut consumed_indices = Vec::new();
1440 for (i, event) in self.events.iter().enumerate() {
1441 if self.consumed[i] {
1442 continue;
1443 }
1444 if let Event::Key(key) = event {
1445 if key.kind != KeyEventKind::Press {
1446 continue;
1447 }
1448 match key.code {
1449 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1450 let _ = handle_vertical_nav(
1451 &mut state.selected,
1452 state.items.len().saturating_sub(1),
1453 key.code.clone(),
1454 );
1455 consumed_indices.push(i);
1456 }
1457 KeyCode::Enter | KeyCode::Char(' ') => {
1458 consumed_indices.push(i);
1459 }
1460 _ => {}
1461 }
1462 }
1463 }
1464 for idx in consumed_indices {
1465 self.consumed[idx] = true;
1466 }
1467 }
1468
1469 let interaction_id = self.interaction_count;
1470 self.interaction_count += 1;
1471 let mut response = self.response_for(interaction_id);
1472 response.focused = focused;
1473
1474 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1475 for (i, event) in self.events.iter().enumerate() {
1476 if self.consumed[i] {
1477 continue;
1478 }
1479 if let Event::Mouse(mouse) = event {
1480 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1481 continue;
1482 }
1483 let in_bounds = mouse.x >= rect.x
1484 && mouse.x < rect.right()
1485 && mouse.y >= rect.y
1486 && mouse.y < rect.bottom();
1487 if !in_bounds {
1488 continue;
1489 }
1490 let clicked_idx = (mouse.y - rect.y) as usize;
1491 if clicked_idx < state.items.len() {
1492 state.selected = clicked_idx;
1493 self.consumed[i] = true;
1494 }
1495 }
1496 }
1497 }
1498
1499 self.commands.push(Command::BeginContainer {
1500 direction: Direction::Column,
1501 gap: 0,
1502 align: Align::Start,
1503 align_self: None,
1504 justify: Justify::Start,
1505 border: None,
1506 border_sides: BorderSides::all(),
1507 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1508 bg_color: None,
1509 padding: Padding::default(),
1510 margin: Margin::default(),
1511 constraints: Constraints::default(),
1512 title: None,
1513 grow: 0,
1514 group_name: None,
1515 });
1516
1517 for (idx, item) in state.items.iter().enumerate() {
1518 let is_selected = idx == state.selected;
1519 let marker = if is_selected { "●" } else { "○" };
1520 let style = if is_selected {
1521 if focused {
1522 Style::new()
1523 .bold()
1524 .fg(colors.accent.unwrap_or(self.theme.primary))
1525 } else {
1526 Style::new().fg(colors.accent.unwrap_or(self.theme.primary))
1527 }
1528 } else {
1529 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1530 };
1531 let prefix = if focused && idx == state.selected {
1532 "▸ "
1533 } else {
1534 " "
1535 };
1536 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1537 row.push_str(prefix);
1538 row.push_str(marker);
1539 row.push(' ');
1540 row.push_str(item);
1541 self.styled(row, style);
1542 }
1543
1544 self.commands.push(Command::EndContainer);
1545 self.last_text_idx = None;
1546 response.changed = state.selected != old_selected;
1547 response
1548 }
1549
1550 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> Response {
1554 if state.items.is_empty() {
1555 return Response::none();
1556 }
1557 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
1558 let focused = self.register_focusable();
1559 let old_selected = state.selected.clone();
1560
1561 if focused {
1562 let mut consumed_indices = Vec::new();
1563 for (i, event) in self.events.iter().enumerate() {
1564 if self.consumed[i] {
1565 continue;
1566 }
1567 if let Event::Key(key) = event {
1568 if key.kind != KeyEventKind::Press {
1569 continue;
1570 }
1571 match key.code {
1572 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1573 let _ = handle_vertical_nav(
1574 &mut state.cursor,
1575 state.items.len().saturating_sub(1),
1576 key.code.clone(),
1577 );
1578 consumed_indices.push(i);
1579 }
1580 KeyCode::Char(' ') | KeyCode::Enter => {
1581 state.toggle(state.cursor);
1582 consumed_indices.push(i);
1583 }
1584 _ => {}
1585 }
1586 }
1587 }
1588 for idx in consumed_indices {
1589 self.consumed[idx] = true;
1590 }
1591 }
1592
1593 let interaction_id = self.interaction_count;
1594 self.interaction_count += 1;
1595 let mut response = self.response_for(interaction_id);
1596 response.focused = focused;
1597
1598 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1599 for (i, event) in self.events.iter().enumerate() {
1600 if self.consumed[i] {
1601 continue;
1602 }
1603 if let Event::Mouse(mouse) = event {
1604 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1605 continue;
1606 }
1607 let in_bounds = mouse.x >= rect.x
1608 && mouse.x < rect.right()
1609 && mouse.y >= rect.y
1610 && mouse.y < rect.bottom();
1611 if !in_bounds {
1612 continue;
1613 }
1614 let clicked_idx = (mouse.y - rect.y) as usize;
1615 if clicked_idx < state.items.len() {
1616 state.toggle(clicked_idx);
1617 state.cursor = clicked_idx;
1618 self.consumed[i] = true;
1619 }
1620 }
1621 }
1622 }
1623
1624 self.commands.push(Command::BeginContainer {
1625 direction: Direction::Column,
1626 gap: 0,
1627 align: Align::Start,
1628 align_self: None,
1629 justify: Justify::Start,
1630 border: None,
1631 border_sides: BorderSides::all(),
1632 border_style: Style::new().fg(self.theme.border),
1633 bg_color: None,
1634 padding: Padding::default(),
1635 margin: Margin::default(),
1636 constraints: Constraints::default(),
1637 title: None,
1638 grow: 0,
1639 group_name: None,
1640 });
1641
1642 for (idx, item) in state.items.iter().enumerate() {
1643 let checked = state.selected.contains(&idx);
1644 let marker = if checked { "[x]" } else { "[ ]" };
1645 let is_cursor = idx == state.cursor;
1646 let style = if is_cursor && focused {
1647 Style::new().bold().fg(self.theme.primary)
1648 } else if checked {
1649 Style::new().fg(self.theme.success)
1650 } else {
1651 Style::new().fg(self.theme.text)
1652 };
1653 let prefix = if is_cursor && focused { "▸ " } else { " " };
1654 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1655 row.push_str(prefix);
1656 row.push_str(marker);
1657 row.push(' ');
1658 row.push_str(item);
1659 self.styled(row, style);
1660 }
1661
1662 self.commands.push(Command::EndContainer);
1663 self.last_text_idx = None;
1664 response.changed = state.selected != old_selected;
1665 response
1666 }
1667
1668 pub fn tree(&mut self, state: &mut TreeState) -> Response {
1672 let entries = state.flatten();
1673 if entries.is_empty() {
1674 return Response::none();
1675 }
1676 state.selected = state.selected.min(entries.len().saturating_sub(1));
1677 let old_selected = state.selected;
1678 let focused = self.register_focusable();
1679 let interaction_id = self.interaction_count;
1680 self.interaction_count += 1;
1681 let mut response = self.response_for(interaction_id);
1682 response.focused = focused;
1683 let mut changed = false;
1684
1685 if focused {
1686 let mut consumed_indices = Vec::new();
1687 for (i, event) in self.events.iter().enumerate() {
1688 if self.consumed[i] {
1689 continue;
1690 }
1691 if let Event::Key(key) = event {
1692 if key.kind != KeyEventKind::Press {
1693 continue;
1694 }
1695 match key.code {
1696 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1697 let max_index = state.flatten().len().saturating_sub(1);
1698 let _ = handle_vertical_nav(
1699 &mut state.selected,
1700 max_index,
1701 key.code.clone(),
1702 );
1703 changed = changed || state.selected != old_selected;
1704 consumed_indices.push(i);
1705 }
1706 KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
1707 state.toggle_at(state.selected);
1708 changed = true;
1709 consumed_indices.push(i);
1710 }
1711 KeyCode::Left => {
1712 let entry = &entries[state.selected.min(entries.len() - 1)];
1713 if entry.expanded {
1714 state.toggle_at(state.selected);
1715 changed = true;
1716 }
1717 consumed_indices.push(i);
1718 }
1719 _ => {}
1720 }
1721 }
1722 }
1723 for idx in consumed_indices {
1724 self.consumed[idx] = true;
1725 }
1726 }
1727
1728 self.commands.push(Command::BeginContainer {
1729 direction: Direction::Column,
1730 gap: 0,
1731 align: Align::Start,
1732 align_self: None,
1733 justify: Justify::Start,
1734 border: None,
1735 border_sides: BorderSides::all(),
1736 border_style: Style::new().fg(self.theme.border),
1737 bg_color: None,
1738 padding: Padding::default(),
1739 margin: Margin::default(),
1740 constraints: Constraints::default(),
1741 title: None,
1742 grow: 0,
1743 group_name: None,
1744 });
1745
1746 let entries = state.flatten();
1747 for (idx, entry) in entries.iter().enumerate() {
1748 let indent = " ".repeat(entry.depth);
1749 let icon = if entry.is_leaf {
1750 " "
1751 } else if entry.expanded {
1752 "▾ "
1753 } else {
1754 "▸ "
1755 };
1756 let is_selected = idx == state.selected;
1757 let style = if is_selected && focused {
1758 Style::new().bold().fg(self.theme.primary)
1759 } else if is_selected {
1760 Style::new().fg(self.theme.primary)
1761 } else {
1762 Style::new().fg(self.theme.text)
1763 };
1764 let cursor = if is_selected && focused { "▸" } else { " " };
1765 let mut row =
1766 String::with_capacity(cursor.len() + indent.len() + icon.len() + entry.label.len());
1767 row.push_str(cursor);
1768 row.push_str(&indent);
1769 row.push_str(icon);
1770 row.push_str(&entry.label);
1771 self.styled(row, style);
1772 }
1773
1774 self.commands.push(Command::EndContainer);
1775 self.last_text_idx = None;
1776 response.changed = changed || state.selected != old_selected;
1777 response
1778 }
1779
1780 pub fn virtual_list(
1787 &mut self,
1788 state: &mut ListState,
1789 visible_height: usize,
1790 f: impl Fn(&mut Context, usize),
1791 ) -> Response {
1792 if state.items.is_empty() {
1793 return Response::none();
1794 }
1795 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1796 let interaction_id = self.interaction_count;
1797 self.interaction_count += 1;
1798 let focused = self.register_focusable();
1799 let old_selected = state.selected;
1800
1801 if focused {
1802 let mut consumed_indices = Vec::new();
1803 for (i, event) in self.events.iter().enumerate() {
1804 if self.consumed[i] {
1805 continue;
1806 }
1807 if let Event::Key(key) = event {
1808 if key.kind != KeyEventKind::Press {
1809 continue;
1810 }
1811 match key.code {
1812 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1813 let _ = handle_vertical_nav(
1814 &mut state.selected,
1815 state.items.len().saturating_sub(1),
1816 key.code.clone(),
1817 );
1818 consumed_indices.push(i);
1819 }
1820 KeyCode::PageUp => {
1821 state.selected = state.selected.saturating_sub(visible_height);
1822 consumed_indices.push(i);
1823 }
1824 KeyCode::PageDown => {
1825 state.selected = (state.selected + visible_height)
1826 .min(state.items.len().saturating_sub(1));
1827 consumed_indices.push(i);
1828 }
1829 KeyCode::Home => {
1830 state.selected = 0;
1831 consumed_indices.push(i);
1832 }
1833 KeyCode::End => {
1834 state.selected = state.items.len().saturating_sub(1);
1835 consumed_indices.push(i);
1836 }
1837 _ => {}
1838 }
1839 }
1840 }
1841 for idx in consumed_indices {
1842 self.consumed[idx] = true;
1843 }
1844 }
1845
1846 let start = if state.selected >= visible_height {
1847 state.selected - visible_height + 1
1848 } else {
1849 0
1850 };
1851 let end = (start + visible_height).min(state.items.len());
1852
1853 self.commands.push(Command::BeginContainer {
1854 direction: Direction::Column,
1855 gap: 0,
1856 align: Align::Start,
1857 align_self: None,
1858 justify: Justify::Start,
1859 border: None,
1860 border_sides: BorderSides::all(),
1861 border_style: Style::new().fg(self.theme.border),
1862 bg_color: None,
1863 padding: Padding::default(),
1864 margin: Margin::default(),
1865 constraints: Constraints::default(),
1866 title: None,
1867 grow: 0,
1868 group_name: None,
1869 });
1870
1871 if start > 0 {
1872 let hidden = start.to_string();
1873 let mut line = String::with_capacity(hidden.len() + 10);
1874 line.push_str(" ↑ ");
1875 line.push_str(&hidden);
1876 line.push_str(" more");
1877 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
1878 }
1879
1880 for idx in start..end {
1881 f(self, idx);
1882 }
1883
1884 let remaining = state.items.len().saturating_sub(end);
1885 if remaining > 0 {
1886 let hidden = remaining.to_string();
1887 let mut line = String::with_capacity(hidden.len() + 10);
1888 line.push_str(" ↓ ");
1889 line.push_str(&hidden);
1890 line.push_str(" more");
1891 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
1892 }
1893
1894 self.commands.push(Command::EndContainer);
1895 self.last_text_idx = None;
1896 let mut response = self.response_for(interaction_id);
1897 response.focused = focused;
1898 response.changed = state.selected != old_selected;
1899 response
1900 }
1901
1902 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
1906 if !state.open {
1907 return Response::none();
1908 }
1909
1910 state.last_selected = None;
1911 let interaction_id = self.interaction_count;
1912 self.interaction_count += 1;
1913
1914 let filtered = state.filtered_indices();
1915 let sel = state.selected().min(filtered.len().saturating_sub(1));
1916 state.set_selected(sel);
1917
1918 let mut consumed_indices = Vec::new();
1919
1920 for (i, event) in self.events.iter().enumerate() {
1921 if self.consumed[i] {
1922 continue;
1923 }
1924 if let Event::Key(key) = event {
1925 if key.kind != KeyEventKind::Press {
1926 continue;
1927 }
1928 match key.code {
1929 KeyCode::Esc => {
1930 state.open = false;
1931 consumed_indices.push(i);
1932 }
1933 KeyCode::Up => {
1934 let s = state.selected();
1935 state.set_selected(s.saturating_sub(1));
1936 consumed_indices.push(i);
1937 }
1938 KeyCode::Down => {
1939 let s = state.selected();
1940 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
1941 consumed_indices.push(i);
1942 }
1943 KeyCode::Enter => {
1944 if let Some(&cmd_idx) = filtered.get(state.selected()) {
1945 state.last_selected = Some(cmd_idx);
1946 state.open = false;
1947 }
1948 consumed_indices.push(i);
1949 }
1950 KeyCode::Backspace => {
1951 if state.cursor > 0 {
1952 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
1953 let end_idx = byte_index_for_char(&state.input, state.cursor);
1954 state.input.replace_range(byte_idx..end_idx, "");
1955 state.cursor -= 1;
1956 state.set_selected(0);
1957 }
1958 consumed_indices.push(i);
1959 }
1960 KeyCode::Char(ch) => {
1961 let byte_idx = byte_index_for_char(&state.input, state.cursor);
1962 state.input.insert(byte_idx, ch);
1963 state.cursor += 1;
1964 state.set_selected(0);
1965 consumed_indices.push(i);
1966 }
1967 _ => {}
1968 }
1969 }
1970 }
1971 for idx in consumed_indices {
1972 self.consumed[idx] = true;
1973 }
1974
1975 let filtered = state.filtered_indices();
1976
1977 let _ = self.modal(|ui| {
1978 let primary = ui.theme.primary;
1979 let _ = ui
1980 .container()
1981 .border(Border::Rounded)
1982 .border_style(Style::new().fg(primary))
1983 .pad(1)
1984 .max_w(60)
1985 .col(|ui| {
1986 let border_color = ui.theme.primary;
1987 let _ = ui
1988 .bordered(Border::Rounded)
1989 .border_style(Style::new().fg(border_color))
1990 .px(1)
1991 .col(|ui| {
1992 let display = if state.input.is_empty() {
1993 "Type to search...".to_string()
1994 } else {
1995 state.input.clone()
1996 };
1997 let style = if state.input.is_empty() {
1998 Style::new().dim().fg(ui.theme.text_dim)
1999 } else {
2000 Style::new().fg(ui.theme.text)
2001 };
2002 ui.styled(display, style);
2003 });
2004
2005 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
2006 let cmd = &state.commands[cmd_idx];
2007 let is_selected = list_idx == state.selected();
2008 let style = if is_selected {
2009 Style::new().bold().fg(ui.theme.primary)
2010 } else {
2011 Style::new().fg(ui.theme.text)
2012 };
2013 let prefix = if is_selected { "▸ " } else { " " };
2014 let shortcut_text = cmd
2015 .shortcut
2016 .as_deref()
2017 .map(|s| {
2018 let mut text = String::with_capacity(s.len() + 4);
2019 text.push_str(" (");
2020 text.push_str(s);
2021 text.push(')');
2022 text
2023 })
2024 .unwrap_or_default();
2025 let mut line = String::with_capacity(
2026 prefix.len() + cmd.label.len() + shortcut_text.len(),
2027 );
2028 line.push_str(prefix);
2029 line.push_str(&cmd.label);
2030 line.push_str(&shortcut_text);
2031 ui.styled(line, style);
2032 if is_selected && !cmd.description.is_empty() {
2033 let mut desc = String::with_capacity(4 + cmd.description.len());
2034 desc.push_str(" ");
2035 desc.push_str(&cmd.description);
2036 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
2037 }
2038 }
2039
2040 if filtered.is_empty() {
2041 ui.styled(
2042 " No matching commands",
2043 Style::new().dim().fg(ui.theme.text_dim),
2044 );
2045 }
2046 });
2047 });
2048
2049 let mut response = self.response_for(interaction_id);
2050 response.changed = state.last_selected.is_some();
2051 response
2052 }
2053
2054 pub fn markdown(&mut self, text: &str) -> Response {
2061 self.commands.push(Command::BeginContainer {
2062 direction: Direction::Column,
2063 gap: 0,
2064 align: Align::Start,
2065 align_self: None,
2066 justify: Justify::Start,
2067 border: None,
2068 border_sides: BorderSides::all(),
2069 border_style: Style::new().fg(self.theme.border),
2070 bg_color: None,
2071 padding: Padding::default(),
2072 margin: Margin::default(),
2073 constraints: Constraints::default(),
2074 title: None,
2075 grow: 0,
2076 group_name: None,
2077 });
2078 self.interaction_count += 1;
2079
2080 let text_style = Style::new().fg(self.theme.text);
2081 let bold_style = Style::new().fg(self.theme.text).bold();
2082 let code_style = Style::new().fg(self.theme.accent);
2083
2084 for line in text.lines() {
2085 let trimmed = line.trim();
2086 if trimmed.is_empty() {
2087 self.text(" ");
2088 continue;
2089 }
2090 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
2091 self.styled("─".repeat(40), Style::new().fg(self.theme.border).dim());
2092 continue;
2093 }
2094 if let Some(heading) = trimmed.strip_prefix("### ") {
2095 self.styled(heading, Style::new().bold().fg(self.theme.accent));
2096 } else if let Some(heading) = trimmed.strip_prefix("## ") {
2097 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
2098 } else if let Some(heading) = trimmed.strip_prefix("# ") {
2099 self.styled(heading, Style::new().bold().fg(self.theme.primary));
2100 } else if let Some(item) = trimmed
2101 .strip_prefix("- ")
2102 .or_else(|| trimmed.strip_prefix("* "))
2103 {
2104 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
2105 if segs.len() <= 1 {
2106 let mut line = String::with_capacity(4 + item.len());
2107 line.push_str(" • ");
2108 line.push_str(item);
2109 self.styled(line, text_style);
2110 } else {
2111 self.line(|ui| {
2112 ui.styled(" • ", text_style);
2113 for (s, st) in segs {
2114 ui.styled(s, st);
2115 }
2116 });
2117 }
2118 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
2119 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
2120 if parts.len() == 2 {
2121 let segs =
2122 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
2123 if segs.len() <= 1 {
2124 let mut line = String::with_capacity(4 + parts[0].len() + parts[1].len());
2125 line.push_str(" ");
2126 line.push_str(parts[0]);
2127 line.push_str(". ");
2128 line.push_str(parts[1]);
2129 self.styled(line, text_style);
2130 } else {
2131 self.line(|ui| {
2132 let mut prefix = String::with_capacity(4 + parts[0].len());
2133 prefix.push_str(" ");
2134 prefix.push_str(parts[0]);
2135 prefix.push_str(". ");
2136 ui.styled(prefix, text_style);
2137 for (s, st) in segs {
2138 ui.styled(s, st);
2139 }
2140 });
2141 }
2142 } else {
2143 self.text(trimmed);
2144 }
2145 } else if let Some(code) = trimmed.strip_prefix("```") {
2146 let _ = code;
2147 self.styled(" ┌─code─", Style::new().fg(self.theme.border).dim());
2148 } else {
2149 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
2150 if segs.len() <= 1 {
2151 self.styled(trimmed, text_style);
2152 } else {
2153 self.line(|ui| {
2154 for (s, st) in segs {
2155 ui.styled(s, st);
2156 }
2157 });
2158 }
2159 }
2160 }
2161
2162 self.commands.push(Command::EndContainer);
2163 self.last_text_idx = None;
2164 Response::none()
2165 }
2166
2167 pub(crate) fn parse_inline_segments(
2168 text: &str,
2169 base: Style,
2170 bold: Style,
2171 code: Style,
2172 ) -> Vec<(String, Style)> {
2173 let mut segments: Vec<(String, Style)> = Vec::new();
2174 let mut current = String::new();
2175 let chars: Vec<char> = text.chars().collect();
2176 let mut i = 0;
2177 while i < chars.len() {
2178 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
2179 let rest: String = chars[i + 2..].iter().collect();
2180 if let Some(end) = rest.find("**") {
2181 if !current.is_empty() {
2182 segments.push((std::mem::take(&mut current), base));
2183 }
2184 let inner: String = rest[..end].to_string();
2185 let char_count = inner.chars().count();
2186 segments.push((inner, bold));
2187 i += 2 + char_count + 2;
2188 continue;
2189 }
2190 }
2191 if chars[i] == '*'
2192 && (i + 1 >= chars.len() || chars[i + 1] != '*')
2193 && (i == 0 || chars[i - 1] != '*')
2194 {
2195 let rest: String = chars[i + 1..].iter().collect();
2196 if let Some(end) = rest.find('*') {
2197 if !current.is_empty() {
2198 segments.push((std::mem::take(&mut current), base));
2199 }
2200 let inner: String = rest[..end].to_string();
2201 let char_count = inner.chars().count();
2202 segments.push((inner, base.italic()));
2203 i += 1 + char_count + 1;
2204 continue;
2205 }
2206 }
2207 if chars[i] == '`' {
2208 let rest: String = chars[i + 1..].iter().collect();
2209 if let Some(end) = rest.find('`') {
2210 if !current.is_empty() {
2211 segments.push((std::mem::take(&mut current), base));
2212 }
2213 let inner: String = rest[..end].to_string();
2214 let char_count = inner.chars().count();
2215 segments.push((inner, code));
2216 i += 1 + char_count + 1;
2217 continue;
2218 }
2219 }
2220 current.push(chars[i]);
2221 i += 1;
2222 }
2223 if !current.is_empty() {
2224 segments.push((current, base));
2225 }
2226 segments
2227 }
2228
2229 pub fn key_seq(&self, seq: &str) -> bool {
2236 if seq.is_empty() {
2237 return false;
2238 }
2239 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2240 return false;
2241 }
2242 let target: Vec<char> = seq.chars().collect();
2243 let mut matched = 0;
2244 for (i, event) in self.events.iter().enumerate() {
2245 if self.consumed[i] {
2246 continue;
2247 }
2248 if let Event::Key(key) = event {
2249 if key.kind != KeyEventKind::Press {
2250 continue;
2251 }
2252 if let KeyCode::Char(c) = key.code {
2253 if c == target[matched] {
2254 matched += 1;
2255 if matched == target.len() {
2256 return true;
2257 }
2258 } else {
2259 matched = 0;
2260 if c == target[0] {
2261 matched = 1;
2262 }
2263 }
2264 }
2265 }
2266 }
2267 false
2268 }
2269
2270 pub fn separator(&mut self) -> &mut Self {
2275 self.commands.push(Command::Text {
2276 content: "─".repeat(200),
2277 style: Style::new().fg(self.theme.border).dim(),
2278 grow: 0,
2279 align: Align::Start,
2280 wrap: false,
2281 truncate: false,
2282 margin: Margin::default(),
2283 constraints: Constraints::default(),
2284 });
2285 self.last_text_idx = Some(self.commands.len() - 1);
2286 self
2287 }
2288
2289 pub fn separator_colored(&mut self, color: Color) -> &mut Self {
2290 self.commands.push(Command::Text {
2291 content: "─".repeat(200),
2292 style: Style::new().fg(color),
2293 grow: 0,
2294 align: Align::Start,
2295 wrap: false,
2296 truncate: false,
2297 margin: Margin::default(),
2298 constraints: Constraints::default(),
2299 });
2300 self.last_text_idx = Some(self.commands.len() - 1);
2301 self
2302 }
2303
2304 pub fn help(&mut self, bindings: &[(&str, &str)]) -> Response {
2310 if bindings.is_empty() {
2311 return Response::none();
2312 }
2313
2314 self.interaction_count += 1;
2315 self.commands.push(Command::BeginContainer {
2316 direction: Direction::Row,
2317 gap: 2,
2318 align: Align::Start,
2319 align_self: None,
2320 justify: Justify::Start,
2321 border: None,
2322 border_sides: BorderSides::all(),
2323 border_style: Style::new().fg(self.theme.border),
2324 bg_color: None,
2325 padding: Padding::default(),
2326 margin: Margin::default(),
2327 constraints: Constraints::default(),
2328 title: None,
2329 grow: 0,
2330 group_name: None,
2331 });
2332 for (idx, (key, action)) in bindings.iter().enumerate() {
2333 if idx > 0 {
2334 self.styled("·", Style::new().fg(self.theme.text_dim));
2335 }
2336 self.styled(*key, Style::new().bold().fg(self.theme.primary));
2337 self.styled(*action, Style::new().fg(self.theme.text_dim));
2338 }
2339 self.commands.push(Command::EndContainer);
2340 self.last_text_idx = None;
2341
2342 Response::none()
2343 }
2344
2345 pub fn help_colored(
2346 &mut self,
2347 bindings: &[(&str, &str)],
2348 key_color: Color,
2349 text_color: Color,
2350 ) -> Response {
2351 if bindings.is_empty() {
2352 return Response::none();
2353 }
2354
2355 self.interaction_count += 1;
2356 self.commands.push(Command::BeginContainer {
2357 direction: Direction::Row,
2358 gap: 2,
2359 align: Align::Start,
2360 align_self: None,
2361 justify: Justify::Start,
2362 border: None,
2363 border_sides: BorderSides::all(),
2364 border_style: Style::new().fg(self.theme.border),
2365 bg_color: None,
2366 padding: Padding::default(),
2367 margin: Margin::default(),
2368 constraints: Constraints::default(),
2369 title: None,
2370 grow: 0,
2371 group_name: None,
2372 });
2373 for (idx, (key, action)) in bindings.iter().enumerate() {
2374 if idx > 0 {
2375 self.styled("·", Style::new().fg(text_color));
2376 }
2377 self.styled(*key, Style::new().bold().fg(key_color));
2378 self.styled(*action, Style::new().fg(text_color));
2379 }
2380 self.commands.push(Command::EndContainer);
2381 self.last_text_idx = None;
2382
2383 Response::none()
2384 }
2385
2386 pub fn key(&self, c: char) -> bool {
2392 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2393 return false;
2394 }
2395 self.events.iter().enumerate().any(|(i, e)| {
2396 !self.consumed[i]
2397 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
2398 })
2399 }
2400
2401 pub fn key_code(&self, code: KeyCode) -> bool {
2405 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2406 return false;
2407 }
2408 self.events.iter().enumerate().any(|(i, e)| {
2409 !self.consumed[i]
2410 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
2411 })
2412 }
2413
2414 pub fn key_release(&self, c: char) -> bool {
2418 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2419 return false;
2420 }
2421 self.events.iter().enumerate().any(|(i, e)| {
2422 !self.consumed[i]
2423 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
2424 })
2425 }
2426
2427 pub fn key_code_release(&self, code: KeyCode) -> bool {
2431 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2432 return false;
2433 }
2434 self.events.iter().enumerate().any(|(i, e)| {
2435 !self.consumed[i]
2436 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
2437 })
2438 }
2439
2440 pub fn consume_key(&mut self, c: char) -> bool {
2450 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2451 return false;
2452 }
2453 for (i, event) in self.events.iter().enumerate() {
2454 if self.consumed[i] {
2455 continue;
2456 }
2457 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
2458 {
2459 self.consumed[i] = true;
2460 return true;
2461 }
2462 }
2463 false
2464 }
2465
2466 pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
2476 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2477 return false;
2478 }
2479 for (i, event) in self.events.iter().enumerate() {
2480 if self.consumed[i] {
2481 continue;
2482 }
2483 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code) {
2484 self.consumed[i] = true;
2485 return true;
2486 }
2487 }
2488 false
2489 }
2490
2491 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
2495 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2496 return false;
2497 }
2498 self.events.iter().enumerate().any(|(i, e)| {
2499 !self.consumed[i]
2500 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
2501 })
2502 }
2503
2504 pub fn mouse_down(&self) -> Option<(u32, u32)> {
2508 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2509 return None;
2510 }
2511 self.events.iter().enumerate().find_map(|(i, event)| {
2512 if self.consumed[i] {
2513 return None;
2514 }
2515 if let Event::Mouse(mouse) = event {
2516 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
2517 return Some((mouse.x, mouse.y));
2518 }
2519 }
2520 None
2521 })
2522 }
2523
2524 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
2529 self.mouse_pos
2530 }
2531
2532 pub fn paste(&self) -> Option<&str> {
2534 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2535 return None;
2536 }
2537 self.events.iter().enumerate().find_map(|(i, event)| {
2538 if self.consumed[i] {
2539 return None;
2540 }
2541 if let Event::Paste(ref text) = event {
2542 return Some(text.as_str());
2543 }
2544 None
2545 })
2546 }
2547
2548 pub fn scroll_up(&self) -> bool {
2550 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2551 return false;
2552 }
2553 self.events.iter().enumerate().any(|(i, event)| {
2554 !self.consumed[i]
2555 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
2556 })
2557 }
2558
2559 pub fn scroll_down(&self) -> bool {
2561 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2562 return false;
2563 }
2564 self.events.iter().enumerate().any(|(i, event)| {
2565 !self.consumed[i]
2566 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
2567 })
2568 }
2569
2570 pub fn quit(&mut self) {
2572 self.should_quit = true;
2573 }
2574
2575 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
2583 self.clipboard_text = Some(text.into());
2584 }
2585
2586 pub fn theme(&self) -> &Theme {
2588 &self.theme
2589 }
2590
2591 pub fn set_theme(&mut self, theme: Theme) {
2595 self.theme = theme;
2596 }
2597
2598 pub fn is_dark_mode(&self) -> bool {
2600 self.dark_mode
2601 }
2602
2603 pub fn set_dark_mode(&mut self, dark: bool) {
2605 self.dark_mode = dark;
2606 }
2607
2608 pub fn width(&self) -> u32 {
2612 self.area_width
2613 }
2614
2615 pub fn breakpoint(&self) -> Breakpoint {
2639 let w = self.area_width;
2640 if w < 40 {
2641 Breakpoint::Xs
2642 } else if w < 80 {
2643 Breakpoint::Sm
2644 } else if w < 120 {
2645 Breakpoint::Md
2646 } else if w < 160 {
2647 Breakpoint::Lg
2648 } else {
2649 Breakpoint::Xl
2650 }
2651 }
2652
2653 pub fn height(&self) -> u32 {
2655 self.area_height
2656 }
2657
2658 pub fn tick(&self) -> u64 {
2663 self.tick
2664 }
2665
2666 pub fn debug_enabled(&self) -> bool {
2670 self.debug
2671 }
2672}