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 {
270 let apply_group_style = self
271 .group_stack
272 .last()
273 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
274 .unwrap_or(false);
275 if apply_group_style {
276 self.modify_last_style(|s| s.fg = Some(color));
277 }
278 self
279 }
280
281 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
283 let apply_group_style = self
284 .group_stack
285 .last()
286 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
287 .unwrap_or(false);
288 if apply_group_style {
289 self.modify_last_style(|s| s.bg = Some(color));
290 }
291 self
292 }
293
294 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
299 self.commands.push(Command::Text {
300 content: s.into(),
301 style,
302 grow: 0,
303 align: Align::Start,
304 wrap: false,
305 truncate: false,
306 margin: Margin::default(),
307 constraints: Constraints::default(),
308 });
309 self.last_text_idx = Some(self.commands.len() - 1);
310 self
311 }
312
313 pub fn big_text(&mut self, s: impl Into<String>) -> Response {
315 let text = s.into();
316 let glyphs: Vec<[u8; 8]> = text.chars().map(glyph_8x8).collect();
317 let total_width = (glyphs.len() as u32).saturating_mul(8);
318 let on_color = self.theme.primary;
319
320 self.container().w(total_width).h(4).draw(move |buf, rect| {
321 if rect.width == 0 || rect.height == 0 {
322 return;
323 }
324
325 for (glyph_idx, glyph) in glyphs.iter().enumerate() {
326 let base_x = rect.x + (glyph_idx as u32) * 8;
327 if base_x >= rect.right() {
328 break;
329 }
330
331 for pair in 0..4usize {
332 let y = rect.y + pair as u32;
333 if y >= rect.bottom() {
334 continue;
335 }
336
337 let upper = glyph[pair * 2];
338 let lower = glyph[pair * 2 + 1];
339
340 for bit in 0..8u32 {
341 let x = base_x + bit;
342 if x >= rect.right() {
343 break;
344 }
345
346 let mask = 1u8 << (bit as u8);
347 let upper_on = (upper & mask) != 0;
348 let lower_on = (lower & mask) != 0;
349 let (ch, fg, bg) = match (upper_on, lower_on) {
350 (true, true) => ('█', on_color, on_color),
351 (true, false) => ('▀', on_color, Color::Reset),
352 (false, true) => ('▄', on_color, Color::Reset),
353 (false, false) => (' ', Color::Reset, Color::Reset),
354 };
355 buf.set_char(x, y, ch, Style::new().fg(fg).bg(bg));
356 }
357 }
358 }
359 });
360
361 Response::none()
362 }
363
364 pub fn image(&mut self, img: &HalfBlockImage) -> Response {
386 let width = img.width;
387 let height = img.height;
388
389 let _ = self.container().w(width).h(height).gap(0).col(|ui| {
390 for row in 0..height {
391 let _ = ui.container().gap(0).row(|ui| {
392 for col in 0..width {
393 let idx = (row * width + col) as usize;
394 if let Some(&(upper, lower)) = img.pixels.get(idx) {
395 ui.styled("▀", Style::new().fg(upper).bg(lower));
396 }
397 }
398 });
399 }
400 });
401
402 Response::none()
403 }
404
405 pub fn kitty_image(
421 &mut self,
422 rgba: &[u8],
423 pixel_width: u32,
424 pixel_height: u32,
425 cols: u32,
426 rows: u32,
427 ) -> Response {
428 let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
429 let encoded = base64_encode(&rgba);
430 let pw = pixel_width;
431 let ph = pixel_height;
432 let c = cols;
433 let r = rows;
434
435 self.container().w(cols).h(rows).draw(move |buf, rect| {
436 let chunks = split_base64(&encoded, 4096);
437 let mut all_sequences = String::new();
438
439 for (i, chunk) in chunks.iter().enumerate() {
440 let more = if i < chunks.len() - 1 { 1 } else { 0 };
441 if i == 0 {
442 all_sequences.push_str(&format!(
443 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
444 pw, ph, c, r, more, chunk
445 ));
446 } else {
447 all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
448 }
449 }
450
451 buf.raw_sequence(rect.x, rect.y, all_sequences);
452 });
453 Response::none()
454 }
455
456 pub fn kitty_image_fit(
465 &mut self,
466 rgba: &[u8],
467 src_width: u32,
468 src_height: u32,
469 cols: u32,
470 ) -> Response {
471 let rows = if src_width == 0 {
472 1
473 } else {
474 ((cols as f64 * src_height as f64 * 8.0) / (src_width as f64 * 16.0))
475 .ceil()
476 .max(1.0) as u32
477 };
478 let rgba = normalize_rgba(rgba, src_width, src_height);
479 let sw = src_width;
480 let sh = src_height;
481 let c = cols;
482 let r = rows;
483
484 self.container().w(cols).h(rows).draw(move |buf, rect| {
485 if rect.width == 0 || rect.height == 0 {
486 return;
487 }
488 let encoded = base64_encode(&rgba);
489 let chunks = split_base64(&encoded, 4096);
490 let mut seq = String::new();
491 for (i, chunk) in chunks.iter().enumerate() {
492 let more = if i < chunks.len() - 1 { 1 } else { 0 };
493 if i == 0 {
494 seq.push_str(&format!(
495 "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
496 sw, sh, c, r, more, chunk
497 ));
498 } else {
499 seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
500 }
501 }
502 buf.raw_sequence(rect.x, rect.y, seq);
503 });
504 Response::none()
505 }
506
507 #[cfg(feature = "crossterm")]
509 pub fn sixel_image(
510 &mut self,
511 rgba: &[u8],
512 pixel_w: u32,
513 pixel_h: u32,
514 cols: u32,
515 rows: u32,
516 ) -> Response {
517 let sixel_supported = self.is_real_terminal && terminal_supports_sixel();
518 if !sixel_supported {
519 self.container().w(cols).h(rows).draw(|buf, rect| {
520 if rect.width == 0 || rect.height == 0 {
521 return;
522 }
523 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
524 });
525 return Response::none();
526 }
527
528 let rgba = normalize_rgba(rgba, pixel_w, pixel_h);
529 let encoded = crate::sixel::encode_sixel(&rgba, pixel_w, pixel_h, 256);
530
531 if encoded.is_empty() {
532 self.container().w(cols).h(rows).draw(|buf, rect| {
533 if rect.width == 0 || rect.height == 0 {
534 return;
535 }
536 buf.set_string(rect.x, rect.y, "[sixel empty]", Style::new());
537 });
538 return Response::none();
539 }
540
541 self.container().w(cols).h(rows).draw(move |buf, rect| {
542 if rect.width == 0 || rect.height == 0 {
543 return;
544 }
545 buf.raw_sequence(rect.x, rect.y, encoded);
546 });
547 Response::none()
548 }
549
550 #[cfg(not(feature = "crossterm"))]
552 pub fn sixel_image(
553 &mut self,
554 _rgba: &[u8],
555 _pixel_w: u32,
556 _pixel_h: u32,
557 cols: u32,
558 rows: u32,
559 ) -> Response {
560 self.container().w(cols).h(rows).draw(|buf, rect| {
561 if rect.width == 0 || rect.height == 0 {
562 return;
563 }
564 buf.set_string(rect.x, rect.y, "[sixel unsupported]", Style::new());
565 });
566 Response::none()
567 }
568
569 pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
585 if state.streaming {
586 state.cursor_tick = state.cursor_tick.wrapping_add(1);
587 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
588 }
589
590 if state.content.is_empty() && state.streaming {
591 let cursor = if state.cursor_visible { "▌" } else { " " };
592 let primary = self.theme.primary;
593 self.text(cursor).fg(primary);
594 return Response::none();
595 }
596
597 if !state.content.is_empty() {
598 if state.streaming && state.cursor_visible {
599 self.text_wrap(format!("{}▌", state.content));
600 } else {
601 self.text_wrap(&state.content);
602 }
603 }
604
605 Response::none()
606 }
607
608 pub fn streaming_markdown(
626 &mut self,
627 state: &mut crate::widgets::StreamingMarkdownState,
628 ) -> Response {
629 if state.streaming {
630 state.cursor_tick = state.cursor_tick.wrapping_add(1);
631 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
632 }
633
634 if state.content.is_empty() && state.streaming {
635 let cursor = if state.cursor_visible { "▌" } else { " " };
636 let primary = self.theme.primary;
637 self.text(cursor).fg(primary);
638 return Response::none();
639 }
640
641 let show_cursor = state.streaming && state.cursor_visible;
642 let trailing_newline = state.content.ends_with('\n');
643 let lines: Vec<&str> = state.content.lines().collect();
644 let last_line_index = lines.len().saturating_sub(1);
645
646 self.commands.push(Command::BeginContainer {
647 direction: Direction::Column,
648 gap: 0,
649 align: Align::Start,
650 align_self: None,
651 justify: Justify::Start,
652 border: None,
653 border_sides: BorderSides::all(),
654 border_style: Style::new().fg(self.theme.border),
655 bg_color: None,
656 padding: Padding::default(),
657 margin: Margin::default(),
658 constraints: Constraints::default(),
659 title: None,
660 grow: 0,
661 group_name: None,
662 });
663 self.interaction_count += 1;
664
665 let text_style = Style::new().fg(self.theme.text);
666 let bold_style = Style::new().fg(self.theme.text).bold();
667 let code_style = Style::new().fg(self.theme.accent);
668 let border_style = Style::new().fg(self.theme.border).dim();
669
670 let mut in_code_block = false;
671 let mut code_block_lang = String::new();
672
673 for (idx, line) in lines.iter().enumerate() {
674 let line = *line;
675 let trimmed = line.trim();
676 let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
677 let cursor = if append_cursor { "▌" } else { "" };
678
679 if in_code_block {
680 if trimmed.starts_with("```") {
681 in_code_block = false;
682 code_block_lang.clear();
683 let mut line = String::from(" └────");
684 line.push_str(cursor);
685 self.styled(line, border_style);
686 } else {
687 self.line(|ui| {
688 ui.text(" ");
689 render_highlighted_line(ui, line);
690 if !cursor.is_empty() {
691 ui.styled(cursor, Style::new().fg(ui.theme.primary));
692 }
693 });
694 }
695 continue;
696 }
697
698 if trimmed.is_empty() {
699 if append_cursor {
700 self.styled("▌", Style::new().fg(self.theme.primary));
701 } else {
702 self.text(" ");
703 }
704 continue;
705 }
706
707 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
708 let mut line = "─".repeat(40);
709 line.push_str(cursor);
710 self.styled(line, border_style);
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.accent));
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.secondary));
727 continue;
728 }
729
730 if let Some(heading) = trimmed.strip_prefix("# ") {
731 let mut line = String::with_capacity(heading.len() + cursor.len());
732 line.push_str(heading);
733 line.push_str(cursor);
734 self.styled(line, Style::new().bold().fg(self.theme.primary));
735 continue;
736 }
737
738 if let Some(code) = trimmed.strip_prefix("```") {
739 in_code_block = true;
740 code_block_lang = code.trim().to_string();
741 let label = if code_block_lang.is_empty() {
742 "code".to_string()
743 } else {
744 let mut label = String::from("code:");
745 label.push_str(&code_block_lang);
746 label
747 };
748 let mut line = String::with_capacity(5 + label.len() + cursor.len());
749 line.push_str(" ┌─");
750 line.push_str(&label);
751 line.push('─');
752 line.push_str(cursor);
753 self.styled(line, border_style);
754 continue;
755 }
756
757 if let Some(item) = trimmed
758 .strip_prefix("- ")
759 .or_else(|| trimmed.strip_prefix("* "))
760 {
761 let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
762 if segs.len() <= 1 {
763 let mut line = String::with_capacity(4 + item.len() + cursor.len());
764 line.push_str(" • ");
765 line.push_str(item);
766 line.push_str(cursor);
767 self.styled(line, text_style);
768 } else {
769 self.line(|ui| {
770 ui.styled(" • ", text_style);
771 for (s, st) in segs {
772 ui.styled(s, st);
773 }
774 if append_cursor {
775 ui.styled("▌", Style::new().fg(ui.theme.primary));
776 }
777 });
778 }
779 continue;
780 }
781
782 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
783 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
784 if parts.len() == 2 {
785 let segs =
786 Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
787 if segs.len() <= 1 {
788 let mut line = String::with_capacity(
789 4 + parts[0].len() + parts[1].len() + cursor.len(),
790 );
791 line.push_str(" ");
792 line.push_str(parts[0]);
793 line.push_str(". ");
794 line.push_str(parts[1]);
795 line.push_str(cursor);
796 self.styled(line, text_style);
797 } else {
798 self.line(|ui| {
799 let mut prefix = String::with_capacity(4 + parts[0].len());
800 prefix.push_str(" ");
801 prefix.push_str(parts[0]);
802 prefix.push_str(". ");
803 ui.styled(prefix, text_style);
804 for (s, st) in segs {
805 ui.styled(s, st);
806 }
807 if append_cursor {
808 ui.styled("▌", Style::new().fg(ui.theme.primary));
809 }
810 });
811 }
812 } else {
813 let mut line = String::with_capacity(trimmed.len() + cursor.len());
814 line.push_str(trimmed);
815 line.push_str(cursor);
816 self.styled(line, text_style);
817 }
818 continue;
819 }
820
821 let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
822 if segs.len() <= 1 {
823 let mut line = String::with_capacity(trimmed.len() + cursor.len());
824 line.push_str(trimmed);
825 line.push_str(cursor);
826 self.styled(line, text_style);
827 } else {
828 self.line(|ui| {
829 for (s, st) in segs {
830 ui.styled(s, st);
831 }
832 if append_cursor {
833 ui.styled("▌", Style::new().fg(ui.theme.primary));
834 }
835 });
836 }
837 }
838
839 if show_cursor && trailing_newline {
840 if in_code_block {
841 self.styled(" ▌", code_style);
842 } else {
843 self.styled("▌", Style::new().fg(self.theme.primary));
844 }
845 }
846
847 state.in_code_block = in_code_block;
848 state.code_block_lang = code_block_lang;
849
850 self.commands.push(Command::EndContainer);
851 self.last_text_idx = None;
852 Response::none()
853 }
854
855 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
870 let old_action = state.action;
871 let theme = self.theme;
872 let _ = self.bordered(Border::Rounded).col(|ui| {
873 let _ = ui.row(|ui| {
874 ui.text("⚡").fg(theme.warning);
875 ui.text(&state.tool_name).bold().fg(theme.primary);
876 });
877 ui.text(&state.description).dim();
878
879 if state.action == ApprovalAction::Pending {
880 let _ = ui.row(|ui| {
881 if ui.button("✓ Approve").clicked {
882 state.action = ApprovalAction::Approved;
883 }
884 if ui.button("✗ Reject").clicked {
885 state.action = ApprovalAction::Rejected;
886 }
887 });
888 } else {
889 let (label, color) = match state.action {
890 ApprovalAction::Approved => ("✓ Approved", theme.success),
891 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
892 ApprovalAction::Pending => unreachable!(),
893 };
894 ui.text(label).fg(color).bold();
895 }
896 });
897
898 Response {
899 changed: state.action != old_action,
900 ..Response::none()
901 }
902 }
903
904 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
917 if items.is_empty() {
918 return Response::none();
919 }
920
921 let theme = self.theme;
922 let total: usize = items.iter().map(|item| item.tokens).sum();
923
924 let _ = self.container().row(|ui| {
925 ui.text("📎").dim();
926 for item in items {
927 let token_count = format_token_count(item.tokens);
928 let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
929 line.push_str(&item.label);
930 line.push_str(" (");
931 line.push_str(&token_count);
932 line.push(')');
933 ui.text(line).fg(theme.secondary);
934 }
935 ui.spacer();
936 let total_text = format_token_count(total);
937 let mut line = String::with_capacity(2 + total_text.len());
938 line.push_str("Σ ");
939 line.push_str(&total_text);
940 ui.text(line).dim();
941 });
942
943 Response::none()
944 }
945
946 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
948 use crate::widgets::AlertLevel;
949
950 let theme = self.theme;
951 let (icon, color) = match level {
952 AlertLevel::Info => ("ℹ", theme.accent),
953 AlertLevel::Success => ("✓", theme.success),
954 AlertLevel::Warning => ("⚠", theme.warning),
955 AlertLevel::Error => ("✕", theme.error),
956 };
957
958 let focused = self.register_focusable();
959 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
960
961 let mut response = self.container().col(|ui| {
962 ui.line(|ui| {
963 let mut icon_text = String::with_capacity(icon.len() + 2);
964 icon_text.push(' ');
965 icon_text.push_str(icon);
966 icon_text.push(' ');
967 ui.text(icon_text).fg(color).bold();
968 ui.text(message).grow(1);
969 ui.text(" [×] ").dim();
970 });
971 });
972 response.focused = focused;
973 if key_dismiss {
974 response.clicked = true;
975 }
976
977 response
978 }
979
980 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
994 let focused = self.register_focusable();
995 let mut is_yes = *result;
996 let mut clicked = false;
997
998 if focused {
999 let mut consumed_indices = Vec::new();
1000 for (i, event) in self.events.iter().enumerate() {
1001 if let Event::Key(key) = event {
1002 if key.kind != KeyEventKind::Press {
1003 continue;
1004 }
1005
1006 match key.code {
1007 KeyCode::Char('y') => {
1008 is_yes = true;
1009 *result = true;
1010 clicked = true;
1011 consumed_indices.push(i);
1012 }
1013 KeyCode::Char('n') => {
1014 is_yes = false;
1015 *result = false;
1016 clicked = true;
1017 consumed_indices.push(i);
1018 }
1019 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
1020 is_yes = !is_yes;
1021 *result = is_yes;
1022 consumed_indices.push(i);
1023 }
1024 KeyCode::Enter => {
1025 *result = is_yes;
1026 clicked = true;
1027 consumed_indices.push(i);
1028 }
1029 _ => {}
1030 }
1031 }
1032 }
1033
1034 for idx in consumed_indices {
1035 self.consumed[idx] = true;
1036 }
1037 }
1038
1039 let yes_style = if is_yes {
1040 if focused {
1041 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
1042 } else {
1043 Style::new().fg(self.theme.success).bold()
1044 }
1045 } else {
1046 Style::new().fg(self.theme.text_dim)
1047 };
1048 let no_style = if !is_yes {
1049 if focused {
1050 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
1051 } else {
1052 Style::new().fg(self.theme.error).bold()
1053 }
1054 } else {
1055 Style::new().fg(self.theme.text_dim)
1056 };
1057
1058 let q_width = UnicodeWidthStr::width(question) as u32;
1059 let mut response = self.row(|ui| {
1060 ui.text(question);
1061 ui.text(" ");
1062 ui.styled("[Yes]", yes_style);
1063 ui.text(" ");
1064 ui.styled("[No]", no_style);
1065 });
1066
1067 if !clicked && response.clicked {
1068 if let Some((mx, _)) = self.click_pos {
1069 let yes_start = response.rect.x + q_width + 1;
1070 let yes_end = yes_start + 5;
1071 let no_start = yes_end + 1;
1072 if mx >= yes_start && mx < yes_end {
1073 is_yes = true;
1074 *result = true;
1075 clicked = true;
1076 } else if mx >= no_start {
1077 is_yes = false;
1078 *result = false;
1079 clicked = true;
1080 }
1081 }
1082 }
1083
1084 response.focused = focused;
1085 response.clicked = clicked;
1086 response.changed = clicked;
1087 let _ = is_yes;
1088 response
1089 }
1090
1091 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
1093 self.breadcrumb_with(segments, " › ")
1094 }
1095
1096 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
1098 let theme = self.theme;
1099 let last_idx = segments.len().saturating_sub(1);
1100 let mut clicked_idx: Option<usize> = None;
1101
1102 let _ = self.row(|ui| {
1103 for (i, segment) in segments.iter().enumerate() {
1104 let is_last = i == last_idx;
1105 if is_last {
1106 ui.text(*segment).bold();
1107 } else {
1108 let focused = ui.register_focusable();
1109 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
1110 let resp = ui.interaction();
1111 let color = if resp.hovered || focused {
1112 theme.accent
1113 } else {
1114 theme.primary
1115 };
1116 ui.text(*segment).fg(color).underline();
1117 if resp.clicked || pressed {
1118 clicked_idx = Some(i);
1119 }
1120 ui.text(separator).dim();
1121 }
1122 }
1123 });
1124
1125 clicked_idx
1126 }
1127
1128 pub fn accordion(
1130 &mut self,
1131 title: &str,
1132 open: &mut bool,
1133 f: impl FnOnce(&mut Context),
1134 ) -> Response {
1135 let theme = self.theme;
1136 let focused = self.register_focusable();
1137 let old_open = *open;
1138
1139 if focused && self.key_code(KeyCode::Enter) {
1140 *open = !*open;
1141 }
1142
1143 let icon = if *open { "▾" } else { "▸" };
1144 let title_color = if focused { theme.primary } else { theme.text };
1145
1146 let mut response = self.container().col(|ui| {
1147 ui.line(|ui| {
1148 ui.text(icon).fg(title_color);
1149 let mut title_text = String::with_capacity(1 + title.len());
1150 title_text.push(' ');
1151 title_text.push_str(title);
1152 ui.text(title_text).bold().fg(title_color);
1153 });
1154 });
1155
1156 if response.clicked {
1157 *open = !*open;
1158 }
1159
1160 if *open {
1161 let _ = self.container().pl(2).col(f);
1162 }
1163
1164 response.focused = focused;
1165 response.changed = *open != old_open;
1166 response
1167 }
1168
1169 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
1171 let max_key_width = items
1172 .iter()
1173 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
1174 .max()
1175 .unwrap_or(0);
1176
1177 let _ = self.col(|ui| {
1178 for (key, value) in items {
1179 ui.line(|ui| {
1180 let padded = format!("{:>width$}", key, width = max_key_width);
1181 ui.text(padded).dim();
1182 ui.text(" ");
1183 ui.text(*value);
1184 });
1185 }
1186 });
1187
1188 Response::none()
1189 }
1190
1191 pub fn divider_text(&mut self, label: &str) -> Response {
1193 let w = self.width();
1194 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
1195 let pad = 1u32;
1196 let left_len = 4u32;
1197 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
1198 let left: String = "─".repeat(left_len as usize);
1199 let right: String = "─".repeat(right_len as usize);
1200 let theme = self.theme;
1201 self.line(|ui| {
1202 ui.text(&left).fg(theme.border);
1203 let mut label_text = String::with_capacity(label.len() + 2);
1204 label_text.push(' ');
1205 label_text.push_str(label);
1206 label_text.push(' ');
1207 ui.text(label_text).fg(theme.text);
1208 ui.text(&right).fg(theme.border);
1209 });
1210
1211 Response::none()
1212 }
1213
1214 pub fn badge(&mut self, label: &str) -> Response {
1216 let theme = self.theme;
1217 self.badge_colored(label, theme.primary)
1218 }
1219
1220 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
1222 let fg = Color::contrast_fg(color);
1223 let mut label_text = String::with_capacity(label.len() + 2);
1224 label_text.push(' ');
1225 label_text.push_str(label);
1226 label_text.push(' ');
1227 self.text(label_text).fg(fg).bg(color);
1228
1229 Response::none()
1230 }
1231
1232 pub fn key_hint(&mut self, key: &str) -> Response {
1234 let theme = self.theme;
1235 let mut key_text = String::with_capacity(key.len() + 2);
1236 key_text.push(' ');
1237 key_text.push_str(key);
1238 key_text.push(' ');
1239 self.text(key_text).reversed().fg(theme.text_dim);
1240
1241 Response::none()
1242 }
1243
1244 pub fn stat(&mut self, label: &str, value: &str) -> Response {
1246 let _ = self.col(|ui| {
1247 ui.text(label).dim();
1248 ui.text(value).bold();
1249 });
1250
1251 Response::none()
1252 }
1253
1254 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
1256 let _ = self.col(|ui| {
1257 ui.text(label).dim();
1258 ui.text(value).bold().fg(color);
1259 });
1260
1261 Response::none()
1262 }
1263
1264 pub fn stat_trend(
1266 &mut self,
1267 label: &str,
1268 value: &str,
1269 trend: crate::widgets::Trend,
1270 ) -> Response {
1271 let theme = self.theme;
1272 let (arrow, color) = match trend {
1273 crate::widgets::Trend::Up => ("↑", theme.success),
1274 crate::widgets::Trend::Down => ("↓", theme.error),
1275 };
1276 let _ = self.col(|ui| {
1277 ui.text(label).dim();
1278 ui.line(|ui| {
1279 ui.text(value).bold();
1280 let mut arrow_text = String::with_capacity(1 + arrow.len());
1281 arrow_text.push(' ');
1282 arrow_text.push_str(arrow);
1283 ui.text(arrow_text).fg(color);
1284 });
1285 });
1286
1287 Response::none()
1288 }
1289
1290 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
1292 let _ = self.container().center().col(|ui| {
1293 ui.text(title).align(Align::Center);
1294 ui.text(description).dim().align(Align::Center);
1295 });
1296
1297 Response::none()
1298 }
1299
1300 pub fn empty_state_action(
1302 &mut self,
1303 title: &str,
1304 description: &str,
1305 action_label: &str,
1306 ) -> Response {
1307 let mut clicked = false;
1308 let _ = self.container().center().col(|ui| {
1309 ui.text(title).align(Align::Center);
1310 ui.text(description).dim().align(Align::Center);
1311 if ui.button(action_label).clicked {
1312 clicked = true;
1313 }
1314 });
1315
1316 Response {
1317 clicked,
1318 changed: clicked,
1319 ..Response::none()
1320 }
1321 }
1322
1323 pub fn code_block(&mut self, code: &str) -> Response {
1325 self.code_block_lang(code, "")
1326 }
1327
1328 pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
1330 let theme = self.theme;
1331 let highlighted: Option<Vec<Vec<(String, Style)>>> =
1332 crate::syntax::highlight_code(code, lang, &theme);
1333 let _ = self
1334 .bordered(Border::Rounded)
1335 .bg(theme.surface)
1336 .pad(1)
1337 .col(|ui| {
1338 if let Some(ref lines) = highlighted {
1339 render_tree_sitter_lines(ui, lines);
1340 } else {
1341 for line in code.lines() {
1342 render_highlighted_line(ui, line);
1343 }
1344 }
1345 });
1346
1347 Response::none()
1348 }
1349
1350 pub fn code_block_numbered(&mut self, code: &str) -> Response {
1352 self.code_block_numbered_lang(code, "")
1353 }
1354
1355 pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
1357 let lines: Vec<&str> = code.lines().collect();
1358 let gutter_w = format!("{}", lines.len()).len();
1359 let theme = self.theme;
1360 let highlighted: Option<Vec<Vec<(String, Style)>>> =
1361 crate::syntax::highlight_code(code, lang, &theme);
1362 let _ = self
1363 .bordered(Border::Rounded)
1364 .bg(theme.surface)
1365 .pad(1)
1366 .col(|ui| {
1367 if let Some(ref hl_lines) = highlighted {
1368 for (i, segs) in hl_lines.iter().enumerate() {
1369 ui.line(|ui| {
1370 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1371 .fg(theme.text_dim);
1372 for (text, style) in segs {
1373 ui.styled(text, *style);
1374 }
1375 });
1376 }
1377 } else {
1378 for (i, line) in lines.iter().enumerate() {
1379 ui.line(|ui| {
1380 ui.text(format!("{:>gutter_w$} │ ", i + 1))
1381 .fg(theme.text_dim);
1382 render_highlighted_line(ui, line);
1383 });
1384 }
1385 }
1386 });
1387
1388 Response::none()
1389 }
1390
1391 pub fn wrap(&mut self) -> &mut Self {
1393 if let Some(idx) = self.last_text_idx {
1394 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1395 *wrap = true;
1396 }
1397 }
1398 self
1399 }
1400
1401 pub fn truncate(&mut self) -> &mut Self {
1404 if let Some(idx) = self.last_text_idx {
1405 if let Command::Text { truncate, .. } = &mut self.commands[idx] {
1406 *truncate = true;
1407 }
1408 }
1409 self
1410 }
1411
1412 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1413 if let Some(idx) = self.last_text_idx {
1414 match &mut self.commands[idx] {
1415 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1416 _ => {}
1417 }
1418 }
1419 }
1420
1421 fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1422 if let Some(idx) = self.last_text_idx {
1423 match &mut self.commands[idx] {
1424 Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1425 f(constraints)
1426 }
1427 _ => {}
1428 }
1429 }
1430 }
1431
1432 fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1433 if let Some(idx) = self.last_text_idx {
1434 match &mut self.commands[idx] {
1435 Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1436 _ => {}
1437 }
1438 }
1439 }
1440
1441 pub fn screen(&mut self, name: &str, screens: &ScreenState, f: impl FnOnce(&mut Context)) {
1445 if screens.current() == name {
1446 f(self);
1447 }
1448 }
1449
1450 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1466 self.push_container(Direction::Column, 0, f)
1467 }
1468
1469 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1473 self.push_container(Direction::Column, gap, f)
1474 }
1475
1476 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1493 self.push_container(Direction::Row, 0, f)
1494 }
1495
1496 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1500 self.push_container(Direction::Row, gap, f)
1501 }
1502
1503 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1520 let _ = self.push_container(Direction::Row, 0, f);
1521 self
1522 }
1523
1524 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1543 let start = self.commands.len();
1544 f(self);
1545 let mut segments: Vec<(String, Style)> = Vec::new();
1546 for cmd in self.commands.drain(start..) {
1547 if let Command::Text { content, style, .. } = cmd {
1548 segments.push((content, style));
1549 }
1550 }
1551 self.commands.push(Command::RichText {
1552 segments,
1553 wrap: true,
1554 align: Align::Start,
1555 margin: Margin::default(),
1556 constraints: Constraints::default(),
1557 });
1558 self.last_text_idx = None;
1559 self
1560 }
1561
1562 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1571 let interaction_id = self.next_interaction_id();
1572 self.commands.push(Command::BeginOverlay { modal: true });
1573 self.overlay_depth += 1;
1574 self.modal_active = true;
1575 self.modal_focus_start = self.focus_count;
1576 f(self);
1577 self.modal_focus_count = self.focus_count.saturating_sub(self.modal_focus_start);
1578 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1579 self.commands.push(Command::EndOverlay);
1580 self.last_text_idx = None;
1581 self.response_for(interaction_id)
1582 }
1583
1584 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1586 let interaction_id = self.next_interaction_id();
1587 self.commands.push(Command::BeginOverlay { modal: false });
1588 self.overlay_depth += 1;
1589 f(self);
1590 self.overlay_depth = self.overlay_depth.saturating_sub(1);
1591 self.commands.push(Command::EndOverlay);
1592 self.last_text_idx = None;
1593 self.response_for(interaction_id)
1594 }
1595
1596 pub fn tooltip(&mut self, text: impl Into<String>) {
1604 let tooltip_text = text.into();
1605 if tooltip_text.is_empty() {
1606 return;
1607 }
1608 let last_interaction_id = self.interaction_count.saturating_sub(1);
1609 let last_response = self.response_for(last_interaction_id);
1610 if !last_response.hovered || last_response.rect.width == 0 || last_response.rect.height == 0
1611 {
1612 return;
1613 }
1614 let lines = wrap_tooltip_text(&tooltip_text, 38);
1615 self.pending_tooltips.push(PendingTooltip {
1616 anchor_rect: last_response.rect,
1617 lines,
1618 });
1619 }
1620
1621 pub(crate) fn emit_pending_tooltips(&mut self) {
1622 let tooltips = std::mem::take(&mut self.pending_tooltips);
1623 if tooltips.is_empty() {
1624 return;
1625 }
1626 let area_w = self.area_width;
1627 let area_h = self.area_height;
1628 let surface = self.theme.surface;
1629 let border_color = self.theme.border;
1630 let text_color = self.theme.surface_text;
1631
1632 for tooltip in tooltips {
1633 let content_w = tooltip
1634 .lines
1635 .iter()
1636 .map(|l| UnicodeWidthStr::width(l.as_str()) as u32)
1637 .max()
1638 .unwrap_or(0);
1639 let box_w = content_w.saturating_add(4).min(area_w);
1640 let box_h = (tooltip.lines.len() as u32).saturating_add(4).min(area_h);
1641
1642 let tooltip_x = tooltip.anchor_rect.x.min(area_w.saturating_sub(box_w));
1643 let below_y = tooltip.anchor_rect.bottom();
1644 let tooltip_y = if below_y.saturating_add(box_h) <= area_h {
1645 below_y
1646 } else {
1647 tooltip.anchor_rect.y.saturating_sub(box_h)
1648 };
1649
1650 let lines = tooltip.lines;
1651 let _ = self.overlay(|ui| {
1652 let _ = ui.container().w(area_w).h(area_h).col(|ui| {
1653 let _ = ui
1654 .container()
1655 .ml(tooltip_x)
1656 .mt(tooltip_y)
1657 .max_w(box_w)
1658 .border(Border::Rounded)
1659 .border_fg(border_color)
1660 .bg(surface)
1661 .p(1)
1662 .col(|ui| {
1663 for line in &lines {
1664 ui.text(line.as_str()).fg(text_color);
1665 }
1666 });
1667 });
1668 });
1669 }
1670 }
1671
1672 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1680 self.group_count = self.group_count.saturating_add(1);
1681 self.group_stack.push(name.to_string());
1682 self.container().group_name(name.to_string())
1683 }
1684
1685 pub fn container(&mut self) -> ContainerBuilder<'_> {
1706 let border = self.theme.border;
1707 ContainerBuilder {
1708 ctx: self,
1709 gap: 0,
1710 row_gap: None,
1711 col_gap: None,
1712 align: Align::Start,
1713 align_self_value: None,
1714 justify: Justify::Start,
1715 border: None,
1716 border_sides: BorderSides::all(),
1717 border_style: Style::new().fg(border),
1718 bg: None,
1719 text_color: None,
1720 dark_bg: None,
1721 dark_border_style: None,
1722 group_hover_bg: None,
1723 group_hover_border_style: None,
1724 group_name: None,
1725 padding: Padding::default(),
1726 margin: Margin::default(),
1727 constraints: Constraints::default(),
1728 title: None,
1729 grow: 0,
1730 scroll_offset: None,
1731 }
1732 }
1733
1734 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1753 let index = self.scroll_count;
1754 self.scroll_count += 1;
1755 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1756 state.set_bounds(ch, vh);
1757 let max = ch.saturating_sub(vh) as usize;
1758 state.offset = state.offset.min(max);
1759 }
1760
1761 let next_id = self.interaction_count;
1762 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1763 let inner_rects: Vec<Rect> = self
1764 .prev_scroll_rects
1765 .iter()
1766 .enumerate()
1767 .filter(|&(j, sr)| {
1768 j != index
1769 && sr.width > 0
1770 && sr.height > 0
1771 && sr.x >= rect.x
1772 && sr.right() <= rect.right()
1773 && sr.y >= rect.y
1774 && sr.bottom() <= rect.bottom()
1775 })
1776 .map(|(_, sr)| *sr)
1777 .collect();
1778 self.auto_scroll_nested(&rect, state, &inner_rects);
1779 }
1780
1781 self.container().scroll_offset(state.offset as u32)
1782 }
1783
1784 pub fn scrollbar(&mut self, state: &ScrollState) {
1804 let vh = state.viewport_height();
1805 let ch = state.content_height();
1806 if vh == 0 || ch <= vh {
1807 return;
1808 }
1809
1810 let track_height = vh;
1811 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1812 let max_offset = ch.saturating_sub(vh);
1813 let thumb_pos = if max_offset == 0 {
1814 0
1815 } else {
1816 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1817 .round() as u32
1818 };
1819
1820 let theme = self.theme;
1821 let track_char = '│';
1822 let thumb_char = '█';
1823
1824 let _ = self.container().w(1).h(track_height).col(|ui| {
1825 for i in 0..track_height {
1826 if i >= thumb_pos && i < thumb_pos + thumb_height {
1827 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1828 } else {
1829 ui.styled(
1830 track_char.to_string(),
1831 Style::new().fg(theme.text_dim).dim(),
1832 );
1833 }
1834 }
1835 });
1836 }
1837
1838 fn auto_scroll_nested(
1839 &mut self,
1840 rect: &Rect,
1841 state: &mut ScrollState,
1842 inner_scroll_rects: &[Rect],
1843 ) {
1844 let mut to_consume: Vec<usize> = Vec::new();
1845
1846 for (i, event) in self.events.iter().enumerate() {
1847 if self.consumed[i] {
1848 continue;
1849 }
1850 if let Event::Mouse(mouse) = event {
1851 let in_bounds = mouse.x >= rect.x
1852 && mouse.x < rect.right()
1853 && mouse.y >= rect.y
1854 && mouse.y < rect.bottom();
1855 if !in_bounds {
1856 continue;
1857 }
1858 let in_inner = inner_scroll_rects.iter().any(|sr| {
1859 mouse.x >= sr.x
1860 && mouse.x < sr.right()
1861 && mouse.y >= sr.y
1862 && mouse.y < sr.bottom()
1863 });
1864 if in_inner {
1865 continue;
1866 }
1867 let delta = self.scroll_lines_per_event as usize;
1868 match mouse.kind {
1869 MouseKind::ScrollUp => {
1870 state.scroll_up(delta);
1871 to_consume.push(i);
1872 }
1873 MouseKind::ScrollDown => {
1874 state.scroll_down(delta);
1875 to_consume.push(i);
1876 }
1877 MouseKind::Drag(MouseButton::Left) => {}
1878 _ => {}
1879 }
1880 }
1881 }
1882
1883 for i in to_consume {
1884 self.consumed[i] = true;
1885 }
1886 }
1887
1888 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1892 self.container()
1893 .border(border)
1894 .border_sides(BorderSides::all())
1895 }
1896
1897 fn push_container(
1898 &mut self,
1899 direction: Direction,
1900 gap: u32,
1901 f: impl FnOnce(&mut Context),
1902 ) -> Response {
1903 let interaction_id = self.next_interaction_id();
1904 let border = self.theme.border;
1905
1906 self.commands.push(Command::BeginContainer {
1907 direction,
1908 gap,
1909 align: Align::Start,
1910 align_self: None,
1911 justify: Justify::Start,
1912 border: None,
1913 border_sides: BorderSides::all(),
1914 border_style: Style::new().fg(border),
1915 bg_color: None,
1916 padding: Padding::default(),
1917 margin: Margin::default(),
1918 constraints: Constraints::default(),
1919 title: None,
1920 grow: 0,
1921 group_name: None,
1922 });
1923 self.text_color_stack.push(None);
1924 f(self);
1925 self.text_color_stack.pop();
1926 self.commands.push(Command::EndContainer);
1927 self.last_text_idx = None;
1928
1929 self.response_for(interaction_id)
1930 }
1931
1932 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1933 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1934 return Response::none();
1935 }
1936 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1937 let clicked = self
1938 .click_pos
1939 .map(|(mx, my)| {
1940 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1941 })
1942 .unwrap_or(false);
1943 let hovered = self
1944 .mouse_pos
1945 .map(|(mx, my)| {
1946 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1947 })
1948 .unwrap_or(false);
1949 Response {
1950 clicked,
1951 hovered,
1952 changed: false,
1953 focused: false,
1954 rect: *rect,
1955 }
1956 } else {
1957 Response::none()
1958 }
1959 }
1960
1961 pub fn is_group_hovered(&self, name: &str) -> bool {
1963 if let Some(pos) = self.mouse_pos {
1964 self.prev_group_rects.iter().any(|(n, rect)| {
1965 n == name
1966 && pos.0 >= rect.x
1967 && pos.0 < rect.x + rect.width
1968 && pos.1 >= rect.y
1969 && pos.1 < rect.y + rect.height
1970 })
1971 } else {
1972 false
1973 }
1974 }
1975
1976 pub fn is_group_focused(&self, name: &str) -> bool {
1978 if self.prev_focus_count == 0 {
1979 return false;
1980 }
1981 let focused_index = self.focus_index % self.prev_focus_count;
1982 self.prev_focus_groups
1983 .get(focused_index)
1984 .and_then(|group| group.as_deref())
1985 .map(|group| group == name)
1986 .unwrap_or(false)
1987 }
1988
1989 pub fn grow(&mut self, value: u16) -> &mut Self {
1994 if let Some(idx) = self.last_text_idx {
1995 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1996 *grow = value;
1997 }
1998 }
1999 self
2000 }
2001
2002 pub fn align(&mut self, align: Align) -> &mut Self {
2004 if let Some(idx) = self.last_text_idx {
2005 if let Command::Text {
2006 align: text_align, ..
2007 } = &mut self.commands[idx]
2008 {
2009 *text_align = align;
2010 }
2011 }
2012 self
2013 }
2014
2015 pub fn text_center(&mut self) -> &mut Self {
2019 self.align(Align::Center)
2020 }
2021
2022 pub fn text_right(&mut self) -> &mut Self {
2025 self.align(Align::End)
2026 }
2027
2028 pub fn w(&mut self, value: u32) -> &mut Self {
2035 self.modify_last_constraints(|c| {
2036 c.min_width = Some(value);
2037 c.max_width = Some(value);
2038 });
2039 self
2040 }
2041
2042 pub fn h(&mut self, value: u32) -> &mut Self {
2046 self.modify_last_constraints(|c| {
2047 c.min_height = Some(value);
2048 c.max_height = Some(value);
2049 });
2050 self
2051 }
2052
2053 pub fn min_w(&mut self, value: u32) -> &mut Self {
2055 self.modify_last_constraints(|c| c.min_width = Some(value));
2056 self
2057 }
2058
2059 pub fn max_w(&mut self, value: u32) -> &mut Self {
2061 self.modify_last_constraints(|c| c.max_width = Some(value));
2062 self
2063 }
2064
2065 pub fn min_h(&mut self, value: u32) -> &mut Self {
2067 self.modify_last_constraints(|c| c.min_height = Some(value));
2068 self
2069 }
2070
2071 pub fn max_h(&mut self, value: u32) -> &mut Self {
2073 self.modify_last_constraints(|c| c.max_height = Some(value));
2074 self
2075 }
2076
2077 pub fn m(&mut self, value: u32) -> &mut Self {
2081 self.modify_last_margin(|m| *m = Margin::all(value));
2082 self
2083 }
2084
2085 pub fn mx(&mut self, value: u32) -> &mut Self {
2087 self.modify_last_margin(|m| {
2088 m.left = value;
2089 m.right = value;
2090 });
2091 self
2092 }
2093
2094 pub fn my(&mut self, value: u32) -> &mut Self {
2096 self.modify_last_margin(|m| {
2097 m.top = value;
2098 m.bottom = value;
2099 });
2100 self
2101 }
2102
2103 pub fn mt(&mut self, value: u32) -> &mut Self {
2105 self.modify_last_margin(|m| m.top = value);
2106 self
2107 }
2108
2109 pub fn mr(&mut self, value: u32) -> &mut Self {
2111 self.modify_last_margin(|m| m.right = value);
2112 self
2113 }
2114
2115 pub fn mb(&mut self, value: u32) -> &mut Self {
2117 self.modify_last_margin(|m| m.bottom = value);
2118 self
2119 }
2120
2121 pub fn ml(&mut self, value: u32) -> &mut Self {
2123 self.modify_last_margin(|m| m.left = value);
2124 self
2125 }
2126
2127 pub fn spacer(&mut self) -> &mut Self {
2131 self.commands.push(Command::Spacer { grow: 1 });
2132 self.last_text_idx = None;
2133 self
2134 }
2135
2136 pub fn form(
2140 &mut self,
2141 state: &mut FormState,
2142 f: impl FnOnce(&mut Context, &mut FormState),
2143 ) -> &mut Self {
2144 let _ = self.col(|ui| {
2145 f(ui, state);
2146 });
2147 self
2148 }
2149
2150 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
2154 let _ = self.col(|ui| {
2155 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
2156 let _ = ui.text_input(&mut field.input);
2157 if let Some(error) = field.error.as_deref() {
2158 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
2159 }
2160 });
2161 self
2162 }
2163
2164 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
2168 self.button(label)
2169 }
2170}
2171
2172fn wrap_tooltip_text(text: &str, max_width: usize) -> Vec<String> {
2173 let max_width = max_width.max(1);
2174 let mut lines = Vec::new();
2175
2176 for paragraph in text.lines() {
2177 if paragraph.trim().is_empty() {
2178 lines.push(String::new());
2179 continue;
2180 }
2181
2182 let mut current = String::new();
2183 let mut current_width = 0usize;
2184
2185 for word in paragraph.split_whitespace() {
2186 for chunk in split_word_for_width(word, max_width) {
2187 let chunk_width = UnicodeWidthStr::width(chunk.as_str());
2188
2189 if current.is_empty() {
2190 current = chunk;
2191 current_width = chunk_width;
2192 continue;
2193 }
2194
2195 if current_width + 1 + chunk_width <= max_width {
2196 current.push(' ');
2197 current.push_str(&chunk);
2198 current_width += 1 + chunk_width;
2199 } else {
2200 lines.push(std::mem::take(&mut current));
2201 current = chunk;
2202 current_width = chunk_width;
2203 }
2204 }
2205 }
2206
2207 if !current.is_empty() {
2208 lines.push(current);
2209 }
2210 }
2211
2212 if lines.is_empty() {
2213 lines.push(String::new());
2214 }
2215
2216 lines
2217}
2218
2219fn split_word_for_width(word: &str, max_width: usize) -> Vec<String> {
2220 let mut chunks = Vec::new();
2221 let mut current = String::new();
2222 let mut current_width = 0usize;
2223
2224 for ch in word.chars() {
2225 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
2226 if !current.is_empty() && current_width + ch_width > max_width {
2227 chunks.push(std::mem::take(&mut current));
2228 current_width = 0;
2229 }
2230 current.push(ch);
2231 current_width += ch_width;
2232
2233 if current_width >= max_width {
2234 chunks.push(std::mem::take(&mut current));
2235 current_width = 0;
2236 }
2237 }
2238
2239 if !current.is_empty() {
2240 chunks.push(current);
2241 }
2242
2243 if chunks.is_empty() {
2244 chunks.push(String::new());
2245 }
2246
2247 chunks
2248}
2249
2250fn glyph_8x8(ch: char) -> [u8; 8] {
2251 if ch.is_ascii() {
2252 let code = ch as u8;
2253 if (32..=126).contains(&code) {
2254 return FONT_8X8_PRINTABLE[(code - 32) as usize];
2255 }
2256 }
2257
2258 FONT_8X8_PRINTABLE[(b'?' - 32) as usize]
2259}
2260
2261const FONT_8X8_PRINTABLE: [[u8; 8]; 95] = [
2262 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2263 [0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00],
2264 [0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2265 [0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00],
2266 [0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00],
2267 [0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00],
2268 [0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00],
2269 [0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00],
2270 [0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00],
2271 [0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00],
2272 [0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00],
2273 [0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00],
2274 [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06],
2275 [0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00],
2276 [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00],
2277 [0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00],
2278 [0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00],
2279 [0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00],
2280 [0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00],
2281 [0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00],
2282 [0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00],
2283 [0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00],
2284 [0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00],
2285 [0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00],
2286 [0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00],
2287 [0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00],
2288 [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00],
2289 [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06],
2290 [0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00],
2291 [0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00],
2292 [0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00],
2293 [0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00],
2294 [0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00],
2295 [0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00],
2296 [0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00],
2297 [0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00],
2298 [0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00],
2299 [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00],
2300 [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00],
2301 [0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00],
2302 [0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00],
2303 [0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2304 [0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00],
2305 [0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00],
2306 [0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00],
2307 [0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00],
2308 [0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00],
2309 [0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00],
2310 [0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00],
2311 [0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00],
2312 [0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00],
2313 [0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00],
2314 [0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2315 [0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00],
2316 [0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00],
2317 [0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00],
2318 [0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00],
2319 [0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00],
2320 [0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00],
2321 [0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00],
2322 [0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00],
2323 [0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00],
2324 [0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00],
2325 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF],
2326 [0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00],
2327 [0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00],
2328 [0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00],
2329 [0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00],
2330 [0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00],
2331 [0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00],
2332 [0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00],
2333 [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F],
2334 [0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00],
2335 [0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2336 [0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E],
2337 [0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00],
2338 [0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00],
2339 [0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00],
2340 [0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00],
2341 [0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00],
2342 [0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F],
2343 [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78],
2344 [0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00],
2345 [0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00],
2346 [0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00],
2347 [0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00],
2348 [0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00],
2349 [0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00],
2350 [0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00],
2351 [0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F],
2352 [0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00],
2353 [0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00],
2354 [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00],
2355 [0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00],
2356 [0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
2357];
2358
2359const KEYWORDS: &[&str] = &[
2360 "fn",
2361 "let",
2362 "mut",
2363 "pub",
2364 "use",
2365 "impl",
2366 "struct",
2367 "enum",
2368 "trait",
2369 "type",
2370 "const",
2371 "static",
2372 "if",
2373 "else",
2374 "match",
2375 "for",
2376 "while",
2377 "loop",
2378 "return",
2379 "break",
2380 "continue",
2381 "where",
2382 "self",
2383 "super",
2384 "crate",
2385 "mod",
2386 "async",
2387 "await",
2388 "move",
2389 "ref",
2390 "in",
2391 "as",
2392 "true",
2393 "false",
2394 "Some",
2395 "None",
2396 "Ok",
2397 "Err",
2398 "Self",
2399 "def",
2400 "class",
2401 "import",
2402 "from",
2403 "pass",
2404 "lambda",
2405 "yield",
2406 "with",
2407 "try",
2408 "except",
2409 "raise",
2410 "finally",
2411 "elif",
2412 "del",
2413 "global",
2414 "nonlocal",
2415 "assert",
2416 "is",
2417 "not",
2418 "and",
2419 "or",
2420 "function",
2421 "var",
2422 "const",
2423 "export",
2424 "default",
2425 "switch",
2426 "case",
2427 "throw",
2428 "catch",
2429 "typeof",
2430 "instanceof",
2431 "new",
2432 "delete",
2433 "void",
2434 "this",
2435 "null",
2436 "undefined",
2437 "func",
2438 "package",
2439 "defer",
2440 "go",
2441 "chan",
2442 "select",
2443 "range",
2444 "map",
2445 "interface",
2446 "fallthrough",
2447 "nil",
2448];
2449
2450fn render_tree_sitter_lines(ui: &mut Context, lines: &[Vec<(String, crate::style::Style)>]) {
2451 for segs in lines {
2452 if segs.is_empty() {
2453 ui.text(" ");
2454 } else {
2455 ui.line(|ui| {
2456 for (text, style) in segs {
2457 ui.styled(text, *style);
2458 }
2459 });
2460 }
2461 }
2462}
2463
2464fn render_highlighted_line(ui: &mut Context, line: &str) {
2465 let theme = ui.theme;
2466 let is_light = matches!(
2467 theme.bg,
2468 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
2469 );
2470 let keyword_color = if is_light {
2471 Color::Rgb(166, 38, 164)
2472 } else {
2473 Color::Rgb(198, 120, 221)
2474 };
2475 let string_color = if is_light {
2476 Color::Rgb(80, 161, 79)
2477 } else {
2478 Color::Rgb(152, 195, 121)
2479 };
2480 let comment_color = theme.text_dim;
2481 let number_color = if is_light {
2482 Color::Rgb(152, 104, 1)
2483 } else {
2484 Color::Rgb(209, 154, 102)
2485 };
2486 let fn_color = if is_light {
2487 Color::Rgb(64, 120, 242)
2488 } else {
2489 Color::Rgb(97, 175, 239)
2490 };
2491 let macro_color = if is_light {
2492 Color::Rgb(1, 132, 188)
2493 } else {
2494 Color::Rgb(86, 182, 194)
2495 };
2496
2497 let trimmed = line.trim_start();
2498 let indent = &line[..line.len() - trimmed.len()];
2499 if !indent.is_empty() {
2500 ui.text(indent);
2501 }
2502
2503 if trimmed.starts_with("//") {
2504 ui.text(trimmed).fg(comment_color).italic();
2505 return;
2506 }
2507
2508 let mut pos = 0;
2509
2510 while pos < trimmed.len() {
2511 let ch = trimmed.as_bytes()[pos];
2512
2513 if ch == b'"' {
2514 if let Some(end) = trimmed[pos + 1..].find('"') {
2515 let s = &trimmed[pos..pos + end + 2];
2516 ui.text(s).fg(string_color);
2517 pos += end + 2;
2518 continue;
2519 }
2520 }
2521
2522 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
2523 {
2524 let end = trimmed[pos..]
2525 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
2526 .map_or(trimmed.len(), |e| pos + e);
2527 ui.text(&trimmed[pos..end]).fg(number_color);
2528 pos = end;
2529 continue;
2530 }
2531
2532 if ch.is_ascii_alphabetic() || ch == b'_' {
2533 let end = trimmed[pos..]
2534 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
2535 .map_or(trimmed.len(), |e| pos + e);
2536 let word = &trimmed[pos..end];
2537
2538 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
2539 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
2540 pos = end + 1;
2541 } else if end < trimmed.len()
2542 && trimmed.as_bytes()[end] == b'('
2543 && !KEYWORDS.contains(&word)
2544 {
2545 ui.text(word).fg(fn_color);
2546 pos = end;
2547 } else if KEYWORDS.contains(&word) {
2548 ui.text(word).fg(keyword_color);
2549 pos = end;
2550 } else {
2551 ui.text(word);
2552 pos = end;
2553 }
2554 continue;
2555 }
2556
2557 let end = trimmed[pos..]
2558 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
2559 .map_or(trimmed.len(), |e| pos + e);
2560 ui.text(&trimmed[pos..end]);
2561 pos = end;
2562 }
2563}
2564
2565fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
2566 let expected = (width as usize) * (height as usize) * 4;
2567 if data.len() >= expected {
2568 return data[..expected].to_vec();
2569 }
2570 let mut buf = Vec::with_capacity(expected);
2571 buf.extend_from_slice(data);
2572 buf.resize(expected, 0);
2573 buf
2574}
2575
2576fn base64_encode(data: &[u8]) -> String {
2577 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2578 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
2579 for chunk in data.chunks(3) {
2580 let b0 = chunk[0] as u32;
2581 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2582 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2583 let triple = (b0 << 16) | (b1 << 8) | b2;
2584 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
2585 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
2586 if chunk.len() > 1 {
2587 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
2588 } else {
2589 result.push('=');
2590 }
2591 if chunk.len() > 2 {
2592 result.push(CHARS[(triple & 0x3F) as usize] as char);
2593 } else {
2594 result.push('=');
2595 }
2596 }
2597 result
2598}
2599
2600fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
2601 let mut chunks = Vec::new();
2602 let bytes = encoded.as_bytes();
2603 let mut offset = 0;
2604 while offset < bytes.len() {
2605 let end = (offset + chunk_size).min(bytes.len());
2606 chunks.push(&encoded[offset..end]);
2607 offset = end;
2608 }
2609 if chunks.is_empty() {
2610 chunks.push("");
2611 }
2612 chunks
2613}
2614
2615#[cfg(feature = "crossterm")]
2616fn terminal_supports_sixel() -> bool {
2617 let force = std::env::var("SLT_FORCE_SIXEL")
2618 .ok()
2619 .map(|v| v.to_ascii_lowercase())
2620 .unwrap_or_default();
2621 if matches!(force.as_str(), "1" | "true" | "yes" | "on") {
2622 return true;
2623 }
2624
2625 let term = std::env::var("TERM")
2626 .ok()
2627 .map(|v| v.to_ascii_lowercase())
2628 .unwrap_or_default();
2629 let term_program = std::env::var("TERM_PROGRAM")
2630 .ok()
2631 .map(|v| v.to_ascii_lowercase())
2632 .unwrap_or_default();
2633
2634 term.contains("sixel")
2635 || term.contains("mlterm")
2636 || term.contains("xterm")
2637 || term.contains("foot")
2638 || term_program.contains("foot")
2639}
2640
2641#[cfg(test)]
2642mod tests {
2643 use super::*;
2644 use crate::TestBackend;
2645 use std::time::Duration;
2646
2647 #[test]
2648 fn gradient_text_renders_content() {
2649 let mut backend = TestBackend::new(20, 4);
2650 backend.render(|ui| {
2651 ui.text("ABCD").gradient(Color::Red, Color::Blue);
2652 });
2653
2654 backend.assert_contains("ABCD");
2655 }
2656
2657 #[test]
2658 fn big_text_renders_half_block_grid() {
2659 let mut backend = TestBackend::new(16, 4);
2660 backend.render(|ui| {
2661 let _ = ui.big_text("A");
2662 });
2663
2664 let output = backend.to_string();
2665 assert!(
2667 output.contains('▀') || output.contains('▄') || output.contains('█'),
2668 "output should contain half-block glyphs: {output:?}"
2669 );
2670 }
2671
2672 #[test]
2673 fn timer_display_formats_minutes_seconds_centis() {
2674 let mut backend = TestBackend::new(20, 4);
2675 backend.render(|ui| {
2676 ui.timer_display(Duration::from_secs(83) + Duration::from_millis(450));
2677 });
2678
2679 backend.assert_contains("01:23.45");
2680 }
2681}