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 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
45 let url_str = url.into();
46 let focused = self.register_focusable();
47 let interaction_id = self.interaction_count;
48 self.interaction_count += 1;
49 let response = self.response_for(interaction_id);
50
51 let mut activated = response.clicked;
52 if focused {
53 for (i, event) in self.events.iter().enumerate() {
54 if let Event::Key(key) = event {
55 if key.kind != KeyEventKind::Press {
56 continue;
57 }
58 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
59 activated = true;
60 self.consumed[i] = true;
61 }
62 }
63 }
64 }
65
66 if activated {
67 let _ = open_url(&url_str);
68 }
69
70 let style = if focused {
71 Style::new()
72 .fg(self.theme.primary)
73 .bg(self.theme.surface_hover)
74 .underline()
75 .bold()
76 } else if response.hovered {
77 Style::new()
78 .fg(self.theme.accent)
79 .bg(self.theme.surface_hover)
80 .underline()
81 } else {
82 Style::new().fg(self.theme.primary).underline()
83 };
84
85 self.commands.push(Command::Link {
86 text: text.into(),
87 url: url_str,
88 style,
89 margin: Margin::default(),
90 constraints: Constraints::default(),
91 });
92 self.last_text_idx = Some(self.commands.len() - 1);
93 self
94 }
95
96 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
101 let content = s.into();
102 let default_fg = self
103 .text_color_stack
104 .iter()
105 .rev()
106 .find_map(|c| *c)
107 .unwrap_or(self.theme.text);
108 self.commands.push(Command::Text {
109 content,
110 style: Style::new().fg(default_fg),
111 grow: 0,
112 align: Align::Start,
113 wrap: true,
114 truncate: false,
115 margin: Margin::default(),
116 constraints: Constraints::default(),
117 });
118 self.last_text_idx = Some(self.commands.len() - 1);
119 self
120 }
121
122 pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
124 let pairs: Vec<(&str, &str)> = keymap
125 .visible_bindings()
126 .map(|binding| (binding.display.as_str(), binding.description.as_str()))
127 .collect();
128 self.help(&pairs)
129 }
130
131 pub fn bold(&mut self) -> &mut Self {
135 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
136 self
137 }
138
139 pub fn dim(&mut self) -> &mut Self {
144 let text_dim = self.theme.text_dim;
145 self.modify_last_style(|s| {
146 s.modifiers |= Modifiers::DIM;
147 if s.fg.is_none() {
148 s.fg = Some(text_dim);
149 }
150 });
151 self
152 }
153
154 pub fn italic(&mut self) -> &mut Self {
156 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
157 self
158 }
159
160 pub fn underline(&mut self) -> &mut Self {
162 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
163 self
164 }
165
166 pub fn reversed(&mut self) -> &mut Self {
168 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
169 self
170 }
171
172 pub fn strikethrough(&mut self) -> &mut Self {
174 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
175 self
176 }
177
178 pub fn fg(&mut self, color: Color) -> &mut Self {
180 self.modify_last_style(|s| s.fg = Some(color));
181 self
182 }
183
184 pub fn bg(&mut self, color: Color) -> &mut Self {
186 self.modify_last_style(|s| s.bg = Some(color));
187 self
188 }
189
190 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
191 let apply_group_style = self
192 .group_stack
193 .last()
194 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
195 .unwrap_or(false);
196 if apply_group_style {
197 self.modify_last_style(|s| s.fg = Some(color));
198 }
199 self
200 }
201
202 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
203 let apply_group_style = self
204 .group_stack
205 .last()
206 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
207 .unwrap_or(false);
208 if apply_group_style {
209 self.modify_last_style(|s| s.bg = Some(color));
210 }
211 self
212 }
213
214 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
219 self.commands.push(Command::Text {
220 content: s.into(),
221 style,
222 grow: 0,
223 align: Align::Start,
224 wrap: false,
225 truncate: false,
226 margin: Margin::default(),
227 constraints: Constraints::default(),
228 });
229 self.last_text_idx = Some(self.commands.len() - 1);
230 self
231 }
232
233 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
255 let width = img.width;
256 let height = img.height;
257
258 self.container().w(width).h(height).gap(0).col(|ui| {
259 for row in 0..height {
260 ui.container().gap(0).row(|ui| {
261 for col in 0..width {
262 let idx = (row * width + col) as usize;
263 if let Some(&(upper, lower)) = img.pixels.get(idx) {
264 ui.styled("▀", Style::new().fg(upper).bg(lower));
265 }
266 }
267 });
268 }
269 });
270
271 Response::none()
272 }
273
274 pub fn kitty_image(
290 &mut self,
291 rgba: &[u8],
292 pixel_width: u32,
293 pixel_height: u32,
294 cols: u32,
295 rows: u32,
296 ) -> Response {
297 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
298 let encoded = base64_encode(&rgba);
299 let pw = pixel_width;
300 let ph = pixel_height;
301 let c = cols;
302 let r = rows;
303
304 self.container().w(cols).h(rows).draw(move |buf, rect| {
305 let chunks = split_base64(&encoded, 4096);
306 let mut all_sequences = String::new();
307
308 for (i, chunk) in chunks.iter().enumerate() {
309 let more = if i < chunks.len() - 1 { 1 } else { 0 };
310 if i == 0 {
311 all_sequences.push_str(&format!(
312 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
313 pw, ph, c, r, more, chunk
314 ));
315 } else {
316 all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
317 }
318 }
319
320 buf.raw_sequence(rect.x, rect.y, all_sequences);
321 });
322 Response::none()
323 }
324
325 pub fn kitty_image_fit(
334 &mut self,
335 rgba: &[u8],
336 src_width: u32,
337 src_height: u32,
338 cols: u32,
339 ) -> Response {
340 let rows = if src_width == 0 {
341 1
342 } else {
343 ((cols as f64 * src_height as f64 * 8.0) / (src_width as f64 * 16.0))
344 .ceil()
345 .max(1.0) as u32
346 };
347 let rgba = normalize_rgba(rgba, src_width, src_height);
348 let sw = src_width;
349 let sh = src_height;
350 let c = cols;
351 let r = rows;
352
353 self.container().w(cols).h(rows).draw(move |buf, rect| {
354 if rect.width == 0 || rect.height == 0 {
355 return;
356 }
357 let encoded = base64_encode(&rgba);
358 let chunks = split_base64(&encoded, 4096);
359 let mut seq = String::new();
360 for (i, chunk) in chunks.iter().enumerate() {
361 let more = if i < chunks.len() - 1 { 1 } else { 0 };
362 if i == 0 {
363 seq.push_str(&format!(
364 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
365 sw, sh, c, r, more, chunk
366 ));
367 } else {
368 seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
369 }
370 }
371 buf.raw_sequence(rect.x, rect.y, seq);
372 });
373 Response::none()
374 }
375
376 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
392 if state.streaming {
393 state.cursor_tick = state.cursor_tick.wrapping_add(1);
394 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
395 }
396
397 if state.content.is_empty() && state.streaming {
398 let cursor = if state.cursor_visible { "▌" } else { " " };
399 let primary = self.theme.primary;
400 self.text(cursor).fg(primary);
401 return Response::none();
402 }
403
404 if !state.content.is_empty() {
405 if state.streaming && state.cursor_visible {
406 self.text_wrap(format!("{}▌", state.content));
407 } else {
408 self.text_wrap(&state.content);
409 }
410 }
411
412 Response::none()
413 }
414
415 pub fn streaming_markdown(
433 &mut self,
434 state: &mut crate::widgets::StreamingMarkdownState,
435 ) -> Response {
436 if state.streaming {
437 state.cursor_tick = state.cursor_tick.wrapping_add(1);
438 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
439 }
440
441 if state.content.is_empty() && state.streaming {
442 let cursor = if state.cursor_visible { "▌" } else { " " };
443 let primary = self.theme.primary;
444 self.text(cursor).fg(primary);
445 return Response::none();
446 }
447
448 let show_cursor = state.streaming && state.cursor_visible;
449 let trailing_newline = state.content.ends_with('\n');
450 let lines: Vec<&str> = state.content.lines().collect();
451 let last_line_index = lines.len().saturating_sub(1);
452
453 self.commands.push(Command::BeginContainer {
454 direction: Direction::Column,
455 gap: 0,
456 align: Align::Start,
457 align_self: None,
458 justify: Justify::Start,
459 border: None,
460 border_sides: BorderSides::all(),
461 border_style: Style::new().fg(self.theme.border),
462 bg_color: None,
463 padding: Padding::default(),
464 margin: Margin::default(),
465 constraints: Constraints::default(),
466 title: None,
467 grow: 0,
468 group_name: None,
469 });
470 self.interaction_count += 1;
471
472 let text_style = Style::new().fg(self.theme.text);
473 let bold_style = Style::new().fg(self.theme.text).bold();
474 let code_style = Style::new().fg(self.theme.accent);
475 let border_style = Style::new().fg(self.theme.border).dim();
476
477 let mut in_code_block = false;
478 let mut code_block_lang = String::new();
479
480 for (idx, line) in lines.iter().enumerate() {
481 let line = *line;
482 let trimmed = line.trim();
483 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
484 let cursor = if append_cursor { "▌" } else { "" };
485
486 if in_code_block {
487 if trimmed.starts_with("```") {
488 in_code_block = false;
489 code_block_lang.clear();
490 self.styled(format!(" └────{cursor}"), border_style);
491 } else {
492 self.styled(format!(" {line}{cursor}"), code_style);
493 }
494 continue;
495 }
496
497 if trimmed.is_empty() {
498 if append_cursor {
499 self.styled("▌", Style::new().fg(self.theme.primary));
500 } else {
501 self.text(" ");
502 }
503 continue;
504 }
505
506 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
507 self.styled(format!("{}{}", "─".repeat(40), cursor), border_style);
508 continue;
509 }
510
511 if let Some(heading) = trimmed.strip_prefix("### ") {
512 self.styled(
513 format!("{heading}{cursor}"),
514 Style::new().bold().fg(self.theme.accent),
515 );
516 continue;
517 }
518
519 if let Some(heading) = trimmed.strip_prefix("## ") {
520 self.styled(
521 format!("{heading}{cursor}"),
522 Style::new().bold().fg(self.theme.secondary),
523 );
524 continue;
525 }
526
527 if let Some(heading) = trimmed.strip_prefix("# ") {
528 self.styled(
529 format!("{heading}{cursor}"),
530 Style::new().bold().fg(self.theme.primary),
531 );
532 continue;
533 }
534
535 if let Some(code) = trimmed.strip_prefix("```") {
536 in_code_block = true;
537 code_block_lang = code.trim().to_string();
538 let label = if code_block_lang.is_empty() {
539 "code".to_string()
540 } else {
541 format!("code:{}", code_block_lang)
542 };
543 self.styled(format!(" ┌─{label}─{cursor}"), border_style);
544 continue;
545 }
546
547 if let Some(item) = trimmed
548 .strip_prefix("- ")
549 .or_else(|| trimmed.strip_prefix("* "))
550 {
551 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
552 if segs.len() <= 1 {
553 self.styled(format!(" • {item}{cursor}"), text_style);
554 } else {
555 self.line(|ui| {
556 ui.styled(" • ", text_style);
557 for (s, st) in segs {
558 ui.styled(s, st);
559 }
560 if append_cursor {
561 ui.styled("▌", Style::new().fg(ui.theme.primary));
562 }
563 });
564 }
565 continue;
566 }
567
568 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
569 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
570 if parts.len() == 2 {
571 let segs =
572 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
573 if segs.len() <= 1 {
574 self.styled(
575 format!(" {}. {}{}", parts[0], parts[1], cursor),
576 text_style,
577 );
578 } else {
579 self.line(|ui| {
580 ui.styled(format!(" {}. ", parts[0]), text_style);
581 for (s, st) in segs {
582 ui.styled(s, st);
583 }
584 if append_cursor {
585 ui.styled("▌", Style::new().fg(ui.theme.primary));
586 }
587 });
588 }
589 } else {
590 self.styled(format!("{trimmed}{cursor}"), text_style);
591 }
592 continue;
593 }
594
595 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
596 if segs.len() <= 1 {
597 self.styled(format!("{trimmed}{cursor}"), text_style);
598 } else {
599 self.line(|ui| {
600 for (s, st) in segs {
601 ui.styled(s, st);
602 }
603 if append_cursor {
604 ui.styled("▌", Style::new().fg(ui.theme.primary));
605 }
606 });
607 }
608 }
609
610 if show_cursor && trailing_newline {
611 if in_code_block {
612 self.styled(" ▌", code_style);
613 } else {
614 self.styled("▌", Style::new().fg(self.theme.primary));
615 }
616 }
617
618 state.in_code_block = in_code_block;
619 state.code_block_lang = code_block_lang;
620
621 self.commands.push(Command::EndContainer);
622 self.last_text_idx = None;
623 Response::none()
624 }
625
626 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
641 let old_action = state.action;
642 let theme = self.theme;
643 self.bordered(Border::Rounded).col(|ui| {
644 ui.row(|ui| {
645 ui.text("⚡").fg(theme.warning);
646 ui.text(&state.tool_name).bold().fg(theme.primary);
647 });
648 ui.text(&state.description).dim();
649
650 if state.action == ApprovalAction::Pending {
651 ui.row(|ui| {
652 if ui.button("✓ Approve").clicked {
653 state.action = ApprovalAction::Approved;
654 }
655 if ui.button("✗ Reject").clicked {
656 state.action = ApprovalAction::Rejected;
657 }
658 });
659 } else {
660 let (label, color) = match state.action {
661 ApprovalAction::Approved => ("✓ Approved", theme.success),
662 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
663 ApprovalAction::Pending => unreachable!(),
664 };
665 ui.text(label).fg(color).bold();
666 }
667 });
668
669 Response {
670 changed: state.action != old_action,
671 ..Response::none()
672 }
673 }
674
675 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
688 if items.is_empty() {
689 return Response::none();
690 }
691
692 let theme = self.theme;
693 let total: usize = items.iter().map(|item| item.tokens).sum();
694
695 self.container().row(|ui| {
696 ui.text("📎").dim();
697 for item in items {
698 ui.text(format!(
699 "{} ({})",
700 item.label,
701 format_token_count(item.tokens)
702 ))
703 .fg(theme.secondary);
704 }
705 ui.spacer();
706 ui.text(format!("Σ {}", format_token_count(total))).dim();
707 });
708
709 Response::none()
710 }
711
712 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
713 use crate::widgets::AlertLevel;
714
715 let theme = self.theme;
716 let (icon, color) = match level {
717 AlertLevel::Info => ("ℹ", theme.accent),
718 AlertLevel::Success => ("✓", theme.success),
719 AlertLevel::Warning => ("⚠", theme.warning),
720 AlertLevel::Error => ("✕", theme.error),
721 };
722
723 let focused = self.register_focusable();
724 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
725
726 let mut response = self.container().col(|ui| {
727 ui.line(|ui| {
728 ui.text(format!(" {icon} ")).fg(color).bold();
729 ui.text(message).grow(1);
730 ui.text(" [×] ").dim();
731 });
732 });
733 response.focused = focused;
734 if key_dismiss {
735 response.clicked = true;
736 }
737
738 response
739 }
740
741 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
755 let focused = self.register_focusable();
756 let mut is_yes = *result;
757 let mut clicked = false;
758
759 if focused {
760 let mut consumed_indices = Vec::new();
761 for (i, event) in self.events.iter().enumerate() {
762 if let Event::Key(key) = event {
763 if key.kind != KeyEventKind::Press {
764 continue;
765 }
766
767 match key.code {
768 KeyCode::Char('y') => {
769 is_yes = true;
770 *result = true;
771 clicked = true;
772 consumed_indices.push(i);
773 }
774 KeyCode::Char('n') => {
775 is_yes = false;
776 *result = false;
777 clicked = true;
778 consumed_indices.push(i);
779 }
780 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
781 is_yes = !is_yes;
782 *result = is_yes;
783 consumed_indices.push(i);
784 }
785 KeyCode::Enter => {
786 *result = is_yes;
787 clicked = true;
788 consumed_indices.push(i);
789 }
790 _ => {}
791 }
792 }
793 }
794
795 for idx in consumed_indices {
796 self.consumed[idx] = true;
797 }
798 }
799
800 let yes_style = if is_yes {
801 if focused {
802 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
803 } else {
804 Style::new().fg(self.theme.success).bold()
805 }
806 } else {
807 Style::new().fg(self.theme.text_dim)
808 };
809 let no_style = if !is_yes {
810 if focused {
811 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
812 } else {
813 Style::new().fg(self.theme.error).bold()
814 }
815 } else {
816 Style::new().fg(self.theme.text_dim)
817 };
818
819 let mut response = self.row(|ui| {
820 ui.text(question);
821 ui.text(" ");
822 ui.styled("[Yes]", yes_style);
823 ui.text(" ");
824 ui.styled("[No]", no_style);
825 });
826 response.focused = focused;
827 response.clicked = clicked;
828 response.changed = clicked;
829 response
830 }
831
832 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
833 self.breadcrumb_with(segments, " › ")
834 }
835
836 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
837 let theme = self.theme;
838 let last_idx = segments.len().saturating_sub(1);
839 let mut clicked_idx: Option<usize> = None;
840
841 self.row(|ui| {
842 for (i, segment) in segments.iter().enumerate() {
843 let is_last = i == last_idx;
844 if is_last {
845 ui.text(*segment).bold();
846 } else {
847 let focused = ui.register_focusable();
848 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
849 let resp = ui.interaction();
850 let color = if resp.hovered || focused {
851 theme.accent
852 } else {
853 theme.primary
854 };
855 ui.text(*segment).fg(color).underline();
856 if resp.clicked || pressed {
857 clicked_idx = Some(i);
858 }
859 ui.text(separator).dim();
860 }
861 }
862 });
863
864 clicked_idx
865 }
866
867 pub fn accordion(
868 &mut self,
869 title: &str,
870 open: &mut bool,
871 f: impl FnOnce(&mut Context),
872 ) -> Response {
873 let theme = self.theme;
874 let focused = self.register_focusable();
875 let old_open = *open;
876
877 if focused && self.key_code(KeyCode::Enter) {
878 *open = !*open;
879 }
880
881 let icon = if *open { "▾" } else { "▸" };
882 let title_color = if focused { theme.primary } else { theme.text };
883
884 let mut response = self.container().col(|ui| {
885 ui.line(|ui| {
886 ui.text(icon).fg(title_color);
887 ui.text(format!(" {title}")).bold().fg(title_color);
888 });
889 });
890
891 if response.clicked {
892 *open = !*open;
893 }
894
895 if *open {
896 self.container().pl(2).col(f);
897 }
898
899 response.focused = focused;
900 response.changed = *open != old_open;
901 response
902 }
903
904 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
905 let max_key_width = items
906 .iter()
907 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
908 .max()
909 .unwrap_or(0);
910
911 self.col(|ui| {
912 for (key, value) in items {
913 ui.line(|ui| {
914 let padded = format!("{:>width$}", key, width = max_key_width);
915 ui.text(padded).dim();
916 ui.text(" ");
917 ui.text(*value);
918 });
919 }
920 });
921
922 Response::none()
923 }
924
925 pub fn divider_text(&mut self, label: &str) -> Response {
926 let w = self.width();
927 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
928 let pad = 1u32;
929 let left_len = 4u32;
930 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
931 let left: String = "─".repeat(left_len as usize);
932 let right: String = "─".repeat(right_len as usize);
933 let theme = self.theme;
934 self.line(|ui| {
935 ui.text(&left).fg(theme.border);
936 ui.text(format!(" {} ", label)).fg(theme.text);
937 ui.text(&right).fg(theme.border);
938 });
939
940 Response::none()
941 }
942
943 pub fn badge(&mut self, label: &str) -> Response {
944 let theme = self.theme;
945 self.badge_colored(label, theme.primary)
946 }
947
948 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
949 let fg = Color::contrast_fg(color);
950 self.text(format!(" {} ", label)).fg(fg).bg(color);
951
952 Response::none()
953 }
954
955 pub fn key_hint(&mut self, key: &str) -> Response {
956 let theme = self.theme;
957 self.text(format!(" {} ", key))
958 .reversed()
959 .fg(theme.text_dim);
960
961 Response::none()
962 }
963
964 pub fn stat(&mut self, label: &str, value: &str) -> Response {
965 self.col(|ui| {
966 ui.text(label).dim();
967 ui.text(value).bold();
968 });
969
970 Response::none()
971 }
972
973 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
974 self.col(|ui| {
975 ui.text(label).dim();
976 ui.text(value).bold().fg(color);
977 });
978
979 Response::none()
980 }
981
982 pub fn stat_trend(
983 &mut self,
984 label: &str,
985 value: &str,
986 trend: crate::widgets::Trend,
987 ) -> Response {
988 let theme = self.theme;
989 let (arrow, color) = match trend {
990 crate::widgets::Trend::Up => ("↑", theme.success),
991 crate::widgets::Trend::Down => ("↓", theme.error),
992 };
993 self.col(|ui| {
994 ui.text(label).dim();
995 ui.line(|ui| {
996 ui.text(value).bold();
997 ui.text(format!(" {arrow}")).fg(color);
998 });
999 });
1000
1001 Response::none()
1002 }
1003
1004 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
1005 self.container().center().col(|ui| {
1006 ui.text(title).align(Align::Center);
1007 ui.text(description).dim().align(Align::Center);
1008 });
1009
1010 Response::none()
1011 }
1012
1013 pub fn empty_state_action(
1014 &mut self,
1015 title: &str,
1016 description: &str,
1017 action_label: &str,
1018 ) -> Response {
1019 let mut clicked = false;
1020 self.container().center().col(|ui| {
1021 ui.text(title).align(Align::Center);
1022 ui.text(description).dim().align(Align::Center);
1023 if ui.button(action_label).clicked {
1024 clicked = true;
1025 }
1026 });
1027
1028 Response {
1029 clicked,
1030 changed: clicked,
1031 ..Response::none()
1032 }
1033 }
1034
1035 pub fn code_block(&mut self, code: &str) -> Response {
1036 let theme = self.theme;
1037 self.bordered(Border::Rounded)
1038 .bg(theme.surface)
1039 .pad(1)
1040 .col(|ui| {
1041 for line in code.lines() {
1042 render_highlighted_line(ui, line);
1043 }
1044 });
1045
1046 Response::none()
1047 }
1048
1049 pub fn code_block_numbered(&mut self, code: &str) -> Response {
1050 let lines: Vec<&str> = code.lines().collect();
1051 let gutter_w = format!("{}", lines.len()).len();
1052 let theme = self.theme;
1053 self.bordered(Border::Rounded)
1054 .bg(theme.surface)
1055 .pad(1)
1056 .col(|ui| {
1057 for (i, line) in lines.iter().enumerate() {
1058 ui.line(|ui| {
1059 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1060 .fg(theme.text_dim);
1061 render_highlighted_line(ui, line);
1062 });
1063 }
1064 });
1065
1066 Response::none()
1067 }
1068
1069 pub fn wrap(&mut self) -> &mut Self {
1071 if let Some(idx) = self.last_text_idx {
1072 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1073 *wrap = true;
1074 }
1075 }
1076 self
1077 }
1078
1079 pub fn truncate(&mut self) -> &mut Self {
1082 if let Some(idx) = self.last_text_idx {
1083 if let Command::Text { truncate, .. } = &mut self.commands[idx] {
1084 *truncate = true;
1085 }
1086 }
1087 self
1088 }
1089
1090 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1091 if let Some(idx) = self.last_text_idx {
1092 match &mut self.commands[idx] {
1093 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1094 _ => {}
1095 }
1096 }
1097 }
1098
1099 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1100 if let Some(idx) = self.last_text_idx {
1101 match &mut self.commands[idx] {
1102 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1103 f(constraints)
1104 }
1105 _ => {}
1106 }
1107 }
1108 }
1109
1110 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1111 if let Some(idx) = self.last_text_idx {
1112 match &mut self.commands[idx] {
1113 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1114 _ => {}
1115 }
1116 }
1117 }
1118
1119 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1137 self.push_container(Direction::Column, 0, f)
1138 }
1139
1140 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1144 self.push_container(Direction::Column, gap, f)
1145 }
1146
1147 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1164 self.push_container(Direction::Row, 0, f)
1165 }
1166
1167 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1171 self.push_container(Direction::Row, gap, f)
1172 }
1173
1174 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1191 let _ = self.push_container(Direction::Row, 0, f);
1192 self
1193 }
1194
1195 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1214 let start = self.commands.len();
1215 f(self);
1216 let mut segments: Vec<(String, Style)> = Vec::new();
1217 for cmd in self.commands.drain(start..) {
1218 if let Command::Text { content, style, .. } = cmd {
1219 segments.push((content, style));
1220 }
1221 }
1222 self.commands.push(Command::RichText {
1223 segments,
1224 wrap: true,
1225 align: Align::Start,
1226 margin: Margin::default(),
1227 constraints: Constraints::default(),
1228 });
1229 self.last_text_idx = None;
1230 self
1231 }
1232
1233 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1242 self.commands.push(Command::BeginOverlay { modal: true });
1243 self.overlay_depth += 1;
1244 self.modal_active = true;
1245 f(self);
1246 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1247 self.commands.push(Command::EndOverlay);
1248 self.last_text_idx = None;
1249 }
1250
1251 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1253 self.commands.push(Command::BeginOverlay { modal: false });
1254 self.overlay_depth += 1;
1255 f(self);
1256 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1257 self.commands.push(Command::EndOverlay);
1258 self.last_text_idx = None;
1259 }
1260
1261 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1269 self.group_count = self.group_count.saturating_add(1);
1270 self.group_stack.push(name.to_string());
1271 self.container().group_name(name.to_string())
1272 }
1273
1274 pub fn container(&mut self) -> ContainerBuilder<'_> {
1295 let border = self.theme.border;
1296 ContainerBuilder {
1297 ctx: self,
1298 gap: 0,
1299 row_gap: None,
1300 col_gap: None,
1301 align: Align::Start,
1302 align_self_value: None,
1303 justify: Justify::Start,
1304 border: None,
1305 border_sides: BorderSides::all(),
1306 border_style: Style::new().fg(border),
1307 bg: None,
1308 text_color: None,
1309 dark_bg: None,
1310 dark_border_style: None,
1311 group_hover_bg: None,
1312 group_hover_border_style: None,
1313 group_name: None,
1314 padding: Padding::default(),
1315 margin: Margin::default(),
1316 constraints: Constraints::default(),
1317 title: None,
1318 grow: 0,
1319 scroll_offset: None,
1320 }
1321 }
1322
1323 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1342 let index = self.scroll_count;
1343 self.scroll_count += 1;
1344 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1345 state.set_bounds(ch, vh);
1346 let max = ch.saturating_sub(vh) as usize;
1347 state.offset = state.offset.min(max);
1348 }
1349
1350 let next_id = self.interaction_count;
1351 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1352 let inner_rects: Vec<Rect> = self
1353 .prev_scroll_rects
1354 .iter()
1355 .enumerate()
1356 .filter(|&(j, sr)| {
1357 j != index
1358 && sr.width > 0
1359 && sr.height > 0
1360 && sr.x >= rect.x
1361 && sr.right() <= rect.right()
1362 && sr.y >= rect.y
1363 && sr.bottom() <= rect.bottom()
1364 })
1365 .map(|(_, sr)| *sr)
1366 .collect();
1367 self.auto_scroll_nested(&rect, state, &inner_rects);
1368 }
1369
1370 self.container().scroll_offset(state.offset as u32)
1371 }
1372
1373 pub fn scrollbar(&mut self, state: &ScrollState) {
1393 let vh = state.viewport_height();
1394 let ch = state.content_height();
1395 if vh == 0 || ch <= vh {
1396 return;
1397 }
1398
1399 let track_height = vh;
1400 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1401 let max_offset = ch.saturating_sub(vh);
1402 let thumb_pos = if max_offset == 0 {
1403 0
1404 } else {
1405 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1406 .round() as u32
1407 };
1408
1409 let theme = self.theme;
1410 let track_char = '│';
1411 let thumb_char = '█';
1412
1413 self.container().w(1).h(track_height).col(|ui| {
1414 for i in 0..track_height {
1415 if i >= thumb_pos && i < thumb_pos + thumb_height {
1416 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1417 } else {
1418 ui.styled(
1419 track_char.to_string(),
1420 Style::new().fg(theme.text_dim).dim(),
1421 );
1422 }
1423 }
1424 });
1425 }
1426
1427 fn auto_scroll_nested(
1428 &mut self,
1429 rect: &Rect,
1430 state: &mut ScrollState,
1431 inner_scroll_rects: &[Rect],
1432 ) {
1433 let mut to_consume: Vec<usize> = Vec::new();
1434
1435 for (i, event) in self.events.iter().enumerate() {
1436 if self.consumed[i] {
1437 continue;
1438 }
1439 if let Event::Mouse(mouse) = event {
1440 let in_bounds = mouse.x >= rect.x
1441 && mouse.x < rect.right()
1442 && mouse.y >= rect.y
1443 && mouse.y < rect.bottom();
1444 if !in_bounds {
1445 continue;
1446 }
1447 let in_inner = inner_scroll_rects.iter().any(|sr| {
1448 mouse.x >= sr.x
1449 && mouse.x < sr.right()
1450 && mouse.y >= sr.y
1451 && mouse.y < sr.bottom()
1452 });
1453 if in_inner {
1454 continue;
1455 }
1456 match mouse.kind {
1457 MouseKind::ScrollUp => {
1458 state.scroll_up(1);
1459 to_consume.push(i);
1460 }
1461 MouseKind::ScrollDown => {
1462 state.scroll_down(1);
1463 to_consume.push(i);
1464 }
1465 MouseKind::Drag(MouseButton::Left) => {}
1466 _ => {}
1467 }
1468 }
1469 }
1470
1471 for i in to_consume {
1472 self.consumed[i] = true;
1473 }
1474 }
1475
1476 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1480 self.container()
1481 .border(border)
1482 .border_sides(BorderSides::all())
1483 }
1484
1485 fn push_container(
1486 &mut self,
1487 direction: Direction,
1488 gap: u32,
1489 f: impl FnOnce(&mut Context),
1490 ) -> Response {
1491 let interaction_id = self.interaction_count;
1492 self.interaction_count += 1;
1493 let border = self.theme.border;
1494
1495 self.commands.push(Command::BeginContainer {
1496 direction,
1497 gap,
1498 align: Align::Start,
1499 align_self: None,
1500 justify: Justify::Start,
1501 border: None,
1502 border_sides: BorderSides::all(),
1503 border_style: Style::new().fg(border),
1504 bg_color: None,
1505 padding: Padding::default(),
1506 margin: Margin::default(),
1507 constraints: Constraints::default(),
1508 title: None,
1509 grow: 0,
1510 group_name: None,
1511 });
1512 self.text_color_stack.push(None);
1513 f(self);
1514 self.text_color_stack.pop();
1515 self.commands.push(Command::EndContainer);
1516 self.last_text_idx = None;
1517
1518 self.response_for(interaction_id)
1519 }
1520
1521 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1522 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1523 return Response::none();
1524 }
1525 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1526 let clicked = self
1527 .click_pos
1528 .map(|(mx, my)| {
1529 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1530 })
1531 .unwrap_or(false);
1532 let hovered = self
1533 .mouse_pos
1534 .map(|(mx, my)| {
1535 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1536 })
1537 .unwrap_or(false);
1538 Response {
1539 clicked,
1540 hovered,
1541 changed: false,
1542 focused: false,
1543 rect: *rect,
1544 }
1545 } else {
1546 Response::none()
1547 }
1548 }
1549
1550 pub fn is_group_hovered(&self, name: &str) -> bool {
1552 if let Some(pos) = self.mouse_pos {
1553 self.prev_group_rects.iter().any(|(n, rect)| {
1554 n == name
1555 && pos.0 >= rect.x
1556 && pos.0 < rect.x + rect.width
1557 && pos.1 >= rect.y
1558 && pos.1 < rect.y + rect.height
1559 })
1560 } else {
1561 false
1562 }
1563 }
1564
1565 pub fn is_group_focused(&self, name: &str) -> bool {
1567 if self.prev_focus_count == 0 {
1568 return false;
1569 }
1570 let focused_index = self.focus_index % self.prev_focus_count;
1571 self.prev_focus_groups
1572 .get(focused_index)
1573 .and_then(|group| group.as_deref())
1574 .map(|group| group == name)
1575 .unwrap_or(false)
1576 }
1577
1578 pub fn grow(&mut self, value: u16) -> &mut Self {
1583 if let Some(idx) = self.last_text_idx {
1584 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1585 *grow = value;
1586 }
1587 }
1588 self
1589 }
1590
1591 pub fn align(&mut self, align: Align) -> &mut Self {
1593 if let Some(idx) = self.last_text_idx {
1594 if let Command::Text {
1595 align: text_align, ..
1596 } = &mut self.commands[idx]
1597 {
1598 *text_align = align;
1599 }
1600 }
1601 self
1602 }
1603
1604 pub fn text_center(&mut self) -> &mut Self {
1608 self.align(Align::Center)
1609 }
1610
1611 pub fn text_right(&mut self) -> &mut Self {
1614 self.align(Align::End)
1615 }
1616
1617 pub fn w(&mut self, value: u32) -> &mut Self {
1624 self.modify_last_constraints(|c| {
1625 c.min_width = Some(value);
1626 c.max_width = Some(value);
1627 });
1628 self
1629 }
1630
1631 pub fn h(&mut self, value: u32) -> &mut Self {
1635 self.modify_last_constraints(|c| {
1636 c.min_height = Some(value);
1637 c.max_height = Some(value);
1638 });
1639 self
1640 }
1641
1642 pub fn min_w(&mut self, value: u32) -> &mut Self {
1644 self.modify_last_constraints(|c| c.min_width = Some(value));
1645 self
1646 }
1647
1648 pub fn max_w(&mut self, value: u32) -> &mut Self {
1650 self.modify_last_constraints(|c| c.max_width = Some(value));
1651 self
1652 }
1653
1654 pub fn min_h(&mut self, value: u32) -> &mut Self {
1656 self.modify_last_constraints(|c| c.min_height = Some(value));
1657 self
1658 }
1659
1660 pub fn max_h(&mut self, value: u32) -> &mut Self {
1662 self.modify_last_constraints(|c| c.max_height = Some(value));
1663 self
1664 }
1665
1666 pub fn m(&mut self, value: u32) -> &mut Self {
1670 self.modify_last_margin(|m| *m = Margin::all(value));
1671 self
1672 }
1673
1674 pub fn mx(&mut self, value: u32) -> &mut Self {
1676 self.modify_last_margin(|m| {
1677 m.left = value;
1678 m.right = value;
1679 });
1680 self
1681 }
1682
1683 pub fn my(&mut self, value: u32) -> &mut Self {
1685 self.modify_last_margin(|m| {
1686 m.top = value;
1687 m.bottom = value;
1688 });
1689 self
1690 }
1691
1692 pub fn mt(&mut self, value: u32) -> &mut Self {
1694 self.modify_last_margin(|m| m.top = value);
1695 self
1696 }
1697
1698 pub fn mr(&mut self, value: u32) -> &mut Self {
1700 self.modify_last_margin(|m| m.right = value);
1701 self
1702 }
1703
1704 pub fn mb(&mut self, value: u32) -> &mut Self {
1706 self.modify_last_margin(|m| m.bottom = value);
1707 self
1708 }
1709
1710 pub fn ml(&mut self, value: u32) -> &mut Self {
1712 self.modify_last_margin(|m| m.left = value);
1713 self
1714 }
1715
1716 pub fn spacer(&mut self) -> &mut Self {
1720 self.commands.push(Command::Spacer { grow: 1 });
1721 self.last_text_idx = None;
1722 self
1723 }
1724
1725 pub fn form(
1729 &mut self,
1730 state: &mut FormState,
1731 f: impl FnOnce(&mut Context, &mut FormState),
1732 ) -> &mut Self {
1733 self.col(|ui| {
1734 f(ui, state);
1735 });
1736 self
1737 }
1738
1739 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1743 self.col(|ui| {
1744 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1745 ui.text_input(&mut field.input);
1746 if let Some(error) = field.error.as_deref() {
1747 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1748 }
1749 });
1750 self
1751 }
1752
1753 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1757 self.button(label)
1758 }
1759}
1760
1761const KEYWORDS: &[&str] = &[
1762 "fn",
1763 "let",
1764 "mut",
1765 "pub",
1766 "use",
1767 "impl",
1768 "struct",
1769 "enum",
1770 "trait",
1771 "type",
1772 "const",
1773 "static",
1774 "if",
1775 "else",
1776 "match",
1777 "for",
1778 "while",
1779 "loop",
1780 "return",
1781 "break",
1782 "continue",
1783 "where",
1784 "self",
1785 "super",
1786 "crate",
1787 "mod",
1788 "async",
1789 "await",
1790 "move",
1791 "ref",
1792 "in",
1793 "as",
1794 "true",
1795 "false",
1796 "Some",
1797 "None",
1798 "Ok",
1799 "Err",
1800 "Self",
1801 "def",
1802 "class",
1803 "import",
1804 "from",
1805 "pass",
1806 "lambda",
1807 "yield",
1808 "with",
1809 "try",
1810 "except",
1811 "raise",
1812 "finally",
1813 "elif",
1814 "del",
1815 "global",
1816 "nonlocal",
1817 "assert",
1818 "is",
1819 "not",
1820 "and",
1821 "or",
1822 "function",
1823 "var",
1824 "const",
1825 "export",
1826 "default",
1827 "switch",
1828 "case",
1829 "throw",
1830 "catch",
1831 "typeof",
1832 "instanceof",
1833 "new",
1834 "delete",
1835 "void",
1836 "this",
1837 "null",
1838 "undefined",
1839 "func",
1840 "package",
1841 "defer",
1842 "go",
1843 "chan",
1844 "select",
1845 "range",
1846 "map",
1847 "interface",
1848 "fallthrough",
1849 "nil",
1850];
1851
1852fn render_highlighted_line(ui: &mut Context, line: &str) {
1853 let theme = ui.theme;
1854 let is_light = matches!(
1855 theme.bg,
1856 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1857 );
1858 let keyword_color = if is_light {
1859 Color::Rgb(166, 38, 164)
1860 } else {
1861 Color::Rgb(198, 120, 221)
1862 };
1863 let string_color = if is_light {
1864 Color::Rgb(80, 161, 79)
1865 } else {
1866 Color::Rgb(152, 195, 121)
1867 };
1868 let comment_color = theme.text_dim;
1869 let number_color = if is_light {
1870 Color::Rgb(152, 104, 1)
1871 } else {
1872 Color::Rgb(209, 154, 102)
1873 };
1874 let fn_color = if is_light {
1875 Color::Rgb(64, 120, 242)
1876 } else {
1877 Color::Rgb(97, 175, 239)
1878 };
1879 let macro_color = if is_light {
1880 Color::Rgb(1, 132, 188)
1881 } else {
1882 Color::Rgb(86, 182, 194)
1883 };
1884
1885 let trimmed = line.trim_start();
1886 let indent = &line[..line.len() - trimmed.len()];
1887 if !indent.is_empty() {
1888 ui.text(indent);
1889 }
1890
1891 if trimmed.starts_with("//") {
1892 ui.text(trimmed).fg(comment_color).italic();
1893 return;
1894 }
1895
1896 let mut pos = 0;
1897
1898 while pos < trimmed.len() {
1899 let ch = trimmed.as_bytes()[pos];
1900
1901 if ch == b'"' {
1902 if let Some(end) = trimmed[pos + 1..].find('"') {
1903 let s = &trimmed[pos..pos + end + 2];
1904 ui.text(s).fg(string_color);
1905 pos += end + 2;
1906 continue;
1907 }
1908 }
1909
1910 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1911 {
1912 let end = trimmed[pos..]
1913 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1914 .map_or(trimmed.len(), |e| pos + e);
1915 ui.text(&trimmed[pos..end]).fg(number_color);
1916 pos = end;
1917 continue;
1918 }
1919
1920 if ch.is_ascii_alphabetic() || ch == b'_' {
1921 let end = trimmed[pos..]
1922 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1923 .map_or(trimmed.len(), |e| pos + e);
1924 let word = &trimmed[pos..end];
1925
1926 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1927 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1928 pos = end + 1;
1929 } else if end < trimmed.len()
1930 && trimmed.as_bytes()[end] == b'('
1931 && !KEYWORDS.contains(&word)
1932 {
1933 ui.text(word).fg(fn_color);
1934 pos = end;
1935 } else if KEYWORDS.contains(&word) {
1936 ui.text(word).fg(keyword_color);
1937 pos = end;
1938 } else {
1939 ui.text(word);
1940 pos = end;
1941 }
1942 continue;
1943 }
1944
1945 let end = trimmed[pos..]
1946 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1947 .map_or(trimmed.len(), |e| pos + e);
1948 ui.text(&trimmed[pos..end]);
1949 pos = end;
1950 }
1951}
1952
1953fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
1954 let expected = (width as usize) * (height as usize) * 4;
1955 if data.len() >= expected {
1956 return data[..expected].to_vec();
1957 }
1958 let mut buf = Vec::with_capacity(expected);
1959 buf.extend_from_slice(data);
1960 buf.resize(expected, 0);
1961 buf
1962}
1963
1964fn base64_encode(data: &[u8]) -> String {
1965 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1966 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
1967 for chunk in data.chunks(3) {
1968 let b0 = chunk[0] as u32;
1969 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
1970 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
1971 let triple = (b0 << 16) | (b1 << 8) | b2;
1972 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
1973 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
1974 if chunk.len() > 1 {
1975 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
1976 } else {
1977 result.push('=');
1978 }
1979 if chunk.len() > 2 {
1980 result.push(CHARS[(triple & 0x3F) as usize] as char);
1981 } else {
1982 result.push('=');
1983 }
1984 }
1985 result
1986}
1987
1988fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
1989 let mut chunks = Vec::new();
1990 let bytes = encoded.as_bytes();
1991 let mut offset = 0;
1992 while offset < bytes.len() {
1993 let end = (offset + chunk_size).min(bytes.len());
1994 chunks.push(&encoded[offset..end]);
1995 offset = end;
1996 }
1997 if chunks.is_empty() {
1998 chunks.push("");
1999 }
2000 chunks
2001}