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