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 kitty_image_fit(&mut self, rgba: &[u8], src_width: u32, src_height: u32) {
319 let rgba = rgba.to_vec();
320 let sw = src_width;
321 let sh = src_height;
322
323 self.container().grow(1).draw(move |buf, rect| {
324 if rect.width == 0 || rect.height == 0 {
325 return;
326 }
327
328 let target_pw = rect.width * 8;
329 let target_ph = rect.height * 16;
330
331 let resized = resize_cover_crop(&rgba, sw, sh, target_pw, target_ph);
332 let encoded = base64_encode(&resized);
333 let chunks = split_base64(&encoded, 4096);
334 let mut seq = String::new();
335 for (i, chunk) in chunks.iter().enumerate() {
336 let more = if i < chunks.len() - 1 { 1 } else { 0 };
337 if i == 0 {
338 seq.push_str(&format!(
339 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
340 target_pw, target_ph, rect.width, rect.height, more, chunk
341 ));
342 } else {
343 seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
344 }
345 }
346 buf.raw_sequence(rect.x, rect.y, seq);
347 });
348 }
349
350 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
366 if state.streaming {
367 state.cursor_tick = state.cursor_tick.wrapping_add(1);
368 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
369 }
370
371 if state.content.is_empty() && state.streaming {
372 let cursor = if state.cursor_visible { "▌" } else { " " };
373 let primary = self.theme.primary;
374 self.text(cursor).fg(primary);
375 return Response::none();
376 }
377
378 if !state.content.is_empty() {
379 if state.streaming && state.cursor_visible {
380 self.text_wrap(format!("{}▌", state.content));
381 } else {
382 self.text_wrap(&state.content);
383 }
384 }
385
386 Response::none()
387 }
388
389 pub fn streaming_markdown(
407 &mut self,
408 state: &mut crate::widgets::StreamingMarkdownState,
409 ) -> Response {
410 if state.streaming {
411 state.cursor_tick = state.cursor_tick.wrapping_add(1);
412 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
413 }
414
415 if state.content.is_empty() && state.streaming {
416 let cursor = if state.cursor_visible { "▌" } else { " " };
417 let primary = self.theme.primary;
418 self.text(cursor).fg(primary);
419 return Response::none();
420 }
421
422 let show_cursor = state.streaming && state.cursor_visible;
423 let trailing_newline = state.content.ends_with('\n');
424 let lines: Vec<&str> = state.content.lines().collect();
425 let last_line_index = lines.len().saturating_sub(1);
426
427 self.commands.push(Command::BeginContainer {
428 direction: Direction::Column,
429 gap: 0,
430 align: Align::Start,
431 justify: Justify::Start,
432 border: None,
433 border_sides: BorderSides::all(),
434 border_style: Style::new().fg(self.theme.border),
435 bg_color: None,
436 padding: Padding::default(),
437 margin: Margin::default(),
438 constraints: Constraints::default(),
439 title: None,
440 grow: 0,
441 group_name: None,
442 });
443 self.interaction_count += 1;
444
445 let text_style = Style::new().fg(self.theme.text);
446 let bold_style = Style::new().fg(self.theme.text).bold();
447 let code_style = Style::new().fg(self.theme.accent);
448 let border_style = Style::new().fg(self.theme.border).dim();
449
450 let mut in_code_block = false;
451 let mut code_block_lang = String::new();
452
453 for (idx, line) in lines.iter().enumerate() {
454 let line = *line;
455 let trimmed = line.trim();
456 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
457 let cursor = if append_cursor { "▌" } else { "" };
458
459 if in_code_block {
460 if trimmed.starts_with("```") {
461 in_code_block = false;
462 code_block_lang.clear();
463 self.styled(format!(" └────{cursor}"), border_style);
464 } else {
465 self.styled(format!(" {line}{cursor}"), code_style);
466 }
467 continue;
468 }
469
470 if trimmed.is_empty() {
471 if append_cursor {
472 self.styled("▌", Style::new().fg(self.theme.primary));
473 } else {
474 self.text(" ");
475 }
476 continue;
477 }
478
479 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
480 self.styled(format!("{}{}", "─".repeat(40), cursor), border_style);
481 continue;
482 }
483
484 if let Some(heading) = trimmed.strip_prefix("### ") {
485 self.styled(
486 format!("{heading}{cursor}"),
487 Style::new().bold().fg(self.theme.accent),
488 );
489 continue;
490 }
491
492 if let Some(heading) = trimmed.strip_prefix("## ") {
493 self.styled(
494 format!("{heading}{cursor}"),
495 Style::new().bold().fg(self.theme.secondary),
496 );
497 continue;
498 }
499
500 if let Some(heading) = trimmed.strip_prefix("# ") {
501 self.styled(
502 format!("{heading}{cursor}"),
503 Style::new().bold().fg(self.theme.primary),
504 );
505 continue;
506 }
507
508 if let Some(code) = trimmed.strip_prefix("```") {
509 in_code_block = true;
510 code_block_lang = code.trim().to_string();
511 let label = if code_block_lang.is_empty() {
512 "code".to_string()
513 } else {
514 format!("code:{}", code_block_lang)
515 };
516 self.styled(format!(" ┌─{label}─{cursor}"), border_style);
517 continue;
518 }
519
520 if let Some(item) = trimmed
521 .strip_prefix("- ")
522 .or_else(|| trimmed.strip_prefix("* "))
523 {
524 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
525 if segs.len() <= 1 {
526 self.styled(format!(" • {item}{cursor}"), text_style);
527 } else {
528 self.line(|ui| {
529 ui.styled(" • ", text_style);
530 for (s, st) in segs {
531 ui.styled(s, st);
532 }
533 if append_cursor {
534 ui.styled("▌", Style::new().fg(ui.theme.primary));
535 }
536 });
537 }
538 continue;
539 }
540
541 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
542 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
543 if parts.len() == 2 {
544 let segs =
545 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
546 if segs.len() <= 1 {
547 self.styled(
548 format!(" {}. {}{}", parts[0], parts[1], cursor),
549 text_style,
550 );
551 } else {
552 self.line(|ui| {
553 ui.styled(format!(" {}. ", parts[0]), text_style);
554 for (s, st) in segs {
555 ui.styled(s, st);
556 }
557 if append_cursor {
558 ui.styled("▌", Style::new().fg(ui.theme.primary));
559 }
560 });
561 }
562 } else {
563 self.styled(format!("{trimmed}{cursor}"), text_style);
564 }
565 continue;
566 }
567
568 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
569 if segs.len() <= 1 {
570 self.styled(format!("{trimmed}{cursor}"), text_style);
571 } else {
572 self.line(|ui| {
573 for (s, st) in segs {
574 ui.styled(s, st);
575 }
576 if append_cursor {
577 ui.styled("▌", Style::new().fg(ui.theme.primary));
578 }
579 });
580 }
581 }
582
583 if show_cursor && trailing_newline {
584 if in_code_block {
585 self.styled(" ▌", code_style);
586 } else {
587 self.styled("▌", Style::new().fg(self.theme.primary));
588 }
589 }
590
591 state.in_code_block = in_code_block;
592 state.code_block_lang = code_block_lang;
593
594 self.commands.push(Command::EndContainer);
595 self.last_text_idx = None;
596 Response::none()
597 }
598
599 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
614 let old_action = state.action;
615 let theme = self.theme;
616 self.bordered(Border::Rounded).col(|ui| {
617 ui.row(|ui| {
618 ui.text("⚡").fg(theme.warning);
619 ui.text(&state.tool_name).bold().fg(theme.primary);
620 });
621 ui.text(&state.description).dim();
622
623 if state.action == ApprovalAction::Pending {
624 ui.row(|ui| {
625 if ui.button("✓ Approve").clicked {
626 state.action = ApprovalAction::Approved;
627 }
628 if ui.button("✗ Reject").clicked {
629 state.action = ApprovalAction::Rejected;
630 }
631 });
632 } else {
633 let (label, color) = match state.action {
634 ApprovalAction::Approved => ("✓ Approved", theme.success),
635 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
636 ApprovalAction::Pending => unreachable!(),
637 };
638 ui.text(label).fg(color).bold();
639 }
640 });
641
642 Response {
643 changed: state.action != old_action,
644 ..Response::none()
645 }
646 }
647
648 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
661 if items.is_empty() {
662 return Response::none();
663 }
664
665 let theme = self.theme;
666 let total: usize = items.iter().map(|item| item.tokens).sum();
667
668 self.container().row(|ui| {
669 ui.text("📎").dim();
670 for item in items {
671 ui.text(format!(
672 "{} ({})",
673 item.label,
674 format_token_count(item.tokens)
675 ))
676 .fg(theme.secondary);
677 }
678 ui.spacer();
679 ui.text(format!("Σ {}", format_token_count(total))).dim();
680 });
681
682 Response::none()
683 }
684
685 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
686 use crate::widgets::AlertLevel;
687
688 let theme = self.theme;
689 let (icon, color) = match level {
690 AlertLevel::Info => ("ℹ", theme.accent),
691 AlertLevel::Success => ("✓", theme.success),
692 AlertLevel::Warning => ("⚠", theme.warning),
693 AlertLevel::Error => ("✕", theme.error),
694 };
695
696 let focused = self.register_focusable();
697 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
698
699 let mut response = self.container().col(|ui| {
700 ui.line(|ui| {
701 ui.text(format!(" {icon} ")).fg(color).bold();
702 ui.text(message).grow(1);
703 ui.text(" [×] ").dim();
704 });
705 });
706 response.focused = focused;
707 if key_dismiss {
708 response.clicked = true;
709 }
710
711 response
712 }
713
714 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
728 let focused = self.register_focusable();
729 let mut is_yes = *result;
730 let mut clicked = false;
731
732 if focused {
733 let mut consumed_indices = Vec::new();
734 for (i, event) in self.events.iter().enumerate() {
735 if let Event::Key(key) = event {
736 if key.kind != KeyEventKind::Press {
737 continue;
738 }
739
740 match key.code {
741 KeyCode::Char('y') => {
742 is_yes = true;
743 *result = true;
744 clicked = true;
745 consumed_indices.push(i);
746 }
747 KeyCode::Char('n') => {
748 is_yes = false;
749 *result = false;
750 clicked = true;
751 consumed_indices.push(i);
752 }
753 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
754 is_yes = !is_yes;
755 *result = is_yes;
756 consumed_indices.push(i);
757 }
758 KeyCode::Enter => {
759 *result = is_yes;
760 clicked = true;
761 consumed_indices.push(i);
762 }
763 _ => {}
764 }
765 }
766 }
767
768 for idx in consumed_indices {
769 self.consumed[idx] = true;
770 }
771 }
772
773 let yes_style = if is_yes {
774 if focused {
775 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
776 } else {
777 Style::new().fg(self.theme.success).bold()
778 }
779 } else {
780 Style::new().fg(self.theme.text_dim)
781 };
782 let no_style = if !is_yes {
783 if focused {
784 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
785 } else {
786 Style::new().fg(self.theme.error).bold()
787 }
788 } else {
789 Style::new().fg(self.theme.text_dim)
790 };
791
792 let mut response = self.row(|ui| {
793 ui.text(question);
794 ui.text(" ");
795 ui.styled("[Yes]", yes_style);
796 ui.text(" ");
797 ui.styled("[No]", no_style);
798 });
799 response.focused = focused;
800 response.clicked = clicked;
801 response.changed = clicked;
802 response
803 }
804
805 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
806 self.breadcrumb_with(segments, " › ")
807 }
808
809 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
810 let theme = self.theme;
811 let last_idx = segments.len().saturating_sub(1);
812 let mut clicked_idx: Option<usize> = None;
813
814 self.row(|ui| {
815 for (i, segment) in segments.iter().enumerate() {
816 let is_last = i == last_idx;
817 if is_last {
818 ui.text(*segment).bold();
819 } else {
820 let focused = ui.register_focusable();
821 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
822 let resp = ui.interaction();
823 let color = if resp.hovered || focused {
824 theme.accent
825 } else {
826 theme.primary
827 };
828 ui.text(*segment).fg(color).underline();
829 if resp.clicked || pressed {
830 clicked_idx = Some(i);
831 }
832 ui.text(separator).dim();
833 }
834 }
835 });
836
837 clicked_idx
838 }
839
840 pub fn accordion(
841 &mut self,
842 title: &str,
843 open: &mut bool,
844 f: impl FnOnce(&mut Context),
845 ) -> Response {
846 let theme = self.theme;
847 let focused = self.register_focusable();
848 let old_open = *open;
849
850 if focused && self.key_code(KeyCode::Enter) {
851 *open = !*open;
852 }
853
854 let icon = if *open { "▾" } else { "▸" };
855 let title_color = if focused { theme.primary } else { theme.text };
856
857 let mut response = self.container().col(|ui| {
858 ui.line(|ui| {
859 ui.text(icon).fg(title_color);
860 ui.text(format!(" {title}")).bold().fg(title_color);
861 });
862 });
863
864 if response.clicked {
865 *open = !*open;
866 }
867
868 if *open {
869 self.container().pl(2).col(f);
870 }
871
872 response.focused = focused;
873 response.changed = *open != old_open;
874 response
875 }
876
877 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
878 let max_key_width = items
879 .iter()
880 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
881 .max()
882 .unwrap_or(0);
883
884 self.col(|ui| {
885 for (key, value) in items {
886 ui.line(|ui| {
887 let padded = format!("{:>width$}", key, width = max_key_width);
888 ui.text(padded).dim();
889 ui.text(" ");
890 ui.text(*value);
891 });
892 }
893 });
894
895 Response::none()
896 }
897
898 pub fn divider_text(&mut self, label: &str) -> Response {
899 let w = self.width();
900 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
901 let pad = 1u32;
902 let left_len = 4u32;
903 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
904 let left: String = "─".repeat(left_len as usize);
905 let right: String = "─".repeat(right_len as usize);
906 let theme = self.theme;
907 self.line(|ui| {
908 ui.text(&left).fg(theme.border);
909 ui.text(format!(" {} ", label)).fg(theme.text);
910 ui.text(&right).fg(theme.border);
911 });
912
913 Response::none()
914 }
915
916 pub fn badge(&mut self, label: &str) -> Response {
917 let theme = self.theme;
918 self.badge_colored(label, theme.primary)
919 }
920
921 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
922 let fg = Color::contrast_fg(color);
923 self.text(format!(" {} ", label)).fg(fg).bg(color);
924
925 Response::none()
926 }
927
928 pub fn key_hint(&mut self, key: &str) -> Response {
929 let theme = self.theme;
930 self.text(format!(" {} ", key))
931 .reversed()
932 .fg(theme.text_dim);
933
934 Response::none()
935 }
936
937 pub fn stat(&mut self, label: &str, value: &str) -> Response {
938 self.col(|ui| {
939 ui.text(label).dim();
940 ui.text(value).bold();
941 });
942
943 Response::none()
944 }
945
946 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
947 self.col(|ui| {
948 ui.text(label).dim();
949 ui.text(value).bold().fg(color);
950 });
951
952 Response::none()
953 }
954
955 pub fn stat_trend(
956 &mut self,
957 label: &str,
958 value: &str,
959 trend: crate::widgets::Trend,
960 ) -> Response {
961 let theme = self.theme;
962 let (arrow, color) = match trend {
963 crate::widgets::Trend::Up => ("↑", theme.success),
964 crate::widgets::Trend::Down => ("↓", theme.error),
965 };
966 self.col(|ui| {
967 ui.text(label).dim();
968 ui.line(|ui| {
969 ui.text(value).bold();
970 ui.text(format!(" {arrow}")).fg(color);
971 });
972 });
973
974 Response::none()
975 }
976
977 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
978 self.container().center().col(|ui| {
979 ui.text(title).align(Align::Center);
980 ui.text(description).dim().align(Align::Center);
981 });
982
983 Response::none()
984 }
985
986 pub fn empty_state_action(
987 &mut self,
988 title: &str,
989 description: &str,
990 action_label: &str,
991 ) -> Response {
992 let mut clicked = false;
993 self.container().center().col(|ui| {
994 ui.text(title).align(Align::Center);
995 ui.text(description).dim().align(Align::Center);
996 if ui.button(action_label).clicked {
997 clicked = true;
998 }
999 });
1000
1001 Response {
1002 clicked,
1003 changed: clicked,
1004 ..Response::none()
1005 }
1006 }
1007
1008 pub fn code_block(&mut self, code: &str) -> Response {
1009 let theme = self.theme;
1010 self.bordered(Border::Rounded)
1011 .bg(theme.surface)
1012 .pad(1)
1013 .col(|ui| {
1014 for line in code.lines() {
1015 render_highlighted_line(ui, line);
1016 }
1017 });
1018
1019 Response::none()
1020 }
1021
1022 pub fn code_block_numbered(&mut self, code: &str) -> Response {
1023 let lines: Vec<&str> = code.lines().collect();
1024 let gutter_w = format!("{}", lines.len()).len();
1025 let theme = self.theme;
1026 self.bordered(Border::Rounded)
1027 .bg(theme.surface)
1028 .pad(1)
1029 .col(|ui| {
1030 for (i, line) in lines.iter().enumerate() {
1031 ui.line(|ui| {
1032 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1033 .fg(theme.text_dim);
1034 render_highlighted_line(ui, line);
1035 });
1036 }
1037 });
1038
1039 Response::none()
1040 }
1041
1042 pub fn wrap(&mut self) -> &mut Self {
1044 if let Some(idx) = self.last_text_idx {
1045 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1046 *wrap = true;
1047 }
1048 }
1049 self
1050 }
1051
1052 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1053 if let Some(idx) = self.last_text_idx {
1054 match &mut self.commands[idx] {
1055 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1056 _ => {}
1057 }
1058 }
1059 }
1060
1061 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1062 if let Some(idx) = self.last_text_idx {
1063 match &mut self.commands[idx] {
1064 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1065 f(constraints)
1066 }
1067 _ => {}
1068 }
1069 }
1070 }
1071
1072 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1073 if let Some(idx) = self.last_text_idx {
1074 match &mut self.commands[idx] {
1075 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1076 _ => {}
1077 }
1078 }
1079 }
1080
1081 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1099 self.push_container(Direction::Column, 0, f)
1100 }
1101
1102 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1106 self.push_container(Direction::Column, gap, f)
1107 }
1108
1109 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1126 self.push_container(Direction::Row, 0, f)
1127 }
1128
1129 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1133 self.push_container(Direction::Row, gap, f)
1134 }
1135
1136 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1153 let _ = self.push_container(Direction::Row, 0, f);
1154 self
1155 }
1156
1157 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1176 let start = self.commands.len();
1177 f(self);
1178 let mut segments: Vec<(String, Style)> = Vec::new();
1179 for cmd in self.commands.drain(start..) {
1180 if let Command::Text { content, style, .. } = cmd {
1181 segments.push((content, style));
1182 }
1183 }
1184 self.commands.push(Command::RichText {
1185 segments,
1186 wrap: true,
1187 align: Align::Start,
1188 margin: Margin::default(),
1189 constraints: Constraints::default(),
1190 });
1191 self.last_text_idx = None;
1192 self
1193 }
1194
1195 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1204 self.commands.push(Command::BeginOverlay { modal: true });
1205 self.overlay_depth += 1;
1206 self.modal_active = true;
1207 f(self);
1208 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1209 self.commands.push(Command::EndOverlay);
1210 self.last_text_idx = None;
1211 }
1212
1213 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1215 self.commands.push(Command::BeginOverlay { modal: false });
1216 self.overlay_depth += 1;
1217 f(self);
1218 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1219 self.commands.push(Command::EndOverlay);
1220 self.last_text_idx = None;
1221 }
1222
1223 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1231 self.group_count = self.group_count.saturating_add(1);
1232 self.group_stack.push(name.to_string());
1233 self.container().group_name(name.to_string())
1234 }
1235
1236 pub fn container(&mut self) -> ContainerBuilder<'_> {
1257 let border = self.theme.border;
1258 ContainerBuilder {
1259 ctx: self,
1260 gap: 0,
1261 align: Align::Start,
1262 justify: Justify::Start,
1263 border: None,
1264 border_sides: BorderSides::all(),
1265 border_style: Style::new().fg(border),
1266 bg: None,
1267 dark_bg: None,
1268 dark_border_style: None,
1269 group_hover_bg: None,
1270 group_hover_border_style: None,
1271 group_name: None,
1272 padding: Padding::default(),
1273 margin: Margin::default(),
1274 constraints: Constraints::default(),
1275 title: None,
1276 grow: 0,
1277 scroll_offset: None,
1278 }
1279 }
1280
1281 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1300 let index = self.scroll_count;
1301 self.scroll_count += 1;
1302 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1303 state.set_bounds(ch, vh);
1304 let max = ch.saturating_sub(vh) as usize;
1305 state.offset = state.offset.min(max);
1306 }
1307
1308 let next_id = self.interaction_count;
1309 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1310 let inner_rects: Vec<Rect> = self
1311 .prev_scroll_rects
1312 .iter()
1313 .enumerate()
1314 .filter(|&(j, sr)| {
1315 j != index
1316 && sr.width > 0
1317 && sr.height > 0
1318 && sr.x >= rect.x
1319 && sr.right() <= rect.right()
1320 && sr.y >= rect.y
1321 && sr.bottom() <= rect.bottom()
1322 })
1323 .map(|(_, sr)| *sr)
1324 .collect();
1325 self.auto_scroll_nested(&rect, state, &inner_rects);
1326 }
1327
1328 self.container().scroll_offset(state.offset as u32)
1329 }
1330
1331 pub fn scrollbar(&mut self, state: &ScrollState) {
1351 let vh = state.viewport_height();
1352 let ch = state.content_height();
1353 if vh == 0 || ch <= vh {
1354 return;
1355 }
1356
1357 let track_height = vh;
1358 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1359 let max_offset = ch.saturating_sub(vh);
1360 let thumb_pos = if max_offset == 0 {
1361 0
1362 } else {
1363 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1364 .round() as u32
1365 };
1366
1367 let theme = self.theme;
1368 let track_char = '│';
1369 let thumb_char = '█';
1370
1371 self.container().w(1).h(track_height).col(|ui| {
1372 for i in 0..track_height {
1373 if i >= thumb_pos && i < thumb_pos + thumb_height {
1374 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1375 } else {
1376 ui.styled(
1377 track_char.to_string(),
1378 Style::new().fg(theme.text_dim).dim(),
1379 );
1380 }
1381 }
1382 });
1383 }
1384
1385 fn auto_scroll_nested(
1386 &mut self,
1387 rect: &Rect,
1388 state: &mut ScrollState,
1389 inner_scroll_rects: &[Rect],
1390 ) {
1391 let mut to_consume: Vec<usize> = Vec::new();
1392
1393 for (i, event) in self.events.iter().enumerate() {
1394 if self.consumed[i] {
1395 continue;
1396 }
1397 if let Event::Mouse(mouse) = event {
1398 let in_bounds = mouse.x >= rect.x
1399 && mouse.x < rect.right()
1400 && mouse.y >= rect.y
1401 && mouse.y < rect.bottom();
1402 if !in_bounds {
1403 continue;
1404 }
1405 let in_inner = inner_scroll_rects.iter().any(|sr| {
1406 mouse.x >= sr.x
1407 && mouse.x < sr.right()
1408 && mouse.y >= sr.y
1409 && mouse.y < sr.bottom()
1410 });
1411 if in_inner {
1412 continue;
1413 }
1414 match mouse.kind {
1415 MouseKind::ScrollUp => {
1416 state.scroll_up(1);
1417 to_consume.push(i);
1418 }
1419 MouseKind::ScrollDown => {
1420 state.scroll_down(1);
1421 to_consume.push(i);
1422 }
1423 MouseKind::Drag(MouseButton::Left) => {}
1424 _ => {}
1425 }
1426 }
1427 }
1428
1429 for i in to_consume {
1430 self.consumed[i] = true;
1431 }
1432 }
1433
1434 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1438 self.container()
1439 .border(border)
1440 .border_sides(BorderSides::all())
1441 }
1442
1443 fn push_container(
1444 &mut self,
1445 direction: Direction,
1446 gap: u32,
1447 f: impl FnOnce(&mut Context),
1448 ) -> Response {
1449 let interaction_id = self.interaction_count;
1450 self.interaction_count += 1;
1451 let border = self.theme.border;
1452
1453 self.commands.push(Command::BeginContainer {
1454 direction,
1455 gap,
1456 align: Align::Start,
1457 justify: Justify::Start,
1458 border: None,
1459 border_sides: BorderSides::all(),
1460 border_style: Style::new().fg(border),
1461 bg_color: None,
1462 padding: Padding::default(),
1463 margin: Margin::default(),
1464 constraints: Constraints::default(),
1465 title: None,
1466 grow: 0,
1467 group_name: None,
1468 });
1469 f(self);
1470 self.commands.push(Command::EndContainer);
1471 self.last_text_idx = None;
1472
1473 self.response_for(interaction_id)
1474 }
1475
1476 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1477 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1478 return Response::none();
1479 }
1480 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1481 let clicked = self
1482 .click_pos
1483 .map(|(mx, my)| {
1484 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1485 })
1486 .unwrap_or(false);
1487 let hovered = self
1488 .mouse_pos
1489 .map(|(mx, my)| {
1490 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1491 })
1492 .unwrap_or(false);
1493 Response {
1494 clicked,
1495 hovered,
1496 changed: false,
1497 focused: false,
1498 rect: *rect,
1499 }
1500 } else {
1501 Response::none()
1502 }
1503 }
1504
1505 pub fn is_group_hovered(&self, name: &str) -> bool {
1507 if let Some(pos) = self.mouse_pos {
1508 self.prev_group_rects.iter().any(|(n, rect)| {
1509 n == name
1510 && pos.0 >= rect.x
1511 && pos.0 < rect.x + rect.width
1512 && pos.1 >= rect.y
1513 && pos.1 < rect.y + rect.height
1514 })
1515 } else {
1516 false
1517 }
1518 }
1519
1520 pub fn is_group_focused(&self, name: &str) -> bool {
1522 if self.prev_focus_count == 0 {
1523 return false;
1524 }
1525 let focused_index = self.focus_index % self.prev_focus_count;
1526 self.prev_focus_groups
1527 .get(focused_index)
1528 .and_then(|group| group.as_deref())
1529 .map(|group| group == name)
1530 .unwrap_or(false)
1531 }
1532
1533 pub fn grow(&mut self, value: u16) -> &mut Self {
1538 if let Some(idx) = self.last_text_idx {
1539 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1540 *grow = value;
1541 }
1542 }
1543 self
1544 }
1545
1546 pub fn align(&mut self, align: Align) -> &mut Self {
1548 if let Some(idx) = self.last_text_idx {
1549 if let Command::Text {
1550 align: text_align, ..
1551 } = &mut self.commands[idx]
1552 {
1553 *text_align = align;
1554 }
1555 }
1556 self
1557 }
1558
1559 pub fn w(&mut self, value: u32) -> &mut Self {
1566 self.modify_last_constraints(|c| {
1567 c.min_width = Some(value);
1568 c.max_width = Some(value);
1569 });
1570 self
1571 }
1572
1573 pub fn h(&mut self, value: u32) -> &mut Self {
1577 self.modify_last_constraints(|c| {
1578 c.min_height = Some(value);
1579 c.max_height = Some(value);
1580 });
1581 self
1582 }
1583
1584 pub fn min_w(&mut self, value: u32) -> &mut Self {
1586 self.modify_last_constraints(|c| c.min_width = Some(value));
1587 self
1588 }
1589
1590 pub fn max_w(&mut self, value: u32) -> &mut Self {
1592 self.modify_last_constraints(|c| c.max_width = Some(value));
1593 self
1594 }
1595
1596 pub fn min_h(&mut self, value: u32) -> &mut Self {
1598 self.modify_last_constraints(|c| c.min_height = Some(value));
1599 self
1600 }
1601
1602 pub fn max_h(&mut self, value: u32) -> &mut Self {
1604 self.modify_last_constraints(|c| c.max_height = Some(value));
1605 self
1606 }
1607
1608 pub fn m(&mut self, value: u32) -> &mut Self {
1612 self.modify_last_margin(|m| *m = Margin::all(value));
1613 self
1614 }
1615
1616 pub fn mx(&mut self, value: u32) -> &mut Self {
1618 self.modify_last_margin(|m| {
1619 m.left = value;
1620 m.right = value;
1621 });
1622 self
1623 }
1624
1625 pub fn my(&mut self, value: u32) -> &mut Self {
1627 self.modify_last_margin(|m| {
1628 m.top = value;
1629 m.bottom = value;
1630 });
1631 self
1632 }
1633
1634 pub fn mt(&mut self, value: u32) -> &mut Self {
1636 self.modify_last_margin(|m| m.top = value);
1637 self
1638 }
1639
1640 pub fn mr(&mut self, value: u32) -> &mut Self {
1642 self.modify_last_margin(|m| m.right = value);
1643 self
1644 }
1645
1646 pub fn mb(&mut self, value: u32) -> &mut Self {
1648 self.modify_last_margin(|m| m.bottom = value);
1649 self
1650 }
1651
1652 pub fn ml(&mut self, value: u32) -> &mut Self {
1654 self.modify_last_margin(|m| m.left = value);
1655 self
1656 }
1657
1658 pub fn spacer(&mut self) -> &mut Self {
1662 self.commands.push(Command::Spacer { grow: 1 });
1663 self.last_text_idx = None;
1664 self
1665 }
1666
1667 pub fn form(
1671 &mut self,
1672 state: &mut FormState,
1673 f: impl FnOnce(&mut Context, &mut FormState),
1674 ) -> &mut Self {
1675 self.col(|ui| {
1676 f(ui, state);
1677 });
1678 self
1679 }
1680
1681 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1685 self.col(|ui| {
1686 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1687 ui.text_input(&mut field.input);
1688 if let Some(error) = field.error.as_deref() {
1689 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1690 }
1691 });
1692 self
1693 }
1694
1695 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1699 self.button(label)
1700 }
1701}
1702
1703const KEYWORDS: &[&str] = &[
1704 "fn",
1705 "let",
1706 "mut",
1707 "pub",
1708 "use",
1709 "impl",
1710 "struct",
1711 "enum",
1712 "trait",
1713 "type",
1714 "const",
1715 "static",
1716 "if",
1717 "else",
1718 "match",
1719 "for",
1720 "while",
1721 "loop",
1722 "return",
1723 "break",
1724 "continue",
1725 "where",
1726 "self",
1727 "super",
1728 "crate",
1729 "mod",
1730 "async",
1731 "await",
1732 "move",
1733 "ref",
1734 "in",
1735 "as",
1736 "true",
1737 "false",
1738 "Some",
1739 "None",
1740 "Ok",
1741 "Err",
1742 "Self",
1743 "def",
1744 "class",
1745 "import",
1746 "from",
1747 "pass",
1748 "lambda",
1749 "yield",
1750 "with",
1751 "try",
1752 "except",
1753 "raise",
1754 "finally",
1755 "elif",
1756 "del",
1757 "global",
1758 "nonlocal",
1759 "assert",
1760 "is",
1761 "not",
1762 "and",
1763 "or",
1764 "function",
1765 "var",
1766 "const",
1767 "export",
1768 "default",
1769 "switch",
1770 "case",
1771 "throw",
1772 "catch",
1773 "typeof",
1774 "instanceof",
1775 "new",
1776 "delete",
1777 "void",
1778 "this",
1779 "null",
1780 "undefined",
1781 "func",
1782 "package",
1783 "defer",
1784 "go",
1785 "chan",
1786 "select",
1787 "range",
1788 "map",
1789 "interface",
1790 "fallthrough",
1791 "nil",
1792];
1793
1794fn render_highlighted_line(ui: &mut Context, line: &str) {
1795 let theme = ui.theme;
1796 let is_light = matches!(
1797 theme.bg,
1798 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1799 );
1800 let keyword_color = if is_light {
1801 Color::Rgb(166, 38, 164)
1802 } else {
1803 Color::Rgb(198, 120, 221)
1804 };
1805 let string_color = if is_light {
1806 Color::Rgb(80, 161, 79)
1807 } else {
1808 Color::Rgb(152, 195, 121)
1809 };
1810 let comment_color = theme.text_dim;
1811 let number_color = if is_light {
1812 Color::Rgb(152, 104, 1)
1813 } else {
1814 Color::Rgb(209, 154, 102)
1815 };
1816 let fn_color = if is_light {
1817 Color::Rgb(64, 120, 242)
1818 } else {
1819 Color::Rgb(97, 175, 239)
1820 };
1821 let macro_color = if is_light {
1822 Color::Rgb(1, 132, 188)
1823 } else {
1824 Color::Rgb(86, 182, 194)
1825 };
1826
1827 let trimmed = line.trim_start();
1828 let indent = &line[..line.len() - trimmed.len()];
1829 if !indent.is_empty() {
1830 ui.text(indent);
1831 }
1832
1833 if trimmed.starts_with("//") {
1834 ui.text(trimmed).fg(comment_color).italic();
1835 return;
1836 }
1837
1838 let mut pos = 0;
1839
1840 while pos < trimmed.len() {
1841 let ch = trimmed.as_bytes()[pos];
1842
1843 if ch == b'"' {
1844 if let Some(end) = trimmed[pos + 1..].find('"') {
1845 let s = &trimmed[pos..pos + end + 2];
1846 ui.text(s).fg(string_color);
1847 pos += end + 2;
1848 continue;
1849 }
1850 }
1851
1852 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1853 {
1854 let end = trimmed[pos..]
1855 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1856 .map_or(trimmed.len(), |e| pos + e);
1857 ui.text(&trimmed[pos..end]).fg(number_color);
1858 pos = end;
1859 continue;
1860 }
1861
1862 if ch.is_ascii_alphabetic() || ch == b'_' {
1863 let end = trimmed[pos..]
1864 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1865 .map_or(trimmed.len(), |e| pos + e);
1866 let word = &trimmed[pos..end];
1867
1868 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1869 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1870 pos = end + 1;
1871 } else if end < trimmed.len()
1872 && trimmed.as_bytes()[end] == b'('
1873 && !KEYWORDS.contains(&word)
1874 {
1875 ui.text(word).fg(fn_color);
1876 pos = end;
1877 } else if KEYWORDS.contains(&word) {
1878 ui.text(word).fg(keyword_color);
1879 pos = end;
1880 } else {
1881 ui.text(word);
1882 pos = end;
1883 }
1884 continue;
1885 }
1886
1887 let end = trimmed[pos..]
1888 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1889 .map_or(trimmed.len(), |e| pos + e);
1890 ui.text(&trimmed[pos..end]);
1891 pos = end;
1892 }
1893}
1894
1895fn resize_cover_crop(rgba: &[u8], src_w: u32, src_h: u32, dst_w: u32, dst_h: u32) -> Vec<u8> {
1896 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
1897 return vec![0u8; (dst_w * dst_h * 4) as usize];
1898 }
1899
1900 let scale_x = dst_w as f64 / src_w as f64;
1901 let scale_y = dst_h as f64 / src_h as f64;
1902 let scale = scale_x.max(scale_y);
1903
1904 let scaled_w = (src_w as f64 * scale).round() as u32;
1905 let scaled_h = (src_h as f64 * scale).round() as u32;
1906
1907 let mut scaled = Vec::with_capacity((scaled_w * scaled_h * 4) as usize);
1908 for y in 0..scaled_h {
1909 for x in 0..scaled_w {
1910 let sx = ((x as f64 / scale).floor() as u32).min(src_w - 1);
1911 let sy = ((y as f64 / scale).floor() as u32).min(src_h - 1);
1912 let idx = ((sy * src_w + sx) * 4) as usize;
1913 if idx + 3 < rgba.len() {
1914 scaled.extend_from_slice(&rgba[idx..idx + 4]);
1915 } else {
1916 scaled.extend_from_slice(&[0, 0, 0, 255]);
1917 }
1918 }
1919 }
1920
1921 let crop_x = scaled_w.saturating_sub(dst_w) / 2;
1922 let crop_y = scaled_h.saturating_sub(dst_h) / 2;
1923
1924 let mut result = Vec::with_capacity((dst_w * dst_h * 4) as usize);
1925 for y in 0..dst_h {
1926 let src_y = crop_y + y;
1927 if src_y >= scaled_h {
1928 break;
1929 }
1930 for x in 0..dst_w {
1931 let src_x = crop_x + x;
1932 if src_x >= scaled_w {
1933 result.extend_from_slice(&[0, 0, 0, 255]);
1934 continue;
1935 }
1936 let idx = ((src_y * scaled_w + src_x) * 4) as usize;
1937 if idx + 3 < scaled.len() {
1938 result.extend_from_slice(&scaled[idx..idx + 4]);
1939 } else {
1940 result.extend_from_slice(&[0, 0, 0, 255]);
1941 }
1942 }
1943 }
1944
1945 result
1946}
1947
1948fn base64_encode(data: &[u8]) -> String {
1949 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1950 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
1951 for chunk in data.chunks(3) {
1952 let b0 = chunk[0] as u32;
1953 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
1954 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
1955 let triple = (b0 << 16) | (b1 << 8) | b2;
1956 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
1957 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
1958 if chunk.len() > 1 {
1959 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
1960 } else {
1961 result.push('=');
1962 }
1963 if chunk.len() > 2 {
1964 result.push(CHARS[(triple & 0x3F) as usize] as char);
1965 } else {
1966 result.push('=');
1967 }
1968 }
1969 result
1970}
1971
1972fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
1973 let mut chunks = Vec::new();
1974 let bytes = encoded.as_bytes();
1975 let mut offset = 0;
1976 while offset < bytes.len() {
1977 let end = (offset + chunk_size).min(bytes.len());
1978 chunks.push(&encoded[offset..end]);
1979 offset = end;
1980 }
1981 if chunks.is_empty() {
1982 chunks.push("");
1983 }
1984 chunks
1985}