1use super::*;
2use crate::KeyMap;
3
4impl Context {
5 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
18 let content = s.into();
19 self.commands.push(Command::Text {
20 content,
21 style: Style::new().fg(self.theme.text),
22 grow: 0,
23 align: Align::Start,
24 wrap: false,
25 margin: Margin::default(),
26 constraints: Constraints::default(),
27 });
28 self.last_text_idx = Some(self.commands.len() - 1);
29 self
30 }
31
32 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
38 let url_str = url.into();
39 let focused = self.register_focusable();
40 let interaction_id = self.interaction_count;
41 self.interaction_count += 1;
42 let response = self.response_for(interaction_id);
43
44 let mut activated = response.clicked;
45 if focused {
46 for (i, event) in self.events.iter().enumerate() {
47 if let Event::Key(key) = event {
48 if key.kind != KeyEventKind::Press {
49 continue;
50 }
51 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
52 activated = true;
53 self.consumed[i] = true;
54 }
55 }
56 }
57 }
58
59 if activated {
60 let _ = open_url(&url_str);
61 }
62
63 let style = if focused {
64 Style::new()
65 .fg(self.theme.primary)
66 .bg(self.theme.surface_hover)
67 .underline()
68 .bold()
69 } else if response.hovered {
70 Style::new()
71 .fg(self.theme.accent)
72 .bg(self.theme.surface_hover)
73 .underline()
74 } else {
75 Style::new().fg(self.theme.primary).underline()
76 };
77
78 self.commands.push(Command::Link {
79 text: text.into(),
80 url: url_str,
81 style,
82 margin: Margin::default(),
83 constraints: Constraints::default(),
84 });
85 self.last_text_idx = Some(self.commands.len() - 1);
86 self
87 }
88
89 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
94 let content = s.into();
95 self.commands.push(Command::Text {
96 content,
97 style: Style::new().fg(self.theme.text),
98 grow: 0,
99 align: Align::Start,
100 wrap: true,
101 margin: Margin::default(),
102 constraints: Constraints::default(),
103 });
104 self.last_text_idx = Some(self.commands.len() - 1);
105 self
106 }
107
108 pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
110 let pairs: Vec<(&str, &str)> = keymap
111 .visible_bindings()
112 .map(|binding| (binding.display.as_str(), binding.description.as_str()))
113 .collect();
114 self.help(&pairs)
115 }
116
117 pub fn bold(&mut self) -> &mut Self {
121 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
122 self
123 }
124
125 pub fn dim(&mut self) -> &mut Self {
130 let text_dim = self.theme.text_dim;
131 self.modify_last_style(|s| {
132 s.modifiers |= Modifiers::DIM;
133 if s.fg.is_none() {
134 s.fg = Some(text_dim);
135 }
136 });
137 self
138 }
139
140 pub fn italic(&mut self) -> &mut Self {
142 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
143 self
144 }
145
146 pub fn underline(&mut self) -> &mut Self {
148 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
149 self
150 }
151
152 pub fn reversed(&mut self) -> &mut Self {
154 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
155 self
156 }
157
158 pub fn strikethrough(&mut self) -> &mut Self {
160 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
161 self
162 }
163
164 pub fn fg(&mut self, color: Color) -> &mut Self {
166 self.modify_last_style(|s| s.fg = Some(color));
167 self
168 }
169
170 pub fn bg(&mut self, color: Color) -> &mut Self {
172 self.modify_last_style(|s| s.bg = Some(color));
173 self
174 }
175
176 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
177 let apply_group_style = self
178 .group_stack
179 .last()
180 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
181 .unwrap_or(false);
182 if apply_group_style {
183 self.modify_last_style(|s| s.fg = Some(color));
184 }
185 self
186 }
187
188 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
189 let apply_group_style = self
190 .group_stack
191 .last()
192 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
193 .unwrap_or(false);
194 if apply_group_style {
195 self.modify_last_style(|s| s.bg = Some(color));
196 }
197 self
198 }
199
200 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
205 self.commands.push(Command::Text {
206 content: s.into(),
207 style,
208 grow: 0,
209 align: Align::Start,
210 wrap: false,
211 margin: Margin::default(),
212 constraints: Constraints::default(),
213 });
214 self.last_text_idx = Some(self.commands.len() - 1);
215 self
216 }
217
218 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
240 let width = img.width;
241 let height = img.height;
242
243 self.container().w(width).h(height).gap(0).col(|ui| {
244 for row in 0..height {
245 ui.container().gap(0).row(|ui| {
246 for col in 0..width {
247 let idx = (row * width + col) as usize;
248 if let Some(&(upper, lower)) = img.pixels.get(idx) {
249 ui.styled("▀", Style::new().fg(upper).bg(lower));
250 }
251 }
252 });
253 }
254 });
255
256 Response::none()
257 }
258
259 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
275 if state.streaming {
276 state.cursor_tick = state.cursor_tick.wrapping_add(1);
277 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
278 }
279
280 if state.content.is_empty() && state.streaming {
281 let cursor = if state.cursor_visible { "▌" } else { " " };
282 let primary = self.theme.primary;
283 self.text(cursor).fg(primary);
284 return Response::none();
285 }
286
287 if !state.content.is_empty() {
288 if state.streaming && state.cursor_visible {
289 self.text_wrap(format!("{}▌", state.content));
290 } else {
291 self.text_wrap(&state.content);
292 }
293 }
294
295 Response::none()
296 }
297
298 pub fn streaming_markdown(
316 &mut self,
317 state: &mut crate::widgets::StreamingMarkdownState,
318 ) -> Response {
319 if state.streaming {
320 state.cursor_tick = state.cursor_tick.wrapping_add(1);
321 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
322 }
323
324 if state.content.is_empty() && state.streaming {
325 let cursor = if state.cursor_visible { "▌" } else { " " };
326 let primary = self.theme.primary;
327 self.text(cursor).fg(primary);
328 return Response::none();
329 }
330
331 let show_cursor = state.streaming && state.cursor_visible;
332 let trailing_newline = state.content.ends_with('\n');
333 let lines: Vec<&str> = state.content.lines().collect();
334 let last_line_index = lines.len().saturating_sub(1);
335
336 self.commands.push(Command::BeginContainer {
337 direction: Direction::Column,
338 gap: 0,
339 align: Align::Start,
340 justify: Justify::Start,
341 border: None,
342 border_sides: BorderSides::all(),
343 border_style: Style::new().fg(self.theme.border),
344 bg_color: None,
345 padding: Padding::default(),
346 margin: Margin::default(),
347 constraints: Constraints::default(),
348 title: None,
349 grow: 0,
350 group_name: None,
351 });
352 self.interaction_count += 1;
353
354 let text_style = Style::new().fg(self.theme.text);
355 let bold_style = Style::new().fg(self.theme.text).bold();
356 let code_style = Style::new().fg(self.theme.accent);
357 let border_style = Style::new().fg(self.theme.border).dim();
358
359 let mut in_code_block = false;
360 let mut code_block_lang = String::new();
361
362 for (idx, line) in lines.iter().enumerate() {
363 let line = *line;
364 let trimmed = line.trim();
365 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
366 let cursor = if append_cursor { "▌" } else { "" };
367
368 if in_code_block {
369 if trimmed.starts_with("```") {
370 in_code_block = false;
371 code_block_lang.clear();
372 self.styled(format!(" └────{cursor}"), border_style);
373 } else {
374 self.styled(format!(" {line}{cursor}"), code_style);
375 }
376 continue;
377 }
378
379 if trimmed.is_empty() {
380 if append_cursor {
381 self.styled("▌", Style::new().fg(self.theme.primary));
382 } else {
383 self.text(" ");
384 }
385 continue;
386 }
387
388 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
389 self.styled(format!("{}{}", "─".repeat(40), cursor), border_style);
390 continue;
391 }
392
393 if let Some(heading) = trimmed.strip_prefix("### ") {
394 self.styled(
395 format!("{heading}{cursor}"),
396 Style::new().bold().fg(self.theme.accent),
397 );
398 continue;
399 }
400
401 if let Some(heading) = trimmed.strip_prefix("## ") {
402 self.styled(
403 format!("{heading}{cursor}"),
404 Style::new().bold().fg(self.theme.secondary),
405 );
406 continue;
407 }
408
409 if let Some(heading) = trimmed.strip_prefix("# ") {
410 self.styled(
411 format!("{heading}{cursor}"),
412 Style::new().bold().fg(self.theme.primary),
413 );
414 continue;
415 }
416
417 if let Some(code) = trimmed.strip_prefix("```") {
418 in_code_block = true;
419 code_block_lang = code.trim().to_string();
420 let label = if code_block_lang.is_empty() {
421 "code".to_string()
422 } else {
423 format!("code:{}", code_block_lang)
424 };
425 self.styled(format!(" ┌─{label}─{cursor}"), border_style);
426 continue;
427 }
428
429 if let Some(item) = trimmed
430 .strip_prefix("- ")
431 .or_else(|| trimmed.strip_prefix("* "))
432 {
433 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
434 if segs.len() <= 1 {
435 self.styled(format!(" • {item}{cursor}"), text_style);
436 } else {
437 self.line(|ui| {
438 ui.styled(" • ", text_style);
439 for (s, st) in segs {
440 ui.styled(s, st);
441 }
442 if append_cursor {
443 ui.styled("▌", Style::new().fg(ui.theme.primary));
444 }
445 });
446 }
447 continue;
448 }
449
450 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
451 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
452 if parts.len() == 2 {
453 let segs =
454 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
455 if segs.len() <= 1 {
456 self.styled(
457 format!(" {}. {}{}", parts[0], parts[1], cursor),
458 text_style,
459 );
460 } else {
461 self.line(|ui| {
462 ui.styled(format!(" {}. ", parts[0]), text_style);
463 for (s, st) in segs {
464 ui.styled(s, st);
465 }
466 if append_cursor {
467 ui.styled("▌", Style::new().fg(ui.theme.primary));
468 }
469 });
470 }
471 } else {
472 self.styled(format!("{trimmed}{cursor}"), text_style);
473 }
474 continue;
475 }
476
477 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
478 if segs.len() <= 1 {
479 self.styled(format!("{trimmed}{cursor}"), text_style);
480 } else {
481 self.line(|ui| {
482 for (s, st) in segs {
483 ui.styled(s, st);
484 }
485 if append_cursor {
486 ui.styled("▌", Style::new().fg(ui.theme.primary));
487 }
488 });
489 }
490 }
491
492 if show_cursor && trailing_newline {
493 if in_code_block {
494 self.styled(" ▌", code_style);
495 } else {
496 self.styled("▌", Style::new().fg(self.theme.primary));
497 }
498 }
499
500 state.in_code_block = in_code_block;
501 state.code_block_lang = code_block_lang;
502
503 self.commands.push(Command::EndContainer);
504 self.last_text_idx = None;
505 Response::none()
506 }
507
508 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
523 let old_action = state.action;
524 let theme = self.theme;
525 self.bordered(Border::Rounded).col(|ui| {
526 ui.row(|ui| {
527 ui.text("⚡").fg(theme.warning);
528 ui.text(&state.tool_name).bold().fg(theme.primary);
529 });
530 ui.text(&state.description).dim();
531
532 if state.action == ApprovalAction::Pending {
533 ui.row(|ui| {
534 if ui.button("✓ Approve").clicked {
535 state.action = ApprovalAction::Approved;
536 }
537 if ui.button("✗ Reject").clicked {
538 state.action = ApprovalAction::Rejected;
539 }
540 });
541 } else {
542 let (label, color) = match state.action {
543 ApprovalAction::Approved => ("✓ Approved", theme.success),
544 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
545 ApprovalAction::Pending => unreachable!(),
546 };
547 ui.text(label).fg(color).bold();
548 }
549 });
550
551 Response {
552 changed: state.action != old_action,
553 ..Response::none()
554 }
555 }
556
557 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
570 if items.is_empty() {
571 return Response::none();
572 }
573
574 let theme = self.theme;
575 let total: usize = items.iter().map(|item| item.tokens).sum();
576
577 self.container().row(|ui| {
578 ui.text("📎").dim();
579 for item in items {
580 ui.text(format!(
581 "{} ({})",
582 item.label,
583 format_token_count(item.tokens)
584 ))
585 .fg(theme.secondary);
586 }
587 ui.spacer();
588 ui.text(format!("Σ {}", format_token_count(total))).dim();
589 });
590
591 Response::none()
592 }
593
594 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
595 use crate::widgets::AlertLevel;
596
597 let theme = self.theme;
598 let (icon, color) = match level {
599 AlertLevel::Info => ("ℹ", theme.accent),
600 AlertLevel::Success => ("✓", theme.success),
601 AlertLevel::Warning => ("⚠", theme.warning),
602 AlertLevel::Error => ("✕", theme.error),
603 };
604
605 let focused = self.register_focusable();
606 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
607
608 let mut response = self.container().col(|ui| {
609 ui.line(|ui| {
610 ui.text(format!(" {icon} ")).fg(color).bold();
611 ui.text(message).grow(1);
612 ui.text(" [×] ").dim();
613 });
614 });
615 response.focused = focused;
616 if key_dismiss {
617 response.clicked = true;
618 }
619
620 response
621 }
622
623 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
637 let focused = self.register_focusable();
638 let mut is_yes = *result;
639 let mut clicked = false;
640
641 if focused {
642 let mut consumed_indices = Vec::new();
643 for (i, event) in self.events.iter().enumerate() {
644 if let Event::Key(key) = event {
645 if key.kind != KeyEventKind::Press {
646 continue;
647 }
648
649 match key.code {
650 KeyCode::Char('y') => {
651 is_yes = true;
652 *result = true;
653 clicked = true;
654 consumed_indices.push(i);
655 }
656 KeyCode::Char('n') => {
657 is_yes = false;
658 *result = false;
659 clicked = true;
660 consumed_indices.push(i);
661 }
662 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
663 is_yes = !is_yes;
664 *result = is_yes;
665 consumed_indices.push(i);
666 }
667 KeyCode::Enter => {
668 *result = is_yes;
669 clicked = true;
670 consumed_indices.push(i);
671 }
672 _ => {}
673 }
674 }
675 }
676
677 for idx in consumed_indices {
678 self.consumed[idx] = true;
679 }
680 }
681
682 let yes_style = if is_yes {
683 if focused {
684 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
685 } else {
686 Style::new().fg(self.theme.success).bold()
687 }
688 } else {
689 Style::new().fg(self.theme.text_dim)
690 };
691 let no_style = if !is_yes {
692 if focused {
693 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
694 } else {
695 Style::new().fg(self.theme.error).bold()
696 }
697 } else {
698 Style::new().fg(self.theme.text_dim)
699 };
700
701 let mut response = self.row(|ui| {
702 ui.text(question);
703 ui.text(" ");
704 ui.styled("[Yes]", yes_style);
705 ui.text(" ");
706 ui.styled("[No]", no_style);
707 });
708 response.focused = focused;
709 response.clicked = clicked;
710 response.changed = clicked;
711 response
712 }
713
714 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
715 self.breadcrumb_with(segments, " › ")
716 }
717
718 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
719 let theme = self.theme;
720 let last_idx = segments.len().saturating_sub(1);
721 let mut clicked_idx: Option<usize> = None;
722
723 self.row(|ui| {
724 for (i, segment) in segments.iter().enumerate() {
725 let is_last = i == last_idx;
726 if is_last {
727 ui.text(*segment).bold();
728 } else {
729 let focused = ui.register_focusable();
730 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
731 let resp = ui.interaction();
732 let color = if resp.hovered || focused {
733 theme.accent
734 } else {
735 theme.primary
736 };
737 ui.text(*segment).fg(color).underline();
738 if resp.clicked || pressed {
739 clicked_idx = Some(i);
740 }
741 ui.text(separator).dim();
742 }
743 }
744 });
745
746 clicked_idx
747 }
748
749 pub fn accordion(
750 &mut self,
751 title: &str,
752 open: &mut bool,
753 f: impl FnOnce(&mut Context),
754 ) -> Response {
755 let theme = self.theme;
756 let focused = self.register_focusable();
757 let old_open = *open;
758
759 if focused && self.key_code(KeyCode::Enter) {
760 *open = !*open;
761 }
762
763 let icon = if *open { "▾" } else { "▸" };
764 let title_color = if focused { theme.primary } else { theme.text };
765
766 let mut response = self.container().col(|ui| {
767 ui.line(|ui| {
768 ui.text(icon).fg(title_color);
769 ui.text(format!(" {title}")).bold().fg(title_color);
770 });
771 });
772
773 if response.clicked {
774 *open = !*open;
775 }
776
777 if *open {
778 self.container().pl(2).col(f);
779 }
780
781 response.focused = focused;
782 response.changed = *open != old_open;
783 response
784 }
785
786 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
787 let max_key_width = items
788 .iter()
789 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
790 .max()
791 .unwrap_or(0);
792
793 self.col(|ui| {
794 for (key, value) in items {
795 ui.line(|ui| {
796 let padded = format!("{:>width$}", key, width = max_key_width);
797 ui.text(padded).dim();
798 ui.text(" ");
799 ui.text(*value);
800 });
801 }
802 });
803
804 Response::none()
805 }
806
807 pub fn divider_text(&mut self, label: &str) -> Response {
808 let w = self.width();
809 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
810 let pad = 1u32;
811 let left_len = 4u32;
812 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
813 let left: String = "─".repeat(left_len as usize);
814 let right: String = "─".repeat(right_len as usize);
815 let theme = self.theme;
816 self.line(|ui| {
817 ui.text(&left).fg(theme.border);
818 ui.text(format!(" {} ", label)).fg(theme.text);
819 ui.text(&right).fg(theme.border);
820 });
821
822 Response::none()
823 }
824
825 pub fn badge(&mut self, label: &str) -> Response {
826 let theme = self.theme;
827 self.badge_colored(label, theme.primary)
828 }
829
830 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
831 let fg = Color::contrast_fg(color);
832 self.text(format!(" {} ", label)).fg(fg).bg(color);
833
834 Response::none()
835 }
836
837 pub fn key_hint(&mut self, key: &str) -> Response {
838 let theme = self.theme;
839 self.text(format!(" {} ", key))
840 .reversed()
841 .fg(theme.text_dim);
842
843 Response::none()
844 }
845
846 pub fn stat(&mut self, label: &str, value: &str) -> Response {
847 self.col(|ui| {
848 ui.text(label).dim();
849 ui.text(value).bold();
850 });
851
852 Response::none()
853 }
854
855 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
856 self.col(|ui| {
857 ui.text(label).dim();
858 ui.text(value).bold().fg(color);
859 });
860
861 Response::none()
862 }
863
864 pub fn stat_trend(
865 &mut self,
866 label: &str,
867 value: &str,
868 trend: crate::widgets::Trend,
869 ) -> Response {
870 let theme = self.theme;
871 let (arrow, color) = match trend {
872 crate::widgets::Trend::Up => ("↑", theme.success),
873 crate::widgets::Trend::Down => ("↓", theme.error),
874 };
875 self.col(|ui| {
876 ui.text(label).dim();
877 ui.line(|ui| {
878 ui.text(value).bold();
879 ui.text(format!(" {arrow}")).fg(color);
880 });
881 });
882
883 Response::none()
884 }
885
886 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
887 self.container().center().col(|ui| {
888 ui.text(title).align(Align::Center);
889 ui.text(description).dim().align(Align::Center);
890 });
891
892 Response::none()
893 }
894
895 pub fn empty_state_action(
896 &mut self,
897 title: &str,
898 description: &str,
899 action_label: &str,
900 ) -> Response {
901 let mut clicked = false;
902 self.container().center().col(|ui| {
903 ui.text(title).align(Align::Center);
904 ui.text(description).dim().align(Align::Center);
905 if ui.button(action_label).clicked {
906 clicked = true;
907 }
908 });
909
910 Response {
911 clicked,
912 changed: clicked,
913 ..Response::none()
914 }
915 }
916
917 pub fn code_block(&mut self, code: &str) -> Response {
918 let theme = self.theme;
919 self.bordered(Border::Rounded)
920 .bg(theme.surface)
921 .pad(1)
922 .col(|ui| {
923 for line in code.lines() {
924 render_highlighted_line(ui, line);
925 }
926 });
927
928 Response::none()
929 }
930
931 pub fn code_block_numbered(&mut self, code: &str) -> Response {
932 let lines: Vec<&str> = code.lines().collect();
933 let gutter_w = format!("{}", lines.len()).len();
934 let theme = self.theme;
935 self.bordered(Border::Rounded)
936 .bg(theme.surface)
937 .pad(1)
938 .col(|ui| {
939 for (i, line) in lines.iter().enumerate() {
940 ui.line(|ui| {
941 ui.text(format!("{:>gutter_w$} │ ", i + 1))
942 .fg(theme.text_dim);
943 render_highlighted_line(ui, line);
944 });
945 }
946 });
947
948 Response::none()
949 }
950
951 pub fn wrap(&mut self) -> &mut Self {
953 if let Some(idx) = self.last_text_idx {
954 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
955 *wrap = true;
956 }
957 }
958 self
959 }
960
961 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
962 if let Some(idx) = self.last_text_idx {
963 match &mut self.commands[idx] {
964 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
965 _ => {}
966 }
967 }
968 }
969
970 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
971 if let Some(idx) = self.last_text_idx {
972 match &mut self.commands[idx] {
973 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
974 f(constraints)
975 }
976 _ => {}
977 }
978 }
979 }
980
981 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
982 if let Some(idx) = self.last_text_idx {
983 match &mut self.commands[idx] {
984 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
985 _ => {}
986 }
987 }
988 }
989
990 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1008 self.push_container(Direction::Column, 0, f)
1009 }
1010
1011 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1015 self.push_container(Direction::Column, gap, f)
1016 }
1017
1018 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1035 self.push_container(Direction::Row, 0, f)
1036 }
1037
1038 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1042 self.push_container(Direction::Row, gap, f)
1043 }
1044
1045 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1062 let _ = self.push_container(Direction::Row, 0, f);
1063 self
1064 }
1065
1066 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1085 let start = self.commands.len();
1086 f(self);
1087 let mut segments: Vec<(String, Style)> = Vec::new();
1088 for cmd in self.commands.drain(start..) {
1089 if let Command::Text { content, style, .. } = cmd {
1090 segments.push((content, style));
1091 }
1092 }
1093 self.commands.push(Command::RichText {
1094 segments,
1095 wrap: true,
1096 align: Align::Start,
1097 margin: Margin::default(),
1098 constraints: Constraints::default(),
1099 });
1100 self.last_text_idx = None;
1101 self
1102 }
1103
1104 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1113 self.commands.push(Command::BeginOverlay { modal: true });
1114 self.overlay_depth += 1;
1115 self.modal_active = true;
1116 f(self);
1117 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1118 self.commands.push(Command::EndOverlay);
1119 self.last_text_idx = None;
1120 }
1121
1122 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1124 self.commands.push(Command::BeginOverlay { modal: false });
1125 self.overlay_depth += 1;
1126 f(self);
1127 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1128 self.commands.push(Command::EndOverlay);
1129 self.last_text_idx = None;
1130 }
1131
1132 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1140 self.group_count = self.group_count.saturating_add(1);
1141 self.group_stack.push(name.to_string());
1142 self.container().group_name(name.to_string())
1143 }
1144
1145 pub fn container(&mut self) -> ContainerBuilder<'_> {
1166 let border = self.theme.border;
1167 ContainerBuilder {
1168 ctx: self,
1169 gap: 0,
1170 align: Align::Start,
1171 justify: Justify::Start,
1172 border: None,
1173 border_sides: BorderSides::all(),
1174 border_style: Style::new().fg(border),
1175 bg: None,
1176 dark_bg: None,
1177 dark_border_style: None,
1178 group_hover_bg: None,
1179 group_hover_border_style: None,
1180 group_name: None,
1181 padding: Padding::default(),
1182 margin: Margin::default(),
1183 constraints: Constraints::default(),
1184 title: None,
1185 grow: 0,
1186 scroll_offset: None,
1187 }
1188 }
1189
1190 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1209 let index = self.scroll_count;
1210 self.scroll_count += 1;
1211 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1212 state.set_bounds(ch, vh);
1213 let max = ch.saturating_sub(vh) as usize;
1214 state.offset = state.offset.min(max);
1215 }
1216
1217 let next_id = self.interaction_count;
1218 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1219 let inner_rects: Vec<Rect> = self
1220 .prev_scroll_rects
1221 .iter()
1222 .enumerate()
1223 .filter(|&(j, sr)| {
1224 j != index
1225 && sr.width > 0
1226 && sr.height > 0
1227 && sr.x >= rect.x
1228 && sr.right() <= rect.right()
1229 && sr.y >= rect.y
1230 && sr.bottom() <= rect.bottom()
1231 })
1232 .map(|(_, sr)| *sr)
1233 .collect();
1234 self.auto_scroll_nested(&rect, state, &inner_rects);
1235 }
1236
1237 self.container().scroll_offset(state.offset as u32)
1238 }
1239
1240 pub fn scrollbar(&mut self, state: &ScrollState) {
1260 let vh = state.viewport_height();
1261 let ch = state.content_height();
1262 if vh == 0 || ch <= vh {
1263 return;
1264 }
1265
1266 let track_height = vh;
1267 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1268 let max_offset = ch.saturating_sub(vh);
1269 let thumb_pos = if max_offset == 0 {
1270 0
1271 } else {
1272 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1273 .round() as u32
1274 };
1275
1276 let theme = self.theme;
1277 let track_char = '│';
1278 let thumb_char = '█';
1279
1280 self.container().w(1).h(track_height).col(|ui| {
1281 for i in 0..track_height {
1282 if i >= thumb_pos && i < thumb_pos + thumb_height {
1283 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1284 } else {
1285 ui.styled(
1286 track_char.to_string(),
1287 Style::new().fg(theme.text_dim).dim(),
1288 );
1289 }
1290 }
1291 });
1292 }
1293
1294 fn auto_scroll_nested(
1295 &mut self,
1296 rect: &Rect,
1297 state: &mut ScrollState,
1298 inner_scroll_rects: &[Rect],
1299 ) {
1300 let mut to_consume: Vec<usize> = Vec::new();
1301
1302 for (i, event) in self.events.iter().enumerate() {
1303 if self.consumed[i] {
1304 continue;
1305 }
1306 if let Event::Mouse(mouse) = event {
1307 let in_bounds = mouse.x >= rect.x
1308 && mouse.x < rect.right()
1309 && mouse.y >= rect.y
1310 && mouse.y < rect.bottom();
1311 if !in_bounds {
1312 continue;
1313 }
1314 let in_inner = inner_scroll_rects.iter().any(|sr| {
1315 mouse.x >= sr.x
1316 && mouse.x < sr.right()
1317 && mouse.y >= sr.y
1318 && mouse.y < sr.bottom()
1319 });
1320 if in_inner {
1321 continue;
1322 }
1323 match mouse.kind {
1324 MouseKind::ScrollUp => {
1325 state.scroll_up(1);
1326 to_consume.push(i);
1327 }
1328 MouseKind::ScrollDown => {
1329 state.scroll_down(1);
1330 to_consume.push(i);
1331 }
1332 MouseKind::Drag(MouseButton::Left) => {}
1333 _ => {}
1334 }
1335 }
1336 }
1337
1338 for i in to_consume {
1339 self.consumed[i] = true;
1340 }
1341 }
1342
1343 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1347 self.container()
1348 .border(border)
1349 .border_sides(BorderSides::all())
1350 }
1351
1352 fn push_container(
1353 &mut self,
1354 direction: Direction,
1355 gap: u32,
1356 f: impl FnOnce(&mut Context),
1357 ) -> Response {
1358 let interaction_id = self.interaction_count;
1359 self.interaction_count += 1;
1360 let border = self.theme.border;
1361
1362 self.commands.push(Command::BeginContainer {
1363 direction,
1364 gap,
1365 align: Align::Start,
1366 justify: Justify::Start,
1367 border: None,
1368 border_sides: BorderSides::all(),
1369 border_style: Style::new().fg(border),
1370 bg_color: None,
1371 padding: Padding::default(),
1372 margin: Margin::default(),
1373 constraints: Constraints::default(),
1374 title: None,
1375 grow: 0,
1376 group_name: None,
1377 });
1378 f(self);
1379 self.commands.push(Command::EndContainer);
1380 self.last_text_idx = None;
1381
1382 self.response_for(interaction_id)
1383 }
1384
1385 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1386 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1387 return Response::none();
1388 }
1389 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1390 let clicked = self
1391 .click_pos
1392 .map(|(mx, my)| {
1393 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1394 })
1395 .unwrap_or(false);
1396 let hovered = self
1397 .mouse_pos
1398 .map(|(mx, my)| {
1399 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1400 })
1401 .unwrap_or(false);
1402 Response {
1403 clicked,
1404 hovered,
1405 changed: false,
1406 focused: false,
1407 rect: *rect,
1408 }
1409 } else {
1410 Response::none()
1411 }
1412 }
1413
1414 pub fn is_group_hovered(&self, name: &str) -> bool {
1416 if let Some(pos) = self.mouse_pos {
1417 self.prev_group_rects.iter().any(|(n, rect)| {
1418 n == name
1419 && pos.0 >= rect.x
1420 && pos.0 < rect.x + rect.width
1421 && pos.1 >= rect.y
1422 && pos.1 < rect.y + rect.height
1423 })
1424 } else {
1425 false
1426 }
1427 }
1428
1429 pub fn is_group_focused(&self, name: &str) -> bool {
1431 if self.prev_focus_count == 0 {
1432 return false;
1433 }
1434 let focused_index = self.focus_index % self.prev_focus_count;
1435 self.prev_focus_groups
1436 .get(focused_index)
1437 .and_then(|group| group.as_deref())
1438 .map(|group| group == name)
1439 .unwrap_or(false)
1440 }
1441
1442 pub fn grow(&mut self, value: u16) -> &mut Self {
1447 if let Some(idx) = self.last_text_idx {
1448 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1449 *grow = value;
1450 }
1451 }
1452 self
1453 }
1454
1455 pub fn align(&mut self, align: Align) -> &mut Self {
1457 if let Some(idx) = self.last_text_idx {
1458 if let Command::Text {
1459 align: text_align, ..
1460 } = &mut self.commands[idx]
1461 {
1462 *text_align = align;
1463 }
1464 }
1465 self
1466 }
1467
1468 pub fn w(&mut self, value: u32) -> &mut Self {
1475 self.modify_last_constraints(|c| {
1476 c.min_width = Some(value);
1477 c.max_width = Some(value);
1478 });
1479 self
1480 }
1481
1482 pub fn h(&mut self, value: u32) -> &mut Self {
1486 self.modify_last_constraints(|c| {
1487 c.min_height = Some(value);
1488 c.max_height = Some(value);
1489 });
1490 self
1491 }
1492
1493 pub fn min_w(&mut self, value: u32) -> &mut Self {
1495 self.modify_last_constraints(|c| c.min_width = Some(value));
1496 self
1497 }
1498
1499 pub fn max_w(&mut self, value: u32) -> &mut Self {
1501 self.modify_last_constraints(|c| c.max_width = Some(value));
1502 self
1503 }
1504
1505 pub fn min_h(&mut self, value: u32) -> &mut Self {
1507 self.modify_last_constraints(|c| c.min_height = Some(value));
1508 self
1509 }
1510
1511 pub fn max_h(&mut self, value: u32) -> &mut Self {
1513 self.modify_last_constraints(|c| c.max_height = Some(value));
1514 self
1515 }
1516
1517 pub fn m(&mut self, value: u32) -> &mut Self {
1521 self.modify_last_margin(|m| *m = Margin::all(value));
1522 self
1523 }
1524
1525 pub fn mx(&mut self, value: u32) -> &mut Self {
1527 self.modify_last_margin(|m| {
1528 m.left = value;
1529 m.right = value;
1530 });
1531 self
1532 }
1533
1534 pub fn my(&mut self, value: u32) -> &mut Self {
1536 self.modify_last_margin(|m| {
1537 m.top = value;
1538 m.bottom = value;
1539 });
1540 self
1541 }
1542
1543 pub fn mt(&mut self, value: u32) -> &mut Self {
1545 self.modify_last_margin(|m| m.top = value);
1546 self
1547 }
1548
1549 pub fn mr(&mut self, value: u32) -> &mut Self {
1551 self.modify_last_margin(|m| m.right = value);
1552 self
1553 }
1554
1555 pub fn mb(&mut self, value: u32) -> &mut Self {
1557 self.modify_last_margin(|m| m.bottom = value);
1558 self
1559 }
1560
1561 pub fn ml(&mut self, value: u32) -> &mut Self {
1563 self.modify_last_margin(|m| m.left = value);
1564 self
1565 }
1566
1567 pub fn spacer(&mut self) -> &mut Self {
1571 self.commands.push(Command::Spacer { grow: 1 });
1572 self.last_text_idx = None;
1573 self
1574 }
1575
1576 pub fn form(
1580 &mut self,
1581 state: &mut FormState,
1582 f: impl FnOnce(&mut Context, &mut FormState),
1583 ) -> &mut Self {
1584 self.col(|ui| {
1585 f(ui, state);
1586 });
1587 self
1588 }
1589
1590 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1594 self.col(|ui| {
1595 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1596 ui.text_input(&mut field.input);
1597 if let Some(error) = field.error.as_deref() {
1598 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1599 }
1600 });
1601 self
1602 }
1603
1604 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1608 self.button(label)
1609 }
1610}
1611
1612const KEYWORDS: &[&str] = &[
1613 "fn",
1614 "let",
1615 "mut",
1616 "pub",
1617 "use",
1618 "impl",
1619 "struct",
1620 "enum",
1621 "trait",
1622 "type",
1623 "const",
1624 "static",
1625 "if",
1626 "else",
1627 "match",
1628 "for",
1629 "while",
1630 "loop",
1631 "return",
1632 "break",
1633 "continue",
1634 "where",
1635 "self",
1636 "super",
1637 "crate",
1638 "mod",
1639 "async",
1640 "await",
1641 "move",
1642 "ref",
1643 "in",
1644 "as",
1645 "true",
1646 "false",
1647 "Some",
1648 "None",
1649 "Ok",
1650 "Err",
1651 "Self",
1652 "def",
1653 "class",
1654 "import",
1655 "from",
1656 "pass",
1657 "lambda",
1658 "yield",
1659 "with",
1660 "try",
1661 "except",
1662 "raise",
1663 "finally",
1664 "elif",
1665 "del",
1666 "global",
1667 "nonlocal",
1668 "assert",
1669 "is",
1670 "not",
1671 "and",
1672 "or",
1673 "function",
1674 "var",
1675 "const",
1676 "export",
1677 "default",
1678 "switch",
1679 "case",
1680 "throw",
1681 "catch",
1682 "typeof",
1683 "instanceof",
1684 "new",
1685 "delete",
1686 "void",
1687 "this",
1688 "null",
1689 "undefined",
1690 "func",
1691 "package",
1692 "defer",
1693 "go",
1694 "chan",
1695 "select",
1696 "range",
1697 "map",
1698 "interface",
1699 "fallthrough",
1700 "nil",
1701];
1702
1703fn render_highlighted_line(ui: &mut Context, line: &str) {
1704 let theme = ui.theme;
1705 let is_light = matches!(
1706 theme.bg,
1707 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1708 );
1709 let keyword_color = if is_light {
1710 Color::Rgb(166, 38, 164)
1711 } else {
1712 Color::Rgb(198, 120, 221)
1713 };
1714 let string_color = if is_light {
1715 Color::Rgb(80, 161, 79)
1716 } else {
1717 Color::Rgb(152, 195, 121)
1718 };
1719 let comment_color = theme.text_dim;
1720 let number_color = if is_light {
1721 Color::Rgb(152, 104, 1)
1722 } else {
1723 Color::Rgb(209, 154, 102)
1724 };
1725 let fn_color = if is_light {
1726 Color::Rgb(64, 120, 242)
1727 } else {
1728 Color::Rgb(97, 175, 239)
1729 };
1730 let macro_color = if is_light {
1731 Color::Rgb(1, 132, 188)
1732 } else {
1733 Color::Rgb(86, 182, 194)
1734 };
1735
1736 let trimmed = line.trim_start();
1737 let indent = &line[..line.len() - trimmed.len()];
1738 if !indent.is_empty() {
1739 ui.text(indent);
1740 }
1741
1742 if trimmed.starts_with("//") {
1743 ui.text(trimmed).fg(comment_color).italic();
1744 return;
1745 }
1746
1747 let mut pos = 0;
1748
1749 while pos < trimmed.len() {
1750 let ch = trimmed.as_bytes()[pos];
1751
1752 if ch == b'"' {
1753 if let Some(end) = trimmed[pos + 1..].find('"') {
1754 let s = &trimmed[pos..pos + end + 2];
1755 ui.text(s).fg(string_color);
1756 pos += end + 2;
1757 continue;
1758 }
1759 }
1760
1761 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1762 {
1763 let end = trimmed[pos..]
1764 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1765 .map_or(trimmed.len(), |e| pos + e);
1766 ui.text(&trimmed[pos..end]).fg(number_color);
1767 pos = end;
1768 continue;
1769 }
1770
1771 if ch.is_ascii_alphabetic() || ch == b'_' {
1772 let end = trimmed[pos..]
1773 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1774 .map_or(trimmed.len(), |e| pos + e);
1775 let word = &trimmed[pos..end];
1776
1777 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1778 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1779 pos = end + 1;
1780 } else if end < trimmed.len()
1781 && trimmed.as_bytes()[end] == b'('
1782 && !KEYWORDS.contains(&word)
1783 {
1784 ui.text(word).fg(fn_color);
1785 pos = end;
1786 } else if KEYWORDS.contains(&word) {
1787 ui.text(word).fg(keyword_color);
1788 pos = end;
1789 } else {
1790 ui.text(word);
1791 pos = end;
1792 }
1793 continue;
1794 }
1795
1796 let end = trimmed[pos..]
1797 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1798 .map_or(trimmed.len(), |e| pos + e);
1799 ui.text(&trimmed[pos..end]);
1800 pos = end;
1801 }
1802}