1use super::*;
2use crate::RichLogState;
3
4mod tree_widgets;
5
6enum MdInline {
8 Text(String),
9 Link { text: String, url: String },
10 Image { alt: String },
11}
12
13impl Context {
14 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
31 slt_assert(cols > 0, "grid() requires at least 1 column");
32 let interaction_id = self.next_interaction_id();
33 let border = self.theme.border;
34
35 self.commands.push(Command::BeginContainer {
36 direction: Direction::Column,
37 gap: 0,
38 align: Align::Start,
39 align_self: None,
40 justify: Justify::Start,
41 border: None,
42 border_sides: BorderSides::all(),
43 border_style: Style::new().fg(border),
44 bg_color: None,
45 padding: Padding::default(),
46 margin: Margin::default(),
47 constraints: Constraints::default(),
48 title: None,
49 grow: 0,
50 group_name: None,
51 });
52
53 let children_start = self.commands.len();
54 f(self);
55 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
56
57 let mut elements: Vec<Vec<Command>> = Vec::new();
58 let mut iter = child_commands.into_iter().peekable();
59 while let Some(cmd) = iter.next() {
60 match cmd {
61 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
62 let mut depth = 1_u32;
63 let mut element = vec![cmd];
64 for next in iter.by_ref() {
65 match next {
66 Command::BeginContainer { .. } | Command::BeginScrollable { .. } => {
67 depth += 1;
68 }
69 Command::EndContainer => {
70 depth = depth.saturating_sub(1);
71 }
72 _ => {}
73 }
74 let at_end = matches!(next, Command::EndContainer) && depth == 0;
75 element.push(next);
76 if at_end {
77 break;
78 }
79 }
80 elements.push(element);
81 }
82 Command::EndContainer => {}
83 _ => elements.push(vec![cmd]),
84 }
85 }
86
87 let cols = cols.max(1) as usize;
88 for row in elements.chunks(cols) {
89 self.interaction_count += 1;
90 self.commands.push(Command::BeginContainer {
91 direction: Direction::Row,
92 gap: 0,
93 align: Align::Start,
94 align_self: None,
95 justify: Justify::Start,
96 border: None,
97 border_sides: BorderSides::all(),
98 border_style: Style::new().fg(border),
99 bg_color: None,
100 padding: Padding::default(),
101 margin: Margin::default(),
102 constraints: Constraints::default(),
103 title: None,
104 grow: 0,
105 group_name: None,
106 });
107
108 for element in row {
109 self.interaction_count += 1;
110 self.commands.push(Command::BeginContainer {
111 direction: Direction::Column,
112 gap: 0,
113 align: Align::Start,
114 align_self: None,
115 justify: Justify::Start,
116 border: None,
117 border_sides: BorderSides::all(),
118 border_style: Style::new().fg(border),
119 bg_color: None,
120 padding: Padding::default(),
121 margin: Margin::default(),
122 constraints: Constraints::default(),
123 title: None,
124 grow: 1,
125 group_name: None,
126 });
127 self.commands.extend(element.iter().cloned());
128 self.commands.push(Command::EndContainer);
129 }
130
131 self.commands.push(Command::EndContainer);
132 }
133
134 self.commands.push(Command::EndContainer);
135 self.last_text_idx = None;
136
137 self.response_for(interaction_id)
138 }
139
140 pub fn list(&mut self, state: &mut ListState) -> Response {
146 self.list_colored(state, &WidgetColors::new())
147 }
148
149 pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
151 let visible = state.visible_indices().to_vec();
152 if visible.is_empty() && state.items.is_empty() {
153 state.selected = 0;
154 return Response::none();
155 }
156
157 if !visible.is_empty() {
158 state.selected = state.selected.min(visible.len().saturating_sub(1));
159 }
160
161 let old_selected = state.selected;
162 let focused = self.register_focusable();
163 let interaction_id = self.next_interaction_id();
164 let mut response = self.response_for(interaction_id);
165 response.focused = focused;
166
167 if focused {
168 let mut consumed_indices = Vec::new();
169 for (i, event) in self.events.iter().enumerate() {
170 if let Event::Key(key) = event {
171 if key.kind != KeyEventKind::Press {
172 continue;
173 }
174 match key.code {
175 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
176 let _ = handle_vertical_nav(
177 &mut state.selected,
178 visible.len().saturating_sub(1),
179 key.code.clone(),
180 );
181 consumed_indices.push(i);
182 }
183 _ => {}
184 }
185 }
186 }
187
188 for index in consumed_indices {
189 self.consumed[index] = true;
190 }
191 }
192
193 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
194 for (i, event) in self.events.iter().enumerate() {
195 if self.consumed[i] {
196 continue;
197 }
198 if let Event::Mouse(mouse) = event {
199 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
200 continue;
201 }
202 let in_bounds = mouse.x >= rect.x
203 && mouse.x < rect.right()
204 && mouse.y >= rect.y
205 && mouse.y < rect.bottom();
206 if !in_bounds {
207 continue;
208 }
209 let clicked_idx = (mouse.y - rect.y) as usize;
210 if clicked_idx < visible.len() {
211 state.selected = clicked_idx;
212 self.consumed[i] = true;
213 }
214 }
215 }
216 }
217
218 self.commands.push(Command::BeginContainer {
219 direction: Direction::Column,
220 gap: 0,
221 align: Align::Start,
222 align_self: None,
223 justify: Justify::Start,
224 border: None,
225 border_sides: BorderSides::all(),
226 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
227 bg_color: None,
228 padding: Padding::default(),
229 margin: Margin::default(),
230 constraints: Constraints::default(),
231 title: None,
232 grow: 0,
233 group_name: None,
234 });
235
236 for (view_idx, &item_idx) in visible.iter().enumerate() {
237 let item = &state.items[item_idx];
238 if view_idx == state.selected {
239 let mut selected_style = Style::new()
240 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
241 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
242 if focused {
243 selected_style = selected_style.bold();
244 }
245 let mut row = String::with_capacity(2 + item.len());
246 row.push_str("▸ ");
247 row.push_str(item);
248 self.styled(row, selected_style);
249 } else {
250 let mut row = String::with_capacity(2 + item.len());
251 row.push_str(" ");
252 row.push_str(item);
253 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
254 }
255 }
256
257 self.commands.push(Command::EndContainer);
258 self.last_text_idx = None;
259
260 response.changed = state.selected != old_selected;
261 response
262 }
263
264 pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
266 let focused = self.register_focusable();
267 let interaction_id = self.next_interaction_id();
268 let mut response = self.response_for(interaction_id);
269 response.focused = focused;
270
271 let month_days = CalendarState::days_in_month(state.year, state.month);
272 state.cursor_day = state.cursor_day.clamp(1, month_days);
273 if let Some(day) = state.selected_day {
274 state.selected_day = Some(day.min(month_days));
275 }
276 let old_selected = state.selected_day;
277
278 if focused {
279 let mut consumed_indices = Vec::new();
280 for (i, event) in self.events.iter().enumerate() {
281 if self.consumed[i] {
282 continue;
283 }
284 if let Event::Key(key) = event {
285 if key.kind != KeyEventKind::Press {
286 continue;
287 }
288 match key.code {
289 KeyCode::Left => {
290 calendar_move_cursor_by_days(state, -1);
291 consumed_indices.push(i);
292 }
293 KeyCode::Right => {
294 calendar_move_cursor_by_days(state, 1);
295 consumed_indices.push(i);
296 }
297 KeyCode::Up => {
298 calendar_move_cursor_by_days(state, -7);
299 consumed_indices.push(i);
300 }
301 KeyCode::Down => {
302 calendar_move_cursor_by_days(state, 7);
303 consumed_indices.push(i);
304 }
305 KeyCode::Char('h') => {
306 state.prev_month();
307 consumed_indices.push(i);
308 }
309 KeyCode::Char('l') => {
310 state.next_month();
311 consumed_indices.push(i);
312 }
313 KeyCode::Enter | KeyCode::Char(' ') => {
314 state.selected_day = Some(state.cursor_day);
315 consumed_indices.push(i);
316 }
317 _ => {}
318 }
319 }
320 }
321
322 for index in consumed_indices {
323 self.consumed[index] = true;
324 }
325 }
326
327 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
328 for (i, event) in self.events.iter().enumerate() {
329 if self.consumed[i] {
330 continue;
331 }
332 if let Event::Mouse(mouse) = event {
333 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
334 continue;
335 }
336 let in_bounds = mouse.x >= rect.x
337 && mouse.x < rect.right()
338 && mouse.y >= rect.y
339 && mouse.y < rect.bottom();
340 if !in_bounds {
341 continue;
342 }
343
344 let rel_x = mouse.x.saturating_sub(rect.x);
345 let rel_y = mouse.y.saturating_sub(rect.y);
346 if rel_y == 0 {
347 if rel_x <= 2 {
348 state.prev_month();
349 self.consumed[i] = true;
350 continue;
351 }
352 if rel_x + 3 >= rect.width {
353 state.next_month();
354 self.consumed[i] = true;
355 continue;
356 }
357 }
358
359 if !(2..8).contains(&rel_y) {
360 continue;
361 }
362 if rel_x >= 21 {
363 continue;
364 }
365
366 let week = rel_y - 2;
367 let col = rel_x / 3;
368 let day_index = week * 7 + col;
369 let first = CalendarState::first_weekday(state.year, state.month);
370 let days = CalendarState::days_in_month(state.year, state.month);
371 if day_index < first {
372 continue;
373 }
374 let day = day_index - first + 1;
375 if day == 0 || day > days {
376 continue;
377 }
378 state.cursor_day = day;
379 state.selected_day = Some(day);
380 self.consumed[i] = true;
381 }
382 }
383 }
384
385 let title = {
386 let month_name = calendar_month_name(state.month);
387 let mut s = String::with_capacity(16);
388 s.push_str(&state.year.to_string());
389 s.push(' ');
390 s.push_str(month_name);
391 s
392 };
393
394 self.commands.push(Command::BeginContainer {
395 direction: Direction::Column,
396 gap: 0,
397 align: Align::Start,
398 align_self: None,
399 justify: Justify::Start,
400 border: None,
401 border_sides: BorderSides::all(),
402 border_style: Style::new().fg(self.theme.border),
403 bg_color: None,
404 padding: Padding::default(),
405 margin: Margin::default(),
406 constraints: Constraints::default(),
407 title: None,
408 grow: 0,
409 group_name: None,
410 });
411
412 self.commands.push(Command::BeginContainer {
413 direction: Direction::Row,
414 gap: 1,
415 align: Align::Start,
416 align_self: None,
417 justify: Justify::Start,
418 border: None,
419 border_sides: BorderSides::all(),
420 border_style: Style::new().fg(self.theme.border),
421 bg_color: None,
422 padding: Padding::default(),
423 margin: Margin::default(),
424 constraints: Constraints::default(),
425 title: None,
426 grow: 0,
427 group_name: None,
428 });
429 self.styled("◀", Style::new().fg(self.theme.text));
430 self.styled(title, Style::new().bold().fg(self.theme.text));
431 self.styled("▶", Style::new().fg(self.theme.text));
432 self.commands.push(Command::EndContainer);
433
434 self.commands.push(Command::BeginContainer {
435 direction: Direction::Row,
436 gap: 0,
437 align: Align::Start,
438 align_self: None,
439 justify: Justify::Start,
440 border: None,
441 border_sides: BorderSides::all(),
442 border_style: Style::new().fg(self.theme.border),
443 bg_color: None,
444 padding: Padding::default(),
445 margin: Margin::default(),
446 constraints: Constraints::default(),
447 title: None,
448 grow: 0,
449 group_name: None,
450 });
451 for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
452 self.styled(
453 format!("{wd:>2} "),
454 Style::new().fg(self.theme.text_dim).bold(),
455 );
456 }
457 self.commands.push(Command::EndContainer);
458
459 let first = CalendarState::first_weekday(state.year, state.month);
460 let days = CalendarState::days_in_month(state.year, state.month);
461 for week in 0..6_u32 {
462 self.commands.push(Command::BeginContainer {
463 direction: Direction::Row,
464 gap: 0,
465 align: Align::Start,
466 align_self: None,
467 justify: Justify::Start,
468 border: None,
469 border_sides: BorderSides::all(),
470 border_style: Style::new().fg(self.theme.border),
471 bg_color: None,
472 padding: Padding::default(),
473 margin: Margin::default(),
474 constraints: Constraints::default(),
475 title: None,
476 grow: 0,
477 group_name: None,
478 });
479
480 for col in 0..7_u32 {
481 let idx = week * 7 + col;
482 if idx < first || idx >= first + days {
483 self.styled(" ", Style::new().fg(self.theme.text_dim));
484 continue;
485 }
486 let day = idx - first + 1;
487 let text = format!("{day:>2} ");
488 let style = if state.selected_day == Some(day) {
489 Style::new()
490 .bg(self.theme.selected_bg)
491 .fg(self.theme.selected_fg)
492 } else if state.cursor_day == day {
493 Style::new().fg(self.theme.primary).bold()
494 } else {
495 Style::new().fg(self.theme.text)
496 };
497 self.styled(text, style);
498 }
499
500 self.commands.push(Command::EndContainer);
501 }
502
503 self.commands.push(Command::EndContainer);
504 self.last_text_idx = None;
505 response.changed = state.selected_day != old_selected;
506 response
507 }
508
509 pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
511 if state.dirty {
512 state.refresh();
513 }
514 if !state.entries.is_empty() {
515 state.selected = state.selected.min(state.entries.len().saturating_sub(1));
516 }
517
518 let focused = self.register_focusable();
519 let interaction_id = self.next_interaction_id();
520 let mut response = self.response_for(interaction_id);
521 response.focused = focused;
522 let mut file_selected = false;
523
524 if focused {
525 let mut consumed_indices = Vec::new();
526 for (i, event) in self.events.iter().enumerate() {
527 if self.consumed[i] {
528 continue;
529 }
530 if let Event::Key(key) = event {
531 if key.kind != KeyEventKind::Press {
532 continue;
533 }
534 match key.code {
535 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
536 if !state.entries.is_empty() {
537 let _ = handle_vertical_nav(
538 &mut state.selected,
539 state.entries.len().saturating_sub(1),
540 key.code.clone(),
541 );
542 }
543 consumed_indices.push(i);
544 }
545 KeyCode::Enter => {
546 if let Some(entry) = state.entries.get(state.selected).cloned() {
547 if entry.is_dir {
548 state.current_dir = entry.path;
549 state.selected = 0;
550 state.selected_file = None;
551 state.dirty = true;
552 } else {
553 state.selected_file = Some(entry.path);
554 file_selected = true;
555 }
556 }
557 consumed_indices.push(i);
558 }
559 KeyCode::Backspace => {
560 if let Some(parent) =
561 state.current_dir.parent().map(|p| p.to_path_buf())
562 {
563 state.current_dir = parent;
564 state.selected = 0;
565 state.selected_file = None;
566 state.dirty = true;
567 }
568 consumed_indices.push(i);
569 }
570 KeyCode::Char('h') => {
571 state.show_hidden = !state.show_hidden;
572 state.selected = 0;
573 state.dirty = true;
574 consumed_indices.push(i);
575 }
576 KeyCode::Esc => {
577 state.selected_file = None;
578 consumed_indices.push(i);
579 }
580 _ => {}
581 }
582 }
583 }
584
585 for index in consumed_indices {
586 self.consumed[index] = true;
587 }
588 }
589
590 if state.dirty {
591 state.refresh();
592 }
593
594 self.commands.push(Command::BeginContainer {
595 direction: Direction::Column,
596 gap: 0,
597 align: Align::Start,
598 align_self: None,
599 justify: Justify::Start,
600 border: None,
601 border_sides: BorderSides::all(),
602 border_style: Style::new().fg(self.theme.border),
603 bg_color: None,
604 padding: Padding::default(),
605 margin: Margin::default(),
606 constraints: Constraints::default(),
607 title: None,
608 grow: 0,
609 group_name: None,
610 });
611
612 let dir_text = {
613 let dir = state.current_dir.display().to_string();
614 let mut text = String::with_capacity(5 + dir.len());
615 text.push_str("Dir: ");
616 text.push_str(&dir);
617 text
618 };
619 self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
620
621 if state.entries.is_empty() {
622 self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
623 } else {
624 for (idx, entry) in state.entries.iter().enumerate() {
625 let icon = if entry.is_dir { "▸ " } else { " " };
626 let row = if entry.is_dir {
627 let mut row = String::with_capacity(icon.len() + entry.name.len());
628 row.push_str(icon);
629 row.push_str(&entry.name);
630 row
631 } else {
632 let size_text = entry.size.to_string();
633 let mut row =
634 String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
635 row.push_str(icon);
636 row.push_str(&entry.name);
637 row.push_str(" ");
638 row.push_str(&size_text);
639 row.push_str(" B");
640 row
641 };
642
643 let style = if idx == state.selected {
644 if focused {
645 Style::new().bold().fg(self.theme.primary)
646 } else {
647 Style::new().fg(self.theme.primary)
648 }
649 } else {
650 Style::new().fg(self.theme.text)
651 };
652 self.styled(row, style);
653 }
654 }
655
656 self.commands.push(Command::EndContainer);
657 self.last_text_idx = None;
658
659 response.changed = file_selected;
660 response
661 }
662
663 pub fn table(&mut self, state: &mut TableState) -> Response {
669 self.table_colored(state, &WidgetColors::new())
670 }
671
672 pub fn table_colored(&mut self, state: &mut TableState, colors: &WidgetColors) -> Response {
674 if state.is_dirty() {
675 state.recompute_widths();
676 }
677
678 let old_selected = state.selected;
679 let old_sort_column = state.sort_column;
680 let old_sort_ascending = state.sort_ascending;
681 let old_page = state.page;
682 let old_filter = state.filter.clone();
683
684 let focused = self.register_focusable();
685 let interaction_id = self.next_interaction_id();
686 let mut response = self.response_for(interaction_id);
687 response.focused = focused;
688
689 self.table_handle_events(state, focused, interaction_id);
690
691 if state.is_dirty() {
692 state.recompute_widths();
693 }
694
695 self.table_render(state, focused, colors);
696
697 response.changed = state.selected != old_selected
698 || state.sort_column != old_sort_column
699 || state.sort_ascending != old_sort_ascending
700 || state.page != old_page
701 || state.filter != old_filter;
702 response
703 }
704
705 fn table_handle_events(
706 &mut self,
707 state: &mut TableState,
708 focused: bool,
709 interaction_id: usize,
710 ) {
711 self.handle_table_keys(state, focused);
712
713 if state.visible_indices().is_empty() && state.headers.is_empty() {
714 return;
715 }
716
717 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
718 for (i, event) in self.events.iter().enumerate() {
719 if self.consumed[i] {
720 continue;
721 }
722 if let Event::Mouse(mouse) = event {
723 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
724 continue;
725 }
726 let in_bounds = mouse.x >= rect.x
727 && mouse.x < rect.right()
728 && mouse.y >= rect.y
729 && mouse.y < rect.bottom();
730 if !in_bounds {
731 continue;
732 }
733
734 if mouse.y == rect.y {
735 let rel_x = mouse.x.saturating_sub(rect.x);
736 let mut x_offset = 0u32;
737 for (col_idx, width) in state.column_widths().iter().enumerate() {
738 if rel_x >= x_offset && rel_x < x_offset + *width {
739 state.toggle_sort(col_idx);
740 state.selected = 0;
741 self.consumed[i] = true;
742 break;
743 }
744 x_offset += *width;
745 if col_idx + 1 < state.column_widths().len() {
746 x_offset += 3;
747 }
748 }
749 continue;
750 }
751
752 if mouse.y < rect.y + 2 {
753 continue;
754 }
755
756 let visible_len = if state.page_size > 0 {
757 let start = state
758 .page
759 .saturating_mul(state.page_size)
760 .min(state.visible_indices().len());
761 let end = (start + state.page_size).min(state.visible_indices().len());
762 end.saturating_sub(start)
763 } else {
764 state.visible_indices().len()
765 };
766 let clicked_idx = (mouse.y - rect.y - 2) as usize;
767 if clicked_idx < visible_len {
768 state.selected = clicked_idx;
769 self.consumed[i] = true;
770 }
771 }
772 }
773 }
774 }
775
776 fn table_render(&mut self, state: &mut TableState, focused: bool, colors: &WidgetColors) {
777 let total_visible = state.visible_indices().len();
778 let page_start = if state.page_size > 0 {
779 state
780 .page
781 .saturating_mul(state.page_size)
782 .min(total_visible)
783 } else {
784 0
785 };
786 let page_end = if state.page_size > 0 {
787 (page_start + state.page_size).min(total_visible)
788 } else {
789 total_visible
790 };
791 let visible_len = page_end.saturating_sub(page_start);
792 state.selected = state.selected.min(visible_len.saturating_sub(1));
793
794 self.commands.push(Command::BeginContainer {
795 direction: Direction::Column,
796 gap: 0,
797 align: Align::Start,
798 align_self: None,
799 justify: Justify::Start,
800 border: None,
801 border_sides: BorderSides::all(),
802 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
803 bg_color: None,
804 padding: Padding::default(),
805 margin: Margin::default(),
806 constraints: Constraints::default(),
807 title: None,
808 grow: 0,
809 group_name: None,
810 });
811
812 self.render_table_header(state, colors);
813 self.render_table_rows(state, focused, page_start, visible_len, colors);
814
815 if state.page_size > 0 && state.total_pages() > 1 {
816 let current_page = (state.page + 1).to_string();
817 let total_pages = state.total_pages().to_string();
818 let mut page_text = String::with_capacity(current_page.len() + total_pages.len() + 6);
819 page_text.push_str("Page ");
820 page_text.push_str(¤t_page);
821 page_text.push('/');
822 page_text.push_str(&total_pages);
823 self.styled(
824 page_text,
825 Style::new()
826 .dim()
827 .fg(colors.fg.unwrap_or(self.theme.text_dim)),
828 );
829 }
830
831 self.commands.push(Command::EndContainer);
832 self.last_text_idx = None;
833 }
834
835 fn handle_table_keys(&mut self, state: &mut TableState, focused: bool) {
836 if !focused || state.visible_indices().is_empty() {
837 return;
838 }
839
840 let mut consumed_indices = Vec::new();
841 for (i, event) in self.events.iter().enumerate() {
842 if let Event::Key(key) = event {
843 if key.kind != KeyEventKind::Press {
844 continue;
845 }
846 match key.code {
847 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
848 let visible_len = table_visible_len(state);
849 state.selected = state.selected.min(visible_len.saturating_sub(1));
850 let _ = handle_vertical_nav(
851 &mut state.selected,
852 visible_len.saturating_sub(1),
853 key.code.clone(),
854 );
855 consumed_indices.push(i);
856 }
857 KeyCode::PageUp => {
858 let old_page = state.page;
859 state.prev_page();
860 if state.page != old_page {
861 state.selected = 0;
862 }
863 consumed_indices.push(i);
864 }
865 KeyCode::PageDown => {
866 let old_page = state.page;
867 state.next_page();
868 if state.page != old_page {
869 state.selected = 0;
870 }
871 consumed_indices.push(i);
872 }
873 _ => {}
874 }
875 }
876 }
877 for index in consumed_indices {
878 self.consumed[index] = true;
879 }
880 }
881
882 fn render_table_header(&mut self, state: &TableState, colors: &WidgetColors) {
883 let header_cells = state
884 .headers
885 .iter()
886 .enumerate()
887 .map(|(i, header)| {
888 if state.sort_column == Some(i) {
889 if state.sort_ascending {
890 let mut sorted_header = String::with_capacity(header.len() + 2);
891 sorted_header.push_str(header);
892 sorted_header.push_str(" ▲");
893 sorted_header
894 } else {
895 let mut sorted_header = String::with_capacity(header.len() + 2);
896 sorted_header.push_str(header);
897 sorted_header.push_str(" ▼");
898 sorted_header
899 }
900 } else {
901 header.clone()
902 }
903 })
904 .collect::<Vec<_>>();
905 let header_line = format_table_row(&header_cells, state.column_widths(), " │ ");
906 self.styled(
907 header_line,
908 Style::new().bold().fg(colors.fg.unwrap_or(self.theme.text)),
909 );
910
911 let separator = state
912 .column_widths()
913 .iter()
914 .map(|w| "─".repeat(*w as usize))
915 .collect::<Vec<_>>()
916 .join("─┼─");
917 self.text(separator);
918 }
919
920 fn render_table_rows(
921 &mut self,
922 state: &TableState,
923 focused: bool,
924 page_start: usize,
925 visible_len: usize,
926 colors: &WidgetColors,
927 ) {
928 for idx in 0..visible_len {
929 let data_idx = state.visible_indices()[page_start + idx];
930 let Some(row) = state.rows.get(data_idx) else {
931 continue;
932 };
933 let line = format_table_row(row, state.column_widths(), " │ ");
934 if idx == state.selected {
935 let mut style = Style::new()
936 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
937 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
938 if focused {
939 style = style.bold();
940 }
941 self.styled(line, style);
942 } else {
943 let mut style = Style::new().fg(colors.fg.unwrap_or(self.theme.text));
944 if state.zebra {
945 let zebra_bg = colors.bg.unwrap_or({
946 if idx % 2 == 0 {
947 self.theme.surface
948 } else {
949 self.theme.surface_hover
950 }
951 });
952 style = style.bg(zebra_bg);
953 }
954 self.styled(line, style);
955 }
956 }
957 }
958
959 pub fn tabs(&mut self, state: &mut TabsState) -> Response {
965 self.tabs_colored(state, &WidgetColors::new())
966 }
967
968 pub fn tabs_colored(&mut self, state: &mut TabsState, colors: &WidgetColors) -> Response {
970 if state.labels.is_empty() {
971 state.selected = 0;
972 return Response::none();
973 }
974
975 state.selected = state.selected.min(state.labels.len().saturating_sub(1));
976 let old_selected = state.selected;
977 let focused = self.register_focusable();
978 let interaction_id = self.next_interaction_id();
979 let mut response = self.response_for(interaction_id);
980 response.focused = focused;
981
982 if focused {
983 let mut consumed_indices = Vec::new();
984 for (i, event) in self.events.iter().enumerate() {
985 if let Event::Key(key) = event {
986 if key.kind != KeyEventKind::Press {
987 continue;
988 }
989 match key.code {
990 KeyCode::Left => {
991 state.selected = if state.selected == 0 {
992 state.labels.len().saturating_sub(1)
993 } else {
994 state.selected - 1
995 };
996 consumed_indices.push(i);
997 }
998 KeyCode::Right => {
999 if !state.labels.is_empty() {
1000 state.selected = (state.selected + 1) % state.labels.len();
1001 }
1002 consumed_indices.push(i);
1003 }
1004 _ => {}
1005 }
1006 }
1007 }
1008
1009 for index in consumed_indices {
1010 self.consumed[index] = true;
1011 }
1012 }
1013
1014 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1015 for (i, event) in self.events.iter().enumerate() {
1016 if self.consumed[i] {
1017 continue;
1018 }
1019 if let Event::Mouse(mouse) = event {
1020 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1021 continue;
1022 }
1023 let in_bounds = mouse.x >= rect.x
1024 && mouse.x < rect.right()
1025 && mouse.y >= rect.y
1026 && mouse.y < rect.bottom();
1027 if !in_bounds {
1028 continue;
1029 }
1030
1031 let mut x_offset = 0u32;
1032 let rel_x = mouse.x - rect.x;
1033 for (idx, label) in state.labels.iter().enumerate() {
1034 let tab_width = UnicodeWidthStr::width(label.as_str()) as u32 + 4;
1035 if rel_x >= x_offset && rel_x < x_offset + tab_width {
1036 state.selected = idx;
1037 self.consumed[i] = true;
1038 break;
1039 }
1040 x_offset += tab_width + 1;
1041 }
1042 }
1043 }
1044 }
1045
1046 self.commands.push(Command::BeginContainer {
1047 direction: Direction::Row,
1048 gap: 1,
1049 align: Align::Start,
1050 align_self: None,
1051 justify: Justify::Start,
1052 border: None,
1053 border_sides: BorderSides::all(),
1054 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1055 bg_color: None,
1056 padding: Padding::default(),
1057 margin: Margin::default(),
1058 constraints: Constraints::default(),
1059 title: None,
1060 grow: 0,
1061 group_name: None,
1062 });
1063 for (idx, label) in state.labels.iter().enumerate() {
1064 let style = if idx == state.selected {
1065 let s = Style::new()
1066 .fg(colors.accent.unwrap_or(self.theme.primary))
1067 .bold();
1068 if focused {
1069 s.underline()
1070 } else {
1071 s
1072 }
1073 } else {
1074 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1075 };
1076 let mut tab = String::with_capacity(label.len() + 4);
1077 tab.push_str("[ ");
1078 tab.push_str(label);
1079 tab.push_str(" ]");
1080 self.styled(tab, style);
1081 }
1082 self.commands.push(Command::EndContainer);
1083 self.last_text_idx = None;
1084
1085 response.changed = state.selected != old_selected;
1086 response
1087 }
1088
1089 pub fn button(&mut self, label: impl Into<String>) -> Response {
1095 self.button_colored(label, &WidgetColors::new())
1096 }
1097
1098 pub fn button_colored(&mut self, label: impl Into<String>, colors: &WidgetColors) -> Response {
1100 let focused = self.register_focusable();
1101 let interaction_id = self.next_interaction_id();
1102 let mut response = self.response_for(interaction_id);
1103 response.focused = focused;
1104
1105 let mut activated = response.clicked;
1106 if focused {
1107 let mut consumed_indices = Vec::new();
1108 for (i, event) in self.events.iter().enumerate() {
1109 if let Event::Key(key) = event {
1110 if key.kind != KeyEventKind::Press {
1111 continue;
1112 }
1113 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1114 activated = true;
1115 consumed_indices.push(i);
1116 }
1117 }
1118 }
1119
1120 for index in consumed_indices {
1121 self.consumed[index] = true;
1122 }
1123 }
1124
1125 let hovered = response.hovered;
1126 let base_fg = colors.fg.unwrap_or(self.theme.text);
1127 let accent = colors.accent.unwrap_or(self.theme.accent);
1128 let base_bg = colors.bg.unwrap_or(self.theme.surface_hover);
1129 let style = if focused {
1130 Style::new().fg(accent).bold()
1131 } else if hovered {
1132 Style::new().fg(accent)
1133 } else {
1134 Style::new().fg(base_fg)
1135 };
1136 let has_custom_bg = colors.bg.is_some();
1137 let bg_color = if has_custom_bg || hovered || focused {
1138 Some(base_bg)
1139 } else {
1140 None
1141 };
1142
1143 self.commands.push(Command::BeginContainer {
1144 direction: Direction::Row,
1145 gap: 0,
1146 align: Align::Start,
1147 align_self: None,
1148 justify: Justify::Start,
1149 border: None,
1150 border_sides: BorderSides::all(),
1151 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1152 bg_color,
1153 padding: Padding::default(),
1154 margin: Margin::default(),
1155 constraints: Constraints::default(),
1156 title: None,
1157 grow: 0,
1158 group_name: None,
1159 });
1160 let raw_label = label.into();
1161 let mut label_text = String::with_capacity(raw_label.len() + 4);
1162 label_text.push_str("[ ");
1163 label_text.push_str(&raw_label);
1164 label_text.push_str(" ]");
1165 self.styled(label_text, style);
1166 self.commands.push(Command::EndContainer);
1167 self.last_text_idx = None;
1168
1169 response.clicked = activated;
1170 response
1171 }
1172
1173 pub fn button_with(&mut self, label: impl Into<String>, variant: ButtonVariant) -> Response {
1178 let focused = self.register_focusable();
1179 let interaction_id = self.next_interaction_id();
1180 let mut response = self.response_for(interaction_id);
1181 response.focused = focused;
1182
1183 let mut activated = response.clicked;
1184 if focused {
1185 let mut consumed_indices = Vec::new();
1186 for (i, event) in self.events.iter().enumerate() {
1187 if let Event::Key(key) = event {
1188 if key.kind != KeyEventKind::Press {
1189 continue;
1190 }
1191 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1192 activated = true;
1193 consumed_indices.push(i);
1194 }
1195 }
1196 }
1197 for index in consumed_indices {
1198 self.consumed[index] = true;
1199 }
1200 }
1201
1202 let label = label.into();
1203 let hover_bg = if response.hovered || focused {
1204 Some(self.theme.surface_hover)
1205 } else {
1206 None
1207 };
1208 let (text, style, bg_color, border) = match variant {
1209 ButtonVariant::Default => {
1210 let style = if focused {
1211 Style::new().fg(self.theme.primary).bold()
1212 } else if response.hovered {
1213 Style::new().fg(self.theme.accent)
1214 } else {
1215 Style::new().fg(self.theme.text)
1216 };
1217 let mut text = String::with_capacity(label.len() + 4);
1218 text.push_str("[ ");
1219 text.push_str(&label);
1220 text.push_str(" ]");
1221 (text, style, hover_bg, None)
1222 }
1223 ButtonVariant::Primary => {
1224 let style = if focused {
1225 Style::new().fg(self.theme.bg).bg(self.theme.primary).bold()
1226 } else if response.hovered {
1227 Style::new().fg(self.theme.bg).bg(self.theme.accent)
1228 } else {
1229 Style::new().fg(self.theme.bg).bg(self.theme.primary)
1230 };
1231 let mut text = String::with_capacity(label.len() + 2);
1232 text.push(' ');
1233 text.push_str(&label);
1234 text.push(' ');
1235 (text, style, hover_bg, None)
1236 }
1237 ButtonVariant::Danger => {
1238 let style = if focused {
1239 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
1240 } else if response.hovered {
1241 Style::new().fg(self.theme.bg).bg(self.theme.warning)
1242 } else {
1243 Style::new().fg(self.theme.bg).bg(self.theme.error)
1244 };
1245 let mut text = String::with_capacity(label.len() + 2);
1246 text.push(' ');
1247 text.push_str(&label);
1248 text.push(' ');
1249 (text, style, hover_bg, None)
1250 }
1251 ButtonVariant::Outline => {
1252 let border_color = if focused {
1253 self.theme.primary
1254 } else if response.hovered {
1255 self.theme.accent
1256 } else {
1257 self.theme.border
1258 };
1259 let style = if focused {
1260 Style::new().fg(self.theme.primary).bold()
1261 } else if response.hovered {
1262 Style::new().fg(self.theme.accent)
1263 } else {
1264 Style::new().fg(self.theme.text)
1265 };
1266 (
1267 {
1268 let mut text = String::with_capacity(label.len() + 2);
1269 text.push(' ');
1270 text.push_str(&label);
1271 text.push(' ');
1272 text
1273 },
1274 style,
1275 hover_bg,
1276 Some((Border::Rounded, Style::new().fg(border_color))),
1277 )
1278 }
1279 };
1280
1281 let (btn_border, btn_border_style) = border.unwrap_or((Border::Rounded, Style::new()));
1282 self.commands.push(Command::BeginContainer {
1283 direction: Direction::Row,
1284 gap: 0,
1285 align: Align::Center,
1286 align_self: None,
1287 justify: Justify::Center,
1288 border: if border.is_some() {
1289 Some(btn_border)
1290 } else {
1291 None
1292 },
1293 border_sides: BorderSides::all(),
1294 border_style: btn_border_style,
1295 bg_color,
1296 padding: Padding::default(),
1297 margin: Margin::default(),
1298 constraints: Constraints::default(),
1299 title: None,
1300 grow: 0,
1301 group_name: None,
1302 });
1303 self.styled(text, style);
1304 self.commands.push(Command::EndContainer);
1305 self.last_text_idx = None;
1306
1307 response.clicked = activated;
1308 response
1309 }
1310
1311 pub fn checkbox(&mut self, label: impl Into<String>, checked: &mut bool) -> Response {
1317 self.checkbox_colored(label, checked, &WidgetColors::new())
1318 }
1319
1320 pub fn checkbox_colored(
1322 &mut self,
1323 label: impl Into<String>,
1324 checked: &mut bool,
1325 colors: &WidgetColors,
1326 ) -> Response {
1327 let focused = self.register_focusable();
1328 let interaction_id = self.next_interaction_id();
1329 let mut response = self.response_for(interaction_id);
1330 response.focused = focused;
1331 let mut should_toggle = response.clicked;
1332 let old_checked = *checked;
1333
1334 if focused {
1335 let mut consumed_indices = Vec::new();
1336 for (i, event) in self.events.iter().enumerate() {
1337 if let Event::Key(key) = event {
1338 if key.kind != KeyEventKind::Press {
1339 continue;
1340 }
1341 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1342 should_toggle = true;
1343 consumed_indices.push(i);
1344 }
1345 }
1346 }
1347
1348 for index in consumed_indices {
1349 self.consumed[index] = true;
1350 }
1351 }
1352
1353 if should_toggle {
1354 *checked = !*checked;
1355 }
1356
1357 let hover_bg = if response.hovered || focused {
1358 Some(self.theme.surface_hover)
1359 } else {
1360 None
1361 };
1362 self.commands.push(Command::BeginContainer {
1363 direction: Direction::Row,
1364 gap: 1,
1365 align: Align::Start,
1366 align_self: None,
1367 justify: Justify::Start,
1368 border: None,
1369 border_sides: BorderSides::all(),
1370 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1371 bg_color: hover_bg,
1372 padding: Padding::default(),
1373 margin: Margin::default(),
1374 constraints: Constraints::default(),
1375 title: None,
1376 grow: 0,
1377 group_name: None,
1378 });
1379 let marker_style = if *checked {
1380 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1381 } else {
1382 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1383 };
1384 let marker = if *checked { "[x]" } else { "[ ]" };
1385 let label_text = label.into();
1386 if focused {
1387 let mut marker_text = String::with_capacity(2 + marker.len());
1388 marker_text.push_str("▸ ");
1389 marker_text.push_str(marker);
1390 self.styled(marker_text, marker_style.bold());
1391 self.styled(
1392 label_text,
1393 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1394 );
1395 } else {
1396 self.styled(marker, marker_style);
1397 self.styled(
1398 label_text,
1399 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1400 );
1401 }
1402 self.commands.push(Command::EndContainer);
1403 self.last_text_idx = None;
1404
1405 response.changed = *checked != old_checked;
1406 response
1407 }
1408
1409 pub fn toggle(&mut self, label: impl Into<String>, on: &mut bool) -> Response {
1416 self.toggle_colored(label, on, &WidgetColors::new())
1417 }
1418
1419 pub fn toggle_colored(
1421 &mut self,
1422 label: impl Into<String>,
1423 on: &mut bool,
1424 colors: &WidgetColors,
1425 ) -> Response {
1426 let focused = self.register_focusable();
1427 let interaction_id = self.next_interaction_id();
1428 let mut response = self.response_for(interaction_id);
1429 response.focused = focused;
1430 let mut should_toggle = response.clicked;
1431 let old_on = *on;
1432
1433 if focused {
1434 let mut consumed_indices = Vec::new();
1435 for (i, event) in self.events.iter().enumerate() {
1436 if let Event::Key(key) = event {
1437 if key.kind != KeyEventKind::Press {
1438 continue;
1439 }
1440 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1441 should_toggle = true;
1442 consumed_indices.push(i);
1443 }
1444 }
1445 }
1446
1447 for index in consumed_indices {
1448 self.consumed[index] = true;
1449 }
1450 }
1451
1452 if should_toggle {
1453 *on = !*on;
1454 }
1455
1456 let hover_bg = if response.hovered || focused {
1457 Some(self.theme.surface_hover)
1458 } else {
1459 None
1460 };
1461 self.commands.push(Command::BeginContainer {
1462 direction: Direction::Row,
1463 gap: 2,
1464 align: Align::Start,
1465 align_self: None,
1466 justify: Justify::Start,
1467 border: None,
1468 border_sides: BorderSides::all(),
1469 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1470 bg_color: hover_bg,
1471 padding: Padding::default(),
1472 margin: Margin::default(),
1473 constraints: Constraints::default(),
1474 title: None,
1475 grow: 0,
1476 group_name: None,
1477 });
1478 let label_text = label.into();
1479 let switch = if *on { "●━━ ON" } else { "━━● OFF" };
1480 let switch_style = if *on {
1481 Style::new().fg(colors.accent.unwrap_or(self.theme.success))
1482 } else {
1483 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim))
1484 };
1485 if focused {
1486 let mut focused_label = String::with_capacity(2 + label_text.len());
1487 focused_label.push_str("▸ ");
1488 focused_label.push_str(&label_text);
1489 self.styled(
1490 focused_label,
1491 Style::new().fg(colors.fg.unwrap_or(self.theme.text)).bold(),
1492 );
1493 self.styled(switch, switch_style.bold());
1494 } else {
1495 self.styled(
1496 label_text,
1497 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1498 );
1499 self.styled(switch, switch_style);
1500 }
1501 self.commands.push(Command::EndContainer);
1502 self.last_text_idx = None;
1503
1504 response.changed = *on != old_on;
1505 response
1506 }
1507
1508 pub fn select(&mut self, state: &mut SelectState) -> Response {
1515 self.select_colored(state, &WidgetColors::new())
1516 }
1517
1518 pub fn select_colored(&mut self, state: &mut SelectState, colors: &WidgetColors) -> Response {
1520 if state.items.is_empty() {
1521 return Response::none();
1522 }
1523 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1524
1525 let focused = self.register_focusable();
1526 let interaction_id = self.next_interaction_id();
1527 let mut response = self.response_for(interaction_id);
1528 response.focused = focused;
1529 let old_selected = state.selected;
1530
1531 self.select_handle_events(state, focused, response.clicked);
1532 self.select_render(state, focused, colors);
1533 response.changed = state.selected != old_selected;
1534 response
1535 }
1536
1537 fn select_handle_events(&mut self, state: &mut SelectState, focused: bool, clicked: bool) {
1538 if clicked {
1539 state.open = !state.open;
1540 if state.open {
1541 state.set_cursor(state.selected);
1542 }
1543 }
1544
1545 if !focused {
1546 return;
1547 }
1548
1549 let mut consumed_indices = Vec::new();
1550 for (i, event) in self.events.iter().enumerate() {
1551 if self.consumed[i] {
1552 continue;
1553 }
1554 if let Event::Key(key) = event {
1555 if key.kind != KeyEventKind::Press {
1556 continue;
1557 }
1558 if state.open {
1559 match key.code {
1560 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1561 let mut cursor = state.cursor();
1562 let _ = handle_vertical_nav(
1563 &mut cursor,
1564 state.items.len().saturating_sub(1),
1565 key.code.clone(),
1566 );
1567 state.set_cursor(cursor);
1568 consumed_indices.push(i);
1569 }
1570 KeyCode::Enter | KeyCode::Char(' ') => {
1571 state.selected = state.cursor();
1572 state.open = false;
1573 consumed_indices.push(i);
1574 }
1575 KeyCode::Esc => {
1576 state.open = false;
1577 consumed_indices.push(i);
1578 }
1579 _ => {}
1580 }
1581 } else if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
1582 state.open = true;
1583 state.set_cursor(state.selected);
1584 consumed_indices.push(i);
1585 }
1586 }
1587 }
1588 for idx in consumed_indices {
1589 self.consumed[idx] = true;
1590 }
1591 }
1592
1593 fn select_render(&mut self, state: &SelectState, focused: bool, colors: &WidgetColors) {
1594 let border_color = if focused {
1595 colors.accent.unwrap_or(self.theme.primary)
1596 } else {
1597 colors.border.unwrap_or(self.theme.border)
1598 };
1599 let display_text = state
1600 .items
1601 .get(state.selected)
1602 .cloned()
1603 .unwrap_or_else(|| state.placeholder.clone());
1604 let arrow = if state.open { "▲" } else { "▼" };
1605
1606 self.commands.push(Command::BeginContainer {
1607 direction: Direction::Column,
1608 gap: 0,
1609 align: Align::Start,
1610 align_self: None,
1611 justify: Justify::Start,
1612 border: None,
1613 border_sides: BorderSides::all(),
1614 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1615 bg_color: None,
1616 padding: Padding::default(),
1617 margin: Margin::default(),
1618 constraints: Constraints::default(),
1619 title: None,
1620 grow: 0,
1621 group_name: None,
1622 });
1623
1624 self.render_select_trigger(&display_text, arrow, border_color, colors);
1625
1626 if state.open {
1627 self.render_select_dropdown(state, colors);
1628 }
1629
1630 self.commands.push(Command::EndContainer);
1631 self.last_text_idx = None;
1632 }
1633
1634 fn render_select_trigger(
1635 &mut self,
1636 display_text: &str,
1637 arrow: &str,
1638 border_color: Color,
1639 colors: &WidgetColors,
1640 ) {
1641 self.commands.push(Command::BeginContainer {
1642 direction: Direction::Row,
1643 gap: 1,
1644 align: Align::Start,
1645 align_self: None,
1646 justify: Justify::Start,
1647 border: Some(Border::Rounded),
1648 border_sides: BorderSides::all(),
1649 border_style: Style::new().fg(border_color),
1650 bg_color: None,
1651 padding: Padding {
1652 left: 1,
1653 right: 1,
1654 top: 0,
1655 bottom: 0,
1656 },
1657 margin: Margin::default(),
1658 constraints: Constraints::default(),
1659 title: None,
1660 grow: 0,
1661 group_name: None,
1662 });
1663 self.interaction_count += 1;
1664 self.styled(
1665 display_text,
1666 Style::new().fg(colors.fg.unwrap_or(self.theme.text)),
1667 );
1668 self.styled(
1669 arrow,
1670 Style::new().fg(colors.fg.unwrap_or(self.theme.text_dim)),
1671 );
1672 self.commands.push(Command::EndContainer);
1673 self.last_text_idx = None;
1674 }
1675
1676 fn render_select_dropdown(&mut self, state: &SelectState, colors: &WidgetColors) {
1677 for (idx, item) in state.items.iter().enumerate() {
1678 let is_cursor = idx == state.cursor();
1679 let style = if is_cursor {
1680 Style::new()
1681 .bold()
1682 .fg(colors.accent.unwrap_or(self.theme.primary))
1683 } else {
1684 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1685 };
1686 let prefix = if is_cursor { "▸ " } else { " " };
1687 let mut row = String::with_capacity(prefix.len() + item.len());
1688 row.push_str(prefix);
1689 row.push_str(item);
1690 self.styled(row, style);
1691 }
1692 }
1693
1694 pub fn radio(&mut self, state: &mut RadioState) -> Response {
1699 self.radio_colored(state, &WidgetColors::new())
1700 }
1701
1702 pub fn radio_colored(&mut self, state: &mut RadioState, colors: &WidgetColors) -> Response {
1704 if state.items.is_empty() {
1705 return Response::none();
1706 }
1707 state.selected = state.selected.min(state.items.len().saturating_sub(1));
1708 let focused = self.register_focusable();
1709 let old_selected = state.selected;
1710
1711 if focused {
1712 let mut consumed_indices = Vec::new();
1713 for (i, event) in self.events.iter().enumerate() {
1714 if self.consumed[i] {
1715 continue;
1716 }
1717 if let Event::Key(key) = event {
1718 if key.kind != KeyEventKind::Press {
1719 continue;
1720 }
1721 match key.code {
1722 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1723 let _ = handle_vertical_nav(
1724 &mut state.selected,
1725 state.items.len().saturating_sub(1),
1726 key.code.clone(),
1727 );
1728 consumed_indices.push(i);
1729 }
1730 KeyCode::Enter | KeyCode::Char(' ') => {
1731 consumed_indices.push(i);
1732 }
1733 _ => {}
1734 }
1735 }
1736 }
1737 for idx in consumed_indices {
1738 self.consumed[idx] = true;
1739 }
1740 }
1741
1742 let interaction_id = self.next_interaction_id();
1743 let mut response = self.response_for(interaction_id);
1744 response.focused = focused;
1745
1746 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1747 for (i, event) in self.events.iter().enumerate() {
1748 if self.consumed[i] {
1749 continue;
1750 }
1751 if let Event::Mouse(mouse) = event {
1752 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1753 continue;
1754 }
1755 let in_bounds = mouse.x >= rect.x
1756 && mouse.x < rect.right()
1757 && mouse.y >= rect.y
1758 && mouse.y < rect.bottom();
1759 if !in_bounds {
1760 continue;
1761 }
1762 let clicked_idx = (mouse.y - rect.y) as usize;
1763 if clicked_idx < state.items.len() {
1764 state.selected = clicked_idx;
1765 self.consumed[i] = true;
1766 }
1767 }
1768 }
1769 }
1770
1771 self.commands.push(Command::BeginContainer {
1772 direction: Direction::Column,
1773 gap: 0,
1774 align: Align::Start,
1775 align_self: None,
1776 justify: Justify::Start,
1777 border: None,
1778 border_sides: BorderSides::all(),
1779 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
1780 bg_color: None,
1781 padding: Padding::default(),
1782 margin: Margin::default(),
1783 constraints: Constraints::default(),
1784 title: None,
1785 grow: 0,
1786 group_name: None,
1787 });
1788
1789 for (idx, item) in state.items.iter().enumerate() {
1790 let is_selected = idx == state.selected;
1791 let marker = if is_selected { "●" } else { "○" };
1792 let style = if is_selected {
1793 if focused {
1794 Style::new()
1795 .bold()
1796 .fg(colors.accent.unwrap_or(self.theme.primary))
1797 } else {
1798 Style::new().fg(colors.accent.unwrap_or(self.theme.primary))
1799 }
1800 } else {
1801 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
1802 };
1803 let prefix = if focused && idx == state.selected {
1804 "▸ "
1805 } else {
1806 " "
1807 };
1808 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1809 row.push_str(prefix);
1810 row.push_str(marker);
1811 row.push(' ');
1812 row.push_str(item);
1813 self.styled(row, style);
1814 }
1815
1816 self.commands.push(Command::EndContainer);
1817 self.last_text_idx = None;
1818 response.changed = state.selected != old_selected;
1819 response
1820 }
1821
1822 pub fn multi_select(&mut self, state: &mut MultiSelectState) -> Response {
1826 if state.items.is_empty() {
1827 return Response::none();
1828 }
1829 state.cursor = state.cursor.min(state.items.len().saturating_sub(1));
1830 let focused = self.register_focusable();
1831 let old_selected = state.selected.clone();
1832
1833 if focused {
1834 let mut consumed_indices = Vec::new();
1835 for (i, event) in self.events.iter().enumerate() {
1836 if self.consumed[i] {
1837 continue;
1838 }
1839 if let Event::Key(key) = event {
1840 if key.kind != KeyEventKind::Press {
1841 continue;
1842 }
1843 match key.code {
1844 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
1845 let _ = handle_vertical_nav(
1846 &mut state.cursor,
1847 state.items.len().saturating_sub(1),
1848 key.code.clone(),
1849 );
1850 consumed_indices.push(i);
1851 }
1852 KeyCode::Char(' ') | KeyCode::Enter => {
1853 state.toggle(state.cursor);
1854 consumed_indices.push(i);
1855 }
1856 _ => {}
1857 }
1858 }
1859 }
1860 for idx in consumed_indices {
1861 self.consumed[idx] = true;
1862 }
1863 }
1864
1865 let interaction_id = self.next_interaction_id();
1866 let mut response = self.response_for(interaction_id);
1867 response.focused = focused;
1868
1869 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
1870 for (i, event) in self.events.iter().enumerate() {
1871 if self.consumed[i] {
1872 continue;
1873 }
1874 if let Event::Mouse(mouse) = event {
1875 if !matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
1876 continue;
1877 }
1878 let in_bounds = mouse.x >= rect.x
1879 && mouse.x < rect.right()
1880 && mouse.y >= rect.y
1881 && mouse.y < rect.bottom();
1882 if !in_bounds {
1883 continue;
1884 }
1885 let clicked_idx = (mouse.y - rect.y) as usize;
1886 if clicked_idx < state.items.len() {
1887 state.toggle(clicked_idx);
1888 state.cursor = clicked_idx;
1889 self.consumed[i] = true;
1890 }
1891 }
1892 }
1893 }
1894
1895 self.commands.push(Command::BeginContainer {
1896 direction: Direction::Column,
1897 gap: 0,
1898 align: Align::Start,
1899 align_self: None,
1900 justify: Justify::Start,
1901 border: None,
1902 border_sides: BorderSides::all(),
1903 border_style: Style::new().fg(self.theme.border),
1904 bg_color: None,
1905 padding: Padding::default(),
1906 margin: Margin::default(),
1907 constraints: Constraints::default(),
1908 title: None,
1909 grow: 0,
1910 group_name: None,
1911 });
1912
1913 for (idx, item) in state.items.iter().enumerate() {
1914 let checked = state.selected.contains(&idx);
1915 let marker = if checked { "[x]" } else { "[ ]" };
1916 let is_cursor = idx == state.cursor;
1917 let style = if is_cursor && focused {
1918 Style::new().bold().fg(self.theme.primary)
1919 } else if checked {
1920 Style::new().fg(self.theme.success)
1921 } else {
1922 Style::new().fg(self.theme.text)
1923 };
1924 let prefix = if is_cursor && focused { "▸ " } else { " " };
1925 let mut row = String::with_capacity(prefix.len() + marker.len() + item.len() + 1);
1926 row.push_str(prefix);
1927 row.push_str(marker);
1928 row.push(' ');
1929 row.push_str(item);
1930 self.styled(row, style);
1931 }
1932
1933 self.commands.push(Command::EndContainer);
1934 self.last_text_idx = None;
1935 response.changed = state.selected != old_selected;
1936 response
1937 }
1938
1939 pub fn rich_log(&mut self, state: &mut RichLogState) -> Response {
1943 let focused = self.register_focusable();
1944 let interaction_id = self.next_interaction_id();
1945 let mut response = self.response_for(interaction_id);
1946 response.focused = focused;
1947
1948 let widget_height = if response.rect.height > 0 {
1949 response.rect.height as usize
1950 } else {
1951 self.area_height as usize
1952 };
1953 let viewport_height = widget_height.saturating_sub(2);
1954 let effective_height = if viewport_height == 0 {
1955 state.entries.len().max(1)
1956 } else {
1957 viewport_height
1958 };
1959 let show_indicator = state.entries.len() > effective_height;
1960 let visible_rows = if show_indicator {
1961 effective_height.saturating_sub(1).max(1)
1962 } else {
1963 effective_height
1964 };
1965 let max_offset = state.entries.len().saturating_sub(visible_rows);
1966 if state.auto_scroll && state.scroll_offset == usize::MAX {
1967 state.scroll_offset = max_offset;
1968 } else {
1969 state.scroll_offset = state.scroll_offset.min(max_offset);
1970 }
1971 let old_offset = state.scroll_offset;
1972
1973 if focused {
1974 let mut consumed_indices = Vec::new();
1975 for (i, event) in self.events.iter().enumerate() {
1976 if self.consumed[i] {
1977 continue;
1978 }
1979 if let Event::Key(key) = event {
1980 if key.kind != KeyEventKind::Press {
1981 continue;
1982 }
1983 match key.code {
1984 KeyCode::Up | KeyCode::Char('k') => {
1985 state.scroll_offset = state.scroll_offset.saturating_sub(1);
1986 consumed_indices.push(i);
1987 }
1988 KeyCode::Down | KeyCode::Char('j') => {
1989 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
1990 consumed_indices.push(i);
1991 }
1992 KeyCode::PageUp => {
1993 state.scroll_offset = state.scroll_offset.saturating_sub(10);
1994 consumed_indices.push(i);
1995 }
1996 KeyCode::PageDown => {
1997 state.scroll_offset = (state.scroll_offset + 10).min(max_offset);
1998 consumed_indices.push(i);
1999 }
2000 KeyCode::Home => {
2001 state.scroll_offset = 0;
2002 consumed_indices.push(i);
2003 }
2004 KeyCode::End => {
2005 state.scroll_offset = max_offset;
2006 consumed_indices.push(i);
2007 }
2008 _ => {}
2009 }
2010 }
2011 }
2012 for idx in consumed_indices {
2013 self.consumed[idx] = true;
2014 }
2015 }
2016
2017 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
2018 for (i, event) in self.events.iter().enumerate() {
2019 if self.consumed[i] {
2020 continue;
2021 }
2022 if let Event::Mouse(mouse) = event {
2023 let in_bounds = mouse.x >= rect.x
2024 && mouse.x < rect.right()
2025 && mouse.y >= rect.y
2026 && mouse.y < rect.bottom();
2027 if !in_bounds {
2028 continue;
2029 }
2030 let delta = self.scroll_lines_per_event as usize;
2031 match mouse.kind {
2032 MouseKind::ScrollUp => {
2033 state.scroll_offset = state.scroll_offset.saturating_sub(delta);
2034 self.consumed[i] = true;
2035 }
2036 MouseKind::ScrollDown => {
2037 state.scroll_offset = (state.scroll_offset + delta).min(max_offset);
2038 self.consumed[i] = true;
2039 }
2040 _ => {}
2041 }
2042 }
2043 }
2044 }
2045
2046 state.scroll_offset = state.scroll_offset.min(max_offset);
2047 let start = state
2048 .scroll_offset
2049 .min(state.entries.len().saturating_sub(visible_rows));
2050 let end = (start + visible_rows).min(state.entries.len());
2051
2052 self.commands.push(Command::BeginContainer {
2053 direction: Direction::Column,
2054 gap: 0,
2055 align: Align::Start,
2056 align_self: None,
2057 justify: Justify::Start,
2058 border: Some(Border::Single),
2059 border_sides: BorderSides::all(),
2060 border_style: Style::new().fg(self.theme.border),
2061 bg_color: None,
2062 padding: Padding::default(),
2063 margin: Margin::default(),
2064 constraints: Constraints::default(),
2065 title: None,
2066 grow: 0,
2067 group_name: None,
2068 });
2069
2070 for entry in state
2071 .entries
2072 .iter()
2073 .skip(start)
2074 .take(end.saturating_sub(start))
2075 {
2076 self.commands.push(Command::RichText {
2077 segments: entry.segments.clone(),
2078 wrap: false,
2079 align: Align::Start,
2080 margin: Margin::default(),
2081 constraints: Constraints::default(),
2082 });
2083 }
2084
2085 if show_indicator {
2086 let end_pos = end.min(state.entries.len());
2087 let line = format!(
2088 "{}-{} / {}",
2089 start.saturating_add(1),
2090 end_pos,
2091 state.entries.len()
2092 );
2093 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
2094 }
2095
2096 self.commands.push(Command::EndContainer);
2097 self.last_text_idx = None;
2098 response.changed = state.scroll_offset != old_offset;
2099 response
2100 }
2101
2102 pub fn virtual_list(
2109 &mut self,
2110 state: &mut ListState,
2111 visible_height: usize,
2112 f: impl Fn(&mut Context, usize),
2113 ) -> Response {
2114 if state.items.is_empty() {
2115 return Response::none();
2116 }
2117 state.selected = state.selected.min(state.items.len().saturating_sub(1));
2118 let interaction_id = self.next_interaction_id();
2119 let focused = self.register_focusable();
2120 let old_selected = state.selected;
2121
2122 if focused {
2123 let mut consumed_indices = Vec::new();
2124 for (i, event) in self.events.iter().enumerate() {
2125 if self.consumed[i] {
2126 continue;
2127 }
2128 if let Event::Key(key) = event {
2129 if key.kind != KeyEventKind::Press {
2130 continue;
2131 }
2132 match key.code {
2133 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
2134 let _ = handle_vertical_nav(
2135 &mut state.selected,
2136 state.items.len().saturating_sub(1),
2137 key.code.clone(),
2138 );
2139 consumed_indices.push(i);
2140 }
2141 KeyCode::PageUp => {
2142 state.selected = state.selected.saturating_sub(visible_height);
2143 consumed_indices.push(i);
2144 }
2145 KeyCode::PageDown => {
2146 state.selected = (state.selected + visible_height)
2147 .min(state.items.len().saturating_sub(1));
2148 consumed_indices.push(i);
2149 }
2150 KeyCode::Home => {
2151 state.selected = 0;
2152 consumed_indices.push(i);
2153 }
2154 KeyCode::End => {
2155 state.selected = state.items.len().saturating_sub(1);
2156 consumed_indices.push(i);
2157 }
2158 _ => {}
2159 }
2160 }
2161 }
2162 for idx in consumed_indices {
2163 self.consumed[idx] = true;
2164 }
2165 }
2166
2167 let start = if state.selected >= visible_height {
2168 state.selected - visible_height + 1
2169 } else {
2170 0
2171 };
2172 let end = (start + visible_height).min(state.items.len());
2173
2174 self.commands.push(Command::BeginContainer {
2175 direction: Direction::Column,
2176 gap: 0,
2177 align: Align::Start,
2178 align_self: None,
2179 justify: Justify::Start,
2180 border: None,
2181 border_sides: BorderSides::all(),
2182 border_style: Style::new().fg(self.theme.border),
2183 bg_color: None,
2184 padding: Padding::default(),
2185 margin: Margin::default(),
2186 constraints: Constraints::default(),
2187 title: None,
2188 grow: 0,
2189 group_name: None,
2190 });
2191
2192 if start > 0 {
2193 let hidden = start.to_string();
2194 let mut line = String::with_capacity(hidden.len() + 10);
2195 line.push_str(" ↑ ");
2196 line.push_str(&hidden);
2197 line.push_str(" more");
2198 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2199 }
2200
2201 for idx in start..end {
2202 f(self, idx);
2203 }
2204
2205 let remaining = state.items.len().saturating_sub(end);
2206 if remaining > 0 {
2207 let hidden = remaining.to_string();
2208 let mut line = String::with_capacity(hidden.len() + 10);
2209 line.push_str(" ↓ ");
2210 line.push_str(&hidden);
2211 line.push_str(" more");
2212 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
2213 }
2214
2215 self.commands.push(Command::EndContainer);
2216 self.last_text_idx = None;
2217 let mut response = self.response_for(interaction_id);
2218 response.focused = focused;
2219 response.changed = state.selected != old_selected;
2220 response
2221 }
2222
2223 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
2227 if !state.open {
2228 return Response::none();
2229 }
2230
2231 state.last_selected = None;
2232 let interaction_id = self.next_interaction_id();
2233
2234 let filtered = state.filtered_indices();
2235 let sel = state.selected().min(filtered.len().saturating_sub(1));
2236 state.set_selected(sel);
2237
2238 let mut consumed_indices = Vec::new();
2239
2240 for (i, event) in self.events.iter().enumerate() {
2241 if self.consumed[i] {
2242 continue;
2243 }
2244 if let Event::Key(key) = event {
2245 if key.kind != KeyEventKind::Press {
2246 continue;
2247 }
2248 match key.code {
2249 KeyCode::Esc => {
2250 state.open = false;
2251 consumed_indices.push(i);
2252 }
2253 KeyCode::Up => {
2254 let s = state.selected();
2255 state.set_selected(s.saturating_sub(1));
2256 consumed_indices.push(i);
2257 }
2258 KeyCode::Down => {
2259 let s = state.selected();
2260 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
2261 consumed_indices.push(i);
2262 }
2263 KeyCode::Enter => {
2264 if let Some(&cmd_idx) = filtered.get(state.selected()) {
2265 state.last_selected = Some(cmd_idx);
2266 state.open = false;
2267 }
2268 consumed_indices.push(i);
2269 }
2270 KeyCode::Backspace => {
2271 if state.cursor > 0 {
2272 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
2273 let end_idx = byte_index_for_char(&state.input, state.cursor);
2274 state.input.replace_range(byte_idx..end_idx, "");
2275 state.cursor -= 1;
2276 state.set_selected(0);
2277 }
2278 consumed_indices.push(i);
2279 }
2280 KeyCode::Char(ch) => {
2281 let byte_idx = byte_index_for_char(&state.input, state.cursor);
2282 state.input.insert(byte_idx, ch);
2283 state.cursor += 1;
2284 state.set_selected(0);
2285 consumed_indices.push(i);
2286 }
2287 _ => {}
2288 }
2289 }
2290 }
2291 for idx in consumed_indices {
2292 self.consumed[idx] = true;
2293 }
2294
2295 let filtered = state.filtered_indices();
2296
2297 let _ = self.modal(|ui| {
2298 let primary = ui.theme.primary;
2299 let _ = ui
2300 .container()
2301 .border(Border::Rounded)
2302 .border_style(Style::new().fg(primary))
2303 .pad(1)
2304 .max_w(60)
2305 .col(|ui| {
2306 let border_color = ui.theme.primary;
2307 let _ = ui
2308 .bordered(Border::Rounded)
2309 .border_style(Style::new().fg(border_color))
2310 .px(1)
2311 .col(|ui| {
2312 let display = if state.input.is_empty() {
2313 "Type to search...".to_string()
2314 } else {
2315 state.input.clone()
2316 };
2317 let style = if state.input.is_empty() {
2318 Style::new().dim().fg(ui.theme.text_dim)
2319 } else {
2320 Style::new().fg(ui.theme.text)
2321 };
2322 ui.styled(display, style);
2323 });
2324
2325 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
2326 let cmd = &state.commands[cmd_idx];
2327 let is_selected = list_idx == state.selected();
2328 let style = if is_selected {
2329 Style::new().bold().fg(ui.theme.primary)
2330 } else {
2331 Style::new().fg(ui.theme.text)
2332 };
2333 let prefix = if is_selected { "▸ " } else { " " };
2334 let shortcut_text = cmd
2335 .shortcut
2336 .as_deref()
2337 .map(|s| {
2338 let mut text = String::with_capacity(s.len() + 4);
2339 text.push_str(" (");
2340 text.push_str(s);
2341 text.push(')');
2342 text
2343 })
2344 .unwrap_or_default();
2345 let mut line = String::with_capacity(
2346 prefix.len() + cmd.label.len() + shortcut_text.len(),
2347 );
2348 line.push_str(prefix);
2349 line.push_str(&cmd.label);
2350 line.push_str(&shortcut_text);
2351 ui.styled(line, style);
2352 if is_selected && !cmd.description.is_empty() {
2353 let mut desc = String::with_capacity(4 + cmd.description.len());
2354 desc.push_str(" ");
2355 desc.push_str(&cmd.description);
2356 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
2357 }
2358 }
2359
2360 if filtered.is_empty() {
2361 ui.styled(
2362 " No matching commands",
2363 Style::new().dim().fg(ui.theme.text_dim),
2364 );
2365 }
2366 });
2367 });
2368
2369 let mut response = self.response_for(interaction_id);
2370 response.changed = state.last_selected.is_some();
2371 response
2372 }
2373
2374 pub fn markdown(&mut self, text: &str) -> Response {
2384 self.commands.push(Command::BeginContainer {
2385 direction: Direction::Column,
2386 gap: 0,
2387 align: Align::Start,
2388 align_self: None,
2389 justify: Justify::Start,
2390 border: None,
2391 border_sides: BorderSides::all(),
2392 border_style: Style::new().fg(self.theme.border),
2393 bg_color: None,
2394 padding: Padding::default(),
2395 margin: Margin::default(),
2396 constraints: Constraints::default(),
2397 title: None,
2398 grow: 0,
2399 group_name: None,
2400 });
2401 self.interaction_count += 1;
2402
2403 let text_style = Style::new().fg(self.theme.text);
2404 let bold_style = Style::new().fg(self.theme.text).bold();
2405 let code_style = Style::new().fg(self.theme.accent);
2406 let border_style = Style::new().fg(self.theme.border).dim();
2407
2408 let mut in_code_block = false;
2409 let mut code_block_lang = String::new();
2410 let mut code_block_lines: Vec<String> = Vec::new();
2411 let mut table_lines: Vec<String> = Vec::new();
2412
2413 for line in text.lines() {
2414 let trimmed = line.trim();
2415
2416 if in_code_block {
2417 if trimmed.starts_with("```") {
2418 in_code_block = false;
2419 let code_content = code_block_lines.join("\n");
2420 let theme = self.theme;
2421 let highlighted: Option<Vec<Vec<(String, Style)>>> =
2422 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
2423 let _ = self.container().bg(theme.surface).p(1).col(|ui| {
2424 if let Some(ref hl_lines) = highlighted {
2425 for segs in hl_lines {
2426 if segs.is_empty() {
2427 ui.text(" ");
2428 } else {
2429 ui.line(|ui| {
2430 for (t, s) in segs {
2431 ui.styled(t, *s);
2432 }
2433 });
2434 }
2435 }
2436 } else {
2437 for cl in &code_block_lines {
2438 ui.styled(cl, code_style);
2439 }
2440 }
2441 });
2442 code_block_lang.clear();
2443 code_block_lines.clear();
2444 } else {
2445 code_block_lines.push(line.to_string());
2446 }
2447 continue;
2448 }
2449
2450 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
2452 table_lines.push(trimmed.to_string());
2453 continue;
2454 }
2455 if !table_lines.is_empty() {
2457 self.render_markdown_table(
2458 &table_lines,
2459 text_style,
2460 bold_style,
2461 code_style,
2462 border_style,
2463 );
2464 table_lines.clear();
2465 }
2466
2467 if trimmed.is_empty() {
2468 self.text(" ");
2469 continue;
2470 }
2471 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
2472 self.styled("─".repeat(40), border_style);
2473 continue;
2474 }
2475 if let Some(quote) = trimmed.strip_prefix("> ") {
2476 let quote_style = Style::new().fg(self.theme.text_dim).italic();
2477 let bar_style = Style::new().fg(self.theme.border);
2478 self.line(|ui| {
2479 ui.styled("│ ", bar_style);
2480 ui.styled(quote, quote_style);
2481 });
2482 } else if let Some(heading) = trimmed.strip_prefix("### ") {
2483 self.styled(heading, Style::new().bold().fg(self.theme.accent));
2484 } else if let Some(heading) = trimmed.strip_prefix("## ") {
2485 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
2486 } else if let Some(heading) = trimmed.strip_prefix("# ") {
2487 self.styled(heading, Style::new().bold().fg(self.theme.primary));
2488 } else if let Some(item) = trimmed
2489 .strip_prefix("- ")
2490 .or_else(|| trimmed.strip_prefix("* "))
2491 {
2492 self.line_wrap(|ui| {
2493 ui.styled(" • ", text_style);
2494 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
2495 });
2496 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
2497 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
2498 if parts.len() == 2 {
2499 self.line_wrap(|ui| {
2500 let mut prefix = String::with_capacity(4 + parts[0].len());
2501 prefix.push_str(" ");
2502 prefix.push_str(parts[0]);
2503 prefix.push_str(". ");
2504 ui.styled(prefix, text_style);
2505 Self::render_md_inline_into(
2506 ui, parts[1], text_style, bold_style, code_style,
2507 );
2508 });
2509 } else {
2510 self.text(trimmed);
2511 }
2512 } else if let Some(lang) = trimmed.strip_prefix("```") {
2513 in_code_block = true;
2514 code_block_lang = lang.trim().to_string();
2515 } else {
2516 self.render_md_inline(trimmed, text_style, bold_style, code_style);
2517 }
2518 }
2519
2520 if in_code_block && !code_block_lines.is_empty() {
2521 for cl in &code_block_lines {
2522 self.styled(cl, code_style);
2523 }
2524 }
2525
2526 if !table_lines.is_empty() {
2528 self.render_markdown_table(
2529 &table_lines,
2530 text_style,
2531 bold_style,
2532 code_style,
2533 border_style,
2534 );
2535 }
2536
2537 self.commands.push(Command::EndContainer);
2538 self.last_text_idx = None;
2539 Response::none()
2540 }
2541
2542 fn render_markdown_table(
2544 &mut self,
2545 lines: &[String],
2546 text_style: Style,
2547 bold_style: Style,
2548 code_style: Style,
2549 border_style: Style,
2550 ) {
2551 if lines.is_empty() {
2552 return;
2553 }
2554
2555 let is_separator = |line: &str| -> bool {
2557 let inner = line.trim_matches('|').trim();
2558 !inner.is_empty()
2559 && inner
2560 .chars()
2561 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
2562 };
2563
2564 let parse_row = |line: &str| -> Vec<String> {
2565 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
2566 trimmed.split('|').map(|c| c.trim().to_string()).collect()
2567 };
2568
2569 let mut header: Option<Vec<String>> = None;
2570 let mut data_rows: Vec<Vec<String>> = Vec::new();
2571 let mut found_separator = false;
2572
2573 for (i, line) in lines.iter().enumerate() {
2574 if is_separator(line) {
2575 found_separator = true;
2576 continue;
2577 }
2578 if i == 0 && !found_separator {
2579 header = Some(parse_row(line));
2580 } else {
2581 data_rows.push(parse_row(line));
2582 }
2583 }
2584
2585 if !found_separator && header.is_none() && !data_rows.is_empty() {
2587 header = Some(data_rows.remove(0));
2588 }
2589
2590 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
2592 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
2593 if col_count == 0 {
2594 return;
2595 }
2596 let mut col_widths = vec![0usize; col_count];
2597 let stripped_rows: Vec<Vec<String>> = all_rows
2599 .iter()
2600 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
2601 .collect();
2602 for row in &stripped_rows {
2603 for (i, cell) in row.iter().enumerate() {
2604 if i < col_count {
2605 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
2606 }
2607 }
2608 }
2609
2610 let mut top = String::from("┌");
2612 for (i, &w) in col_widths.iter().enumerate() {
2613 for _ in 0..w + 2 {
2614 top.push('─');
2615 }
2616 top.push(if i < col_count - 1 { '┬' } else { '┐' });
2617 }
2618 self.styled(&top, border_style);
2619
2620 if let Some(ref hdr) = header {
2622 self.line(|ui| {
2623 ui.styled("│", border_style);
2624 for (i, w) in col_widths.iter().enumerate() {
2625 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
2626 let display_text = Self::md_strip(raw);
2627 let cell_w = UnicodeWidthStr::width(display_text.as_str());
2628 let padding: String = " ".repeat(w.saturating_sub(cell_w));
2629 ui.styled(" ", bold_style);
2630 ui.styled(&display_text, bold_style);
2631 ui.styled(padding, bold_style);
2632 ui.styled(" │", border_style);
2633 }
2634 });
2635
2636 let mut sep = String::from("├");
2638 for (i, &w) in col_widths.iter().enumerate() {
2639 for _ in 0..w + 2 {
2640 sep.push('─');
2641 }
2642 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
2643 }
2644 self.styled(&sep, border_style);
2645 }
2646
2647 for row in &data_rows {
2649 self.line(|ui| {
2650 ui.styled("│", border_style);
2651 for (i, w) in col_widths.iter().enumerate() {
2652 let raw = row.get(i).map(String::as_str).unwrap_or("");
2653 let display_text = Self::md_strip(raw);
2654 let cell_w = UnicodeWidthStr::width(display_text.as_str());
2655 let padding: String = " ".repeat(w.saturating_sub(cell_w));
2656 ui.styled(" ", text_style);
2657 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
2658 ui.styled(padding, text_style);
2659 ui.styled(" │", border_style);
2660 }
2661 });
2662 }
2663
2664 let mut bot = String::from("└");
2666 for (i, &w) in col_widths.iter().enumerate() {
2667 for _ in 0..w + 2 {
2668 bot.push('─');
2669 }
2670 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
2671 }
2672 self.styled(&bot, border_style);
2673 }
2674
2675 pub(crate) fn parse_inline_segments(
2676 text: &str,
2677 base: Style,
2678 bold: Style,
2679 code: Style,
2680 ) -> Vec<(String, Style)> {
2681 let mut segments: Vec<(String, Style)> = Vec::new();
2682 let mut current = String::new();
2683 let chars: Vec<char> = text.chars().collect();
2684 let mut i = 0;
2685 while i < chars.len() {
2686 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
2687 let rest: String = chars[i + 2..].iter().collect();
2688 if let Some(end) = rest.find("**") {
2689 if !current.is_empty() {
2690 segments.push((std::mem::take(&mut current), base));
2691 }
2692 let inner: String = rest[..end].to_string();
2693 let char_count = inner.chars().count();
2694 segments.push((inner, bold));
2695 i += 2 + char_count + 2;
2696 continue;
2697 }
2698 }
2699 if chars[i] == '*'
2700 && (i + 1 >= chars.len() || chars[i + 1] != '*')
2701 && (i == 0 || chars[i - 1] != '*')
2702 {
2703 let rest: String = chars[i + 1..].iter().collect();
2704 if let Some(end) = rest.find('*') {
2705 if !current.is_empty() {
2706 segments.push((std::mem::take(&mut current), base));
2707 }
2708 let inner: String = rest[..end].to_string();
2709 let char_count = inner.chars().count();
2710 segments.push((inner, base.italic()));
2711 i += 1 + char_count + 1;
2712 continue;
2713 }
2714 }
2715 if chars[i] == '`' {
2716 let rest: String = chars[i + 1..].iter().collect();
2717 if let Some(end) = rest.find('`') {
2718 if !current.is_empty() {
2719 segments.push((std::mem::take(&mut current), base));
2720 }
2721 let inner: String = rest[..end].to_string();
2722 let char_count = inner.chars().count();
2723 segments.push((inner, code));
2724 i += 1 + char_count + 1;
2725 continue;
2726 }
2727 }
2728 current.push(chars[i]);
2729 i += 1;
2730 }
2731 if !current.is_empty() {
2732 segments.push((current, base));
2733 }
2734 segments
2735 }
2736
2737 fn render_md_inline(
2742 &mut self,
2743 text: &str,
2744 text_style: Style,
2745 bold_style: Style,
2746 code_style: Style,
2747 ) {
2748 let items = Self::split_md_links(text);
2749
2750 if items.len() == 1 {
2752 if let MdInline::Text(ref t) = items[0] {
2753 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
2754 if segs.len() <= 1 {
2755 self.text(text)
2756 .wrap()
2757 .fg(text_style.fg.unwrap_or(Color::Reset));
2758 } else {
2759 self.line_wrap(|ui| {
2760 for (s, st) in segs {
2761 ui.styled(s, st);
2762 }
2763 });
2764 }
2765 return;
2766 }
2767 }
2768
2769 self.line_wrap(|ui| {
2771 for item in &items {
2772 match item {
2773 MdInline::Text(t) => {
2774 let segs =
2775 Self::parse_inline_segments(t, text_style, bold_style, code_style);
2776 for (s, st) in segs {
2777 ui.styled(s, st);
2778 }
2779 }
2780 MdInline::Link { text, url } => {
2781 ui.link(text.clone(), url.clone());
2782 }
2783 MdInline::Image { alt, .. } => {
2784 ui.styled(alt.as_str(), code_style);
2786 }
2787 }
2788 }
2789 });
2790 }
2791
2792 fn render_md_inline_into(
2798 ui: &mut Context,
2799 text: &str,
2800 text_style: Style,
2801 bold_style: Style,
2802 code_style: Style,
2803 ) {
2804 let items = Self::split_md_links(text);
2805 for item in &items {
2806 match item {
2807 MdInline::Text(t) => {
2808 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
2809 for (s, st) in segs {
2810 ui.styled(s, st);
2811 }
2812 }
2813 MdInline::Link { text, url } => {
2814 ui.link(text.clone(), url.clone());
2815 }
2816 MdInline::Image { alt, .. } => {
2817 ui.styled(alt.as_str(), code_style);
2818 }
2819 }
2820 }
2821 }
2822
2823 fn split_md_links(text: &str) -> Vec<MdInline> {
2825 let chars: Vec<char> = text.chars().collect();
2826 let mut items: Vec<MdInline> = Vec::new();
2827 let mut current = String::new();
2828 let mut i = 0;
2829
2830 while i < chars.len() {
2831 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
2833 if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
2834 if !current.is_empty() {
2835 items.push(MdInline::Text(std::mem::take(&mut current)));
2836 }
2837 items.push(MdInline::Image { alt });
2838 i += 1 + consumed;
2839 continue;
2840 }
2841 }
2842 if chars[i] == '[' {
2844 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
2845 if !current.is_empty() {
2846 items.push(MdInline::Text(std::mem::take(&mut current)));
2847 }
2848 items.push(MdInline::Link {
2849 text: link_text,
2850 url,
2851 });
2852 i += consumed;
2853 continue;
2854 }
2855 }
2856 current.push(chars[i]);
2857 i += 1;
2858 }
2859 if !current.is_empty() {
2860 items.push(MdInline::Text(current));
2861 }
2862 if items.is_empty() {
2863 items.push(MdInline::Text(String::new()));
2864 }
2865 items
2866 }
2867
2868 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
2871 if start >= chars.len() || chars[start] != '[' {
2872 return None;
2873 }
2874 let mut depth = 0i32;
2876 let mut bracket_end = None;
2877 for (j, &ch) in chars.iter().enumerate().skip(start) {
2878 if ch == '[' {
2879 depth += 1;
2880 } else if ch == ']' {
2881 depth -= 1;
2882 if depth == 0 {
2883 bracket_end = Some(j);
2884 break;
2885 }
2886 }
2887 }
2888 let bracket_end = bracket_end?;
2889 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
2891 return None;
2892 }
2893 let paren_start = bracket_end + 2;
2895 let mut paren_end = None;
2896 let mut paren_depth = 1i32;
2897 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
2898 if ch == '(' {
2899 paren_depth += 1;
2900 } else if ch == ')' {
2901 paren_depth -= 1;
2902 if paren_depth == 0 {
2903 paren_end = Some(j);
2904 break;
2905 }
2906 }
2907 }
2908 let paren_end = paren_end?;
2909 let text: String = chars[start + 1..bracket_end].iter().collect();
2910 let url: String = chars[paren_start..paren_end].iter().collect();
2911 let consumed = paren_end - start + 1;
2912 Some((text, url, consumed))
2913 }
2914
2915 fn md_strip(text: &str) -> String {
2920 let mut result = String::with_capacity(text.len());
2921 let chars: Vec<char> = text.chars().collect();
2922 let mut i = 0;
2923 while i < chars.len() {
2924 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
2926 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
2927 result.push_str(&alt);
2928 i += 1 + consumed;
2929 continue;
2930 }
2931 }
2932 if chars[i] == '[' {
2934 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
2935 result.push_str(&link_text);
2936 i += consumed;
2937 continue;
2938 }
2939 }
2940 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
2942 let rest: String = chars[i + 2..].iter().collect();
2943 if let Some(end) = rest.find("**") {
2944 let inner = &rest[..end];
2945 result.push_str(inner);
2946 i += 2 + inner.chars().count() + 2;
2947 continue;
2948 }
2949 }
2950 if chars[i] == '*'
2952 && (i + 1 >= chars.len() || chars[i + 1] != '*')
2953 && (i == 0 || chars[i - 1] != '*')
2954 {
2955 let rest: String = chars[i + 1..].iter().collect();
2956 if let Some(end) = rest.find('*') {
2957 let inner = &rest[..end];
2958 result.push_str(inner);
2959 i += 1 + inner.chars().count() + 1;
2960 continue;
2961 }
2962 }
2963 if chars[i] == '`' {
2965 let rest: String = chars[i + 1..].iter().collect();
2966 if let Some(end) = rest.find('`') {
2967 let inner = &rest[..end];
2968 result.push_str(inner);
2969 i += 1 + inner.chars().count() + 1;
2970 continue;
2971 }
2972 }
2973 result.push(chars[i]);
2974 i += 1;
2975 }
2976 result
2977 }
2978
2979 pub fn key_seq(&self, seq: &str) -> bool {
2986 if seq.is_empty() {
2987 return false;
2988 }
2989 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
2990 return false;
2991 }
2992 let target: Vec<char> = seq.chars().collect();
2993 let mut matched = 0;
2994 for (i, event) in self.events.iter().enumerate() {
2995 if self.consumed[i] {
2996 continue;
2997 }
2998 if let Event::Key(key) = event {
2999 if key.kind != KeyEventKind::Press {
3000 continue;
3001 }
3002 if let KeyCode::Char(c) = key.code {
3003 if c == target[matched] {
3004 matched += 1;
3005 if matched == target.len() {
3006 return true;
3007 }
3008 } else {
3009 matched = 0;
3010 if c == target[0] {
3011 matched = 1;
3012 }
3013 }
3014 }
3015 }
3016 }
3017 false
3018 }
3019
3020 pub fn separator(&mut self) -> &mut Self {
3025 self.commands.push(Command::Text {
3026 content: "─".repeat(200),
3027 cursor_offset: None,
3028 style: Style::new().fg(self.theme.border).dim(),
3029 grow: 0,
3030 align: Align::Start,
3031 wrap: false,
3032 truncate: false,
3033 margin: Margin::default(),
3034 constraints: Constraints::default(),
3035 });
3036 self.last_text_idx = Some(self.commands.len() - 1);
3037 self
3038 }
3039
3040 pub fn separator_colored(&mut self, color: Color) -> &mut Self {
3042 self.commands.push(Command::Text {
3043 content: "─".repeat(200),
3044 cursor_offset: None,
3045 style: Style::new().fg(color),
3046 grow: 0,
3047 align: Align::Start,
3048 wrap: false,
3049 truncate: false,
3050 margin: Margin::default(),
3051 constraints: Constraints::default(),
3052 });
3053 self.last_text_idx = Some(self.commands.len() - 1);
3054 self
3055 }
3056
3057 pub fn help(&mut self, bindings: &[(&str, &str)]) -> Response {
3063 if bindings.is_empty() {
3064 return Response::none();
3065 }
3066
3067 self.interaction_count += 1;
3068 self.commands.push(Command::BeginContainer {
3069 direction: Direction::Row,
3070 gap: 2,
3071 align: Align::Start,
3072 align_self: None,
3073 justify: Justify::Start,
3074 border: None,
3075 border_sides: BorderSides::all(),
3076 border_style: Style::new().fg(self.theme.border),
3077 bg_color: None,
3078 padding: Padding::default(),
3079 margin: Margin::default(),
3080 constraints: Constraints::default(),
3081 title: None,
3082 grow: 0,
3083 group_name: None,
3084 });
3085 for (idx, (key, action)) in bindings.iter().enumerate() {
3086 if idx > 0 {
3087 self.styled("·", Style::new().fg(self.theme.text_dim));
3088 }
3089 self.styled(*key, Style::new().bold().fg(self.theme.primary));
3090 self.styled(*action, Style::new().fg(self.theme.text_dim));
3091 }
3092 self.commands.push(Command::EndContainer);
3093 self.last_text_idx = None;
3094
3095 Response::none()
3096 }
3097
3098 pub fn help_colored(
3100 &mut self,
3101 bindings: &[(&str, &str)],
3102 key_color: Color,
3103 text_color: Color,
3104 ) -> Response {
3105 if bindings.is_empty() {
3106 return Response::none();
3107 }
3108
3109 self.interaction_count += 1;
3110 self.commands.push(Command::BeginContainer {
3111 direction: Direction::Row,
3112 gap: 2,
3113 align: Align::Start,
3114 align_self: None,
3115 justify: Justify::Start,
3116 border: None,
3117 border_sides: BorderSides::all(),
3118 border_style: Style::new().fg(self.theme.border),
3119 bg_color: None,
3120 padding: Padding::default(),
3121 margin: Margin::default(),
3122 constraints: Constraints::default(),
3123 title: None,
3124 grow: 0,
3125 group_name: None,
3126 });
3127 for (idx, (key, action)) in bindings.iter().enumerate() {
3128 if idx > 0 {
3129 self.styled("·", Style::new().fg(text_color));
3130 }
3131 self.styled(*key, Style::new().bold().fg(key_color));
3132 self.styled(*action, Style::new().fg(text_color));
3133 }
3134 self.commands.push(Command::EndContainer);
3135 self.last_text_idx = None;
3136
3137 Response::none()
3138 }
3139
3140 pub fn key(&self, c: char) -> bool {
3146 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3147 return false;
3148 }
3149 self.events.iter().enumerate().any(|(i, e)| {
3150 !self.consumed[i]
3151 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3152 })
3153 }
3154
3155 pub fn key_code(&self, code: KeyCode) -> bool {
3162 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3163 return false;
3164 }
3165 self.events.iter().enumerate().any(|(i, e)| {
3166 !self.consumed[i]
3167 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
3168 })
3169 }
3170
3171 pub fn raw_key_code(&self, code: KeyCode) -> bool {
3179 self.events.iter().enumerate().any(|(i, e)| {
3180 !self.consumed[i]
3181 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code)
3182 })
3183 }
3184
3185 pub fn key_release(&self, c: char) -> bool {
3189 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3190 return false;
3191 }
3192 self.events.iter().enumerate().any(|(i, e)| {
3193 !self.consumed[i]
3194 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == KeyCode::Char(c))
3195 })
3196 }
3197
3198 pub fn key_code_release(&self, code: KeyCode) -> bool {
3202 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3203 return false;
3204 }
3205 self.events.iter().enumerate().any(|(i, e)| {
3206 !self.consumed[i]
3207 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Release && k.code == code)
3208 })
3209 }
3210
3211 pub fn consume_key(&mut self, c: char) -> bool {
3221 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3222 return false;
3223 }
3224 for (i, event) in self.events.iter().enumerate() {
3225 if self.consumed[i] {
3226 continue;
3227 }
3228 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c))
3229 {
3230 self.consumed[i] = true;
3231 return true;
3232 }
3233 }
3234 false
3235 }
3236
3237 pub fn consume_key_code(&mut self, code: KeyCode) -> bool {
3247 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3248 return false;
3249 }
3250 for (i, event) in self.events.iter().enumerate() {
3251 if self.consumed[i] {
3252 continue;
3253 }
3254 if matches!(event, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == code) {
3255 self.consumed[i] = true;
3256 return true;
3257 }
3258 }
3259 false
3260 }
3261
3262 pub fn key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3266 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3267 return false;
3268 }
3269 self.events.iter().enumerate().any(|(i, e)| {
3270 !self.consumed[i]
3271 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3272 })
3273 }
3274
3275 pub fn raw_key_mod(&self, c: char, modifiers: KeyModifiers) -> bool {
3277 self.events.iter().enumerate().any(|(i, e)| {
3278 !self.consumed[i]
3279 && matches!(e, Event::Key(k) if k.kind == KeyEventKind::Press && k.code == KeyCode::Char(c) && k.modifiers.contains(modifiers))
3280 })
3281 }
3282
3283 pub fn mouse_down(&self) -> Option<(u32, u32)> {
3287 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3288 return None;
3289 }
3290 self.events.iter().enumerate().find_map(|(i, event)| {
3291 if self.consumed[i] {
3292 return None;
3293 }
3294 if let Event::Mouse(mouse) = event {
3295 if matches!(mouse.kind, MouseKind::Down(MouseButton::Left)) {
3296 return Some((mouse.x, mouse.y));
3297 }
3298 }
3299 None
3300 })
3301 }
3302
3303 pub fn mouse_pos(&self) -> Option<(u32, u32)> {
3308 self.mouse_pos
3309 }
3310
3311 pub fn paste(&self) -> Option<&str> {
3313 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3314 return None;
3315 }
3316 self.events.iter().enumerate().find_map(|(i, event)| {
3317 if self.consumed[i] {
3318 return None;
3319 }
3320 if let Event::Paste(ref text) = event {
3321 return Some(text.as_str());
3322 }
3323 None
3324 })
3325 }
3326
3327 pub fn scroll_up(&self) -> bool {
3329 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3330 return false;
3331 }
3332 self.events.iter().enumerate().any(|(i, event)| {
3333 !self.consumed[i]
3334 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollUp))
3335 })
3336 }
3337
3338 pub fn scroll_down(&self) -> bool {
3340 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3341 return false;
3342 }
3343 self.events.iter().enumerate().any(|(i, event)| {
3344 !self.consumed[i]
3345 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollDown))
3346 })
3347 }
3348
3349 pub fn scroll_left(&self) -> bool {
3351 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3352 return false;
3353 }
3354 self.events.iter().enumerate().any(|(i, event)| {
3355 !self.consumed[i]
3356 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollLeft))
3357 })
3358 }
3359
3360 pub fn scroll_right(&self) -> bool {
3362 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
3363 return false;
3364 }
3365 self.events.iter().enumerate().any(|(i, event)| {
3366 !self.consumed[i]
3367 && matches!(event, Event::Mouse(mouse) if matches!(mouse.kind, MouseKind::ScrollRight))
3368 })
3369 }
3370
3371 pub fn quit(&mut self) {
3373 self.should_quit = true;
3374 }
3375
3376 pub fn copy_to_clipboard(&mut self, text: impl Into<String>) {
3384 self.clipboard_text = Some(text.into());
3385 }
3386
3387 pub fn theme(&self) -> &Theme {
3389 &self.theme
3390 }
3391
3392 pub fn set_theme(&mut self, theme: Theme) {
3396 self.theme = theme;
3397 }
3398
3399 pub fn is_dark_mode(&self) -> bool {
3401 self.dark_mode
3402 }
3403
3404 pub fn set_dark_mode(&mut self, dark: bool) {
3406 self.dark_mode = dark;
3407 }
3408
3409 pub fn width(&self) -> u32 {
3413 self.area_width
3414 }
3415
3416 pub fn breakpoint(&self) -> Breakpoint {
3440 let w = self.area_width;
3441 if w < 40 {
3442 Breakpoint::Xs
3443 } else if w < 80 {
3444 Breakpoint::Sm
3445 } else if w < 120 {
3446 Breakpoint::Md
3447 } else if w < 160 {
3448 Breakpoint::Lg
3449 } else {
3450 Breakpoint::Xl
3451 }
3452 }
3453
3454 pub fn height(&self) -> u32 {
3456 self.area_height
3457 }
3458
3459 pub fn tick(&self) -> u64 {
3464 self.tick
3465 }
3466
3467 pub fn debug_enabled(&self) -> bool {
3471 self.debug
3472 }
3473}
3474
3475fn calendar_month_name(month: u32) -> &'static str {
3476 match month {
3477 1 => "Jan",
3478 2 => "Feb",
3479 3 => "Mar",
3480 4 => "Apr",
3481 5 => "May",
3482 6 => "Jun",
3483 7 => "Jul",
3484 8 => "Aug",
3485 9 => "Sep",
3486 10 => "Oct",
3487 11 => "Nov",
3488 12 => "Dec",
3489 _ => "???",
3490 }
3491}
3492
3493fn calendar_move_cursor_by_days(state: &mut CalendarState, delta: i32) {
3494 let mut remaining = delta;
3495 while remaining != 0 {
3496 let days = CalendarState::days_in_month(state.year, state.month);
3497 if remaining > 0 {
3498 let forward = days.saturating_sub(state.cursor_day) as i32;
3499 if remaining <= forward {
3500 state.cursor_day += remaining as u32;
3501 return;
3502 }
3503
3504 remaining -= forward + 1;
3505 if state.month == 12 {
3506 state.month = 1;
3507 state.year += 1;
3508 } else {
3509 state.month += 1;
3510 }
3511 state.cursor_day = 1;
3512 } else {
3513 let backward = state.cursor_day.saturating_sub(1) as i32;
3514 if -remaining <= backward {
3515 state.cursor_day -= (-remaining) as u32;
3516 return;
3517 }
3518
3519 remaining += backward + 1;
3520 if state.month == 1 {
3521 state.month = 12;
3522 state.year -= 1;
3523 } else {
3524 state.month -= 1;
3525 }
3526 state.cursor_day = CalendarState::days_in_month(state.year, state.month);
3527 }
3528 }
3529}