1use super::*;
2use crate::KeyMap;
3
4impl Context {
5 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
18 let content = s.into();
19 let default_fg = self
20 .text_color_stack
21 .iter()
22 .rev()
23 .find_map(|c| *c)
24 .unwrap_or(self.theme.text);
25 self.commands.push(Command::Text {
26 content,
27 style: Style::new().fg(default_fg),
28 grow: 0,
29 align: Align::Start,
30 wrap: false,
31 truncate: false,
32 margin: Margin::default(),
33 constraints: Constraints::default(),
34 });
35 self.last_text_idx = Some(self.commands.len() - 1);
36 self
37 }
38
39 #[allow(clippy::print_stderr)]
45 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
46 let url_str = url.into();
47 let focused = self.register_focusable();
48 let interaction_id = self.next_interaction_id();
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 if let Err(e) = open_url(&url_str) {
68 eprintln!("[slt] failed to open URL: {e}");
69 }
70 }
71
72 let style = if focused {
73 Style::new()
74 .fg(self.theme.primary)
75 .bg(self.theme.surface_hover)
76 .underline()
77 .bold()
78 } else if response.hovered {
79 Style::new()
80 .fg(self.theme.accent)
81 .bg(self.theme.surface_hover)
82 .underline()
83 } else {
84 Style::new().fg(self.theme.primary).underline()
85 };
86
87 self.commands.push(Command::Link {
88 text: text.into(),
89 url: url_str,
90 style,
91 margin: Margin::default(),
92 constraints: Constraints::default(),
93 });
94 self.last_text_idx = Some(self.commands.len() - 1);
95 self
96 }
97
98 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
103 let content = s.into();
104 let default_fg = self
105 .text_color_stack
106 .iter()
107 .rev()
108 .find_map(|c| *c)
109 .unwrap_or(self.theme.text);
110 self.commands.push(Command::Text {
111 content,
112 style: Style::new().fg(default_fg),
113 grow: 0,
114 align: Align::Start,
115 wrap: true,
116 truncate: false,
117 margin: Margin::default(),
118 constraints: Constraints::default(),
119 });
120 self.last_text_idx = Some(self.commands.len() - 1);
121 self
122 }
123
124 pub fn timer_display(&mut self, elapsed: std::time::Duration) -> &mut Self {
128 let total_centis = elapsed.as_millis() / 10;
129 let centis = total_centis % 100;
130 let total_seconds = total_centis / 100;
131 let seconds = total_seconds % 60;
132 let minutes = (total_seconds / 60) % 60;
133 let hours = total_seconds / 3600;
134
135 let content = if hours > 0 {
136 format!("{hours:02}:{minutes:02}:{seconds:02}.{centis:02}")
137 } else {
138 format!("{minutes:02}:{seconds:02}.{centis:02}")
139 };
140
141 self.commands.push(Command::Text {
142 content,
143 style: Style::new().fg(self.theme.text),
144 grow: 0,
145 align: Align::Start,
146 wrap: false,
147 truncate: false,
148 margin: Margin::default(),
149 constraints: Constraints::default(),
150 });
151 self.last_text_idx = Some(self.commands.len() - 1);
152 self
153 }
154
155 pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
157 let pairs: Vec<(&str, &str)> = keymap
158 .visible_bindings()
159 .map(|binding| (binding.display.as_str(), binding.description.as_str()))
160 .collect();
161 self.help(&pairs)
162 }
163
164 pub fn bold(&mut self) -> &mut Self {
168 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
169 self
170 }
171
172 pub fn dim(&mut self) -> &mut Self {
177 let text_dim = self.theme.text_dim;
178 self.modify_last_style(|s| {
179 s.modifiers |= Modifiers::DIM;
180 if s.fg.is_none() {
181 s.fg = Some(text_dim);
182 }
183 });
184 self
185 }
186
187 pub fn italic(&mut self) -> &mut Self {
189 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
190 self
191 }
192
193 pub fn underline(&mut self) -> &mut Self {
195 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
196 self
197 }
198
199 pub fn reversed(&mut self) -> &mut Self {
201 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
202 self
203 }
204
205 pub fn strikethrough(&mut self) -> &mut Self {
207 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
208 self
209 }
210
211 pub fn fg(&mut self, color: Color) -> &mut Self {
213 self.modify_last_style(|s| s.fg = Some(color));
214 self
215 }
216
217 pub fn bg(&mut self, color: Color) -> &mut Self {
219 self.modify_last_style(|s| s.bg = Some(color));
220 self
221 }
222
223 pub fn gradient(&mut self, from: Color, to: Color) -> &mut Self {
225 if let Some(idx) = self.last_text_idx {
226 let replacement = match &self.commands[idx] {
227 Command::Text {
228 content,
229 style,
230 wrap,
231 align,
232 margin,
233 constraints,
234 ..
235 } => {
236 let chars: Vec<char> = content.chars().collect();
237 let len = chars.len();
238 let denom = len.saturating_sub(1).max(1) as f32;
239 let segments = chars
240 .into_iter()
241 .enumerate()
242 .map(|(i, ch)| {
243 let mut seg_style = *style;
244 seg_style.fg = Some(from.blend(to, i as f32 / denom));
245 (ch.to_string(), seg_style)
246 })
247 .collect();
248
249 Some(Command::RichText {
250 segments,
251 wrap: *wrap,
252 align: *align,
253 margin: *margin,
254 constraints: *constraints,
255 })
256 }
257 _ => None,
258 };
259
260 if let Some(command) = replacement {
261 self.commands[idx] = command;
262 }
263 }
264
265 self
266 }
267
268 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
269 let apply_group_style = self
270 .group_stack
271 .last()
272 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
273 .unwrap_or(false);
274 if apply_group_style {
275 self.modify_last_style(|s| s.fg = Some(color));
276 }
277 self
278 }
279
280 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
281 let apply_group_style = self
282 .group_stack
283 .last()
284 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
285 .unwrap_or(false);
286 if apply_group_style {
287 self.modify_last_style(|s| s.bg = Some(color));
288 }
289 self
290 }
291
292 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
297 self.commands.push(Command::Text {
298 content: s.into(),
299 style,
300 grow: 0,
301 align: Align::Start,
302 wrap: false,
303 truncate: false,
304 margin: Margin::default(),
305 constraints: Constraints::default(),
306 });
307 self.last_text_idx = Some(self.commands.len() - 1);
308 self
309 }
310
311 pub fn big_text(&mut self, s: impl Into<String>) -> Response {
313 let text = s.into();
314 let glyphs: Vec<[u8; 8]> = text.chars().map(glyph_8x8).collect();
315 let total_width = (glyphs.len() as u32).saturating_mul(8);
316 let on_color = self.theme.primary;
317
318 self.container().w(total_width).h(4).draw(move |buf, rect| {
319 if rect.width == 0 || rect.height == 0 {
320 return;
321 }
322
323 for (glyph_idx, glyph) in glyphs.iter().enumerate() {
324 let base_x = rect.x + (glyph_idx as u32) * 8;
325 if base_x >= rect.right() {
326 break;
327 }
328
329 for pair in 0..4usize {
330 let y = rect.y + pair as u32;
331 if y >= rect.bottom() {
332 continue;
333 }
334
335 let upper = glyph[pair * 2];
336 let lower = glyph[pair * 2 + 1];
337
338 for bit in 0..8u32 {
339 let x = base_x + bit;
340 if x >= rect.right() {
341 break;
342 }
343
344 let mask = 1u8 << (bit as u8);
345 let upper_on = (upper & mask) != 0;
346 let lower_on = (lower & mask) != 0;
347 let (ch, fg, bg) = match (upper_on, lower_on) {
348 (true, true) => ('█', on_color, on_color),
349 (true, false) => ('▀', on_color, Color::Reset),
350 (false, true) => ('▄', on_color, Color::Reset),
351 (false, false) => (' ', Color::Reset, Color::Reset),
352 };
353 buf.set_char(x, y, ch, Style::new().fg(fg).bg(bg));
354 }
355 }
356 }
357 });
358
359 Response::none()
360 }
361
362 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
384 let width = img.width;
385 let height = img.height;
386
387 let _ = self.container().w(width).h(height).gap(0).col(|ui| {
388 for row in 0..height {
389 let _ = ui.container().gap(0).row(|ui| {
390 for col in 0..width {
391 let idx = (row * width + col) as usize;
392 if let Some(&(upper, lower)) = img.pixels.get(idx) {
393 ui.styled("▀", Style::new().fg(upper).bg(lower));
394 }
395 }
396 });
397 }
398 });
399
400 Response::none()
401 }
402
403 pub fn kitty_image(
419 &mut self,
420 rgba: &[u8],
421 pixel_width: u32,
422 pixel_height: u32,
423 cols: u32,
424 rows: u32,
425 ) -> Response {
426 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
427 let encoded = base64_encode(&rgba);
428 let pw = pixel_width;
429 let ph = pixel_height;
430 let c = cols;
431 let r = rows;
432
433 self.container().w(cols).h(rows).draw(move |buf, rect| {
434 let chunks = split_base64(&encoded, 4096);
435 let mut all_sequences = String::new();
436
437 for (i, chunk) in chunks.iter().enumerate() {
438 let more = if i < chunks.len() - 1 { 1 } else { 0 };
439 if i == 0 {
440 all_sequences.push_str(&format!(
441 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
442 pw, ph, c, r, more, chunk
443 ));
444 } else {
445 all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
446 }
447 }
448
449 buf.raw_sequence(rect.x, rect.y, all_sequences);
450 });
451 Response::none()
452 }
453
454 pub fn kitty_image_fit(
463 &mut self,
464 rgba: &[u8],
465 src_width: u32,
466 src_height: u32,
467 cols: u32,
468 ) -> Response {
469 let rows = if src_width == 0 {
470 1
471 } else {
472 ((cols as f64 * src_height as f64 * 8.0) / (src_width as f64 * 16.0))
473 .ceil()
474 .max(1.0) as u32
475 };
476 let rgba = normalize_rgba(rgba, src_width, src_height);
477 let sw = src_width;
478 let sh = src_height;
479 let c = cols;
480 let r = rows;
481
482 self.container().w(cols).h(rows).draw(move |buf, rect| {
483 if rect.width == 0 || rect.height == 0 {
484 return;
485 }
486 let encoded = base64_encode(&rgba);
487 let chunks = split_base64(&encoded, 4096);
488 let mut seq = String::new();
489 for (i, chunk) in chunks.iter().enumerate() {
490 let more = if i < chunks.len() - 1 { 1 } else { 0 };
491 if i == 0 {
492 seq.push_str(&format!(
493 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
494 sw, sh, c, r, more, chunk
495 ));
496 } else {
497 seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
498 }
499 }
500 buf.raw_sequence(rect.x, rect.y, seq);
501 });
502 Response::none()
503 }
504
505 pub fn sixel_image(
506 &mut self,
507 rgba: &[u8],
508 pixel_w: u32,
509 pixel_h: u32,
510 cols: u32,
511 rows: u32,
512 ) -> Response {
513 let sixel_supported = self.is_real_terminal && terminal_supports_sixel();
514 if !sixel_supported {
515 self.container().w(cols).h(rows).draw(|buf, rect| {
516 if rect.width == 0 || rect.height == 0 {
517 return;
518 }
519 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
520 });
521 return Response::none();
522 }
523
524 #[cfg(not(feature = "crossterm"))]
525 {
526 self.container().w(cols).h(rows).draw(|buf, rect| {
527 if rect.width == 0 || rect.height == 0 {
528 return;
529 }
530 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
531 });
532 return Response::none();
533 }
534
535 #[cfg(feature = "crossterm")]
536 let rgba = normalize_rgba(rgba, pixel_w, pixel_h);
537 #[cfg(feature = "crossterm")]
538 let encoded = crate::sixel::encode_sixel(&rgba, pixel_w, pixel_h, 256);
539
540 #[cfg(feature = "crossterm")]
541 if encoded.is_empty() {
542 self.container().w(cols).h(rows).draw(|buf, rect| {
543 if rect.width == 0 || rect.height == 0 {
544 return;
545 }
546 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
547 });
548 return Response::none();
549 }
550
551 #[cfg(feature = "crossterm")]
552 self.container().w(cols).h(rows).draw(move |buf, rect| {
553 if rect.width == 0 || rect.height == 0 {
554 return;
555 }
556 buf.raw_sequence(rect.x, rect.y, encoded);
557 });
558 Response::none()
559 }
560
561 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
577 if state.streaming {
578 state.cursor_tick = state.cursor_tick.wrapping_add(1);
579 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
580 }
581
582 if state.content.is_empty() && state.streaming {
583 let cursor = if state.cursor_visible { "▌" } else { " " };
584 let primary = self.theme.primary;
585 self.text(cursor).fg(primary);
586 return Response::none();
587 }
588
589 if !state.content.is_empty() {
590 if state.streaming && state.cursor_visible {
591 self.text_wrap(format!("{}▌", state.content));
592 } else {
593 self.text_wrap(&state.content);
594 }
595 }
596
597 Response::none()
598 }
599
600 pub fn streaming_markdown(
618 &mut self,
619 state: &mut crate::widgets::StreamingMarkdownState,
620 ) -> Response {
621 if state.streaming {
622 state.cursor_tick = state.cursor_tick.wrapping_add(1);
623 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
624 }
625
626 if state.content.is_empty() && state.streaming {
627 let cursor = if state.cursor_visible { "▌" } else { " " };
628 let primary = self.theme.primary;
629 self.text(cursor).fg(primary);
630 return Response::none();
631 }
632
633 let show_cursor = state.streaming && state.cursor_visible;
634 let trailing_newline = state.content.ends_with('\n');
635 let lines: Vec<&str> = state.content.lines().collect();
636 let last_line_index = lines.len().saturating_sub(1);
637
638 self.commands.push(Command::BeginContainer {
639 direction: Direction::Column,
640 gap: 0,
641 align: Align::Start,
642 align_self: None,
643 justify: Justify::Start,
644 border: None,
645 border_sides: BorderSides::all(),
646 border_style: Style::new().fg(self.theme.border),
647 bg_color: None,
648 padding: Padding::default(),
649 margin: Margin::default(),
650 constraints: Constraints::default(),
651 title: None,
652 grow: 0,
653 group_name: None,
654 });
655 self.interaction_count += 1;
656
657 let text_style = Style::new().fg(self.theme.text);
658 let bold_style = Style::new().fg(self.theme.text).bold();
659 let code_style = Style::new().fg(self.theme.accent);
660 let border_style = Style::new().fg(self.theme.border).dim();
661
662 let mut in_code_block = false;
663 let mut code_block_lang = String::new();
664
665 for (idx, line) in lines.iter().enumerate() {
666 let line = *line;
667 let trimmed = line.trim();
668 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
669 let cursor = if append_cursor { "▌" } else { "" };
670
671 if in_code_block {
672 if trimmed.starts_with("```") {
673 in_code_block = false;
674 code_block_lang.clear();
675 let mut line = String::from(" └────");
676 line.push_str(cursor);
677 self.styled(line, border_style);
678 } else {
679 self.line(|ui| {
680 ui.text(" ");
681 render_highlighted_line(ui, line);
682 if !cursor.is_empty() {
683 ui.styled(cursor, Style::new().fg(ui.theme.primary));
684 }
685 });
686 }
687 continue;
688 }
689
690 if trimmed.is_empty() {
691 if append_cursor {
692 self.styled("▌", Style::new().fg(self.theme.primary));
693 } else {
694 self.text(" ");
695 }
696 continue;
697 }
698
699 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
700 let mut line = "─".repeat(40);
701 line.push_str(cursor);
702 self.styled(line, border_style);
703 continue;
704 }
705
706 if let Some(heading) = trimmed.strip_prefix("### ") {
707 let mut line = String::with_capacity(heading.len() + cursor.len());
708 line.push_str(heading);
709 line.push_str(cursor);
710 self.styled(line, Style::new().bold().fg(self.theme.accent));
711 continue;
712 }
713
714 if let Some(heading) = trimmed.strip_prefix("## ") {
715 let mut line = String::with_capacity(heading.len() + cursor.len());
716 line.push_str(heading);
717 line.push_str(cursor);
718 self.styled(line, Style::new().bold().fg(self.theme.secondary));
719 continue;
720 }
721
722 if let Some(heading) = trimmed.strip_prefix("# ") {
723 let mut line = String::with_capacity(heading.len() + cursor.len());
724 line.push_str(heading);
725 line.push_str(cursor);
726 self.styled(line, Style::new().bold().fg(self.theme.primary));
727 continue;
728 }
729
730 if let Some(code) = trimmed.strip_prefix("```") {
731 in_code_block = true;
732 code_block_lang = code.trim().to_string();
733 let label = if code_block_lang.is_empty() {
734 "code".to_string()
735 } else {
736 let mut label = String::from("code:");
737 label.push_str(&code_block_lang);
738 label
739 };
740 let mut line = String::with_capacity(5 + label.len() + cursor.len());
741 line.push_str(" ┌─");
742 line.push_str(&label);
743 line.push('─');
744 line.push_str(cursor);
745 self.styled(line, border_style);
746 continue;
747 }
748
749 if let Some(item) = trimmed
750 .strip_prefix("- ")
751 .or_else(|| trimmed.strip_prefix("* "))
752 {
753 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
754 if segs.len() <= 1 {
755 let mut line = String::with_capacity(4 + item.len() + cursor.len());
756 line.push_str(" • ");
757 line.push_str(item);
758 line.push_str(cursor);
759 self.styled(line, text_style);
760 } else {
761 self.line(|ui| {
762 ui.styled(" • ", text_style);
763 for (s, st) in segs {
764 ui.styled(s, st);
765 }
766 if append_cursor {
767 ui.styled("▌", Style::new().fg(ui.theme.primary));
768 }
769 });
770 }
771 continue;
772 }
773
774 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
775 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
776 if parts.len() == 2 {
777 let segs =
778 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
779 if segs.len() <= 1 {
780 let mut line = String::with_capacity(
781 4 + parts[0].len() + parts[1].len() + cursor.len(),
782 );
783 line.push_str(" ");
784 line.push_str(parts[0]);
785 line.push_str(". ");
786 line.push_str(parts[1]);
787 line.push_str(cursor);
788 self.styled(line, text_style);
789 } else {
790 self.line(|ui| {
791 let mut prefix = String::with_capacity(4 + parts[0].len());
792 prefix.push_str(" ");
793 prefix.push_str(parts[0]);
794 prefix.push_str(". ");
795 ui.styled(prefix, text_style);
796 for (s, st) in segs {
797 ui.styled(s, st);
798 }
799 if append_cursor {
800 ui.styled("▌", Style::new().fg(ui.theme.primary));
801 }
802 });
803 }
804 } else {
805 let mut line = String::with_capacity(trimmed.len() + cursor.len());
806 line.push_str(trimmed);
807 line.push_str(cursor);
808 self.styled(line, text_style);
809 }
810 continue;
811 }
812
813 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
814 if segs.len() <= 1 {
815 let mut line = String::with_capacity(trimmed.len() + cursor.len());
816 line.push_str(trimmed);
817 line.push_str(cursor);
818 self.styled(line, text_style);
819 } else {
820 self.line(|ui| {
821 for (s, st) in segs {
822 ui.styled(s, st);
823 }
824 if append_cursor {
825 ui.styled("▌", Style::new().fg(ui.theme.primary));
826 }
827 });
828 }
829 }
830
831 if show_cursor && trailing_newline {
832 if in_code_block {
833 self.styled(" ▌", code_style);
834 } else {
835 self.styled("▌", Style::new().fg(self.theme.primary));
836 }
837 }
838
839 state.in_code_block = in_code_block;
840 state.code_block_lang = code_block_lang;
841
842 self.commands.push(Command::EndContainer);
843 self.last_text_idx = None;
844 Response::none()
845 }
846
847 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
862 let old_action = state.action;
863 let theme = self.theme;
864 let _ = self.bordered(Border::Rounded).col(|ui| {
865 let _ = ui.row(|ui| {
866 ui.text("⚡").fg(theme.warning);
867 ui.text(&state.tool_name).bold().fg(theme.primary);
868 });
869 ui.text(&state.description).dim();
870
871 if state.action == ApprovalAction::Pending {
872 let _ = ui.row(|ui| {
873 if ui.button("✓ Approve").clicked {
874 state.action = ApprovalAction::Approved;
875 }
876 if ui.button("✗ Reject").clicked {
877 state.action = ApprovalAction::Rejected;
878 }
879 });
880 } else {
881 let (label, color) = match state.action {
882 ApprovalAction::Approved => ("✓ Approved", theme.success),
883 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
884 ApprovalAction::Pending => unreachable!(),
885 };
886 ui.text(label).fg(color).bold();
887 }
888 });
889
890 Response {
891 changed: state.action != old_action,
892 ..Response::none()
893 }
894 }
895
896 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
909 if items.is_empty() {
910 return Response::none();
911 }
912
913 let theme = self.theme;
914 let total: usize = items.iter().map(|item| item.tokens).sum();
915
916 let _ = self.container().row(|ui| {
917 ui.text("📎").dim();
918 for item in items {
919 let token_count = format_token_count(item.tokens);
920 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
921 line.push_str(&item.label);
922 line.push_str(" (");
923 line.push_str(&token_count);
924 line.push(')');
925 ui.text(line).fg(theme.secondary);
926 }
927 ui.spacer();
928 let total_text = format_token_count(total);
929 let mut line = String::with_capacity(2 + total_text.len());
930 line.push_str("Σ ");
931 line.push_str(&total_text);
932 ui.text(line).dim();
933 });
934
935 Response::none()
936 }
937
938 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
939 use crate::widgets::AlertLevel;
940
941 let theme = self.theme;
942 let (icon, color) = match level {
943 AlertLevel::Info => ("ℹ", theme.accent),
944 AlertLevel::Success => ("✓", theme.success),
945 AlertLevel::Warning => ("⚠", theme.warning),
946 AlertLevel::Error => ("✕", theme.error),
947 };
948
949 let focused = self.register_focusable();
950 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
951
952 let mut response = self.container().col(|ui| {
953 ui.line(|ui| {
954 let mut icon_text = String::with_capacity(icon.len() + 2);
955 icon_text.push(' ');
956 icon_text.push_str(icon);
957 icon_text.push(' ');
958 ui.text(icon_text).fg(color).bold();
959 ui.text(message).grow(1);
960 ui.text(" [×] ").dim();
961 });
962 });
963 response.focused = focused;
964 if key_dismiss {
965 response.clicked = true;
966 }
967
968 response
969 }
970
971 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
985 let focused = self.register_focusable();
986 let mut is_yes = *result;
987 let mut clicked = false;
988
989 if focused {
990 let mut consumed_indices = Vec::new();
991 for (i, event) in self.events.iter().enumerate() {
992 if let Event::Key(key) = event {
993 if key.kind != KeyEventKind::Press {
994 continue;
995 }
996
997 match key.code {
998 KeyCode::Char('y') => {
999 is_yes = true;
1000 *result = true;
1001 clicked = true;
1002 consumed_indices.push(i);
1003 }
1004 KeyCode::Char('n') => {
1005 is_yes = false;
1006 *result = false;
1007 clicked = true;
1008 consumed_indices.push(i);
1009 }
1010 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
1011 is_yes = !is_yes;
1012 *result = is_yes;
1013 consumed_indices.push(i);
1014 }
1015 KeyCode::Enter => {
1016 *result = is_yes;
1017 clicked = true;
1018 consumed_indices.push(i);
1019 }
1020 _ => {}
1021 }
1022 }
1023 }
1024
1025 for idx in consumed_indices {
1026 self.consumed[idx] = true;
1027 }
1028 }
1029
1030 let yes_style = if is_yes {
1031 if focused {
1032 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
1033 } else {
1034 Style::new().fg(self.theme.success).bold()
1035 }
1036 } else {
1037 Style::new().fg(self.theme.text_dim)
1038 };
1039 let no_style = if !is_yes {
1040 if focused {
1041 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
1042 } else {
1043 Style::new().fg(self.theme.error).bold()
1044 }
1045 } else {
1046 Style::new().fg(self.theme.text_dim)
1047 };
1048
1049 let q_width = UnicodeWidthStr::width(question) as u32;
1050 let mut response = self.row(|ui| {
1051 ui.text(question);
1052 ui.text(" ");
1053 ui.styled("[Yes]", yes_style);
1054 ui.text(" ");
1055 ui.styled("[No]", no_style);
1056 });
1057
1058 if !clicked && response.clicked {
1059 if let Some((mx, _)) = self.click_pos {
1060 let yes_start = response.rect.x + q_width + 1;
1061 let yes_end = yes_start + 5;
1062 let no_start = yes_end + 1;
1063 if mx >= yes_start && mx < yes_end {
1064 is_yes = true;
1065 *result = true;
1066 clicked = true;
1067 } else if mx >= no_start {
1068 is_yes = false;
1069 *result = false;
1070 clicked = true;
1071 }
1072 }
1073 }
1074
1075 response.focused = focused;
1076 response.clicked = clicked;
1077 response.changed = clicked;
1078 let _ = is_yes;
1079 response
1080 }
1081
1082 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
1083 self.breadcrumb_with(segments, " › ")
1084 }
1085
1086 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
1087 let theme = self.theme;
1088 let last_idx = segments.len().saturating_sub(1);
1089 let mut clicked_idx: Option<usize> = None;
1090
1091 let _ = self.row(|ui| {
1092 for (i, segment) in segments.iter().enumerate() {
1093 let is_last = i == last_idx;
1094 if is_last {
1095 ui.text(*segment).bold();
1096 } else {
1097 let focused = ui.register_focusable();
1098 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
1099 let resp = ui.interaction();
1100 let color = if resp.hovered || focused {
1101 theme.accent
1102 } else {
1103 theme.primary
1104 };
1105 ui.text(*segment).fg(color).underline();
1106 if resp.clicked || pressed {
1107 clicked_idx = Some(i);
1108 }
1109 ui.text(separator).dim();
1110 }
1111 }
1112 });
1113
1114 clicked_idx
1115 }
1116
1117 pub fn accordion(
1118 &mut self,
1119 title: &str,
1120 open: &mut bool,
1121 f: impl FnOnce(&mut Context),
1122 ) -> Response {
1123 let theme = self.theme;
1124 let focused = self.register_focusable();
1125 let old_open = *open;
1126
1127 if focused && self.key_code(KeyCode::Enter) {
1128 *open = !*open;
1129 }
1130
1131 let icon = if *open { "▾" } else { "▸" };
1132 let title_color = if focused { theme.primary } else { theme.text };
1133
1134 let mut response = self.container().col(|ui| {
1135 ui.line(|ui| {
1136 ui.text(icon).fg(title_color);
1137 let mut title_text = String::with_capacity(1 + title.len());
1138 title_text.push(' ');
1139 title_text.push_str(title);
1140 ui.text(title_text).bold().fg(title_color);
1141 });
1142 });
1143
1144 if response.clicked {
1145 *open = !*open;
1146 }
1147
1148 if *open {
1149 let _ = self.container().pl(2).col(f);
1150 }
1151
1152 response.focused = focused;
1153 response.changed = *open != old_open;
1154 response
1155 }
1156
1157 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
1158 let max_key_width = items
1159 .iter()
1160 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
1161 .max()
1162 .unwrap_or(0);
1163
1164 let _ = self.col(|ui| {
1165 for (key, value) in items {
1166 ui.line(|ui| {
1167 let padded = format!("{:>width$}", key, width = max_key_width);
1168 ui.text(padded).dim();
1169 ui.text(" ");
1170 ui.text(*value);
1171 });
1172 }
1173 });
1174
1175 Response::none()
1176 }
1177
1178 pub fn divider_text(&mut self, label: &str) -> Response {
1179 let w = self.width();
1180 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
1181 let pad = 1u32;
1182 let left_len = 4u32;
1183 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
1184 let left: String = "─".repeat(left_len as usize);
1185 let right: String = "─".repeat(right_len as usize);
1186 let theme = self.theme;
1187 self.line(|ui| {
1188 ui.text(&left).fg(theme.border);
1189 let mut label_text = String::with_capacity(label.len() + 2);
1190 label_text.push(' ');
1191 label_text.push_str(label);
1192 label_text.push(' ');
1193 ui.text(label_text).fg(theme.text);
1194 ui.text(&right).fg(theme.border);
1195 });
1196
1197 Response::none()
1198 }
1199
1200 pub fn badge(&mut self, label: &str) -> Response {
1201 let theme = self.theme;
1202 self.badge_colored(label, theme.primary)
1203 }
1204
1205 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
1206 let fg = Color::contrast_fg(color);
1207 let mut label_text = String::with_capacity(label.len() + 2);
1208 label_text.push(' ');
1209 label_text.push_str(label);
1210 label_text.push(' ');
1211 self.text(label_text).fg(fg).bg(color);
1212
1213 Response::none()
1214 }
1215
1216 pub fn key_hint(&mut self, key: &str) -> Response {
1217 let theme = self.theme;
1218 let mut key_text = String::with_capacity(key.len() + 2);
1219 key_text.push(' ');
1220 key_text.push_str(key);
1221 key_text.push(' ');
1222 self.text(key_text).reversed().fg(theme.text_dim);
1223
1224 Response::none()
1225 }
1226
1227 pub fn stat(&mut self, label: &str, value: &str) -> Response {
1228 let _ = self.col(|ui| {
1229 ui.text(label).dim();
1230 ui.text(value).bold();
1231 });
1232
1233 Response::none()
1234 }
1235
1236 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
1237 let _ = self.col(|ui| {
1238 ui.text(label).dim();
1239 ui.text(value).bold().fg(color);
1240 });
1241
1242 Response::none()
1243 }
1244
1245 pub fn stat_trend(
1246 &mut self,
1247 label: &str,
1248 value: &str,
1249 trend: crate::widgets::Trend,
1250 ) -> Response {
1251 let theme = self.theme;
1252 let (arrow, color) = match trend {
1253 crate::widgets::Trend::Up => ("↑", theme.success),
1254 crate::widgets::Trend::Down => ("↓", theme.error),
1255 };
1256 let _ = self.col(|ui| {
1257 ui.text(label).dim();
1258 ui.line(|ui| {
1259 ui.text(value).bold();
1260 let mut arrow_text = String::with_capacity(1 + arrow.len());
1261 arrow_text.push(' ');
1262 arrow_text.push_str(arrow);
1263 ui.text(arrow_text).fg(color);
1264 });
1265 });
1266
1267 Response::none()
1268 }
1269
1270 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
1271 let _ = self.container().center().col(|ui| {
1272 ui.text(title).align(Align::Center);
1273 ui.text(description).dim().align(Align::Center);
1274 });
1275
1276 Response::none()
1277 }
1278
1279 pub fn empty_state_action(
1280 &mut self,
1281 title: &str,
1282 description: &str,
1283 action_label: &str,
1284 ) -> Response {
1285 let mut clicked = false;
1286 let _ = self.container().center().col(|ui| {
1287 ui.text(title).align(Align::Center);
1288 ui.text(description).dim().align(Align::Center);
1289 if ui.button(action_label).clicked {
1290 clicked = true;
1291 }
1292 });
1293
1294 Response {
1295 clicked,
1296 changed: clicked,
1297 ..Response::none()
1298 }
1299 }
1300
1301 pub fn code_block(&mut self, code: &str) -> Response {
1302 self.code_block_lang(code, "")
1303 }
1304
1305 pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
1306 let theme = self.theme;
1307 let highlighted = crate::syntax::highlight_code(code, lang, &theme);
1308 let _ = self
1309 .bordered(Border::Rounded)
1310 .bg(theme.surface)
1311 .pad(1)
1312 .col(|ui| {
1313 if let Some(ref lines) = highlighted {
1314 render_tree_sitter_lines(ui, lines);
1315 } else {
1316 for line in code.lines() {
1317 render_highlighted_line(ui, line);
1318 }
1319 }
1320 });
1321
1322 Response::none()
1323 }
1324
1325 pub fn code_block_numbered(&mut self, code: &str) -> Response {
1326 self.code_block_numbered_lang(code, "")
1327 }
1328
1329 pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
1330 let lines: Vec<&str> = code.lines().collect();
1331 let gutter_w = format!("{}", lines.len()).len();
1332 let theme = self.theme;
1333 let highlighted = crate::syntax::highlight_code(code, lang, &theme);
1334 let _ = self
1335 .bordered(Border::Rounded)
1336 .bg(theme.surface)
1337 .pad(1)
1338 .col(|ui| {
1339 if let Some(ref hl_lines) = highlighted {
1340 for (i, segs) in hl_lines.iter().enumerate() {
1341 ui.line(|ui| {
1342 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1343 .fg(theme.text_dim);
1344 for (text, style) in segs {
1345 ui.styled(text, *style);
1346 }
1347 });
1348 }
1349 } else {
1350 for (i, line) in lines.iter().enumerate() {
1351 ui.line(|ui| {
1352 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1353 .fg(theme.text_dim);
1354 render_highlighted_line(ui, line);
1355 });
1356 }
1357 }
1358 });
1359
1360 Response::none()
1361 }
1362
1363 pub fn wrap(&mut self) -> &mut Self {
1365 if let Some(idx) = self.last_text_idx {
1366 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1367 *wrap = true;
1368 }
1369 }
1370 self
1371 }
1372
1373 pub fn truncate(&mut self) -> &mut Self {
1376 if let Some(idx) = self.last_text_idx {
1377 if let Command::Text { truncate, .. } = &mut self.commands[idx] {
1378 *truncate = true;
1379 }
1380 }
1381 self
1382 }
1383
1384 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1385 if let Some(idx) = self.last_text_idx {
1386 match &mut self.commands[idx] {
1387 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1388 _ => {}
1389 }
1390 }
1391 }
1392
1393 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1394 if let Some(idx) = self.last_text_idx {
1395 match &mut self.commands[idx] {
1396 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1397 f(constraints)
1398 }
1399 _ => {}
1400 }
1401 }
1402 }
1403
1404 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1405 if let Some(idx) = self.last_text_idx {
1406 match &mut self.commands[idx] {
1407 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1408 _ => {}
1409 }
1410 }
1411 }
1412
1413 pub fn screen(&mut self, name: &str, screens: &ScreenState, f: impl FnOnce(&mut Context)) {
1416 if screens.current() == name {
1417 f(self);
1418 }
1419 }
1420
1421 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1437 self.push_container(Direction::Column, 0, f)
1438 }
1439
1440 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1444 self.push_container(Direction::Column, gap, f)
1445 }
1446
1447 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1464 self.push_container(Direction::Row, 0, f)
1465 }
1466
1467 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1471 self.push_container(Direction::Row, gap, f)
1472 }
1473
1474 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1491 let _ = self.push_container(Direction::Row, 0, f);
1492 self
1493 }
1494
1495 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1514 let start = self.commands.len();
1515 f(self);
1516 let mut segments: Vec<(String, Style)> = Vec::new();
1517 for cmd in self.commands.drain(start..) {
1518 if let Command::Text { content, style, .. } = cmd {
1519 segments.push((content, style));
1520 }
1521 }
1522 self.commands.push(Command::RichText {
1523 segments,
1524 wrap: true,
1525 align: Align::Start,
1526 margin: Margin::default(),
1527 constraints: Constraints::default(),
1528 });
1529 self.last_text_idx = None;
1530 self
1531 }
1532
1533 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1542 let interaction_id = self.next_interaction_id();
1543 self.commands.push(Command::BeginOverlay { modal: true });
1544 self.overlay_depth += 1;
1545 self.modal_active = true;
1546 self.modal_focus_start = self.focus_count;
1547 f(self);
1548 self.modal_focus_count = self.focus_count.saturating_sub(self.modal_focus_start);
1549 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1550 self.commands.push(Command::EndOverlay);
1551 self.last_text_idx = None;
1552 self.response_for(interaction_id)
1553 }
1554
1555 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1557 let interaction_id = self.next_interaction_id();
1558 self.commands.push(Command::BeginOverlay { modal: false });
1559 self.overlay_depth += 1;
1560 f(self);
1561 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1562 self.commands.push(Command::EndOverlay);
1563 self.last_text_idx = None;
1564 self.response_for(interaction_id)
1565 }
1566
1567 pub fn tooltip(&mut self, text: impl Into<String>) {
1575 let tooltip_text = text.into();
1576 if tooltip_text.is_empty() {
1577 return;
1578 }
1579 let last_interaction_id = self.interaction_count.saturating_sub(1);
1580 let last_response = self.response_for(last_interaction_id);
1581 if !last_response.hovered || last_response.rect.width == 0 || last_response.rect.height == 0
1582 {
1583 return;
1584 }
1585 let lines = wrap_tooltip_text(&tooltip_text, 38);
1586 self.pending_tooltips.push(PendingTooltip {
1587 anchor_rect: last_response.rect,
1588 lines,
1589 });
1590 }
1591
1592 pub(crate) fn emit_pending_tooltips(&mut self) {
1593 let tooltips = std::mem::take(&mut self.pending_tooltips);
1594 if tooltips.is_empty() {
1595 return;
1596 }
1597 let area_w = self.area_width;
1598 let area_h = self.area_height;
1599 let surface = self.theme.surface;
1600 let border_color = self.theme.border;
1601 let text_color = self.theme.surface_text;
1602
1603 for tooltip in tooltips {
1604 let content_w = tooltip
1605 .lines
1606 .iter()
1607 .map(|l| UnicodeWidthStr::width(l.as_str()) as u32)
1608 .max()
1609 .unwrap_or(0);
1610 let box_w = content_w.saturating_add(4).min(area_w);
1611 let box_h = (tooltip.lines.len() as u32).saturating_add(4).min(area_h);
1612
1613 let tooltip_x = tooltip.anchor_rect.x.min(area_w.saturating_sub(box_w));
1614 let below_y = tooltip.anchor_rect.bottom();
1615 let tooltip_y = if below_y.saturating_add(box_h) <= area_h {
1616 below_y
1617 } else {
1618 tooltip.anchor_rect.y.saturating_sub(box_h)
1619 };
1620
1621 let lines = tooltip.lines;
1622 let _ = self.overlay(|ui| {
1623 let _ = ui.container().w(area_w).h(area_h).col(|ui| {
1624 let _ = ui
1625 .container()
1626 .ml(tooltip_x)
1627 .mt(tooltip_y)
1628 .max_w(box_w)
1629 .border(Border::Rounded)
1630 .border_fg(border_color)
1631 .bg(surface)
1632 .p(1)
1633 .col(|ui| {
1634 for line in &lines {
1635 ui.text(line.as_str()).fg(text_color);
1636 }
1637 });
1638 });
1639 });
1640 }
1641 }
1642
1643 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1651 self.group_count = self.group_count.saturating_add(1);
1652 self.group_stack.push(name.to_string());
1653 self.container().group_name(name.to_string())
1654 }
1655
1656 pub fn container(&mut self) -> ContainerBuilder<'_> {
1677 let border = self.theme.border;
1678 ContainerBuilder {
1679 ctx: self,
1680 gap: 0,
1681 row_gap: None,
1682 col_gap: None,
1683 align: Align::Start,
1684 align_self_value: None,
1685 justify: Justify::Start,
1686 border: None,
1687 border_sides: BorderSides::all(),
1688 border_style: Style::new().fg(border),
1689 bg: None,
1690 text_color: None,
1691 dark_bg: None,
1692 dark_border_style: None,
1693 group_hover_bg: None,
1694 group_hover_border_style: None,
1695 group_name: None,
1696 padding: Padding::default(),
1697 margin: Margin::default(),
1698 constraints: Constraints::default(),
1699 title: None,
1700 grow: 0,
1701 scroll_offset: None,
1702 }
1703 }
1704
1705 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1724 let index = self.scroll_count;
1725 self.scroll_count += 1;
1726 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1727 state.set_bounds(ch, vh);
1728 let max = ch.saturating_sub(vh) as usize;
1729 state.offset = state.offset.min(max);
1730 }
1731
1732 let next_id = self.interaction_count;
1733 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1734 let inner_rects: Vec<Rect> = self
1735 .prev_scroll_rects
1736 .iter()
1737 .enumerate()
1738 .filter(|&(j, sr)| {
1739 j != index
1740 && sr.width > 0
1741 && sr.height > 0
1742 && sr.x >= rect.x
1743 && sr.right() <= rect.right()
1744 && sr.y >= rect.y
1745 && sr.bottom() <= rect.bottom()
1746 })
1747 .map(|(_, sr)| *sr)
1748 .collect();
1749 self.auto_scroll_nested(&rect, state, &inner_rects);
1750 }
1751
1752 self.container().scroll_offset(state.offset as u32)
1753 }
1754
1755 pub fn scrollbar(&mut self, state: &ScrollState) {
1775 let vh = state.viewport_height();
1776 let ch = state.content_height();
1777 if vh == 0 || ch <= vh {
1778 return;
1779 }
1780
1781 let track_height = vh;
1782 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1783 let max_offset = ch.saturating_sub(vh);
1784 let thumb_pos = if max_offset == 0 {
1785 0
1786 } else {
1787 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1788 .round() as u32
1789 };
1790
1791 let theme = self.theme;
1792 let track_char = '│';
1793 let thumb_char = '█';
1794
1795 let _ = self.container().w(1).h(track_height).col(|ui| {
1796 for i in 0..track_height {
1797 if i >= thumb_pos && i < thumb_pos + thumb_height {
1798 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1799 } else {
1800 ui.styled(
1801 track_char.to_string(),
1802 Style::new().fg(theme.text_dim).dim(),
1803 );
1804 }
1805 }
1806 });
1807 }
1808
1809 fn auto_scroll_nested(
1810 &mut self,
1811 rect: &Rect,
1812 state: &mut ScrollState,
1813 inner_scroll_rects: &[Rect],
1814 ) {
1815 let mut to_consume: Vec<usize> = Vec::new();
1816
1817 for (i, event) in self.events.iter().enumerate() {
1818 if self.consumed[i] {
1819 continue;
1820 }
1821 if let Event::Mouse(mouse) = event {
1822 let in_bounds = mouse.x >= rect.x
1823 && mouse.x < rect.right()
1824 && mouse.y >= rect.y
1825 && mouse.y < rect.bottom();
1826 if !in_bounds {
1827 continue;
1828 }
1829 let in_inner = inner_scroll_rects.iter().any(|sr| {
1830 mouse.x >= sr.x
1831 && mouse.x < sr.right()
1832 && mouse.y >= sr.y
1833 && mouse.y < sr.bottom()
1834 });
1835 if in_inner {
1836 continue;
1837 }
1838 match mouse.kind {
1839 MouseKind::ScrollUp => {
1840 state.scroll_up(1);
1841 to_consume.push(i);
1842 }
1843 MouseKind::ScrollDown => {
1844 state.scroll_down(1);
1845 to_consume.push(i);
1846 }
1847 MouseKind::Drag(MouseButton::Left) => {}
1848 _ => {}
1849 }
1850 }
1851 }
1852
1853 for i in to_consume {
1854 self.consumed[i] = true;
1855 }
1856 }
1857
1858 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1862 self.container()
1863 .border(border)
1864 .border_sides(BorderSides::all())
1865 }
1866
1867 fn push_container(
1868 &mut self,
1869 direction: Direction,
1870 gap: u32,
1871 f: impl FnOnce(&mut Context),
1872 ) -> Response {
1873 let interaction_id = self.next_interaction_id();
1874 let border = self.theme.border;
1875
1876 self.commands.push(Command::BeginContainer {
1877 direction,
1878 gap,
1879 align: Align::Start,
1880 align_self: None,
1881 justify: Justify::Start,
1882 border: None,
1883 border_sides: BorderSides::all(),
1884 border_style: Style::new().fg(border),
1885 bg_color: None,
1886 padding: Padding::default(),
1887 margin: Margin::default(),
1888 constraints: Constraints::default(),
1889 title: None,
1890 grow: 0,
1891 group_name: None,
1892 });
1893 self.text_color_stack.push(None);
1894 f(self);
1895 self.text_color_stack.pop();
1896 self.commands.push(Command::EndContainer);
1897 self.last_text_idx = None;
1898
1899 self.response_for(interaction_id)
1900 }
1901
1902 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1903 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1904 return Response::none();
1905 }
1906 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1907 let clicked = self
1908 .click_pos
1909 .map(|(mx, my)| {
1910 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1911 })
1912 .unwrap_or(false);
1913 let hovered = self
1914 .mouse_pos
1915 .map(|(mx, my)| {
1916 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1917 })
1918 .unwrap_or(false);
1919 Response {
1920 clicked,
1921 hovered,
1922 changed: false,
1923 focused: false,
1924 rect: *rect,
1925 }
1926 } else {
1927 Response::none()
1928 }
1929 }
1930
1931 pub fn is_group_hovered(&self, name: &str) -> bool {
1933 if let Some(pos) = self.mouse_pos {
1934 self.prev_group_rects.iter().any(|(n, rect)| {
1935 n == name
1936 && pos.0 >= rect.x
1937 && pos.0 < rect.x + rect.width
1938 && pos.1 >= rect.y
1939 && pos.1 < rect.y + rect.height
1940 })
1941 } else {
1942 false
1943 }
1944 }
1945
1946 pub fn is_group_focused(&self, name: &str) -> bool {
1948 if self.prev_focus_count == 0 {
1949 return false;
1950 }
1951 let focused_index = self.focus_index % self.prev_focus_count;
1952 self.prev_focus_groups
1953 .get(focused_index)
1954 .and_then(|group| group.as_deref())
1955 .map(|group| group == name)
1956 .unwrap_or(false)
1957 }
1958
1959 pub fn grow(&mut self, value: u16) -> &mut Self {
1964 if let Some(idx) = self.last_text_idx {
1965 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1966 *grow = value;
1967 }
1968 }
1969 self
1970 }
1971
1972 pub fn align(&mut self, align: Align) -> &mut Self {
1974 if let Some(idx) = self.last_text_idx {
1975 if let Command::Text {
1976 align: text_align, ..
1977 } = &mut self.commands[idx]
1978 {
1979 *text_align = align;
1980 }
1981 }
1982 self
1983 }
1984
1985 pub fn text_center(&mut self) -> &mut Self {
1989 self.align(Align::Center)
1990 }
1991
1992 pub fn text_right(&mut self) -> &mut Self {
1995 self.align(Align::End)
1996 }
1997
1998 pub fn w(&mut self, value: u32) -> &mut Self {
2005 self.modify_last_constraints(|c| {
2006 c.min_width = Some(value);
2007 c.max_width = Some(value);
2008 });
2009 self
2010 }
2011
2012 pub fn h(&mut self, value: u32) -> &mut Self {
2016 self.modify_last_constraints(|c| {
2017 c.min_height = Some(value);
2018 c.max_height = Some(value);
2019 });
2020 self
2021 }
2022
2023 pub fn min_w(&mut self, value: u32) -> &mut Self {
2025 self.modify_last_constraints(|c| c.min_width = Some(value));
2026 self
2027 }
2028
2029 pub fn max_w(&mut self, value: u32) -> &mut Self {
2031 self.modify_last_constraints(|c| c.max_width = Some(value));
2032 self
2033 }
2034
2035 pub fn min_h(&mut self, value: u32) -> &mut Self {
2037 self.modify_last_constraints(|c| c.min_height = Some(value));
2038 self
2039 }
2040
2041 pub fn max_h(&mut self, value: u32) -> &mut Self {
2043 self.modify_last_constraints(|c| c.max_height = Some(value));
2044 self
2045 }
2046
2047 pub fn m(&mut self, value: u32) -> &mut Self {
2051 self.modify_last_margin(|m| *m = Margin::all(value));
2052 self
2053 }
2054
2055 pub fn mx(&mut self, value: u32) -> &mut Self {
2057 self.modify_last_margin(|m| {
2058 m.left = value;
2059 m.right = value;
2060 });
2061 self
2062 }
2063
2064 pub fn my(&mut self, value: u32) -> &mut Self {
2066 self.modify_last_margin(|m| {
2067 m.top = value;
2068 m.bottom = value;
2069 });
2070 self
2071 }
2072
2073 pub fn mt(&mut self, value: u32) -> &mut Self {
2075 self.modify_last_margin(|m| m.top = value);
2076 self
2077 }
2078
2079 pub fn mr(&mut self, value: u32) -> &mut Self {
2081 self.modify_last_margin(|m| m.right = value);
2082 self
2083 }
2084
2085 pub fn mb(&mut self, value: u32) -> &mut Self {
2087 self.modify_last_margin(|m| m.bottom = value);
2088 self
2089 }
2090
2091 pub fn ml(&mut self, value: u32) -> &mut Self {
2093 self.modify_last_margin(|m| m.left = value);
2094 self
2095 }
2096
2097 pub fn spacer(&mut self) -> &mut Self {
2101 self.commands.push(Command::Spacer { grow: 1 });
2102 self.last_text_idx = None;
2103 self
2104 }
2105
2106 pub fn form(
2110 &mut self,
2111 state: &mut FormState,
2112 f: impl FnOnce(&mut Context, &mut FormState),
2113 ) -> &mut Self {
2114 let _ = self.col(|ui| {
2115 f(ui, state);
2116 });
2117 self
2118 }
2119
2120 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2124 let _ = self.col(|ui| {
2125 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2126 let _ = ui.text_input(&mut field.input);
2127 if let Some(error) = field.error.as_deref() {
2128 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2129 }
2130 });
2131 self
2132 }
2133
2134 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
2138 self.button(label)
2139 }
2140}
2141
2142fn wrap_tooltip_text(text: &str, max_width: usize) -> Vec<String> {
2143 let max_width = max_width.max(1);
2144 let mut lines = Vec::new();
2145
2146 for paragraph in text.lines() {
2147 if paragraph.trim().is_empty() {
2148 lines.push(String::new());
2149 continue;
2150 }
2151
2152 let mut current = String::new();
2153 let mut current_width = 0usize;
2154
2155 for word in paragraph.split_whitespace() {
2156 for chunk in split_word_for_width(word, max_width) {
2157 let chunk_width = UnicodeWidthStr::width(chunk.as_str());
2158
2159 if current.is_empty() {
2160 current = chunk;
2161 current_width = chunk_width;
2162 continue;
2163 }
2164
2165 if current_width + 1 + chunk_width <= max_width {
2166 current.push(' ');
2167 current.push_str(&chunk);
2168 current_width += 1 + chunk_width;
2169 } else {
2170 lines.push(std::mem::take(&mut current));
2171 current = chunk;
2172 current_width = chunk_width;
2173 }
2174 }
2175 }
2176
2177 if !current.is_empty() {
2178 lines.push(current);
2179 }
2180 }
2181
2182 if lines.is_empty() {
2183 lines.push(String::new());
2184 }
2185
2186 lines
2187}
2188
2189fn split_word_for_width(word: &str, max_width: usize) -> Vec<String> {
2190 let mut chunks = Vec::new();
2191 let mut current = String::new();
2192 let mut current_width = 0usize;
2193
2194 for ch in word.chars() {
2195 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
2196 if !current.is_empty() && current_width + ch_width > max_width {
2197 chunks.push(std::mem::take(&mut current));
2198 current_width = 0;
2199 }
2200 current.push(ch);
2201 current_width += ch_width;
2202
2203 if current_width >= max_width {
2204 chunks.push(std::mem::take(&mut current));
2205 current_width = 0;
2206 }
2207 }
2208
2209 if !current.is_empty() {
2210 chunks.push(current);
2211 }
2212
2213 if chunks.is_empty() {
2214 chunks.push(String::new());
2215 }
2216
2217 chunks
2218}
2219
2220fn glyph_8x8(ch: char) -> [u8; 8] {
2221 if ch.is_ascii() {
2222 let code = ch as u8;
2223 if (32..=126).contains(&code) {
2224 return FONT_8X8_PRINTABLE[(code - 32) as usize];
2225 }
2226 }
2227
2228 FONT_8X8_PRINTABLE[(b'?' - 32) as usize]
2229}
2230
2231const FONT_8X8_PRINTABLE: [[u8; 8]; 95] = [
2232 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2233 [0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00],
2234 [0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2235 [0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00],
2236 [0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00],
2237 [0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00],
2238 [0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00],
2239 [0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00],
2240 [0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00],
2241 [0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00],
2242 [0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00],
2243 [0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00],
2244 [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06],
2245 [0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00],
2246 [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00],
2247 [0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00],
2248 [0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00],
2249 [0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00],
2250 [0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00],
2251 [0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00],
2252 [0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00],
2253 [0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00],
2254 [0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00],
2255 [0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00],
2256 [0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00],
2257 [0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00],
2258 [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00],
2259 [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06],
2260 [0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00],
2261 [0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00],
2262 [0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00],
2263 [0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00],
2264 [0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00],
2265 [0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00],
2266 [0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00],
2267 [0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00],
2268 [0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00],
2269 [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00],
2270 [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00],
2271 [0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00],
2272 [0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00],
2273 [0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2274 [0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00],
2275 [0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00],
2276 [0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00],
2277 [0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00],
2278 [0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00],
2279 [0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00],
2280 [0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00],
2281 [0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00],
2282 [0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00],
2283 [0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00],
2284 [0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2285 [0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00],
2286 [0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00],
2287 [0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00],
2288 [0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00],
2289 [0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00],
2290 [0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00],
2291 [0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00],
2292 [0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00],
2293 [0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00],
2294 [0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00],
2295 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF],
2296 [0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00],
2297 [0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00],
2298 [0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00],
2299 [0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00],
2300 [0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00],
2301 [0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00],
2302 [0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00],
2303 [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F],
2304 [0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00],
2305 [0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2306 [0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E],
2307 [0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00],
2308 [0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2309 [0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00],
2310 [0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00],
2311 [0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00],
2312 [0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F],
2313 [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78],
2314 [0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00],
2315 [0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00],
2316 [0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00],
2317 [0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00],
2318 [0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00],
2319 [0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00],
2320 [0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00],
2321 [0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F],
2322 [0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00],
2323 [0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00],
2324 [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00],
2325 [0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00],
2326 [0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2327];
2328
2329const KEYWORDS: &[&str] = &[
2330 "fn",
2331 "let",
2332 "mut",
2333 "pub",
2334 "use",
2335 "impl",
2336 "struct",
2337 "enum",
2338 "trait",
2339 "type",
2340 "const",
2341 "static",
2342 "if",
2343 "else",
2344 "match",
2345 "for",
2346 "while",
2347 "loop",
2348 "return",
2349 "break",
2350 "continue",
2351 "where",
2352 "self",
2353 "super",
2354 "crate",
2355 "mod",
2356 "async",
2357 "await",
2358 "move",
2359 "ref",
2360 "in",
2361 "as",
2362 "true",
2363 "false",
2364 "Some",
2365 "None",
2366 "Ok",
2367 "Err",
2368 "Self",
2369 "def",
2370 "class",
2371 "import",
2372 "from",
2373 "pass",
2374 "lambda",
2375 "yield",
2376 "with",
2377 "try",
2378 "except",
2379 "raise",
2380 "finally",
2381 "elif",
2382 "del",
2383 "global",
2384 "nonlocal",
2385 "assert",
2386 "is",
2387 "not",
2388 "and",
2389 "or",
2390 "function",
2391 "var",
2392 "const",
2393 "export",
2394 "default",
2395 "switch",
2396 "case",
2397 "throw",
2398 "catch",
2399 "typeof",
2400 "instanceof",
2401 "new",
2402 "delete",
2403 "void",
2404 "this",
2405 "null",
2406 "undefined",
2407 "func",
2408 "package",
2409 "defer",
2410 "go",
2411 "chan",
2412 "select",
2413 "range",
2414 "map",
2415 "interface",
2416 "fallthrough",
2417 "nil",
2418];
2419
2420fn render_tree_sitter_lines(ui: &mut Context, lines: &[Vec<(String, crate::style::Style)>]) {
2421 for segs in lines {
2422 if segs.is_empty() {
2423 ui.text(" ");
2424 } else {
2425 ui.line(|ui| {
2426 for (text, style) in segs {
2427 ui.styled(text, *style);
2428 }
2429 });
2430 }
2431 }
2432}
2433
2434fn render_highlighted_line(ui: &mut Context, line: &str) {
2435 let theme = ui.theme;
2436 let is_light = matches!(
2437 theme.bg,
2438 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
2439 );
2440 let keyword_color = if is_light {
2441 Color::Rgb(166, 38, 164)
2442 } else {
2443 Color::Rgb(198, 120, 221)
2444 };
2445 let string_color = if is_light {
2446 Color::Rgb(80, 161, 79)
2447 } else {
2448 Color::Rgb(152, 195, 121)
2449 };
2450 let comment_color = theme.text_dim;
2451 let number_color = if is_light {
2452 Color::Rgb(152, 104, 1)
2453 } else {
2454 Color::Rgb(209, 154, 102)
2455 };
2456 let fn_color = if is_light {
2457 Color::Rgb(64, 120, 242)
2458 } else {
2459 Color::Rgb(97, 175, 239)
2460 };
2461 let macro_color = if is_light {
2462 Color::Rgb(1, 132, 188)
2463 } else {
2464 Color::Rgb(86, 182, 194)
2465 };
2466
2467 let trimmed = line.trim_start();
2468 let indent = &line[..line.len() - trimmed.len()];
2469 if !indent.is_empty() {
2470 ui.text(indent);
2471 }
2472
2473 if trimmed.starts_with("//") {
2474 ui.text(trimmed).fg(comment_color).italic();
2475 return;
2476 }
2477
2478 let mut pos = 0;
2479
2480 while pos < trimmed.len() {
2481 let ch = trimmed.as_bytes()[pos];
2482
2483 if ch == b'"' {
2484 if let Some(end) = trimmed[pos + 1..].find('"') {
2485 let s = &trimmed[pos..pos + end + 2];
2486 ui.text(s).fg(string_color);
2487 pos += end + 2;
2488 continue;
2489 }
2490 }
2491
2492 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
2493 {
2494 let end = trimmed[pos..]
2495 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
2496 .map_or(trimmed.len(), |e| pos + e);
2497 ui.text(&trimmed[pos..end]).fg(number_color);
2498 pos = end;
2499 continue;
2500 }
2501
2502 if ch.is_ascii_alphabetic() || ch == b'_' {
2503 let end = trimmed[pos..]
2504 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
2505 .map_or(trimmed.len(), |e| pos + e);
2506 let word = &trimmed[pos..end];
2507
2508 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
2509 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
2510 pos = end + 1;
2511 } else if end < trimmed.len()
2512 && trimmed.as_bytes()[end] == b'('
2513 && !KEYWORDS.contains(&word)
2514 {
2515 ui.text(word).fg(fn_color);
2516 pos = end;
2517 } else if KEYWORDS.contains(&word) {
2518 ui.text(word).fg(keyword_color);
2519 pos = end;
2520 } else {
2521 ui.text(word);
2522 pos = end;
2523 }
2524 continue;
2525 }
2526
2527 let end = trimmed[pos..]
2528 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
2529 .map_or(trimmed.len(), |e| pos + e);
2530 ui.text(&trimmed[pos..end]);
2531 pos = end;
2532 }
2533}
2534
2535fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
2536 let expected = (width as usize) * (height as usize) * 4;
2537 if data.len() >= expected {
2538 return data[..expected].to_vec();
2539 }
2540 let mut buf = Vec::with_capacity(expected);
2541 buf.extend_from_slice(data);
2542 buf.resize(expected, 0);
2543 buf
2544}
2545
2546fn base64_encode(data: &[u8]) -> String {
2547 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2548 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
2549 for chunk in data.chunks(3) {
2550 let b0 = chunk[0] as u32;
2551 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2552 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2553 let triple = (b0 << 16) | (b1 << 8) | b2;
2554 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
2555 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
2556 if chunk.len() > 1 {
2557 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
2558 } else {
2559 result.push('=');
2560 }
2561 if chunk.len() > 2 {
2562 result.push(CHARS[(triple & 0x3F) as usize] as char);
2563 } else {
2564 result.push('=');
2565 }
2566 }
2567 result
2568}
2569
2570fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
2571 let mut chunks = Vec::new();
2572 let bytes = encoded.as_bytes();
2573 let mut offset = 0;
2574 while offset < bytes.len() {
2575 let end = (offset + chunk_size).min(bytes.len());
2576 chunks.push(&encoded[offset..end]);
2577 offset = end;
2578 }
2579 if chunks.is_empty() {
2580 chunks.push("");
2581 }
2582 chunks
2583}
2584
2585fn terminal_supports_sixel() -> bool {
2586 let force = std::env::var("SLT_FORCE_SIXEL")
2587 .ok()
2588 .map(|v| v.to_ascii_lowercase())
2589 .unwrap_or_default();
2590 if matches!(force.as_str(), "1" | "true" | "yes" | "on") {
2591 return true;
2592 }
2593
2594 let term = std::env::var("TERM")
2595 .ok()
2596 .map(|v| v.to_ascii_lowercase())
2597 .unwrap_or_default();
2598 let term_program = std::env::var("TERM_PROGRAM")
2599 .ok()
2600 .map(|v| v.to_ascii_lowercase())
2601 .unwrap_or_default();
2602
2603 term.contains("sixel")
2604 || term.contains("mlterm")
2605 || term.contains("xterm")
2606 || term.contains("foot")
2607 || term_program.contains("foot")
2608}
2609
2610#[cfg(test)]
2611mod tests {
2612 use super::*;
2613 use crate::TestBackend;
2614 use std::time::Duration;
2615
2616 #[test]
2617 fn gradient_text_renders_content() {
2618 let mut backend = TestBackend::new(20, 4);
2619 backend.render(|ui| {
2620 ui.text("ABCD").gradient(Color::Red, Color::Blue);
2621 });
2622
2623 backend.assert_contains("ABCD");
2624 }
2625
2626 #[test]
2627 fn big_text_renders_half_block_grid() {
2628 let mut backend = TestBackend::new(16, 4);
2629 backend.render(|ui| {
2630 let _ = ui.big_text("A");
2631 });
2632
2633 let output = backend.to_string();
2634 assert!(
2636 output.contains('▀') || output.contains('▄') || output.contains('█'),
2637 "output should contain half-block glyphs: {output:?}"
2638 );
2639 }
2640
2641 #[test]
2642 fn timer_display_formats_minutes_seconds_centis() {
2643 let mut backend = TestBackend::new(20, 4);
2644 backend.render(|ui| {
2645 ui.timer_display(Duration::from_secs(83) + Duration::from_millis(450));
2646 });
2647
2648 backend.assert_contains("01:23.45");
2649 }
2650}