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