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 kitty_image(
275 &mut self,
276 rgba: &[u8],
277 pixel_width: u32,
278 pixel_height: u32,
279 cols: u32,
280 rows: u32,
281 ) {
282 let encoded = base64_encode(rgba);
283 let pw = pixel_width;
284 let ph = pixel_height;
285 let c = cols;
286 let r = rows;
287
288 self.container().w(cols).h(rows).draw(move |buf, rect| {
289 let chunks = split_base64(&encoded, 4096);
290 let mut all_sequences = String::new();
291
292 for (i, chunk) in chunks.iter().enumerate() {
293 let more = if i < chunks.len() - 1 { 1 } else { 0 };
294 if i == 0 {
295 all_sequences.push_str(&format!(
296 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
297 pw, ph, c, r, more, chunk
298 ));
299 } else {
300 all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
301 }
302 }
303
304 buf.raw_sequence(rect.x, rect.y, all_sequences);
305 });
306 }
307
308 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
324 if state.streaming {
325 state.cursor_tick = state.cursor_tick.wrapping_add(1);
326 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
327 }
328
329 if state.content.is_empty() && state.streaming {
330 let cursor = if state.cursor_visible { "▌" } else { " " };
331 let primary = self.theme.primary;
332 self.text(cursor).fg(primary);
333 return Response::none();
334 }
335
336 if !state.content.is_empty() {
337 if state.streaming && state.cursor_visible {
338 self.text_wrap(format!("{}▌", state.content));
339 } else {
340 self.text_wrap(&state.content);
341 }
342 }
343
344 Response::none()
345 }
346
347 pub fn streaming_markdown(
365 &mut self,
366 state: &mut crate::widgets::StreamingMarkdownState,
367 ) -> Response {
368 if state.streaming {
369 state.cursor_tick = state.cursor_tick.wrapping_add(1);
370 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
371 }
372
373 if state.content.is_empty() && state.streaming {
374 let cursor = if state.cursor_visible { "▌" } else { " " };
375 let primary = self.theme.primary;
376 self.text(cursor).fg(primary);
377 return Response::none();
378 }
379
380 let show_cursor = state.streaming && state.cursor_visible;
381 let trailing_newline = state.content.ends_with('\n');
382 let lines: Vec<&str> = state.content.lines().collect();
383 let last_line_index = lines.len().saturating_sub(1);
384
385 self.commands.push(Command::BeginContainer {
386 direction: Direction::Column,
387 gap: 0,
388 align: Align::Start,
389 justify: Justify::Start,
390 border: None,
391 border_sides: BorderSides::all(),
392 border_style: Style::new().fg(self.theme.border),
393 bg_color: None,
394 padding: Padding::default(),
395 margin: Margin::default(),
396 constraints: Constraints::default(),
397 title: None,
398 grow: 0,
399 group_name: None,
400 });
401 self.interaction_count += 1;
402
403 let text_style = Style::new().fg(self.theme.text);
404 let bold_style = Style::new().fg(self.theme.text).bold();
405 let code_style = Style::new().fg(self.theme.accent);
406 let border_style = Style::new().fg(self.theme.border).dim();
407
408 let mut in_code_block = false;
409 let mut code_block_lang = String::new();
410
411 for (idx, line) in lines.iter().enumerate() {
412 let line = *line;
413 let trimmed = line.trim();
414 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
415 let cursor = if append_cursor { "▌" } else { "" };
416
417 if in_code_block {
418 if trimmed.starts_with("```") {
419 in_code_block = false;
420 code_block_lang.clear();
421 self.styled(format!(" └────{cursor}"), border_style);
422 } else {
423 self.styled(format!(" {line}{cursor}"), code_style);
424 }
425 continue;
426 }
427
428 if trimmed.is_empty() {
429 if append_cursor {
430 self.styled("▌", Style::new().fg(self.theme.primary));
431 } else {
432 self.text(" ");
433 }
434 continue;
435 }
436
437 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
438 self.styled(format!("{}{}", "─".repeat(40), cursor), border_style);
439 continue;
440 }
441
442 if let Some(heading) = trimmed.strip_prefix("### ") {
443 self.styled(
444 format!("{heading}{cursor}"),
445 Style::new().bold().fg(self.theme.accent),
446 );
447 continue;
448 }
449
450 if let Some(heading) = trimmed.strip_prefix("## ") {
451 self.styled(
452 format!("{heading}{cursor}"),
453 Style::new().bold().fg(self.theme.secondary),
454 );
455 continue;
456 }
457
458 if let Some(heading) = trimmed.strip_prefix("# ") {
459 self.styled(
460 format!("{heading}{cursor}"),
461 Style::new().bold().fg(self.theme.primary),
462 );
463 continue;
464 }
465
466 if let Some(code) = trimmed.strip_prefix("```") {
467 in_code_block = true;
468 code_block_lang = code.trim().to_string();
469 let label = if code_block_lang.is_empty() {
470 "code".to_string()
471 } else {
472 format!("code:{}", code_block_lang)
473 };
474 self.styled(format!(" ┌─{label}─{cursor}"), border_style);
475 continue;
476 }
477
478 if let Some(item) = trimmed
479 .strip_prefix("- ")
480 .or_else(|| trimmed.strip_prefix("* "))
481 {
482 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
483 if segs.len() <= 1 {
484 self.styled(format!(" • {item}{cursor}"), text_style);
485 } else {
486 self.line(|ui| {
487 ui.styled(" • ", text_style);
488 for (s, st) in segs {
489 ui.styled(s, st);
490 }
491 if append_cursor {
492 ui.styled("▌", Style::new().fg(ui.theme.primary));
493 }
494 });
495 }
496 continue;
497 }
498
499 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
500 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
501 if parts.len() == 2 {
502 let segs =
503 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
504 if segs.len() <= 1 {
505 self.styled(
506 format!(" {}. {}{}", parts[0], parts[1], cursor),
507 text_style,
508 );
509 } else {
510 self.line(|ui| {
511 ui.styled(format!(" {}. ", parts[0]), text_style);
512 for (s, st) in segs {
513 ui.styled(s, st);
514 }
515 if append_cursor {
516 ui.styled("▌", Style::new().fg(ui.theme.primary));
517 }
518 });
519 }
520 } else {
521 self.styled(format!("{trimmed}{cursor}"), text_style);
522 }
523 continue;
524 }
525
526 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
527 if segs.len() <= 1 {
528 self.styled(format!("{trimmed}{cursor}"), text_style);
529 } else {
530 self.line(|ui| {
531 for (s, st) in segs {
532 ui.styled(s, st);
533 }
534 if append_cursor {
535 ui.styled("▌", Style::new().fg(ui.theme.primary));
536 }
537 });
538 }
539 }
540
541 if show_cursor && trailing_newline {
542 if in_code_block {
543 self.styled(" ▌", code_style);
544 } else {
545 self.styled("▌", Style::new().fg(self.theme.primary));
546 }
547 }
548
549 state.in_code_block = in_code_block;
550 state.code_block_lang = code_block_lang;
551
552 self.commands.push(Command::EndContainer);
553 self.last_text_idx = None;
554 Response::none()
555 }
556
557 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
572 let old_action = state.action;
573 let theme = self.theme;
574 self.bordered(Border::Rounded).col(|ui| {
575 ui.row(|ui| {
576 ui.text("⚡").fg(theme.warning);
577 ui.text(&state.tool_name).bold().fg(theme.primary);
578 });
579 ui.text(&state.description).dim();
580
581 if state.action == ApprovalAction::Pending {
582 ui.row(|ui| {
583 if ui.button("✓ Approve").clicked {
584 state.action = ApprovalAction::Approved;
585 }
586 if ui.button("✗ Reject").clicked {
587 state.action = ApprovalAction::Rejected;
588 }
589 });
590 } else {
591 let (label, color) = match state.action {
592 ApprovalAction::Approved => ("✓ Approved", theme.success),
593 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
594 ApprovalAction::Pending => unreachable!(),
595 };
596 ui.text(label).fg(color).bold();
597 }
598 });
599
600 Response {
601 changed: state.action != old_action,
602 ..Response::none()
603 }
604 }
605
606 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
619 if items.is_empty() {
620 return Response::none();
621 }
622
623 let theme = self.theme;
624 let total: usize = items.iter().map(|item| item.tokens).sum();
625
626 self.container().row(|ui| {
627 ui.text("📎").dim();
628 for item in items {
629 ui.text(format!(
630 "{} ({})",
631 item.label,
632 format_token_count(item.tokens)
633 ))
634 .fg(theme.secondary);
635 }
636 ui.spacer();
637 ui.text(format!("Σ {}", format_token_count(total))).dim();
638 });
639
640 Response::none()
641 }
642
643 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
644 use crate::widgets::AlertLevel;
645
646 let theme = self.theme;
647 let (icon, color) = match level {
648 AlertLevel::Info => ("ℹ", theme.accent),
649 AlertLevel::Success => ("✓", theme.success),
650 AlertLevel::Warning => ("⚠", theme.warning),
651 AlertLevel::Error => ("✕", theme.error),
652 };
653
654 let focused = self.register_focusable();
655 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
656
657 let mut response = self.container().col(|ui| {
658 ui.line(|ui| {
659 ui.text(format!(" {icon} ")).fg(color).bold();
660 ui.text(message).grow(1);
661 ui.text(" [×] ").dim();
662 });
663 });
664 response.focused = focused;
665 if key_dismiss {
666 response.clicked = true;
667 }
668
669 response
670 }
671
672 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
686 let focused = self.register_focusable();
687 let mut is_yes = *result;
688 let mut clicked = false;
689
690 if focused {
691 let mut consumed_indices = Vec::new();
692 for (i, event) in self.events.iter().enumerate() {
693 if let Event::Key(key) = event {
694 if key.kind != KeyEventKind::Press {
695 continue;
696 }
697
698 match key.code {
699 KeyCode::Char('y') => {
700 is_yes = true;
701 *result = true;
702 clicked = true;
703 consumed_indices.push(i);
704 }
705 KeyCode::Char('n') => {
706 is_yes = false;
707 *result = false;
708 clicked = true;
709 consumed_indices.push(i);
710 }
711 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
712 is_yes = !is_yes;
713 *result = is_yes;
714 consumed_indices.push(i);
715 }
716 KeyCode::Enter => {
717 *result = is_yes;
718 clicked = true;
719 consumed_indices.push(i);
720 }
721 _ => {}
722 }
723 }
724 }
725
726 for idx in consumed_indices {
727 self.consumed[idx] = true;
728 }
729 }
730
731 let yes_style = if is_yes {
732 if focused {
733 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
734 } else {
735 Style::new().fg(self.theme.success).bold()
736 }
737 } else {
738 Style::new().fg(self.theme.text_dim)
739 };
740 let no_style = if !is_yes {
741 if focused {
742 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
743 } else {
744 Style::new().fg(self.theme.error).bold()
745 }
746 } else {
747 Style::new().fg(self.theme.text_dim)
748 };
749
750 let mut response = self.row(|ui| {
751 ui.text(question);
752 ui.text(" ");
753 ui.styled("[Yes]", yes_style);
754 ui.text(" ");
755 ui.styled("[No]", no_style);
756 });
757 response.focused = focused;
758 response.clicked = clicked;
759 response.changed = clicked;
760 response
761 }
762
763 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
764 self.breadcrumb_with(segments, " › ")
765 }
766
767 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
768 let theme = self.theme;
769 let last_idx = segments.len().saturating_sub(1);
770 let mut clicked_idx: Option<usize> = None;
771
772 self.row(|ui| {
773 for (i, segment) in segments.iter().enumerate() {
774 let is_last = i == last_idx;
775 if is_last {
776 ui.text(*segment).bold();
777 } else {
778 let focused = ui.register_focusable();
779 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
780 let resp = ui.interaction();
781 let color = if resp.hovered || focused {
782 theme.accent
783 } else {
784 theme.primary
785 };
786 ui.text(*segment).fg(color).underline();
787 if resp.clicked || pressed {
788 clicked_idx = Some(i);
789 }
790 ui.text(separator).dim();
791 }
792 }
793 });
794
795 clicked_idx
796 }
797
798 pub fn accordion(
799 &mut self,
800 title: &str,
801 open: &mut bool,
802 f: impl FnOnce(&mut Context),
803 ) -> Response {
804 let theme = self.theme;
805 let focused = self.register_focusable();
806 let old_open = *open;
807
808 if focused && self.key_code(KeyCode::Enter) {
809 *open = !*open;
810 }
811
812 let icon = if *open { "▾" } else { "▸" };
813 let title_color = if focused { theme.primary } else { theme.text };
814
815 let mut response = self.container().col(|ui| {
816 ui.line(|ui| {
817 ui.text(icon).fg(title_color);
818 ui.text(format!(" {title}")).bold().fg(title_color);
819 });
820 });
821
822 if response.clicked {
823 *open = !*open;
824 }
825
826 if *open {
827 self.container().pl(2).col(f);
828 }
829
830 response.focused = focused;
831 response.changed = *open != old_open;
832 response
833 }
834
835 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
836 let max_key_width = items
837 .iter()
838 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
839 .max()
840 .unwrap_or(0);
841
842 self.col(|ui| {
843 for (key, value) in items {
844 ui.line(|ui| {
845 let padded = format!("{:>width$}", key, width = max_key_width);
846 ui.text(padded).dim();
847 ui.text(" ");
848 ui.text(*value);
849 });
850 }
851 });
852
853 Response::none()
854 }
855
856 pub fn divider_text(&mut self, label: &str) -> Response {
857 let w = self.width();
858 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
859 let pad = 1u32;
860 let left_len = 4u32;
861 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
862 let left: String = "─".repeat(left_len as usize);
863 let right: String = "─".repeat(right_len as usize);
864 let theme = self.theme;
865 self.line(|ui| {
866 ui.text(&left).fg(theme.border);
867 ui.text(format!(" {} ", label)).fg(theme.text);
868 ui.text(&right).fg(theme.border);
869 });
870
871 Response::none()
872 }
873
874 pub fn badge(&mut self, label: &str) -> Response {
875 let theme = self.theme;
876 self.badge_colored(label, theme.primary)
877 }
878
879 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
880 let fg = Color::contrast_fg(color);
881 self.text(format!(" {} ", label)).fg(fg).bg(color);
882
883 Response::none()
884 }
885
886 pub fn key_hint(&mut self, key: &str) -> Response {
887 let theme = self.theme;
888 self.text(format!(" {} ", key))
889 .reversed()
890 .fg(theme.text_dim);
891
892 Response::none()
893 }
894
895 pub fn stat(&mut self, label: &str, value: &str) -> Response {
896 self.col(|ui| {
897 ui.text(label).dim();
898 ui.text(value).bold();
899 });
900
901 Response::none()
902 }
903
904 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
905 self.col(|ui| {
906 ui.text(label).dim();
907 ui.text(value).bold().fg(color);
908 });
909
910 Response::none()
911 }
912
913 pub fn stat_trend(
914 &mut self,
915 label: &str,
916 value: &str,
917 trend: crate::widgets::Trend,
918 ) -> Response {
919 let theme = self.theme;
920 let (arrow, color) = match trend {
921 crate::widgets::Trend::Up => ("↑", theme.success),
922 crate::widgets::Trend::Down => ("↓", theme.error),
923 };
924 self.col(|ui| {
925 ui.text(label).dim();
926 ui.line(|ui| {
927 ui.text(value).bold();
928 ui.text(format!(" {arrow}")).fg(color);
929 });
930 });
931
932 Response::none()
933 }
934
935 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
936 self.container().center().col(|ui| {
937 ui.text(title).align(Align::Center);
938 ui.text(description).dim().align(Align::Center);
939 });
940
941 Response::none()
942 }
943
944 pub fn empty_state_action(
945 &mut self,
946 title: &str,
947 description: &str,
948 action_label: &str,
949 ) -> Response {
950 let mut clicked = false;
951 self.container().center().col(|ui| {
952 ui.text(title).align(Align::Center);
953 ui.text(description).dim().align(Align::Center);
954 if ui.button(action_label).clicked {
955 clicked = true;
956 }
957 });
958
959 Response {
960 clicked,
961 changed: clicked,
962 ..Response::none()
963 }
964 }
965
966 pub fn code_block(&mut self, code: &str) -> Response {
967 let theme = self.theme;
968 self.bordered(Border::Rounded)
969 .bg(theme.surface)
970 .pad(1)
971 .col(|ui| {
972 for line in code.lines() {
973 render_highlighted_line(ui, line);
974 }
975 });
976
977 Response::none()
978 }
979
980 pub fn code_block_numbered(&mut self, code: &str) -> Response {
981 let lines: Vec<&str> = code.lines().collect();
982 let gutter_w = format!("{}", lines.len()).len();
983 let theme = self.theme;
984 self.bordered(Border::Rounded)
985 .bg(theme.surface)
986 .pad(1)
987 .col(|ui| {
988 for (i, line) in lines.iter().enumerate() {
989 ui.line(|ui| {
990 ui.text(format!("{:>gutter_w$} │ ", i + 1))
991 .fg(theme.text_dim);
992 render_highlighted_line(ui, line);
993 });
994 }
995 });
996
997 Response::none()
998 }
999
1000 pub fn wrap(&mut self) -> &mut Self {
1002 if let Some(idx) = self.last_text_idx {
1003 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1004 *wrap = true;
1005 }
1006 }
1007 self
1008 }
1009
1010 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1011 if let Some(idx) = self.last_text_idx {
1012 match &mut self.commands[idx] {
1013 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1014 _ => {}
1015 }
1016 }
1017 }
1018
1019 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1020 if let Some(idx) = self.last_text_idx {
1021 match &mut self.commands[idx] {
1022 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1023 f(constraints)
1024 }
1025 _ => {}
1026 }
1027 }
1028 }
1029
1030 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1031 if let Some(idx) = self.last_text_idx {
1032 match &mut self.commands[idx] {
1033 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1034 _ => {}
1035 }
1036 }
1037 }
1038
1039 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1057 self.push_container(Direction::Column, 0, f)
1058 }
1059
1060 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1064 self.push_container(Direction::Column, gap, f)
1065 }
1066
1067 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1084 self.push_container(Direction::Row, 0, f)
1085 }
1086
1087 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1091 self.push_container(Direction::Row, gap, f)
1092 }
1093
1094 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1111 let _ = self.push_container(Direction::Row, 0, f);
1112 self
1113 }
1114
1115 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1134 let start = self.commands.len();
1135 f(self);
1136 let mut segments: Vec<(String, Style)> = Vec::new();
1137 for cmd in self.commands.drain(start..) {
1138 if let Command::Text { content, style, .. } = cmd {
1139 segments.push((content, style));
1140 }
1141 }
1142 self.commands.push(Command::RichText {
1143 segments,
1144 wrap: true,
1145 align: Align::Start,
1146 margin: Margin::default(),
1147 constraints: Constraints::default(),
1148 });
1149 self.last_text_idx = None;
1150 self
1151 }
1152
1153 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1162 self.commands.push(Command::BeginOverlay { modal: true });
1163 self.overlay_depth += 1;
1164 self.modal_active = true;
1165 f(self);
1166 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1167 self.commands.push(Command::EndOverlay);
1168 self.last_text_idx = None;
1169 }
1170
1171 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1173 self.commands.push(Command::BeginOverlay { modal: false });
1174 self.overlay_depth += 1;
1175 f(self);
1176 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1177 self.commands.push(Command::EndOverlay);
1178 self.last_text_idx = None;
1179 }
1180
1181 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1189 self.group_count = self.group_count.saturating_add(1);
1190 self.group_stack.push(name.to_string());
1191 self.container().group_name(name.to_string())
1192 }
1193
1194 pub fn container(&mut self) -> ContainerBuilder<'_> {
1215 let border = self.theme.border;
1216 ContainerBuilder {
1217 ctx: self,
1218 gap: 0,
1219 align: Align::Start,
1220 justify: Justify::Start,
1221 border: None,
1222 border_sides: BorderSides::all(),
1223 border_style: Style::new().fg(border),
1224 bg: None,
1225 dark_bg: None,
1226 dark_border_style: None,
1227 group_hover_bg: None,
1228 group_hover_border_style: None,
1229 group_name: None,
1230 padding: Padding::default(),
1231 margin: Margin::default(),
1232 constraints: Constraints::default(),
1233 title: None,
1234 grow: 0,
1235 scroll_offset: None,
1236 }
1237 }
1238
1239 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1258 let index = self.scroll_count;
1259 self.scroll_count += 1;
1260 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1261 state.set_bounds(ch, vh);
1262 let max = ch.saturating_sub(vh) as usize;
1263 state.offset = state.offset.min(max);
1264 }
1265
1266 let next_id = self.interaction_count;
1267 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1268 let inner_rects: Vec<Rect> = self
1269 .prev_scroll_rects
1270 .iter()
1271 .enumerate()
1272 .filter(|&(j, sr)| {
1273 j != index
1274 && sr.width > 0
1275 && sr.height > 0
1276 && sr.x >= rect.x
1277 && sr.right() <= rect.right()
1278 && sr.y >= rect.y
1279 && sr.bottom() <= rect.bottom()
1280 })
1281 .map(|(_, sr)| *sr)
1282 .collect();
1283 self.auto_scroll_nested(&rect, state, &inner_rects);
1284 }
1285
1286 self.container().scroll_offset(state.offset as u32)
1287 }
1288
1289 pub fn scrollbar(&mut self, state: &ScrollState) {
1309 let vh = state.viewport_height();
1310 let ch = state.content_height();
1311 if vh == 0 || ch <= vh {
1312 return;
1313 }
1314
1315 let track_height = vh;
1316 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1317 let max_offset = ch.saturating_sub(vh);
1318 let thumb_pos = if max_offset == 0 {
1319 0
1320 } else {
1321 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1322 .round() as u32
1323 };
1324
1325 let theme = self.theme;
1326 let track_char = '│';
1327 let thumb_char = '█';
1328
1329 self.container().w(1).h(track_height).col(|ui| {
1330 for i in 0..track_height {
1331 if i >= thumb_pos && i < thumb_pos + thumb_height {
1332 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1333 } else {
1334 ui.styled(
1335 track_char.to_string(),
1336 Style::new().fg(theme.text_dim).dim(),
1337 );
1338 }
1339 }
1340 });
1341 }
1342
1343 fn auto_scroll_nested(
1344 &mut self,
1345 rect: &Rect,
1346 state: &mut ScrollState,
1347 inner_scroll_rects: &[Rect],
1348 ) {
1349 let mut to_consume: Vec<usize> = Vec::new();
1350
1351 for (i, event) in self.events.iter().enumerate() {
1352 if self.consumed[i] {
1353 continue;
1354 }
1355 if let Event::Mouse(mouse) = event {
1356 let in_bounds = mouse.x >= rect.x
1357 && mouse.x < rect.right()
1358 && mouse.y >= rect.y
1359 && mouse.y < rect.bottom();
1360 if !in_bounds {
1361 continue;
1362 }
1363 let in_inner = inner_scroll_rects.iter().any(|sr| {
1364 mouse.x >= sr.x
1365 && mouse.x < sr.right()
1366 && mouse.y >= sr.y
1367 && mouse.y < sr.bottom()
1368 });
1369 if in_inner {
1370 continue;
1371 }
1372 match mouse.kind {
1373 MouseKind::ScrollUp => {
1374 state.scroll_up(1);
1375 to_consume.push(i);
1376 }
1377 MouseKind::ScrollDown => {
1378 state.scroll_down(1);
1379 to_consume.push(i);
1380 }
1381 MouseKind::Drag(MouseButton::Left) => {}
1382 _ => {}
1383 }
1384 }
1385 }
1386
1387 for i in to_consume {
1388 self.consumed[i] = true;
1389 }
1390 }
1391
1392 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1396 self.container()
1397 .border(border)
1398 .border_sides(BorderSides::all())
1399 }
1400
1401 fn push_container(
1402 &mut self,
1403 direction: Direction,
1404 gap: u32,
1405 f: impl FnOnce(&mut Context),
1406 ) -> Response {
1407 let interaction_id = self.interaction_count;
1408 self.interaction_count += 1;
1409 let border = self.theme.border;
1410
1411 self.commands.push(Command::BeginContainer {
1412 direction,
1413 gap,
1414 align: Align::Start,
1415 justify: Justify::Start,
1416 border: None,
1417 border_sides: BorderSides::all(),
1418 border_style: Style::new().fg(border),
1419 bg_color: None,
1420 padding: Padding::default(),
1421 margin: Margin::default(),
1422 constraints: Constraints::default(),
1423 title: None,
1424 grow: 0,
1425 group_name: None,
1426 });
1427 f(self);
1428 self.commands.push(Command::EndContainer);
1429 self.last_text_idx = None;
1430
1431 self.response_for(interaction_id)
1432 }
1433
1434 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1435 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1436 return Response::none();
1437 }
1438 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1439 let clicked = self
1440 .click_pos
1441 .map(|(mx, my)| {
1442 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1443 })
1444 .unwrap_or(false);
1445 let hovered = self
1446 .mouse_pos
1447 .map(|(mx, my)| {
1448 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1449 })
1450 .unwrap_or(false);
1451 Response {
1452 clicked,
1453 hovered,
1454 changed: false,
1455 focused: false,
1456 rect: *rect,
1457 }
1458 } else {
1459 Response::none()
1460 }
1461 }
1462
1463 pub fn is_group_hovered(&self, name: &str) -> bool {
1465 if let Some(pos) = self.mouse_pos {
1466 self.prev_group_rects.iter().any(|(n, rect)| {
1467 n == name
1468 && pos.0 >= rect.x
1469 && pos.0 < rect.x + rect.width
1470 && pos.1 >= rect.y
1471 && pos.1 < rect.y + rect.height
1472 })
1473 } else {
1474 false
1475 }
1476 }
1477
1478 pub fn is_group_focused(&self, name: &str) -> bool {
1480 if self.prev_focus_count == 0 {
1481 return false;
1482 }
1483 let focused_index = self.focus_index % self.prev_focus_count;
1484 self.prev_focus_groups
1485 .get(focused_index)
1486 .and_then(|group| group.as_deref())
1487 .map(|group| group == name)
1488 .unwrap_or(false)
1489 }
1490
1491 pub fn grow(&mut self, value: u16) -> &mut Self {
1496 if let Some(idx) = self.last_text_idx {
1497 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1498 *grow = value;
1499 }
1500 }
1501 self
1502 }
1503
1504 pub fn align(&mut self, align: Align) -> &mut Self {
1506 if let Some(idx) = self.last_text_idx {
1507 if let Command::Text {
1508 align: text_align, ..
1509 } = &mut self.commands[idx]
1510 {
1511 *text_align = align;
1512 }
1513 }
1514 self
1515 }
1516
1517 pub fn w(&mut self, value: u32) -> &mut Self {
1524 self.modify_last_constraints(|c| {
1525 c.min_width = Some(value);
1526 c.max_width = Some(value);
1527 });
1528 self
1529 }
1530
1531 pub fn h(&mut self, value: u32) -> &mut Self {
1535 self.modify_last_constraints(|c| {
1536 c.min_height = Some(value);
1537 c.max_height = Some(value);
1538 });
1539 self
1540 }
1541
1542 pub fn min_w(&mut self, value: u32) -> &mut Self {
1544 self.modify_last_constraints(|c| c.min_width = Some(value));
1545 self
1546 }
1547
1548 pub fn max_w(&mut self, value: u32) -> &mut Self {
1550 self.modify_last_constraints(|c| c.max_width = Some(value));
1551 self
1552 }
1553
1554 pub fn min_h(&mut self, value: u32) -> &mut Self {
1556 self.modify_last_constraints(|c| c.min_height = Some(value));
1557 self
1558 }
1559
1560 pub fn max_h(&mut self, value: u32) -> &mut Self {
1562 self.modify_last_constraints(|c| c.max_height = Some(value));
1563 self
1564 }
1565
1566 pub fn m(&mut self, value: u32) -> &mut Self {
1570 self.modify_last_margin(|m| *m = Margin::all(value));
1571 self
1572 }
1573
1574 pub fn mx(&mut self, value: u32) -> &mut Self {
1576 self.modify_last_margin(|m| {
1577 m.left = value;
1578 m.right = value;
1579 });
1580 self
1581 }
1582
1583 pub fn my(&mut self, value: u32) -> &mut Self {
1585 self.modify_last_margin(|m| {
1586 m.top = value;
1587 m.bottom = value;
1588 });
1589 self
1590 }
1591
1592 pub fn mt(&mut self, value: u32) -> &mut Self {
1594 self.modify_last_margin(|m| m.top = value);
1595 self
1596 }
1597
1598 pub fn mr(&mut self, value: u32) -> &mut Self {
1600 self.modify_last_margin(|m| m.right = value);
1601 self
1602 }
1603
1604 pub fn mb(&mut self, value: u32) -> &mut Self {
1606 self.modify_last_margin(|m| m.bottom = value);
1607 self
1608 }
1609
1610 pub fn ml(&mut self, value: u32) -> &mut Self {
1612 self.modify_last_margin(|m| m.left = value);
1613 self
1614 }
1615
1616 pub fn spacer(&mut self) -> &mut Self {
1620 self.commands.push(Command::Spacer { grow: 1 });
1621 self.last_text_idx = None;
1622 self
1623 }
1624
1625 pub fn form(
1629 &mut self,
1630 state: &mut FormState,
1631 f: impl FnOnce(&mut Context, &mut FormState),
1632 ) -> &mut Self {
1633 self.col(|ui| {
1634 f(ui, state);
1635 });
1636 self
1637 }
1638
1639 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1643 self.col(|ui| {
1644 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1645 ui.text_input(&mut field.input);
1646 if let Some(error) = field.error.as_deref() {
1647 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1648 }
1649 });
1650 self
1651 }
1652
1653 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1657 self.button(label)
1658 }
1659}
1660
1661const KEYWORDS: &[&str] = &[
1662 "fn",
1663 "let",
1664 "mut",
1665 "pub",
1666 "use",
1667 "impl",
1668 "struct",
1669 "enum",
1670 "trait",
1671 "type",
1672 "const",
1673 "static",
1674 "if",
1675 "else",
1676 "match",
1677 "for",
1678 "while",
1679 "loop",
1680 "return",
1681 "break",
1682 "continue",
1683 "where",
1684 "self",
1685 "super",
1686 "crate",
1687 "mod",
1688 "async",
1689 "await",
1690 "move",
1691 "ref",
1692 "in",
1693 "as",
1694 "true",
1695 "false",
1696 "Some",
1697 "None",
1698 "Ok",
1699 "Err",
1700 "Self",
1701 "def",
1702 "class",
1703 "import",
1704 "from",
1705 "pass",
1706 "lambda",
1707 "yield",
1708 "with",
1709 "try",
1710 "except",
1711 "raise",
1712 "finally",
1713 "elif",
1714 "del",
1715 "global",
1716 "nonlocal",
1717 "assert",
1718 "is",
1719 "not",
1720 "and",
1721 "or",
1722 "function",
1723 "var",
1724 "const",
1725 "export",
1726 "default",
1727 "switch",
1728 "case",
1729 "throw",
1730 "catch",
1731 "typeof",
1732 "instanceof",
1733 "new",
1734 "delete",
1735 "void",
1736 "this",
1737 "null",
1738 "undefined",
1739 "func",
1740 "package",
1741 "defer",
1742 "go",
1743 "chan",
1744 "select",
1745 "range",
1746 "map",
1747 "interface",
1748 "fallthrough",
1749 "nil",
1750];
1751
1752fn render_highlighted_line(ui: &mut Context, line: &str) {
1753 let theme = ui.theme;
1754 let is_light = matches!(
1755 theme.bg,
1756 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1757 );
1758 let keyword_color = if is_light {
1759 Color::Rgb(166, 38, 164)
1760 } else {
1761 Color::Rgb(198, 120, 221)
1762 };
1763 let string_color = if is_light {
1764 Color::Rgb(80, 161, 79)
1765 } else {
1766 Color::Rgb(152, 195, 121)
1767 };
1768 let comment_color = theme.text_dim;
1769 let number_color = if is_light {
1770 Color::Rgb(152, 104, 1)
1771 } else {
1772 Color::Rgb(209, 154, 102)
1773 };
1774 let fn_color = if is_light {
1775 Color::Rgb(64, 120, 242)
1776 } else {
1777 Color::Rgb(97, 175, 239)
1778 };
1779 let macro_color = if is_light {
1780 Color::Rgb(1, 132, 188)
1781 } else {
1782 Color::Rgb(86, 182, 194)
1783 };
1784
1785 let trimmed = line.trim_start();
1786 let indent = &line[..line.len() - trimmed.len()];
1787 if !indent.is_empty() {
1788 ui.text(indent);
1789 }
1790
1791 if trimmed.starts_with("//") {
1792 ui.text(trimmed).fg(comment_color).italic();
1793 return;
1794 }
1795
1796 let mut pos = 0;
1797
1798 while pos < trimmed.len() {
1799 let ch = trimmed.as_bytes()[pos];
1800
1801 if ch == b'"' {
1802 if let Some(end) = trimmed[pos + 1..].find('"') {
1803 let s = &trimmed[pos..pos + end + 2];
1804 ui.text(s).fg(string_color);
1805 pos += end + 2;
1806 continue;
1807 }
1808 }
1809
1810 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1811 {
1812 let end = trimmed[pos..]
1813 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1814 .map_or(trimmed.len(), |e| pos + e);
1815 ui.text(&trimmed[pos..end]).fg(number_color);
1816 pos = end;
1817 continue;
1818 }
1819
1820 if ch.is_ascii_alphabetic() || ch == b'_' {
1821 let end = trimmed[pos..]
1822 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1823 .map_or(trimmed.len(), |e| pos + e);
1824 let word = &trimmed[pos..end];
1825
1826 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1827 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1828 pos = end + 1;
1829 } else if end < trimmed.len()
1830 && trimmed.as_bytes()[end] == b'('
1831 && !KEYWORDS.contains(&word)
1832 {
1833 ui.text(word).fg(fn_color);
1834 pos = end;
1835 } else if KEYWORDS.contains(&word) {
1836 ui.text(word).fg(keyword_color);
1837 pos = end;
1838 } else {
1839 ui.text(word);
1840 pos = end;
1841 }
1842 continue;
1843 }
1844
1845 let end = trimmed[pos..]
1846 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1847 .map_or(trimmed.len(), |e| pos + e);
1848 ui.text(&trimmed[pos..end]);
1849 pos = end;
1850 }
1851}
1852
1853fn base64_encode(data: &[u8]) -> String {
1854 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1855 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
1856 for chunk in data.chunks(3) {
1857 let b0 = chunk[0] as u32;
1858 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
1859 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
1860 let triple = (b0 << 16) | (b1 << 8) | b2;
1861 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
1862 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
1863 if chunk.len() > 1 {
1864 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
1865 } else {
1866 result.push('=');
1867 }
1868 if chunk.len() > 2 {
1869 result.push(CHARS[(triple & 0x3F) as usize] as char);
1870 } else {
1871 result.push('=');
1872 }
1873 }
1874 result
1875}
1876
1877fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
1878 let mut chunks = Vec::new();
1879 let bytes = encoded.as_bytes();
1880 let mut offset = 0;
1881 while offset < bytes.len() {
1882 let end = (offset + chunk_size).min(bytes.len());
1883 chunks.push(&encoded[offset..end]);
1884 offset = end;
1885 }
1886 if chunks.is_empty() {
1887 chunks.push("");
1888 }
1889 chunks
1890}