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 let default_fg = self
20 .text_color_stack
21 .iter()
22 .rev()
23 .find_map(|c| *c)
24 .unwrap_or(self.theme.text);
25 self.commands.push(Command::Text {
26 content,
27 style: Style::new().fg(default_fg),
28 grow: 0,
29 align: Align::Start,
30 wrap: false,
31 truncate: false,
32 margin: Margin::default(),
33 constraints: Constraints::default(),
34 });
35 self.last_text_idx = Some(self.commands.len() - 1);
36 self
37 }
38
39 #[allow(clippy::print_stderr)]
45 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
46 let url_str = url.into();
47 let focused = self.register_focusable();
48 let interaction_id = self.interaction_count;
49 self.interaction_count += 1;
50 let response = self.response_for(interaction_id);
51
52 let mut activated = response.clicked;
53 if focused {
54 for (i, event) in self.events.iter().enumerate() {
55 if let Event::Key(key) = event {
56 if key.kind != KeyEventKind::Press {
57 continue;
58 }
59 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
60 activated = true;
61 self.consumed[i] = true;
62 }
63 }
64 }
65 }
66
67 if activated {
68 if let Err(e) = open_url(&url_str) {
69 eprintln!("[slt] failed to open URL: {e}");
70 }
71 }
72
73 let style = if focused {
74 Style::new()
75 .fg(self.theme.primary)
76 .bg(self.theme.surface_hover)
77 .underline()
78 .bold()
79 } else if response.hovered {
80 Style::new()
81 .fg(self.theme.accent)
82 .bg(self.theme.surface_hover)
83 .underline()
84 } else {
85 Style::new().fg(self.theme.primary).underline()
86 };
87
88 self.commands.push(Command::Link {
89 text: text.into(),
90 url: url_str,
91 style,
92 margin: Margin::default(),
93 constraints: Constraints::default(),
94 });
95 self.last_text_idx = Some(self.commands.len() - 1);
96 self
97 }
98
99 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
104 let content = s.into();
105 let default_fg = self
106 .text_color_stack
107 .iter()
108 .rev()
109 .find_map(|c| *c)
110 .unwrap_or(self.theme.text);
111 self.commands.push(Command::Text {
112 content,
113 style: Style::new().fg(default_fg),
114 grow: 0,
115 align: Align::Start,
116 wrap: true,
117 truncate: false,
118 margin: Margin::default(),
119 constraints: Constraints::default(),
120 });
121 self.last_text_idx = Some(self.commands.len() - 1);
122 self
123 }
124
125 pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
127 let pairs: Vec<(&str, &str)> = keymap
128 .visible_bindings()
129 .map(|binding| (binding.display.as_str(), binding.description.as_str()))
130 .collect();
131 self.help(&pairs)
132 }
133
134 pub fn bold(&mut self) -> &mut Self {
138 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
139 self
140 }
141
142 pub fn dim(&mut self) -> &mut Self {
147 let text_dim = self.theme.text_dim;
148 self.modify_last_style(|s| {
149 s.modifiers |= Modifiers::DIM;
150 if s.fg.is_none() {
151 s.fg = Some(text_dim);
152 }
153 });
154 self
155 }
156
157 pub fn italic(&mut self) -> &mut Self {
159 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
160 self
161 }
162
163 pub fn underline(&mut self) -> &mut Self {
165 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
166 self
167 }
168
169 pub fn reversed(&mut self) -> &mut Self {
171 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
172 self
173 }
174
175 pub fn strikethrough(&mut self) -> &mut Self {
177 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
178 self
179 }
180
181 pub fn fg(&mut self, color: Color) -> &mut Self {
183 self.modify_last_style(|s| s.fg = Some(color));
184 self
185 }
186
187 pub fn bg(&mut self, color: Color) -> &mut Self {
189 self.modify_last_style(|s| s.bg = Some(color));
190 self
191 }
192
193 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
194 let apply_group_style = self
195 .group_stack
196 .last()
197 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
198 .unwrap_or(false);
199 if apply_group_style {
200 self.modify_last_style(|s| s.fg = Some(color));
201 }
202 self
203 }
204
205 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
206 let apply_group_style = self
207 .group_stack
208 .last()
209 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
210 .unwrap_or(false);
211 if apply_group_style {
212 self.modify_last_style(|s| s.bg = Some(color));
213 }
214 self
215 }
216
217 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
222 self.commands.push(Command::Text {
223 content: s.into(),
224 style,
225 grow: 0,
226 align: Align::Start,
227 wrap: false,
228 truncate: false,
229 margin: Margin::default(),
230 constraints: Constraints::default(),
231 });
232 self.last_text_idx = Some(self.commands.len() - 1);
233 self
234 }
235
236 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
258 let width = img.width;
259 let height = img.height;
260
261 let _ = self.container().w(width).h(height).gap(0).col(|ui| {
262 for row in 0..height {
263 let _ = ui.container().gap(0).row(|ui| {
264 for col in 0..width {
265 let idx = (row * width + col) as usize;
266 if let Some(&(upper, lower)) = img.pixels.get(idx) {
267 ui.styled("▀", Style::new().fg(upper).bg(lower));
268 }
269 }
270 });
271 }
272 });
273
274 Response::none()
275 }
276
277 pub fn kitty_image(
293 &mut self,
294 rgba: &[u8],
295 pixel_width: u32,
296 pixel_height: u32,
297 cols: u32,
298 rows: u32,
299 ) -> Response {
300 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
301 let encoded = base64_encode(&rgba);
302 let pw = pixel_width;
303 let ph = pixel_height;
304 let c = cols;
305 let r = rows;
306
307 self.container().w(cols).h(rows).draw(move |buf, rect| {
308 let chunks = split_base64(&encoded, 4096);
309 let mut all_sequences = String::new();
310
311 for (i, chunk) in chunks.iter().enumerate() {
312 let more = if i < chunks.len() - 1 { 1 } else { 0 };
313 if i == 0 {
314 all_sequences.push_str(&format!(
315 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
316 pw, ph, c, r, more, chunk
317 ));
318 } else {
319 all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
320 }
321 }
322
323 buf.raw_sequence(rect.x, rect.y, all_sequences);
324 });
325 Response::none()
326 }
327
328 pub fn kitty_image_fit(
337 &mut self,
338 rgba: &[u8],
339 src_width: u32,
340 src_height: u32,
341 cols: u32,
342 ) -> Response {
343 let rows = if src_width == 0 {
344 1
345 } else {
346 ((cols as f64 * src_height as f64 * 8.0) / (src_width as f64 * 16.0))
347 .ceil()
348 .max(1.0) as u32
349 };
350 let rgba = normalize_rgba(rgba, src_width, src_height);
351 let sw = src_width;
352 let sh = src_height;
353 let c = cols;
354 let r = rows;
355
356 self.container().w(cols).h(rows).draw(move |buf, rect| {
357 if rect.width == 0 || rect.height == 0 {
358 return;
359 }
360 let encoded = base64_encode(&rgba);
361 let chunks = split_base64(&encoded, 4096);
362 let mut seq = String::new();
363 for (i, chunk) in chunks.iter().enumerate() {
364 let more = if i < chunks.len() - 1 { 1 } else { 0 };
365 if i == 0 {
366 seq.push_str(&format!(
367 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
368 sw, sh, c, r, more, chunk
369 ));
370 } else {
371 seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
372 }
373 }
374 buf.raw_sequence(rect.x, rect.y, seq);
375 });
376 Response::none()
377 }
378
379 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
395 if state.streaming {
396 state.cursor_tick = state.cursor_tick.wrapping_add(1);
397 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
398 }
399
400 if state.content.is_empty() && state.streaming {
401 let cursor = if state.cursor_visible { "▌" } else { " " };
402 let primary = self.theme.primary;
403 self.text(cursor).fg(primary);
404 return Response::none();
405 }
406
407 if !state.content.is_empty() {
408 if state.streaming && state.cursor_visible {
409 self.text_wrap(format!("{}▌", state.content));
410 } else {
411 self.text_wrap(&state.content);
412 }
413 }
414
415 Response::none()
416 }
417
418 pub fn streaming_markdown(
436 &mut self,
437 state: &mut crate::widgets::StreamingMarkdownState,
438 ) -> Response {
439 if state.streaming {
440 state.cursor_tick = state.cursor_tick.wrapping_add(1);
441 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
442 }
443
444 if state.content.is_empty() && state.streaming {
445 let cursor = if state.cursor_visible { "▌" } else { " " };
446 let primary = self.theme.primary;
447 self.text(cursor).fg(primary);
448 return Response::none();
449 }
450
451 let show_cursor = state.streaming && state.cursor_visible;
452 let trailing_newline = state.content.ends_with('\n');
453 let lines: Vec<&str> = state.content.lines().collect();
454 let last_line_index = lines.len().saturating_sub(1);
455
456 self.commands.push(Command::BeginContainer {
457 direction: Direction::Column,
458 gap: 0,
459 align: Align::Start,
460 align_self: None,
461 justify: Justify::Start,
462 border: None,
463 border_sides: BorderSides::all(),
464 border_style: Style::new().fg(self.theme.border),
465 bg_color: None,
466 padding: Padding::default(),
467 margin: Margin::default(),
468 constraints: Constraints::default(),
469 title: None,
470 grow: 0,
471 group_name: None,
472 });
473 self.interaction_count += 1;
474
475 let text_style = Style::new().fg(self.theme.text);
476 let bold_style = Style::new().fg(self.theme.text).bold();
477 let code_style = Style::new().fg(self.theme.accent);
478 let border_style = Style::new().fg(self.theme.border).dim();
479
480 let mut in_code_block = false;
481 let mut code_block_lang = String::new();
482
483 for (idx, line) in lines.iter().enumerate() {
484 let line = *line;
485 let trimmed = line.trim();
486 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
487 let cursor = if append_cursor { "▌" } else { "" };
488
489 if in_code_block {
490 if trimmed.starts_with("```") {
491 in_code_block = false;
492 code_block_lang.clear();
493 let mut line = String::from(" └────");
494 line.push_str(cursor);
495 self.styled(line, border_style);
496 } else {
497 let mut line_text = String::with_capacity(2 + line.len() + cursor.len());
498 line_text.push_str(" ");
499 line_text.push_str(line);
500 line_text.push_str(cursor);
501 self.styled(line_text, code_style);
502 }
503 continue;
504 }
505
506 if trimmed.is_empty() {
507 if append_cursor {
508 self.styled("▌", Style::new().fg(self.theme.primary));
509 } else {
510 self.text(" ");
511 }
512 continue;
513 }
514
515 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
516 let mut line = "─".repeat(40);
517 line.push_str(cursor);
518 self.styled(line, border_style);
519 continue;
520 }
521
522 if let Some(heading) = trimmed.strip_prefix("### ") {
523 let mut line = String::with_capacity(heading.len() + cursor.len());
524 line.push_str(heading);
525 line.push_str(cursor);
526 self.styled(line, Style::new().bold().fg(self.theme.accent));
527 continue;
528 }
529
530 if let Some(heading) = trimmed.strip_prefix("## ") {
531 let mut line = String::with_capacity(heading.len() + cursor.len());
532 line.push_str(heading);
533 line.push_str(cursor);
534 self.styled(line, Style::new().bold().fg(self.theme.secondary));
535 continue;
536 }
537
538 if let Some(heading) = trimmed.strip_prefix("# ") {
539 let mut line = String::with_capacity(heading.len() + cursor.len());
540 line.push_str(heading);
541 line.push_str(cursor);
542 self.styled(line, Style::new().bold().fg(self.theme.primary));
543 continue;
544 }
545
546 if let Some(code) = trimmed.strip_prefix("```") {
547 in_code_block = true;
548 code_block_lang = code.trim().to_string();
549 let label = if code_block_lang.is_empty() {
550 "code".to_string()
551 } else {
552 let mut label = String::from("code:");
553 label.push_str(&code_block_lang);
554 label
555 };
556 let mut line = String::with_capacity(5 + label.len() + cursor.len());
557 line.push_str(" ┌─");
558 line.push_str(&label);
559 line.push('─');
560 line.push_str(cursor);
561 self.styled(line, border_style);
562 continue;
563 }
564
565 if let Some(item) = trimmed
566 .strip_prefix("- ")
567 .or_else(|| trimmed.strip_prefix("* "))
568 {
569 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
570 if segs.len() <= 1 {
571 let mut line = String::with_capacity(4 + item.len() + cursor.len());
572 line.push_str(" • ");
573 line.push_str(item);
574 line.push_str(cursor);
575 self.styled(line, text_style);
576 } else {
577 self.line(|ui| {
578 ui.styled(" • ", text_style);
579 for (s, st) in segs {
580 ui.styled(s, st);
581 }
582 if append_cursor {
583 ui.styled("▌", Style::new().fg(ui.theme.primary));
584 }
585 });
586 }
587 continue;
588 }
589
590 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
591 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
592 if parts.len() == 2 {
593 let segs =
594 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
595 if segs.len() <= 1 {
596 let mut line = String::with_capacity(
597 4 + parts[0].len() + parts[1].len() + cursor.len(),
598 );
599 line.push_str(" ");
600 line.push_str(parts[0]);
601 line.push_str(". ");
602 line.push_str(parts[1]);
603 line.push_str(cursor);
604 self.styled(line, text_style);
605 } else {
606 self.line(|ui| {
607 let mut prefix = String::with_capacity(4 + parts[0].len());
608 prefix.push_str(" ");
609 prefix.push_str(parts[0]);
610 prefix.push_str(". ");
611 ui.styled(prefix, text_style);
612 for (s, st) in segs {
613 ui.styled(s, st);
614 }
615 if append_cursor {
616 ui.styled("▌", Style::new().fg(ui.theme.primary));
617 }
618 });
619 }
620 } else {
621 let mut line = String::with_capacity(trimmed.len() + cursor.len());
622 line.push_str(trimmed);
623 line.push_str(cursor);
624 self.styled(line, text_style);
625 }
626 continue;
627 }
628
629 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
630 if segs.len() <= 1 {
631 let mut line = String::with_capacity(trimmed.len() + cursor.len());
632 line.push_str(trimmed);
633 line.push_str(cursor);
634 self.styled(line, text_style);
635 } else {
636 self.line(|ui| {
637 for (s, st) in segs {
638 ui.styled(s, st);
639 }
640 if append_cursor {
641 ui.styled("▌", Style::new().fg(ui.theme.primary));
642 }
643 });
644 }
645 }
646
647 if show_cursor && trailing_newline {
648 if in_code_block {
649 self.styled(" ▌", code_style);
650 } else {
651 self.styled("▌", Style::new().fg(self.theme.primary));
652 }
653 }
654
655 state.in_code_block = in_code_block;
656 state.code_block_lang = code_block_lang;
657
658 self.commands.push(Command::EndContainer);
659 self.last_text_idx = None;
660 Response::none()
661 }
662
663 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
678 let old_action = state.action;
679 let theme = self.theme;
680 let _ = self.bordered(Border::Rounded).col(|ui| {
681 let _ = ui.row(|ui| {
682 ui.text("⚡").fg(theme.warning);
683 ui.text(&state.tool_name).bold().fg(theme.primary);
684 });
685 ui.text(&state.description).dim();
686
687 if state.action == ApprovalAction::Pending {
688 let _ = ui.row(|ui| {
689 if ui.button("✓ Approve").clicked {
690 state.action = ApprovalAction::Approved;
691 }
692 if ui.button("✗ Reject").clicked {
693 state.action = ApprovalAction::Rejected;
694 }
695 });
696 } else {
697 let (label, color) = match state.action {
698 ApprovalAction::Approved => ("✓ Approved", theme.success),
699 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
700 ApprovalAction::Pending => unreachable!(),
701 };
702 ui.text(label).fg(color).bold();
703 }
704 });
705
706 Response {
707 changed: state.action != old_action,
708 ..Response::none()
709 }
710 }
711
712 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
725 if items.is_empty() {
726 return Response::none();
727 }
728
729 let theme = self.theme;
730 let total: usize = items.iter().map(|item| item.tokens).sum();
731
732 let _ = self.container().row(|ui| {
733 ui.text("📎").dim();
734 for item in items {
735 let token_count = format_token_count(item.tokens);
736 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
737 line.push_str(&item.label);
738 line.push_str(" (");
739 line.push_str(&token_count);
740 line.push(')');
741 ui.text(line).fg(theme.secondary);
742 }
743 ui.spacer();
744 let total_text = format_token_count(total);
745 let mut line = String::with_capacity(2 + total_text.len());
746 line.push_str("Σ ");
747 line.push_str(&total_text);
748 ui.text(line).dim();
749 });
750
751 Response::none()
752 }
753
754 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
755 use crate::widgets::AlertLevel;
756
757 let theme = self.theme;
758 let (icon, color) = match level {
759 AlertLevel::Info => ("ℹ", theme.accent),
760 AlertLevel::Success => ("✓", theme.success),
761 AlertLevel::Warning => ("⚠", theme.warning),
762 AlertLevel::Error => ("✕", theme.error),
763 };
764
765 let focused = self.register_focusable();
766 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
767
768 let mut response = self.container().col(|ui| {
769 ui.line(|ui| {
770 let mut icon_text = String::with_capacity(icon.len() + 2);
771 icon_text.push(' ');
772 icon_text.push_str(icon);
773 icon_text.push(' ');
774 ui.text(icon_text).fg(color).bold();
775 ui.text(message).grow(1);
776 ui.text(" [×] ").dim();
777 });
778 });
779 response.focused = focused;
780 if key_dismiss {
781 response.clicked = true;
782 }
783
784 response
785 }
786
787 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
801 let focused = self.register_focusable();
802 let mut is_yes = *result;
803 let mut clicked = false;
804
805 if focused {
806 let mut consumed_indices = Vec::new();
807 for (i, event) in self.events.iter().enumerate() {
808 if let Event::Key(key) = event {
809 if key.kind != KeyEventKind::Press {
810 continue;
811 }
812
813 match key.code {
814 KeyCode::Char('y') => {
815 is_yes = true;
816 *result = true;
817 clicked = true;
818 consumed_indices.push(i);
819 }
820 KeyCode::Char('n') => {
821 is_yes = false;
822 *result = false;
823 clicked = true;
824 consumed_indices.push(i);
825 }
826 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
827 is_yes = !is_yes;
828 *result = is_yes;
829 consumed_indices.push(i);
830 }
831 KeyCode::Enter => {
832 *result = is_yes;
833 clicked = true;
834 consumed_indices.push(i);
835 }
836 _ => {}
837 }
838 }
839 }
840
841 for idx in consumed_indices {
842 self.consumed[idx] = true;
843 }
844 }
845
846 let yes_style = if is_yes {
847 if focused {
848 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
849 } else {
850 Style::new().fg(self.theme.success).bold()
851 }
852 } else {
853 Style::new().fg(self.theme.text_dim)
854 };
855 let no_style = if !is_yes {
856 if focused {
857 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
858 } else {
859 Style::new().fg(self.theme.error).bold()
860 }
861 } else {
862 Style::new().fg(self.theme.text_dim)
863 };
864
865 let mut response = self.row(|ui| {
866 ui.text(question);
867 ui.text(" ");
868 ui.styled("[Yes]", yes_style);
869 ui.text(" ");
870 ui.styled("[No]", no_style);
871 });
872 response.focused = focused;
873 response.clicked = clicked;
874 response.changed = clicked;
875 response
876 }
877
878 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
879 self.breadcrumb_with(segments, " › ")
880 }
881
882 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
883 let theme = self.theme;
884 let last_idx = segments.len().saturating_sub(1);
885 let mut clicked_idx: Option<usize> = None;
886
887 let _ = self.row(|ui| {
888 for (i, segment) in segments.iter().enumerate() {
889 let is_last = i == last_idx;
890 if is_last {
891 ui.text(*segment).bold();
892 } else {
893 let focused = ui.register_focusable();
894 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
895 let resp = ui.interaction();
896 let color = if resp.hovered || focused {
897 theme.accent
898 } else {
899 theme.primary
900 };
901 ui.text(*segment).fg(color).underline();
902 if resp.clicked || pressed {
903 clicked_idx = Some(i);
904 }
905 ui.text(separator).dim();
906 }
907 }
908 });
909
910 clicked_idx
911 }
912
913 pub fn accordion(
914 &mut self,
915 title: &str,
916 open: &mut bool,
917 f: impl FnOnce(&mut Context),
918 ) -> Response {
919 let theme = self.theme;
920 let focused = self.register_focusable();
921 let old_open = *open;
922
923 if focused && self.key_code(KeyCode::Enter) {
924 *open = !*open;
925 }
926
927 let icon = if *open { "▾" } else { "▸" };
928 let title_color = if focused { theme.primary } else { theme.text };
929
930 let mut response = self.container().col(|ui| {
931 ui.line(|ui| {
932 ui.text(icon).fg(title_color);
933 let mut title_text = String::with_capacity(1 + title.len());
934 title_text.push(' ');
935 title_text.push_str(title);
936 ui.text(title_text).bold().fg(title_color);
937 });
938 });
939
940 if response.clicked {
941 *open = !*open;
942 }
943
944 if *open {
945 let _ = self.container().pl(2).col(f);
946 }
947
948 response.focused = focused;
949 response.changed = *open != old_open;
950 response
951 }
952
953 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
954 let max_key_width = items
955 .iter()
956 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
957 .max()
958 .unwrap_or(0);
959
960 let _ = self.col(|ui| {
961 for (key, value) in items {
962 ui.line(|ui| {
963 let padded = format!("{:>width$}", key, width = max_key_width);
964 ui.text(padded).dim();
965 ui.text(" ");
966 ui.text(*value);
967 });
968 }
969 });
970
971 Response::none()
972 }
973
974 pub fn divider_text(&mut self, label: &str) -> Response {
975 let w = self.width();
976 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
977 let pad = 1u32;
978 let left_len = 4u32;
979 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
980 let left: String = "─".repeat(left_len as usize);
981 let right: String = "─".repeat(right_len as usize);
982 let theme = self.theme;
983 self.line(|ui| {
984 ui.text(&left).fg(theme.border);
985 let mut label_text = String::with_capacity(label.len() + 2);
986 label_text.push(' ');
987 label_text.push_str(label);
988 label_text.push(' ');
989 ui.text(label_text).fg(theme.text);
990 ui.text(&right).fg(theme.border);
991 });
992
993 Response::none()
994 }
995
996 pub fn badge(&mut self, label: &str) -> Response {
997 let theme = self.theme;
998 self.badge_colored(label, theme.primary)
999 }
1000
1001 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
1002 let fg = Color::contrast_fg(color);
1003 let mut label_text = String::with_capacity(label.len() + 2);
1004 label_text.push(' ');
1005 label_text.push_str(label);
1006 label_text.push(' ');
1007 self.text(label_text).fg(fg).bg(color);
1008
1009 Response::none()
1010 }
1011
1012 pub fn key_hint(&mut self, key: &str) -> Response {
1013 let theme = self.theme;
1014 let mut key_text = String::with_capacity(key.len() + 2);
1015 key_text.push(' ');
1016 key_text.push_str(key);
1017 key_text.push(' ');
1018 self.text(key_text).reversed().fg(theme.text_dim);
1019
1020 Response::none()
1021 }
1022
1023 pub fn stat(&mut self, label: &str, value: &str) -> Response {
1024 let _ = self.col(|ui| {
1025 ui.text(label).dim();
1026 ui.text(value).bold();
1027 });
1028
1029 Response::none()
1030 }
1031
1032 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
1033 let _ = self.col(|ui| {
1034 ui.text(label).dim();
1035 ui.text(value).bold().fg(color);
1036 });
1037
1038 Response::none()
1039 }
1040
1041 pub fn stat_trend(
1042 &mut self,
1043 label: &str,
1044 value: &str,
1045 trend: crate::widgets::Trend,
1046 ) -> Response {
1047 let theme = self.theme;
1048 let (arrow, color) = match trend {
1049 crate::widgets::Trend::Up => ("↑", theme.success),
1050 crate::widgets::Trend::Down => ("↓", theme.error),
1051 };
1052 let _ = self.col(|ui| {
1053 ui.text(label).dim();
1054 ui.line(|ui| {
1055 ui.text(value).bold();
1056 let mut arrow_text = String::with_capacity(1 + arrow.len());
1057 arrow_text.push(' ');
1058 arrow_text.push_str(arrow);
1059 ui.text(arrow_text).fg(color);
1060 });
1061 });
1062
1063 Response::none()
1064 }
1065
1066 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
1067 let _ = self.container().center().col(|ui| {
1068 ui.text(title).align(Align::Center);
1069 ui.text(description).dim().align(Align::Center);
1070 });
1071
1072 Response::none()
1073 }
1074
1075 pub fn empty_state_action(
1076 &mut self,
1077 title: &str,
1078 description: &str,
1079 action_label: &str,
1080 ) -> Response {
1081 let mut clicked = false;
1082 let _ = self.container().center().col(|ui| {
1083 ui.text(title).align(Align::Center);
1084 ui.text(description).dim().align(Align::Center);
1085 if ui.button(action_label).clicked {
1086 clicked = true;
1087 }
1088 });
1089
1090 Response {
1091 clicked,
1092 changed: clicked,
1093 ..Response::none()
1094 }
1095 }
1096
1097 pub fn code_block(&mut self, code: &str) -> Response {
1098 let theme = self.theme;
1099 let _ = self
1100 .bordered(Border::Rounded)
1101 .bg(theme.surface)
1102 .pad(1)
1103 .col(|ui| {
1104 for line in code.lines() {
1105 render_highlighted_line(ui, line);
1106 }
1107 });
1108
1109 Response::none()
1110 }
1111
1112 pub fn code_block_numbered(&mut self, code: &str) -> Response {
1113 let lines: Vec<&str> = code.lines().collect();
1114 let gutter_w = format!("{}", lines.len()).len();
1115 let theme = self.theme;
1116 let _ = self
1117 .bordered(Border::Rounded)
1118 .bg(theme.surface)
1119 .pad(1)
1120 .col(|ui| {
1121 for (i, line) in lines.iter().enumerate() {
1122 ui.line(|ui| {
1123 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1124 .fg(theme.text_dim);
1125 render_highlighted_line(ui, line);
1126 });
1127 }
1128 });
1129
1130 Response::none()
1131 }
1132
1133 pub fn wrap(&mut self) -> &mut Self {
1135 if let Some(idx) = self.last_text_idx {
1136 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1137 *wrap = true;
1138 }
1139 }
1140 self
1141 }
1142
1143 pub fn truncate(&mut self) -> &mut Self {
1146 if let Some(idx) = self.last_text_idx {
1147 if let Command::Text { truncate, .. } = &mut self.commands[idx] {
1148 *truncate = true;
1149 }
1150 }
1151 self
1152 }
1153
1154 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1155 if let Some(idx) = self.last_text_idx {
1156 match &mut self.commands[idx] {
1157 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1158 _ => {}
1159 }
1160 }
1161 }
1162
1163 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1164 if let Some(idx) = self.last_text_idx {
1165 match &mut self.commands[idx] {
1166 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1167 f(constraints)
1168 }
1169 _ => {}
1170 }
1171 }
1172 }
1173
1174 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1175 if let Some(idx) = self.last_text_idx {
1176 match &mut self.commands[idx] {
1177 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1178 _ => {}
1179 }
1180 }
1181 }
1182
1183 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1201 self.push_container(Direction::Column, 0, f)
1202 }
1203
1204 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1208 self.push_container(Direction::Column, gap, f)
1209 }
1210
1211 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1228 self.push_container(Direction::Row, 0, f)
1229 }
1230
1231 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1235 self.push_container(Direction::Row, gap, f)
1236 }
1237
1238 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1255 let _ = self.push_container(Direction::Row, 0, f);
1256 self
1257 }
1258
1259 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1278 let start = self.commands.len();
1279 f(self);
1280 let mut segments: Vec<(String, Style)> = Vec::new();
1281 for cmd in self.commands.drain(start..) {
1282 if let Command::Text { content, style, .. } = cmd {
1283 segments.push((content, style));
1284 }
1285 }
1286 self.commands.push(Command::RichText {
1287 segments,
1288 wrap: true,
1289 align: Align::Start,
1290 margin: Margin::default(),
1291 constraints: Constraints::default(),
1292 });
1293 self.last_text_idx = None;
1294 self
1295 }
1296
1297 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1306 let interaction_id = self.interaction_count;
1307 self.interaction_count += 1;
1308 self.commands.push(Command::BeginOverlay { modal: true });
1309 self.overlay_depth += 1;
1310 self.modal_active = true;
1311 f(self);
1312 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1313 self.commands.push(Command::EndOverlay);
1314 self.last_text_idx = None;
1315 self.response_for(interaction_id)
1316 }
1317
1318 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1320 let interaction_id = self.interaction_count;
1321 self.interaction_count += 1;
1322 self.commands.push(Command::BeginOverlay { modal: false });
1323 self.overlay_depth += 1;
1324 f(self);
1325 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1326 self.commands.push(Command::EndOverlay);
1327 self.last_text_idx = None;
1328 self.response_for(interaction_id)
1329 }
1330
1331 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1339 self.group_count = self.group_count.saturating_add(1);
1340 self.group_stack.push(name.to_string());
1341 self.container().group_name(name.to_string())
1342 }
1343
1344 pub fn container(&mut self) -> ContainerBuilder<'_> {
1365 let border = self.theme.border;
1366 ContainerBuilder {
1367 ctx: self,
1368 gap: 0,
1369 row_gap: None,
1370 col_gap: None,
1371 align: Align::Start,
1372 align_self_value: None,
1373 justify: Justify::Start,
1374 border: None,
1375 border_sides: BorderSides::all(),
1376 border_style: Style::new().fg(border),
1377 bg: None,
1378 text_color: None,
1379 dark_bg: None,
1380 dark_border_style: None,
1381 group_hover_bg: None,
1382 group_hover_border_style: None,
1383 group_name: None,
1384 padding: Padding::default(),
1385 margin: Margin::default(),
1386 constraints: Constraints::default(),
1387 title: None,
1388 grow: 0,
1389 scroll_offset: None,
1390 }
1391 }
1392
1393 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1412 let index = self.scroll_count;
1413 self.scroll_count += 1;
1414 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1415 state.set_bounds(ch, vh);
1416 let max = ch.saturating_sub(vh) as usize;
1417 state.offset = state.offset.min(max);
1418 }
1419
1420 let next_id = self.interaction_count;
1421 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1422 let inner_rects: Vec<Rect> = self
1423 .prev_scroll_rects
1424 .iter()
1425 .enumerate()
1426 .filter(|&(j, sr)| {
1427 j != index
1428 && sr.width > 0
1429 && sr.height > 0
1430 && sr.x >= rect.x
1431 && sr.right() <= rect.right()
1432 && sr.y >= rect.y
1433 && sr.bottom() <= rect.bottom()
1434 })
1435 .map(|(_, sr)| *sr)
1436 .collect();
1437 self.auto_scroll_nested(&rect, state, &inner_rects);
1438 }
1439
1440 self.container().scroll_offset(state.offset as u32)
1441 }
1442
1443 pub fn scrollbar(&mut self, state: &ScrollState) {
1463 let vh = state.viewport_height();
1464 let ch = state.content_height();
1465 if vh == 0 || ch <= vh {
1466 return;
1467 }
1468
1469 let track_height = vh;
1470 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1471 let max_offset = ch.saturating_sub(vh);
1472 let thumb_pos = if max_offset == 0 {
1473 0
1474 } else {
1475 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1476 .round() as u32
1477 };
1478
1479 let theme = self.theme;
1480 let track_char = '│';
1481 let thumb_char = '█';
1482
1483 let _ = self.container().w(1).h(track_height).col(|ui| {
1484 for i in 0..track_height {
1485 if i >= thumb_pos && i < thumb_pos + thumb_height {
1486 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1487 } else {
1488 ui.styled(
1489 track_char.to_string(),
1490 Style::new().fg(theme.text_dim).dim(),
1491 );
1492 }
1493 }
1494 });
1495 }
1496
1497 fn auto_scroll_nested(
1498 &mut self,
1499 rect: &Rect,
1500 state: &mut ScrollState,
1501 inner_scroll_rects: &[Rect],
1502 ) {
1503 let mut to_consume: Vec<usize> = Vec::new();
1504
1505 for (i, event) in self.events.iter().enumerate() {
1506 if self.consumed[i] {
1507 continue;
1508 }
1509 if let Event::Mouse(mouse) = event {
1510 let in_bounds = mouse.x >= rect.x
1511 && mouse.x < rect.right()
1512 && mouse.y >= rect.y
1513 && mouse.y < rect.bottom();
1514 if !in_bounds {
1515 continue;
1516 }
1517 let in_inner = inner_scroll_rects.iter().any(|sr| {
1518 mouse.x >= sr.x
1519 && mouse.x < sr.right()
1520 && mouse.y >= sr.y
1521 && mouse.y < sr.bottom()
1522 });
1523 if in_inner {
1524 continue;
1525 }
1526 match mouse.kind {
1527 MouseKind::ScrollUp => {
1528 state.scroll_up(1);
1529 to_consume.push(i);
1530 }
1531 MouseKind::ScrollDown => {
1532 state.scroll_down(1);
1533 to_consume.push(i);
1534 }
1535 MouseKind::Drag(MouseButton::Left) => {}
1536 _ => {}
1537 }
1538 }
1539 }
1540
1541 for i in to_consume {
1542 self.consumed[i] = true;
1543 }
1544 }
1545
1546 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1550 self.container()
1551 .border(border)
1552 .border_sides(BorderSides::all())
1553 }
1554
1555 fn push_container(
1556 &mut self,
1557 direction: Direction,
1558 gap: u32,
1559 f: impl FnOnce(&mut Context),
1560 ) -> Response {
1561 let interaction_id = self.interaction_count;
1562 self.interaction_count += 1;
1563 let border = self.theme.border;
1564
1565 self.commands.push(Command::BeginContainer {
1566 direction,
1567 gap,
1568 align: Align::Start,
1569 align_self: None,
1570 justify: Justify::Start,
1571 border: None,
1572 border_sides: BorderSides::all(),
1573 border_style: Style::new().fg(border),
1574 bg_color: None,
1575 padding: Padding::default(),
1576 margin: Margin::default(),
1577 constraints: Constraints::default(),
1578 title: None,
1579 grow: 0,
1580 group_name: None,
1581 });
1582 self.text_color_stack.push(None);
1583 f(self);
1584 self.text_color_stack.pop();
1585 self.commands.push(Command::EndContainer);
1586 self.last_text_idx = None;
1587
1588 self.response_for(interaction_id)
1589 }
1590
1591 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1592 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1593 return Response::none();
1594 }
1595 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1596 let clicked = self
1597 .click_pos
1598 .map(|(mx, my)| {
1599 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1600 })
1601 .unwrap_or(false);
1602 let hovered = self
1603 .mouse_pos
1604 .map(|(mx, my)| {
1605 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1606 })
1607 .unwrap_or(false);
1608 Response {
1609 clicked,
1610 hovered,
1611 changed: false,
1612 focused: false,
1613 rect: *rect,
1614 }
1615 } else {
1616 Response::none()
1617 }
1618 }
1619
1620 pub fn is_group_hovered(&self, name: &str) -> bool {
1622 if let Some(pos) = self.mouse_pos {
1623 self.prev_group_rects.iter().any(|(n, rect)| {
1624 n == name
1625 && pos.0 >= rect.x
1626 && pos.0 < rect.x + rect.width
1627 && pos.1 >= rect.y
1628 && pos.1 < rect.y + rect.height
1629 })
1630 } else {
1631 false
1632 }
1633 }
1634
1635 pub fn is_group_focused(&self, name: &str) -> bool {
1637 if self.prev_focus_count == 0 {
1638 return false;
1639 }
1640 let focused_index = self.focus_index % self.prev_focus_count;
1641 self.prev_focus_groups
1642 .get(focused_index)
1643 .and_then(|group| group.as_deref())
1644 .map(|group| group == name)
1645 .unwrap_or(false)
1646 }
1647
1648 pub fn grow(&mut self, value: u16) -> &mut Self {
1653 if let Some(idx) = self.last_text_idx {
1654 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1655 *grow = value;
1656 }
1657 }
1658 self
1659 }
1660
1661 pub fn align(&mut self, align: Align) -> &mut Self {
1663 if let Some(idx) = self.last_text_idx {
1664 if let Command::Text {
1665 align: text_align, ..
1666 } = &mut self.commands[idx]
1667 {
1668 *text_align = align;
1669 }
1670 }
1671 self
1672 }
1673
1674 pub fn text_center(&mut self) -> &mut Self {
1678 self.align(Align::Center)
1679 }
1680
1681 pub fn text_right(&mut self) -> &mut Self {
1684 self.align(Align::End)
1685 }
1686
1687 pub fn w(&mut self, value: u32) -> &mut Self {
1694 self.modify_last_constraints(|c| {
1695 c.min_width = Some(value);
1696 c.max_width = Some(value);
1697 });
1698 self
1699 }
1700
1701 pub fn h(&mut self, value: u32) -> &mut Self {
1705 self.modify_last_constraints(|c| {
1706 c.min_height = Some(value);
1707 c.max_height = Some(value);
1708 });
1709 self
1710 }
1711
1712 pub fn min_w(&mut self, value: u32) -> &mut Self {
1714 self.modify_last_constraints(|c| c.min_width = Some(value));
1715 self
1716 }
1717
1718 pub fn max_w(&mut self, value: u32) -> &mut Self {
1720 self.modify_last_constraints(|c| c.max_width = Some(value));
1721 self
1722 }
1723
1724 pub fn min_h(&mut self, value: u32) -> &mut Self {
1726 self.modify_last_constraints(|c| c.min_height = Some(value));
1727 self
1728 }
1729
1730 pub fn max_h(&mut self, value: u32) -> &mut Self {
1732 self.modify_last_constraints(|c| c.max_height = Some(value));
1733 self
1734 }
1735
1736 pub fn m(&mut self, value: u32) -> &mut Self {
1740 self.modify_last_margin(|m| *m = Margin::all(value));
1741 self
1742 }
1743
1744 pub fn mx(&mut self, value: u32) -> &mut Self {
1746 self.modify_last_margin(|m| {
1747 m.left = value;
1748 m.right = value;
1749 });
1750 self
1751 }
1752
1753 pub fn my(&mut self, value: u32) -> &mut Self {
1755 self.modify_last_margin(|m| {
1756 m.top = value;
1757 m.bottom = value;
1758 });
1759 self
1760 }
1761
1762 pub fn mt(&mut self, value: u32) -> &mut Self {
1764 self.modify_last_margin(|m| m.top = value);
1765 self
1766 }
1767
1768 pub fn mr(&mut self, value: u32) -> &mut Self {
1770 self.modify_last_margin(|m| m.right = value);
1771 self
1772 }
1773
1774 pub fn mb(&mut self, value: u32) -> &mut Self {
1776 self.modify_last_margin(|m| m.bottom = value);
1777 self
1778 }
1779
1780 pub fn ml(&mut self, value: u32) -> &mut Self {
1782 self.modify_last_margin(|m| m.left = value);
1783 self
1784 }
1785
1786 pub fn spacer(&mut self) -> &mut Self {
1790 self.commands.push(Command::Spacer { grow: 1 });
1791 self.last_text_idx = None;
1792 self
1793 }
1794
1795 pub fn form(
1799 &mut self,
1800 state: &mut FormState,
1801 f: impl FnOnce(&mut Context, &mut FormState),
1802 ) -> &mut Self {
1803 let _ = self.col(|ui| {
1804 f(ui, state);
1805 });
1806 self
1807 }
1808
1809 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1813 let _ = self.col(|ui| {
1814 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1815 let _ = ui.text_input(&mut field.input);
1816 if let Some(error) = field.error.as_deref() {
1817 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1818 }
1819 });
1820 self
1821 }
1822
1823 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1827 self.button(label)
1828 }
1829}
1830
1831const KEYWORDS: &[&str] = &[
1832 "fn",
1833 "let",
1834 "mut",
1835 "pub",
1836 "use",
1837 "impl",
1838 "struct",
1839 "enum",
1840 "trait",
1841 "type",
1842 "const",
1843 "static",
1844 "if",
1845 "else",
1846 "match",
1847 "for",
1848 "while",
1849 "loop",
1850 "return",
1851 "break",
1852 "continue",
1853 "where",
1854 "self",
1855 "super",
1856 "crate",
1857 "mod",
1858 "async",
1859 "await",
1860 "move",
1861 "ref",
1862 "in",
1863 "as",
1864 "true",
1865 "false",
1866 "Some",
1867 "None",
1868 "Ok",
1869 "Err",
1870 "Self",
1871 "def",
1872 "class",
1873 "import",
1874 "from",
1875 "pass",
1876 "lambda",
1877 "yield",
1878 "with",
1879 "try",
1880 "except",
1881 "raise",
1882 "finally",
1883 "elif",
1884 "del",
1885 "global",
1886 "nonlocal",
1887 "assert",
1888 "is",
1889 "not",
1890 "and",
1891 "or",
1892 "function",
1893 "var",
1894 "const",
1895 "export",
1896 "default",
1897 "switch",
1898 "case",
1899 "throw",
1900 "catch",
1901 "typeof",
1902 "instanceof",
1903 "new",
1904 "delete",
1905 "void",
1906 "this",
1907 "null",
1908 "undefined",
1909 "func",
1910 "package",
1911 "defer",
1912 "go",
1913 "chan",
1914 "select",
1915 "range",
1916 "map",
1917 "interface",
1918 "fallthrough",
1919 "nil",
1920];
1921
1922fn render_highlighted_line(ui: &mut Context, line: &str) {
1923 let theme = ui.theme;
1924 let is_light = matches!(
1925 theme.bg,
1926 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1927 );
1928 let keyword_color = if is_light {
1929 Color::Rgb(166, 38, 164)
1930 } else {
1931 Color::Rgb(198, 120, 221)
1932 };
1933 let string_color = if is_light {
1934 Color::Rgb(80, 161, 79)
1935 } else {
1936 Color::Rgb(152, 195, 121)
1937 };
1938 let comment_color = theme.text_dim;
1939 let number_color = if is_light {
1940 Color::Rgb(152, 104, 1)
1941 } else {
1942 Color::Rgb(209, 154, 102)
1943 };
1944 let fn_color = if is_light {
1945 Color::Rgb(64, 120, 242)
1946 } else {
1947 Color::Rgb(97, 175, 239)
1948 };
1949 let macro_color = if is_light {
1950 Color::Rgb(1, 132, 188)
1951 } else {
1952 Color::Rgb(86, 182, 194)
1953 };
1954
1955 let trimmed = line.trim_start();
1956 let indent = &line[..line.len() - trimmed.len()];
1957 if !indent.is_empty() {
1958 ui.text(indent);
1959 }
1960
1961 if trimmed.starts_with("//") {
1962 ui.text(trimmed).fg(comment_color).italic();
1963 return;
1964 }
1965
1966 let mut pos = 0;
1967
1968 while pos < trimmed.len() {
1969 let ch = trimmed.as_bytes()[pos];
1970
1971 if ch == b'"' {
1972 if let Some(end) = trimmed[pos + 1..].find('"') {
1973 let s = &trimmed[pos..pos + end + 2];
1974 ui.text(s).fg(string_color);
1975 pos += end + 2;
1976 continue;
1977 }
1978 }
1979
1980 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1981 {
1982 let end = trimmed[pos..]
1983 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1984 .map_or(trimmed.len(), |e| pos + e);
1985 ui.text(&trimmed[pos..end]).fg(number_color);
1986 pos = end;
1987 continue;
1988 }
1989
1990 if ch.is_ascii_alphabetic() || ch == b'_' {
1991 let end = trimmed[pos..]
1992 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1993 .map_or(trimmed.len(), |e| pos + e);
1994 let word = &trimmed[pos..end];
1995
1996 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1997 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1998 pos = end + 1;
1999 } else if end < trimmed.len()
2000 && trimmed.as_bytes()[end] == b'('
2001 && !KEYWORDS.contains(&word)
2002 {
2003 ui.text(word).fg(fn_color);
2004 pos = end;
2005 } else if KEYWORDS.contains(&word) {
2006 ui.text(word).fg(keyword_color);
2007 pos = end;
2008 } else {
2009 ui.text(word);
2010 pos = end;
2011 }
2012 continue;
2013 }
2014
2015 let end = trimmed[pos..]
2016 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
2017 .map_or(trimmed.len(), |e| pos + e);
2018 ui.text(&trimmed[pos..end]);
2019 pos = end;
2020 }
2021}
2022
2023fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
2024 let expected = (width as usize) * (height as usize) * 4;
2025 if data.len() >= expected {
2026 return data[..expected].to_vec();
2027 }
2028 let mut buf = Vec::with_capacity(expected);
2029 buf.extend_from_slice(data);
2030 buf.resize(expected, 0);
2031 buf
2032}
2033
2034fn base64_encode(data: &[u8]) -> String {
2035 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2036 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
2037 for chunk in data.chunks(3) {
2038 let b0 = chunk[0] as u32;
2039 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2040 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2041 let triple = (b0 << 16) | (b1 << 8) | b2;
2042 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
2043 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
2044 if chunk.len() > 1 {
2045 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
2046 } else {
2047 result.push('=');
2048 }
2049 if chunk.len() > 2 {
2050 result.push(CHARS[(triple & 0x3F) as usize] as char);
2051 } else {
2052 result.push('=');
2053 }
2054 }
2055 result
2056}
2057
2058fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
2059 let mut chunks = Vec::new();
2060 let bytes = encoded.as_bytes();
2061 let mut offset = 0;
2062 while offset < bytes.len() {
2063 let end = (offset + chunk_size).min(bytes.len());
2064 chunks.push(&encoded[offset..end]);
2065 offset = end;
2066 }
2067 if chunks.is_empty() {
2068 chunks.push("");
2069 }
2070 chunks
2071}