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