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 self.modal_focus_start = self.focus_count;
1312 f(self);
1313 self.modal_focus_count = self.focus_count.saturating_sub(self.modal_focus_start);
1314 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1315 self.commands.push(Command::EndOverlay);
1316 self.last_text_idx = None;
1317 self.response_for(interaction_id)
1318 }
1319
1320 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1322 let interaction_id = self.interaction_count;
1323 self.interaction_count += 1;
1324 self.commands.push(Command::BeginOverlay { modal: false });
1325 self.overlay_depth += 1;
1326 f(self);
1327 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1328 self.commands.push(Command::EndOverlay);
1329 self.last_text_idx = None;
1330 self.response_for(interaction_id)
1331 }
1332
1333 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1341 self.group_count = self.group_count.saturating_add(1);
1342 self.group_stack.push(name.to_string());
1343 self.container().group_name(name.to_string())
1344 }
1345
1346 pub fn container(&mut self) -> ContainerBuilder<'_> {
1367 let border = self.theme.border;
1368 ContainerBuilder {
1369 ctx: self,
1370 gap: 0,
1371 row_gap: None,
1372 col_gap: None,
1373 align: Align::Start,
1374 align_self_value: None,
1375 justify: Justify::Start,
1376 border: None,
1377 border_sides: BorderSides::all(),
1378 border_style: Style::new().fg(border),
1379 bg: None,
1380 text_color: None,
1381 dark_bg: None,
1382 dark_border_style: None,
1383 group_hover_bg: None,
1384 group_hover_border_style: None,
1385 group_name: None,
1386 padding: Padding::default(),
1387 margin: Margin::default(),
1388 constraints: Constraints::default(),
1389 title: None,
1390 grow: 0,
1391 scroll_offset: None,
1392 }
1393 }
1394
1395 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1414 let index = self.scroll_count;
1415 self.scroll_count += 1;
1416 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1417 state.set_bounds(ch, vh);
1418 let max = ch.saturating_sub(vh) as usize;
1419 state.offset = state.offset.min(max);
1420 }
1421
1422 let next_id = self.interaction_count;
1423 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1424 let inner_rects: Vec<Rect> = self
1425 .prev_scroll_rects
1426 .iter()
1427 .enumerate()
1428 .filter(|&(j, sr)| {
1429 j != index
1430 && sr.width > 0
1431 && sr.height > 0
1432 && sr.x >= rect.x
1433 && sr.right() <= rect.right()
1434 && sr.y >= rect.y
1435 && sr.bottom() <= rect.bottom()
1436 })
1437 .map(|(_, sr)| *sr)
1438 .collect();
1439 self.auto_scroll_nested(&rect, state, &inner_rects);
1440 }
1441
1442 self.container().scroll_offset(state.offset as u32)
1443 }
1444
1445 pub fn scrollbar(&mut self, state: &ScrollState) {
1465 let vh = state.viewport_height();
1466 let ch = state.content_height();
1467 if vh == 0 || ch <= vh {
1468 return;
1469 }
1470
1471 let track_height = vh;
1472 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1473 let max_offset = ch.saturating_sub(vh);
1474 let thumb_pos = if max_offset == 0 {
1475 0
1476 } else {
1477 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1478 .round() as u32
1479 };
1480
1481 let theme = self.theme;
1482 let track_char = '│';
1483 let thumb_char = '█';
1484
1485 let _ = self.container().w(1).h(track_height).col(|ui| {
1486 for i in 0..track_height {
1487 if i >= thumb_pos && i < thumb_pos + thumb_height {
1488 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1489 } else {
1490 ui.styled(
1491 track_char.to_string(),
1492 Style::new().fg(theme.text_dim).dim(),
1493 );
1494 }
1495 }
1496 });
1497 }
1498
1499 fn auto_scroll_nested(
1500 &mut self,
1501 rect: &Rect,
1502 state: &mut ScrollState,
1503 inner_scroll_rects: &[Rect],
1504 ) {
1505 let mut to_consume: Vec<usize> = Vec::new();
1506
1507 for (i, event) in self.events.iter().enumerate() {
1508 if self.consumed[i] {
1509 continue;
1510 }
1511 if let Event::Mouse(mouse) = event {
1512 let in_bounds = mouse.x >= rect.x
1513 && mouse.x < rect.right()
1514 && mouse.y >= rect.y
1515 && mouse.y < rect.bottom();
1516 if !in_bounds {
1517 continue;
1518 }
1519 let in_inner = inner_scroll_rects.iter().any(|sr| {
1520 mouse.x >= sr.x
1521 && mouse.x < sr.right()
1522 && mouse.y >= sr.y
1523 && mouse.y < sr.bottom()
1524 });
1525 if in_inner {
1526 continue;
1527 }
1528 match mouse.kind {
1529 MouseKind::ScrollUp => {
1530 state.scroll_up(1);
1531 to_consume.push(i);
1532 }
1533 MouseKind::ScrollDown => {
1534 state.scroll_down(1);
1535 to_consume.push(i);
1536 }
1537 MouseKind::Drag(MouseButton::Left) => {}
1538 _ => {}
1539 }
1540 }
1541 }
1542
1543 for i in to_consume {
1544 self.consumed[i] = true;
1545 }
1546 }
1547
1548 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1552 self.container()
1553 .border(border)
1554 .border_sides(BorderSides::all())
1555 }
1556
1557 fn push_container(
1558 &mut self,
1559 direction: Direction,
1560 gap: u32,
1561 f: impl FnOnce(&mut Context),
1562 ) -> Response {
1563 let interaction_id = self.interaction_count;
1564 self.interaction_count += 1;
1565 let border = self.theme.border;
1566
1567 self.commands.push(Command::BeginContainer {
1568 direction,
1569 gap,
1570 align: Align::Start,
1571 align_self: None,
1572 justify: Justify::Start,
1573 border: None,
1574 border_sides: BorderSides::all(),
1575 border_style: Style::new().fg(border),
1576 bg_color: None,
1577 padding: Padding::default(),
1578 margin: Margin::default(),
1579 constraints: Constraints::default(),
1580 title: None,
1581 grow: 0,
1582 group_name: None,
1583 });
1584 self.text_color_stack.push(None);
1585 f(self);
1586 self.text_color_stack.pop();
1587 self.commands.push(Command::EndContainer);
1588 self.last_text_idx = None;
1589
1590 self.response_for(interaction_id)
1591 }
1592
1593 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1594 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1595 return Response::none();
1596 }
1597 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1598 let clicked = self
1599 .click_pos
1600 .map(|(mx, my)| {
1601 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1602 })
1603 .unwrap_or(false);
1604 let hovered = self
1605 .mouse_pos
1606 .map(|(mx, my)| {
1607 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1608 })
1609 .unwrap_or(false);
1610 Response {
1611 clicked,
1612 hovered,
1613 changed: false,
1614 focused: false,
1615 rect: *rect,
1616 }
1617 } else {
1618 Response::none()
1619 }
1620 }
1621
1622 pub fn is_group_hovered(&self, name: &str) -> bool {
1624 if let Some(pos) = self.mouse_pos {
1625 self.prev_group_rects.iter().any(|(n, rect)| {
1626 n == name
1627 && pos.0 >= rect.x
1628 && pos.0 < rect.x + rect.width
1629 && pos.1 >= rect.y
1630 && pos.1 < rect.y + rect.height
1631 })
1632 } else {
1633 false
1634 }
1635 }
1636
1637 pub fn is_group_focused(&self, name: &str) -> bool {
1639 if self.prev_focus_count == 0 {
1640 return false;
1641 }
1642 let focused_index = self.focus_index % self.prev_focus_count;
1643 self.prev_focus_groups
1644 .get(focused_index)
1645 .and_then(|group| group.as_deref())
1646 .map(|group| group == name)
1647 .unwrap_or(false)
1648 }
1649
1650 pub fn grow(&mut self, value: u16) -> &mut Self {
1655 if let Some(idx) = self.last_text_idx {
1656 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1657 *grow = value;
1658 }
1659 }
1660 self
1661 }
1662
1663 pub fn align(&mut self, align: Align) -> &mut Self {
1665 if let Some(idx) = self.last_text_idx {
1666 if let Command::Text {
1667 align: text_align, ..
1668 } = &mut self.commands[idx]
1669 {
1670 *text_align = align;
1671 }
1672 }
1673 self
1674 }
1675
1676 pub fn text_center(&mut self) -> &mut Self {
1680 self.align(Align::Center)
1681 }
1682
1683 pub fn text_right(&mut self) -> &mut Self {
1686 self.align(Align::End)
1687 }
1688
1689 pub fn w(&mut self, value: u32) -> &mut Self {
1696 self.modify_last_constraints(|c| {
1697 c.min_width = Some(value);
1698 c.max_width = Some(value);
1699 });
1700 self
1701 }
1702
1703 pub fn h(&mut self, value: u32) -> &mut Self {
1707 self.modify_last_constraints(|c| {
1708 c.min_height = Some(value);
1709 c.max_height = Some(value);
1710 });
1711 self
1712 }
1713
1714 pub fn min_w(&mut self, value: u32) -> &mut Self {
1716 self.modify_last_constraints(|c| c.min_width = Some(value));
1717 self
1718 }
1719
1720 pub fn max_w(&mut self, value: u32) -> &mut Self {
1722 self.modify_last_constraints(|c| c.max_width = Some(value));
1723 self
1724 }
1725
1726 pub fn min_h(&mut self, value: u32) -> &mut Self {
1728 self.modify_last_constraints(|c| c.min_height = Some(value));
1729 self
1730 }
1731
1732 pub fn max_h(&mut self, value: u32) -> &mut Self {
1734 self.modify_last_constraints(|c| c.max_height = Some(value));
1735 self
1736 }
1737
1738 pub fn m(&mut self, value: u32) -> &mut Self {
1742 self.modify_last_margin(|m| *m = Margin::all(value));
1743 self
1744 }
1745
1746 pub fn mx(&mut self, value: u32) -> &mut Self {
1748 self.modify_last_margin(|m| {
1749 m.left = value;
1750 m.right = value;
1751 });
1752 self
1753 }
1754
1755 pub fn my(&mut self, value: u32) -> &mut Self {
1757 self.modify_last_margin(|m| {
1758 m.top = value;
1759 m.bottom = value;
1760 });
1761 self
1762 }
1763
1764 pub fn mt(&mut self, value: u32) -> &mut Self {
1766 self.modify_last_margin(|m| m.top = value);
1767 self
1768 }
1769
1770 pub fn mr(&mut self, value: u32) -> &mut Self {
1772 self.modify_last_margin(|m| m.right = value);
1773 self
1774 }
1775
1776 pub fn mb(&mut self, value: u32) -> &mut Self {
1778 self.modify_last_margin(|m| m.bottom = value);
1779 self
1780 }
1781
1782 pub fn ml(&mut self, value: u32) -> &mut Self {
1784 self.modify_last_margin(|m| m.left = value);
1785 self
1786 }
1787
1788 pub fn spacer(&mut self) -> &mut Self {
1792 self.commands.push(Command::Spacer { grow: 1 });
1793 self.last_text_idx = None;
1794 self
1795 }
1796
1797 pub fn form(
1801 &mut self,
1802 state: &mut FormState,
1803 f: impl FnOnce(&mut Context, &mut FormState),
1804 ) -> &mut Self {
1805 let _ = self.col(|ui| {
1806 f(ui, state);
1807 });
1808 self
1809 }
1810
1811 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1815 let _ = self.col(|ui| {
1816 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1817 let _ = ui.text_input(&mut field.input);
1818 if let Some(error) = field.error.as_deref() {
1819 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1820 }
1821 });
1822 self
1823 }
1824
1825 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1829 self.button(label)
1830 }
1831}
1832
1833const KEYWORDS: &[&str] = &[
1834 "fn",
1835 "let",
1836 "mut",
1837 "pub",
1838 "use",
1839 "impl",
1840 "struct",
1841 "enum",
1842 "trait",
1843 "type",
1844 "const",
1845 "static",
1846 "if",
1847 "else",
1848 "match",
1849 "for",
1850 "while",
1851 "loop",
1852 "return",
1853 "break",
1854 "continue",
1855 "where",
1856 "self",
1857 "super",
1858 "crate",
1859 "mod",
1860 "async",
1861 "await",
1862 "move",
1863 "ref",
1864 "in",
1865 "as",
1866 "true",
1867 "false",
1868 "Some",
1869 "None",
1870 "Ok",
1871 "Err",
1872 "Self",
1873 "def",
1874 "class",
1875 "import",
1876 "from",
1877 "pass",
1878 "lambda",
1879 "yield",
1880 "with",
1881 "try",
1882 "except",
1883 "raise",
1884 "finally",
1885 "elif",
1886 "del",
1887 "global",
1888 "nonlocal",
1889 "assert",
1890 "is",
1891 "not",
1892 "and",
1893 "or",
1894 "function",
1895 "var",
1896 "const",
1897 "export",
1898 "default",
1899 "switch",
1900 "case",
1901 "throw",
1902 "catch",
1903 "typeof",
1904 "instanceof",
1905 "new",
1906 "delete",
1907 "void",
1908 "this",
1909 "null",
1910 "undefined",
1911 "func",
1912 "package",
1913 "defer",
1914 "go",
1915 "chan",
1916 "select",
1917 "range",
1918 "map",
1919 "interface",
1920 "fallthrough",
1921 "nil",
1922];
1923
1924fn render_highlighted_line(ui: &mut Context, line: &str) {
1925 let theme = ui.theme;
1926 let is_light = matches!(
1927 theme.bg,
1928 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1929 );
1930 let keyword_color = if is_light {
1931 Color::Rgb(166, 38, 164)
1932 } else {
1933 Color::Rgb(198, 120, 221)
1934 };
1935 let string_color = if is_light {
1936 Color::Rgb(80, 161, 79)
1937 } else {
1938 Color::Rgb(152, 195, 121)
1939 };
1940 let comment_color = theme.text_dim;
1941 let number_color = if is_light {
1942 Color::Rgb(152, 104, 1)
1943 } else {
1944 Color::Rgb(209, 154, 102)
1945 };
1946 let fn_color = if is_light {
1947 Color::Rgb(64, 120, 242)
1948 } else {
1949 Color::Rgb(97, 175, 239)
1950 };
1951 let macro_color = if is_light {
1952 Color::Rgb(1, 132, 188)
1953 } else {
1954 Color::Rgb(86, 182, 194)
1955 };
1956
1957 let trimmed = line.trim_start();
1958 let indent = &line[..line.len() - trimmed.len()];
1959 if !indent.is_empty() {
1960 ui.text(indent);
1961 }
1962
1963 if trimmed.starts_with("//") {
1964 ui.text(trimmed).fg(comment_color).italic();
1965 return;
1966 }
1967
1968 let mut pos = 0;
1969
1970 while pos < trimmed.len() {
1971 let ch = trimmed.as_bytes()[pos];
1972
1973 if ch == b'"' {
1974 if let Some(end) = trimmed[pos + 1..].find('"') {
1975 let s = &trimmed[pos..pos + end + 2];
1976 ui.text(s).fg(string_color);
1977 pos += end + 2;
1978 continue;
1979 }
1980 }
1981
1982 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1983 {
1984 let end = trimmed[pos..]
1985 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1986 .map_or(trimmed.len(), |e| pos + e);
1987 ui.text(&trimmed[pos..end]).fg(number_color);
1988 pos = end;
1989 continue;
1990 }
1991
1992 if ch.is_ascii_alphabetic() || ch == b'_' {
1993 let end = trimmed[pos..]
1994 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1995 .map_or(trimmed.len(), |e| pos + e);
1996 let word = &trimmed[pos..end];
1997
1998 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1999 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
2000 pos = end + 1;
2001 } else if end < trimmed.len()
2002 && trimmed.as_bytes()[end] == b'('
2003 && !KEYWORDS.contains(&word)
2004 {
2005 ui.text(word).fg(fn_color);
2006 pos = end;
2007 } else if KEYWORDS.contains(&word) {
2008 ui.text(word).fg(keyword_color);
2009 pos = end;
2010 } else {
2011 ui.text(word);
2012 pos = end;
2013 }
2014 continue;
2015 }
2016
2017 let end = trimmed[pos..]
2018 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
2019 .map_or(trimmed.len(), |e| pos + e);
2020 ui.text(&trimmed[pos..end]);
2021 pos = end;
2022 }
2023}
2024
2025fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
2026 let expected = (width as usize) * (height as usize) * 4;
2027 if data.len() >= expected {
2028 return data[..expected].to_vec();
2029 }
2030 let mut buf = Vec::with_capacity(expected);
2031 buf.extend_from_slice(data);
2032 buf.resize(expected, 0);
2033 buf
2034}
2035
2036fn base64_encode(data: &[u8]) -> String {
2037 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2038 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
2039 for chunk in data.chunks(3) {
2040 let b0 = chunk[0] as u32;
2041 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2042 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2043 let triple = (b0 << 16) | (b1 << 8) | b2;
2044 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
2045 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
2046 if chunk.len() > 1 {
2047 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
2048 } else {
2049 result.push('=');
2050 }
2051 if chunk.len() > 2 {
2052 result.push(CHARS[(triple & 0x3F) as usize] as char);
2053 } else {
2054 result.push('=');
2055 }
2056 }
2057 result
2058}
2059
2060fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
2061 let mut chunks = Vec::new();
2062 let bytes = encoded.as_bytes();
2063 let mut offset = 0;
2064 while offset < bytes.len() {
2065 let end = (offset + chunk_size).min(bytes.len());
2066 chunks.push(&encoded[offset..end]);
2067 offset = end;
2068 }
2069 if chunks.is_empty() {
2070 chunks.push("");
2071 }
2072 chunks
2073}