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 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
988 self.push_container(Direction::Column, 0, f)
989 }
990
991 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
995 self.push_container(Direction::Column, gap, f)
996 }
997
998 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1015 self.push_container(Direction::Row, 0, f)
1016 }
1017
1018 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1022 self.push_container(Direction::Row, gap, f)
1023 }
1024
1025 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1042 let _ = self.push_container(Direction::Row, 0, f);
1043 self
1044 }
1045
1046 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1065 let start = self.commands.len();
1066 f(self);
1067 let mut segments: Vec<(String, Style)> = Vec::new();
1068 for cmd in self.commands.drain(start..) {
1069 if let Command::Text { content, style, .. } = cmd {
1070 segments.push((content, style));
1071 }
1072 }
1073 self.commands.push(Command::RichText {
1074 segments,
1075 wrap: true,
1076 align: Align::Start,
1077 margin: Margin::default(),
1078 constraints: Constraints::default(),
1079 });
1080 self.last_text_idx = None;
1081 self
1082 }
1083
1084 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1093 self.commands.push(Command::BeginOverlay { modal: true });
1094 self.overlay_depth += 1;
1095 self.modal_active = true;
1096 f(self);
1097 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1098 self.commands.push(Command::EndOverlay);
1099 self.last_text_idx = None;
1100 }
1101
1102 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1104 self.commands.push(Command::BeginOverlay { modal: false });
1105 self.overlay_depth += 1;
1106 f(self);
1107 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1108 self.commands.push(Command::EndOverlay);
1109 self.last_text_idx = None;
1110 }
1111
1112 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1120 self.group_count = self.group_count.saturating_add(1);
1121 self.group_stack.push(name.to_string());
1122 self.container().group_name(name.to_string())
1123 }
1124
1125 pub fn container(&mut self) -> ContainerBuilder<'_> {
1146 let border = self.theme.border;
1147 ContainerBuilder {
1148 ctx: self,
1149 gap: 0,
1150 align: Align::Start,
1151 justify: Justify::Start,
1152 border: None,
1153 border_sides: BorderSides::all(),
1154 border_style: Style::new().fg(border),
1155 bg: None,
1156 dark_bg: None,
1157 dark_border_style: None,
1158 group_hover_bg: None,
1159 group_hover_border_style: None,
1160 group_name: None,
1161 padding: Padding::default(),
1162 margin: Margin::default(),
1163 constraints: Constraints::default(),
1164 title: None,
1165 grow: 0,
1166 scroll_offset: None,
1167 }
1168 }
1169
1170 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1189 let index = self.scroll_count;
1190 self.scroll_count += 1;
1191 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1192 state.set_bounds(ch, vh);
1193 let max = ch.saturating_sub(vh) as usize;
1194 state.offset = state.offset.min(max);
1195 }
1196
1197 let next_id = self.interaction_count;
1198 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1199 let inner_rects: Vec<Rect> = self
1200 .prev_scroll_rects
1201 .iter()
1202 .enumerate()
1203 .filter(|&(j, sr)| {
1204 j != index
1205 && sr.width > 0
1206 && sr.height > 0
1207 && sr.x >= rect.x
1208 && sr.right() <= rect.right()
1209 && sr.y >= rect.y
1210 && sr.bottom() <= rect.bottom()
1211 })
1212 .map(|(_, sr)| *sr)
1213 .collect();
1214 self.auto_scroll_nested(&rect, state, &inner_rects);
1215 }
1216
1217 self.container().scroll_offset(state.offset as u32)
1218 }
1219
1220 pub fn scrollbar(&mut self, state: &ScrollState) {
1240 let vh = state.viewport_height();
1241 let ch = state.content_height();
1242 if vh == 0 || ch <= vh {
1243 return;
1244 }
1245
1246 let track_height = vh;
1247 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1248 let max_offset = ch.saturating_sub(vh);
1249 let thumb_pos = if max_offset == 0 {
1250 0
1251 } else {
1252 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1253 .round() as u32
1254 };
1255
1256 let theme = self.theme;
1257 let track_char = '│';
1258 let thumb_char = '█';
1259
1260 self.container().w(1).h(track_height).col(|ui| {
1261 for i in 0..track_height {
1262 if i >= thumb_pos && i < thumb_pos + thumb_height {
1263 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1264 } else {
1265 ui.styled(
1266 track_char.to_string(),
1267 Style::new().fg(theme.text_dim).dim(),
1268 );
1269 }
1270 }
1271 });
1272 }
1273
1274 fn auto_scroll_nested(
1275 &mut self,
1276 rect: &Rect,
1277 state: &mut ScrollState,
1278 inner_scroll_rects: &[Rect],
1279 ) {
1280 let mut to_consume: Vec<usize> = Vec::new();
1281
1282 for (i, event) in self.events.iter().enumerate() {
1283 if self.consumed[i] {
1284 continue;
1285 }
1286 if let Event::Mouse(mouse) = event {
1287 let in_bounds = mouse.x >= rect.x
1288 && mouse.x < rect.right()
1289 && mouse.y >= rect.y
1290 && mouse.y < rect.bottom();
1291 if !in_bounds {
1292 continue;
1293 }
1294 let in_inner = inner_scroll_rects.iter().any(|sr| {
1295 mouse.x >= sr.x
1296 && mouse.x < sr.right()
1297 && mouse.y >= sr.y
1298 && mouse.y < sr.bottom()
1299 });
1300 if in_inner {
1301 continue;
1302 }
1303 match mouse.kind {
1304 MouseKind::ScrollUp => {
1305 state.scroll_up(1);
1306 to_consume.push(i);
1307 }
1308 MouseKind::ScrollDown => {
1309 state.scroll_down(1);
1310 to_consume.push(i);
1311 }
1312 MouseKind::Drag(MouseButton::Left) => {}
1313 _ => {}
1314 }
1315 }
1316 }
1317
1318 for i in to_consume {
1319 self.consumed[i] = true;
1320 }
1321 }
1322
1323 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1327 self.container()
1328 .border(border)
1329 .border_sides(BorderSides::all())
1330 }
1331
1332 fn push_container(
1333 &mut self,
1334 direction: Direction,
1335 gap: u32,
1336 f: impl FnOnce(&mut Context),
1337 ) -> Response {
1338 let interaction_id = self.interaction_count;
1339 self.interaction_count += 1;
1340 let border = self.theme.border;
1341
1342 self.commands.push(Command::BeginContainer {
1343 direction,
1344 gap,
1345 align: Align::Start,
1346 justify: Justify::Start,
1347 border: None,
1348 border_sides: BorderSides::all(),
1349 border_style: Style::new().fg(border),
1350 bg_color: None,
1351 padding: Padding::default(),
1352 margin: Margin::default(),
1353 constraints: Constraints::default(),
1354 title: None,
1355 grow: 0,
1356 group_name: None,
1357 });
1358 f(self);
1359 self.commands.push(Command::EndContainer);
1360 self.last_text_idx = None;
1361
1362 self.response_for(interaction_id)
1363 }
1364
1365 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1366 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1367 return Response::none();
1368 }
1369 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1370 let clicked = self
1371 .click_pos
1372 .map(|(mx, my)| {
1373 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1374 })
1375 .unwrap_or(false);
1376 let hovered = self
1377 .mouse_pos
1378 .map(|(mx, my)| {
1379 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1380 })
1381 .unwrap_or(false);
1382 Response {
1383 clicked,
1384 hovered,
1385 changed: false,
1386 focused: false,
1387 rect: *rect,
1388 }
1389 } else {
1390 Response::none()
1391 }
1392 }
1393
1394 pub fn is_group_hovered(&self, name: &str) -> bool {
1396 if let Some(pos) = self.mouse_pos {
1397 self.prev_group_rects.iter().any(|(n, rect)| {
1398 n == name
1399 && pos.0 >= rect.x
1400 && pos.0 < rect.x + rect.width
1401 && pos.1 >= rect.y
1402 && pos.1 < rect.y + rect.height
1403 })
1404 } else {
1405 false
1406 }
1407 }
1408
1409 pub fn is_group_focused(&self, name: &str) -> bool {
1411 if self.prev_focus_count == 0 {
1412 return false;
1413 }
1414 let focused_index = self.focus_index % self.prev_focus_count;
1415 self.prev_focus_groups
1416 .get(focused_index)
1417 .and_then(|group| group.as_deref())
1418 .map(|group| group == name)
1419 .unwrap_or(false)
1420 }
1421
1422 pub fn grow(&mut self, value: u16) -> &mut Self {
1427 if let Some(idx) = self.last_text_idx {
1428 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1429 *grow = value;
1430 }
1431 }
1432 self
1433 }
1434
1435 pub fn align(&mut self, align: Align) -> &mut Self {
1437 if let Some(idx) = self.last_text_idx {
1438 if let Command::Text {
1439 align: text_align, ..
1440 } = &mut self.commands[idx]
1441 {
1442 *text_align = align;
1443 }
1444 }
1445 self
1446 }
1447
1448 pub fn spacer(&mut self) -> &mut Self {
1452 self.commands.push(Command::Spacer { grow: 1 });
1453 self.last_text_idx = None;
1454 self
1455 }
1456
1457 pub fn form(
1461 &mut self,
1462 state: &mut FormState,
1463 f: impl FnOnce(&mut Context, &mut FormState),
1464 ) -> &mut Self {
1465 self.col(|ui| {
1466 f(ui, state);
1467 });
1468 self
1469 }
1470
1471 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1475 self.col(|ui| {
1476 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1477 ui.text_input(&mut field.input);
1478 if let Some(error) = field.error.as_deref() {
1479 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1480 }
1481 });
1482 self
1483 }
1484
1485 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1489 self.button(label)
1490 }
1491}
1492
1493const KEYWORDS: &[&str] = &[
1494 "fn",
1495 "let",
1496 "mut",
1497 "pub",
1498 "use",
1499 "impl",
1500 "struct",
1501 "enum",
1502 "trait",
1503 "type",
1504 "const",
1505 "static",
1506 "if",
1507 "else",
1508 "match",
1509 "for",
1510 "while",
1511 "loop",
1512 "return",
1513 "break",
1514 "continue",
1515 "where",
1516 "self",
1517 "super",
1518 "crate",
1519 "mod",
1520 "async",
1521 "await",
1522 "move",
1523 "ref",
1524 "in",
1525 "as",
1526 "true",
1527 "false",
1528 "Some",
1529 "None",
1530 "Ok",
1531 "Err",
1532 "Self",
1533 "def",
1534 "class",
1535 "import",
1536 "from",
1537 "pass",
1538 "lambda",
1539 "yield",
1540 "with",
1541 "try",
1542 "except",
1543 "raise",
1544 "finally",
1545 "elif",
1546 "del",
1547 "global",
1548 "nonlocal",
1549 "assert",
1550 "is",
1551 "not",
1552 "and",
1553 "or",
1554 "function",
1555 "var",
1556 "const",
1557 "export",
1558 "default",
1559 "switch",
1560 "case",
1561 "throw",
1562 "catch",
1563 "typeof",
1564 "instanceof",
1565 "new",
1566 "delete",
1567 "void",
1568 "this",
1569 "null",
1570 "undefined",
1571 "func",
1572 "package",
1573 "defer",
1574 "go",
1575 "chan",
1576 "select",
1577 "range",
1578 "map",
1579 "interface",
1580 "fallthrough",
1581 "nil",
1582];
1583
1584fn render_highlighted_line(ui: &mut Context, line: &str) {
1585 let theme = ui.theme;
1586 let is_light = matches!(
1587 theme.bg,
1588 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1589 );
1590 let keyword_color = if is_light {
1591 Color::Rgb(166, 38, 164)
1592 } else {
1593 Color::Rgb(198, 120, 221)
1594 };
1595 let string_color = if is_light {
1596 Color::Rgb(80, 161, 79)
1597 } else {
1598 Color::Rgb(152, 195, 121)
1599 };
1600 let comment_color = theme.text_dim;
1601 let number_color = if is_light {
1602 Color::Rgb(152, 104, 1)
1603 } else {
1604 Color::Rgb(209, 154, 102)
1605 };
1606 let fn_color = if is_light {
1607 Color::Rgb(64, 120, 242)
1608 } else {
1609 Color::Rgb(97, 175, 239)
1610 };
1611 let macro_color = if is_light {
1612 Color::Rgb(1, 132, 188)
1613 } else {
1614 Color::Rgb(86, 182, 194)
1615 };
1616
1617 let trimmed = line.trim_start();
1618 let indent = &line[..line.len() - trimmed.len()];
1619 if !indent.is_empty() {
1620 ui.text(indent);
1621 }
1622
1623 if trimmed.starts_with("//") {
1624 ui.text(trimmed).fg(comment_color).italic();
1625 return;
1626 }
1627
1628 let mut pos = 0;
1629
1630 while pos < trimmed.len() {
1631 let ch = trimmed.as_bytes()[pos];
1632
1633 if ch == b'"' {
1634 if let Some(end) = trimmed[pos + 1..].find('"') {
1635 let s = &trimmed[pos..pos + end + 2];
1636 ui.text(s).fg(string_color);
1637 pos += end + 2;
1638 continue;
1639 }
1640 }
1641
1642 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1643 {
1644 let end = trimmed[pos..]
1645 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1646 .map_or(trimmed.len(), |e| pos + e);
1647 ui.text(&trimmed[pos..end]).fg(number_color);
1648 pos = end;
1649 continue;
1650 }
1651
1652 if ch.is_ascii_alphabetic() || ch == b'_' {
1653 let end = trimmed[pos..]
1654 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1655 .map_or(trimmed.len(), |e| pos + e);
1656 let word = &trimmed[pos..end];
1657
1658 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1659 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1660 pos = end + 1;
1661 } else if end < trimmed.len()
1662 && trimmed.as_bytes()[end] == b'('
1663 && !KEYWORDS.contains(&word)
1664 {
1665 ui.text(word).fg(fn_color);
1666 pos = end;
1667 } else if KEYWORDS.contains(&word) {
1668 ui.text(word).fg(keyword_color);
1669 pos = end;
1670 } else {
1671 ui.text(word);
1672 pos = end;
1673 }
1674 continue;
1675 }
1676
1677 let end = trimmed[pos..]
1678 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1679 .map_or(trimmed.len(), |e| pos + e);
1680 ui.text(&trimmed[pos..end]);
1681 pos = end;
1682 }
1683}