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