Skip to main content

slt/context/
widgets_display.rs

1use super::*;
2use crate::KeyMap;
3
4impl Context {
5    // ── text ──────────────────────────────────────────────────────────
6
7    /// Render a text element. Returns `&mut Self` for style chaining.
8    ///
9    /// # Example
10    ///
11    /// ```no_run
12    /// # slt::run(|ui: &mut slt::Context| {
13    /// use slt::Color;
14    /// ui.text("hello").bold().fg(Color::Cyan);
15    /// # });
16    /// ```
17    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    /// Render a clickable hyperlink.
40    ///
41    /// The link is interactive: clicking it (or pressing Enter/Space when
42    /// focused) opens the URL in the system browser. OSC 8 is also emitted
43    /// for terminals that support native hyperlinks.
44    #[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.interaction_count;
49        self.interaction_count += 1;
50        let response = self.response_for(interaction_id);
51
52        let mut activated = response.clicked;
53        if focused {
54            for (i, event) in self.events.iter().enumerate() {
55                if let Event::Key(key) = event {
56                    if key.kind != KeyEventKind::Press {
57                        continue;
58                    }
59                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
60                        activated = true;
61                        self.consumed[i] = true;
62                    }
63                }
64            }
65        }
66
67        if activated {
68            if let Err(e) = open_url(&url_str) {
69                eprintln!("[slt] failed to open URL: {e}");
70            }
71        }
72
73        let style = if focused {
74            Style::new()
75                .fg(self.theme.primary)
76                .bg(self.theme.surface_hover)
77                .underline()
78                .bold()
79        } else if response.hovered {
80            Style::new()
81                .fg(self.theme.accent)
82                .bg(self.theme.surface_hover)
83                .underline()
84        } else {
85            Style::new().fg(self.theme.primary).underline()
86        };
87
88        self.commands.push(Command::Link {
89            text: text.into(),
90            url: url_str,
91            style,
92            margin: Margin::default(),
93            constraints: Constraints::default(),
94        });
95        self.last_text_idx = Some(self.commands.len() - 1);
96        self
97    }
98
99    /// Render a text element with word-boundary wrapping.
100    ///
101    /// Long lines are broken at word boundaries to fit the container width.
102    /// Style chaining works the same as [`Context::text`].
103    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
104        let content = s.into();
105        let default_fg = self
106            .text_color_stack
107            .iter()
108            .rev()
109            .find_map(|c| *c)
110            .unwrap_or(self.theme.text);
111        self.commands.push(Command::Text {
112            content,
113            style: Style::new().fg(default_fg),
114            grow: 0,
115            align: Align::Start,
116            wrap: true,
117            truncate: false,
118            margin: Margin::default(),
119            constraints: Constraints::default(),
120        });
121        self.last_text_idx = Some(self.commands.len() - 1);
122        self
123    }
124
125    /// Render help bar from a KeyMap. Shows visible bindings as key-description pairs.
126    pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
127        let pairs: Vec<(&str, &str)> = keymap
128            .visible_bindings()
129            .map(|binding| (binding.display.as_str(), binding.description.as_str()))
130            .collect();
131        self.help(&pairs)
132    }
133
134    // ── style chain (applies to last text) ───────────────────────────
135
136    /// Apply bold to the last rendered text element.
137    pub fn bold(&mut self) -> &mut Self {
138        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
139        self
140    }
141
142    /// Apply dim styling to the last rendered text element.
143    ///
144    /// Also sets the foreground color to the theme's `text_dim` color if no
145    /// explicit foreground has been set.
146    pub fn dim(&mut self) -> &mut Self {
147        let text_dim = self.theme.text_dim;
148        self.modify_last_style(|s| {
149            s.modifiers |= Modifiers::DIM;
150            if s.fg.is_none() {
151                s.fg = Some(text_dim);
152            }
153        });
154        self
155    }
156
157    /// Apply italic to the last rendered text element.
158    pub fn italic(&mut self) -> &mut Self {
159        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
160        self
161    }
162
163    /// Apply underline to the last rendered text element.
164    pub fn underline(&mut self) -> &mut Self {
165        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
166        self
167    }
168
169    /// Apply reverse-video to the last rendered text element.
170    pub fn reversed(&mut self) -> &mut Self {
171        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
172        self
173    }
174
175    /// Apply strikethrough to the last rendered text element.
176    pub fn strikethrough(&mut self) -> &mut Self {
177        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
178        self
179    }
180
181    /// Set the foreground color of the last rendered text element.
182    pub fn fg(&mut self, color: Color) -> &mut Self {
183        self.modify_last_style(|s| s.fg = Some(color));
184        self
185    }
186
187    /// Set the background color of the last rendered text element.
188    pub fn bg(&mut self, color: Color) -> &mut Self {
189        self.modify_last_style(|s| s.bg = Some(color));
190        self
191    }
192
193    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
194        let apply_group_style = self
195            .group_stack
196            .last()
197            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
198            .unwrap_or(false);
199        if apply_group_style {
200            self.modify_last_style(|s| s.fg = Some(color));
201        }
202        self
203    }
204
205    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
206        let apply_group_style = self
207            .group_stack
208            .last()
209            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
210            .unwrap_or(false);
211        if apply_group_style {
212            self.modify_last_style(|s| s.bg = Some(color));
213        }
214        self
215    }
216
217    /// Render a text element with an explicit [`Style`] applied immediately.
218    ///
219    /// Equivalent to calling `text(s)` followed by style-chain methods, but
220    /// more concise when you already have a `Style` value.
221    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
222        self.commands.push(Command::Text {
223            content: s.into(),
224            style,
225            grow: 0,
226            align: Align::Start,
227            wrap: false,
228            truncate: false,
229            margin: Margin::default(),
230            constraints: Constraints::default(),
231        });
232        self.last_text_idx = Some(self.commands.len() - 1);
233        self
234    }
235
236    /// Render a half-block image in the terminal.
237    ///
238    /// Each terminal cell displays two vertical pixels using the `▀` character
239    /// with foreground (upper pixel) and background (lower pixel) colors.
240    ///
241    /// Create a [`HalfBlockImage`] from a file (requires `image` feature):
242    /// ```ignore
243    /// let img = image::open("photo.png").unwrap();
244    /// let half = HalfBlockImage::from_dynamic(&img, 40, 20);
245    /// ui.image(&half);
246    /// ```
247    ///
248    /// Or from raw RGB data (no feature needed):
249    /// ```no_run
250    /// # use slt::{Context, HalfBlockImage};
251    /// # slt::run(|ui: &mut Context| {
252    /// let rgb = vec![255u8; 30 * 20 * 3];
253    /// let half = HalfBlockImage::from_rgb(&rgb, 30, 10);
254    /// ui.image(&half);
255    /// # });
256    /// ```
257    pub fn image(&mut self, img: &HalfBlockImage) -> Response {
258        let width = img.width;
259        let height = img.height;
260
261        let _ = self.container().w(width).h(height).gap(0).col(|ui| {
262            for row in 0..height {
263                let _ = ui.container().gap(0).row(|ui| {
264                    for col in 0..width {
265                        let idx = (row * width + col) as usize;
266                        if let Some(&(upper, lower)) = img.pixels.get(idx) {
267                            ui.styled("▀", Style::new().fg(upper).bg(lower));
268                        }
269                    }
270                });
271            }
272        });
273
274        Response::none()
275    }
276
277    /// Render a pixel-perfect image using the Kitty graphics protocol.
278    ///
279    /// The image data must be raw RGBA bytes (4 bytes per pixel).
280    /// The widget allocates `cols` x `rows` cells and renders the image
281    /// at full pixel resolution within that space.
282    ///
283    /// Requires a Kitty-compatible terminal (Kitty, Ghostty, WezTerm).
284    /// On unsupported terminals, the area will be blank.
285    ///
286    /// # Arguments
287    /// * `rgba` - Raw RGBA pixel data
288    /// * `pixel_width` - Image width in pixels
289    /// * `pixel_height` - Image height in pixels
290    /// * `cols` - Terminal cell columns to occupy
291    /// * `rows` - Terminal cell rows to occupy
292    pub fn kitty_image(
293        &mut self,
294        rgba: &[u8],
295        pixel_width: u32,
296        pixel_height: u32,
297        cols: u32,
298        rows: u32,
299    ) -> Response {
300        let rgba = normalize_rgba(rgba, pixel_width, pixel_height);
301        let encoded = base64_encode(&rgba);
302        let pw = pixel_width;
303        let ph = pixel_height;
304        let c = cols;
305        let r = rows;
306
307        self.container().w(cols).h(rows).draw(move |buf, rect| {
308            let chunks = split_base64(&encoded, 4096);
309            let mut all_sequences = String::new();
310
311            for (i, chunk) in chunks.iter().enumerate() {
312                let more = if i < chunks.len() - 1 { 1 } else { 0 };
313                if i == 0 {
314                    all_sequences.push_str(&format!(
315                        "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
316                        pw, ph, c, r, more, chunk
317                    ));
318                } else {
319                    all_sequences.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
320                }
321            }
322
323            buf.raw_sequence(rect.x, rect.y, all_sequences);
324        });
325        Response::none()
326    }
327
328    /// Render a pixel-perfect image that preserves aspect ratio.
329    ///
330    /// Sends the original RGBA data to the terminal and lets the Kitty
331    /// protocol handle scaling. The container width is `cols` cells;
332    /// height is calculated automatically from the image aspect ratio
333    /// (assuming 8px wide, 16px tall per cell).
334    ///
335    /// Requires a Kitty-compatible terminal (Kitty, Ghostty, WezTerm).
336    pub fn kitty_image_fit(
337        &mut self,
338        rgba: &[u8],
339        src_width: u32,
340        src_height: u32,
341        cols: u32,
342    ) -> Response {
343        let rows = if src_width == 0 {
344            1
345        } else {
346            ((cols as f64 * src_height as f64 * 8.0) / (src_width as f64 * 16.0))
347                .ceil()
348                .max(1.0) as u32
349        };
350        let rgba = normalize_rgba(rgba, src_width, src_height);
351        let sw = src_width;
352        let sh = src_height;
353        let c = cols;
354        let r = rows;
355
356        self.container().w(cols).h(rows).draw(move |buf, rect| {
357            if rect.width == 0 || rect.height == 0 {
358                return;
359            }
360            let encoded = base64_encode(&rgba);
361            let chunks = split_base64(&encoded, 4096);
362            let mut seq = String::new();
363            for (i, chunk) in chunks.iter().enumerate() {
364                let more = if i < chunks.len() - 1 { 1 } else { 0 };
365                if i == 0 {
366                    seq.push_str(&format!(
367                        "\x1b_Ga=T,f=32,s={},v={},c={},r={},C=1,q=2,m={};{}\x1b\\",
368                        sw, sh, c, r, more, chunk
369                    ));
370                } else {
371                    seq.push_str(&format!("\x1b_Gm={};{}\x1b\\", more, chunk));
372                }
373            }
374            buf.raw_sequence(rect.x, rect.y, seq);
375        });
376        Response::none()
377    }
378
379    /// Render streaming text with a typing cursor indicator.
380    ///
381    /// Displays the accumulated text content. While `streaming` is true,
382    /// shows a blinking cursor (`▌`) at the end.
383    ///
384    /// ```no_run
385    /// # use slt::widgets::StreamingTextState;
386    /// # slt::run(|ui: &mut slt::Context| {
387    /// let mut stream = StreamingTextState::new();
388    /// stream.start();
389    /// stream.push("Hello from ");
390    /// stream.push("the AI!");
391    /// ui.streaming_text(&mut stream);
392    /// # });
393    /// ```
394    pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
395        if state.streaming {
396            state.cursor_tick = state.cursor_tick.wrapping_add(1);
397            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
398        }
399
400        if state.content.is_empty() && state.streaming {
401            let cursor = if state.cursor_visible { "▌" } else { " " };
402            let primary = self.theme.primary;
403            self.text(cursor).fg(primary);
404            return Response::none();
405        }
406
407        if !state.content.is_empty() {
408            if state.streaming && state.cursor_visible {
409                self.text_wrap(format!("{}▌", state.content));
410            } else {
411                self.text_wrap(&state.content);
412            }
413        }
414
415        Response::none()
416    }
417
418    /// Render streaming markdown with a typing cursor indicator.
419    ///
420    /// Parses accumulated markdown content line-by-line while streaming.
421    /// Supports headings, lists, inline formatting, horizontal rules, and
422    /// fenced code blocks with open/close tracking across stream chunks.
423    ///
424    /// ```no_run
425    /// # use slt::widgets::StreamingMarkdownState;
426    /// # slt::run(|ui: &mut slt::Context| {
427    /// let mut stream = StreamingMarkdownState::new();
428    /// stream.start();
429    /// stream.push("# Hello\n");
430    /// stream.push("- **streaming** markdown\n");
431    /// stream.push("```rust\nlet x = 1;\n");
432    /// ui.streaming_markdown(&mut stream);
433    /// # });
434    /// ```
435    pub fn streaming_markdown(
436        &mut self,
437        state: &mut crate::widgets::StreamingMarkdownState,
438    ) -> Response {
439        if state.streaming {
440            state.cursor_tick = state.cursor_tick.wrapping_add(1);
441            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
442        }
443
444        if state.content.is_empty() && state.streaming {
445            let cursor = if state.cursor_visible { "▌" } else { " " };
446            let primary = self.theme.primary;
447            self.text(cursor).fg(primary);
448            return Response::none();
449        }
450
451        let show_cursor = state.streaming && state.cursor_visible;
452        let trailing_newline = state.content.ends_with('\n');
453        let lines: Vec<&str> = state.content.lines().collect();
454        let last_line_index = lines.len().saturating_sub(1);
455
456        self.commands.push(Command::BeginContainer {
457            direction: Direction::Column,
458            gap: 0,
459            align: Align::Start,
460            align_self: None,
461            justify: Justify::Start,
462            border: None,
463            border_sides: BorderSides::all(),
464            border_style: Style::new().fg(self.theme.border),
465            bg_color: None,
466            padding: Padding::default(),
467            margin: Margin::default(),
468            constraints: Constraints::default(),
469            title: None,
470            grow: 0,
471            group_name: None,
472        });
473        self.interaction_count += 1;
474
475        let text_style = Style::new().fg(self.theme.text);
476        let bold_style = Style::new().fg(self.theme.text).bold();
477        let code_style = Style::new().fg(self.theme.accent);
478        let border_style = Style::new().fg(self.theme.border).dim();
479
480        let mut in_code_block = false;
481        let mut code_block_lang = String::new();
482
483        for (idx, line) in lines.iter().enumerate() {
484            let line = *line;
485            let trimmed = line.trim();
486            let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
487            let cursor = if append_cursor { "▌" } else { "" };
488
489            if in_code_block {
490                if trimmed.starts_with("```") {
491                    in_code_block = false;
492                    code_block_lang.clear();
493                    let mut line = String::from("  └────");
494                    line.push_str(cursor);
495                    self.styled(line, border_style);
496                } else {
497                    let mut line_text = String::with_capacity(2 + line.len() + cursor.len());
498                    line_text.push_str("  ");
499                    line_text.push_str(line);
500                    line_text.push_str(cursor);
501                    self.styled(line_text, code_style);
502                }
503                continue;
504            }
505
506            if trimmed.is_empty() {
507                if append_cursor {
508                    self.styled("▌", Style::new().fg(self.theme.primary));
509                } else {
510                    self.text(" ");
511                }
512                continue;
513            }
514
515            if trimmed == "---" || trimmed == "***" || trimmed == "___" {
516                let mut line = "─".repeat(40);
517                line.push_str(cursor);
518                self.styled(line, border_style);
519                continue;
520            }
521
522            if let Some(heading) = trimmed.strip_prefix("### ") {
523                let mut line = String::with_capacity(heading.len() + cursor.len());
524                line.push_str(heading);
525                line.push_str(cursor);
526                self.styled(line, Style::new().bold().fg(self.theme.accent));
527                continue;
528            }
529
530            if let Some(heading) = trimmed.strip_prefix("## ") {
531                let mut line = String::with_capacity(heading.len() + cursor.len());
532                line.push_str(heading);
533                line.push_str(cursor);
534                self.styled(line, Style::new().bold().fg(self.theme.secondary));
535                continue;
536            }
537
538            if let Some(heading) = trimmed.strip_prefix("# ") {
539                let mut line = String::with_capacity(heading.len() + cursor.len());
540                line.push_str(heading);
541                line.push_str(cursor);
542                self.styled(line, Style::new().bold().fg(self.theme.primary));
543                continue;
544            }
545
546            if let Some(code) = trimmed.strip_prefix("```") {
547                in_code_block = true;
548                code_block_lang = code.trim().to_string();
549                let label = if code_block_lang.is_empty() {
550                    "code".to_string()
551                } else {
552                    let mut label = String::from("code:");
553                    label.push_str(&code_block_lang);
554                    label
555                };
556                let mut line = String::with_capacity(5 + label.len() + cursor.len());
557                line.push_str("  ┌─");
558                line.push_str(&label);
559                line.push('─');
560                line.push_str(cursor);
561                self.styled(line, border_style);
562                continue;
563            }
564
565            if let Some(item) = trimmed
566                .strip_prefix("- ")
567                .or_else(|| trimmed.strip_prefix("* "))
568            {
569                let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
570                if segs.len() <= 1 {
571                    let mut line = String::with_capacity(4 + item.len() + cursor.len());
572                    line.push_str("  • ");
573                    line.push_str(item);
574                    line.push_str(cursor);
575                    self.styled(line, text_style);
576                } else {
577                    self.line(|ui| {
578                        ui.styled("  • ", text_style);
579                        for (s, st) in segs {
580                            ui.styled(s, st);
581                        }
582                        if append_cursor {
583                            ui.styled("▌", Style::new().fg(ui.theme.primary));
584                        }
585                    });
586                }
587                continue;
588            }
589
590            if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
591                let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
592                if parts.len() == 2 {
593                    let segs =
594                        Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
595                    if segs.len() <= 1 {
596                        let mut line = String::with_capacity(
597                            4 + parts[0].len() + parts[1].len() + cursor.len(),
598                        );
599                        line.push_str("  ");
600                        line.push_str(parts[0]);
601                        line.push_str(". ");
602                        line.push_str(parts[1]);
603                        line.push_str(cursor);
604                        self.styled(line, text_style);
605                    } else {
606                        self.line(|ui| {
607                            let mut prefix = String::with_capacity(4 + parts[0].len());
608                            prefix.push_str("  ");
609                            prefix.push_str(parts[0]);
610                            prefix.push_str(". ");
611                            ui.styled(prefix, text_style);
612                            for (s, st) in segs {
613                                ui.styled(s, st);
614                            }
615                            if append_cursor {
616                                ui.styled("▌", Style::new().fg(ui.theme.primary));
617                            }
618                        });
619                    }
620                } else {
621                    let mut line = String::with_capacity(trimmed.len() + cursor.len());
622                    line.push_str(trimmed);
623                    line.push_str(cursor);
624                    self.styled(line, text_style);
625                }
626                continue;
627            }
628
629            let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
630            if segs.len() <= 1 {
631                let mut line = String::with_capacity(trimmed.len() + cursor.len());
632                line.push_str(trimmed);
633                line.push_str(cursor);
634                self.styled(line, text_style);
635            } else {
636                self.line(|ui| {
637                    for (s, st) in segs {
638                        ui.styled(s, st);
639                    }
640                    if append_cursor {
641                        ui.styled("▌", Style::new().fg(ui.theme.primary));
642                    }
643                });
644            }
645        }
646
647        if show_cursor && trailing_newline {
648            if in_code_block {
649                self.styled("  ▌", code_style);
650            } else {
651                self.styled("▌", Style::new().fg(self.theme.primary));
652            }
653        }
654
655        state.in_code_block = in_code_block;
656        state.code_block_lang = code_block_lang;
657
658        self.commands.push(Command::EndContainer);
659        self.last_text_idx = None;
660        Response::none()
661    }
662
663    /// Render a tool approval widget with approve/reject buttons.
664    ///
665    /// Shows the tool name, description, and two action buttons.
666    /// Returns the updated [`ApprovalAction`] each frame.
667    ///
668    /// ```no_run
669    /// # use slt::widgets::{ApprovalAction, ToolApprovalState};
670    /// # slt::run(|ui: &mut slt::Context| {
671    /// let mut tool = ToolApprovalState::new("read_file", "Read contents of config.toml");
672    /// ui.tool_approval(&mut tool);
673    /// if tool.action == ApprovalAction::Approved {
674    /// }
675    /// # });
676    /// ```
677    pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
678        let old_action = state.action;
679        let theme = self.theme;
680        let _ = self.bordered(Border::Rounded).col(|ui| {
681            let _ = ui.row(|ui| {
682                ui.text("⚡").fg(theme.warning);
683                ui.text(&state.tool_name).bold().fg(theme.primary);
684            });
685            ui.text(&state.description).dim();
686
687            if state.action == ApprovalAction::Pending {
688                let _ = ui.row(|ui| {
689                    if ui.button("✓ Approve").clicked {
690                        state.action = ApprovalAction::Approved;
691                    }
692                    if ui.button("✗ Reject").clicked {
693                        state.action = ApprovalAction::Rejected;
694                    }
695                });
696            } else {
697                let (label, color) = match state.action {
698                    ApprovalAction::Approved => ("✓ Approved", theme.success),
699                    ApprovalAction::Rejected => ("✗ Rejected", theme.error),
700                    ApprovalAction::Pending => unreachable!(),
701                };
702                ui.text(label).fg(color).bold();
703            }
704        });
705
706        Response {
707            changed: state.action != old_action,
708            ..Response::none()
709        }
710    }
711
712    /// Render a context bar showing active context items with token counts.
713    ///
714    /// Displays a horizontal bar of context sources (files, URLs, etc.)
715    /// with their token counts, useful for AI chat interfaces.
716    ///
717    /// ```no_run
718    /// # use slt::widgets::ContextItem;
719    /// # slt::run(|ui: &mut slt::Context| {
720    /// let items = vec![ContextItem::new("main.rs", 1200), ContextItem::new("lib.rs", 800)];
721    /// ui.context_bar(&items);
722    /// # });
723    /// ```
724    pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
725        if items.is_empty() {
726            return Response::none();
727        }
728
729        let theme = self.theme;
730        let total: usize = items.iter().map(|item| item.tokens).sum();
731
732        let _ = self.container().row(|ui| {
733            ui.text("📎").dim();
734            for item in items {
735                let token_count = format_token_count(item.tokens);
736                let mut line = String::with_capacity(item.label.len() + token_count.len() + 3);
737                line.push_str(&item.label);
738                line.push_str(" (");
739                line.push_str(&token_count);
740                line.push(')');
741                ui.text(line).fg(theme.secondary);
742            }
743            ui.spacer();
744            let total_text = format_token_count(total);
745            let mut line = String::with_capacity(2 + total_text.len());
746            line.push_str("Σ ");
747            line.push_str(&total_text);
748            ui.text(line).dim();
749        });
750
751        Response::none()
752    }
753
754    pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
755        use crate::widgets::AlertLevel;
756
757        let theme = self.theme;
758        let (icon, color) = match level {
759            AlertLevel::Info => ("ℹ", theme.accent),
760            AlertLevel::Success => ("✓", theme.success),
761            AlertLevel::Warning => ("⚠", theme.warning),
762            AlertLevel::Error => ("✕", theme.error),
763        };
764
765        let focused = self.register_focusable();
766        let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
767
768        let mut response = self.container().col(|ui| {
769            ui.line(|ui| {
770                let mut icon_text = String::with_capacity(icon.len() + 2);
771                icon_text.push(' ');
772                icon_text.push_str(icon);
773                icon_text.push(' ');
774                ui.text(icon_text).fg(color).bold();
775                ui.text(message).grow(1);
776                ui.text(" [×] ").dim();
777            });
778        });
779        response.focused = focused;
780        if key_dismiss {
781            response.clicked = true;
782        }
783
784        response
785    }
786
787    /// Yes/No confirmation dialog. Returns Response with .clicked=true when answered.
788    ///
789    /// `result` is set to true for Yes, false for No.
790    ///
791    /// # Examples
792    /// ```
793    /// # use slt::*;
794    /// # TestBackend::new(80, 24).render(|ui| {
795    /// let mut answer = false;
796    /// let r = ui.confirm("Delete this file?", &mut answer);
797    /// if r.clicked && answer { /* user confirmed */ }
798    /// # });
799    /// ```
800    pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
801        let focused = self.register_focusable();
802        let mut is_yes = *result;
803        let mut clicked = false;
804
805        if focused {
806            let mut consumed_indices = Vec::new();
807            for (i, event) in self.events.iter().enumerate() {
808                if let Event::Key(key) = event {
809                    if key.kind != KeyEventKind::Press {
810                        continue;
811                    }
812
813                    match key.code {
814                        KeyCode::Char('y') => {
815                            is_yes = true;
816                            *result = true;
817                            clicked = true;
818                            consumed_indices.push(i);
819                        }
820                        KeyCode::Char('n') => {
821                            is_yes = false;
822                            *result = false;
823                            clicked = true;
824                            consumed_indices.push(i);
825                        }
826                        KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
827                            is_yes = !is_yes;
828                            *result = is_yes;
829                            consumed_indices.push(i);
830                        }
831                        KeyCode::Enter => {
832                            *result = is_yes;
833                            clicked = true;
834                            consumed_indices.push(i);
835                        }
836                        _ => {}
837                    }
838                }
839            }
840
841            for idx in consumed_indices {
842                self.consumed[idx] = true;
843            }
844        }
845
846        let yes_style = if is_yes {
847            if focused {
848                Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
849            } else {
850                Style::new().fg(self.theme.success).bold()
851            }
852        } else {
853            Style::new().fg(self.theme.text_dim)
854        };
855        let no_style = if !is_yes {
856            if focused {
857                Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
858            } else {
859                Style::new().fg(self.theme.error).bold()
860            }
861        } else {
862            Style::new().fg(self.theme.text_dim)
863        };
864
865        let mut response = self.row(|ui| {
866            ui.text(question);
867            ui.text(" ");
868            ui.styled("[Yes]", yes_style);
869            ui.text(" ");
870            ui.styled("[No]", no_style);
871        });
872        response.focused = focused;
873        response.clicked = clicked;
874        response.changed = clicked;
875        response
876    }
877
878    pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
879        self.breadcrumb_with(segments, " › ")
880    }
881
882    pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
883        let theme = self.theme;
884        let last_idx = segments.len().saturating_sub(1);
885        let mut clicked_idx: Option<usize> = None;
886
887        let _ = self.row(|ui| {
888            for (i, segment) in segments.iter().enumerate() {
889                let is_last = i == last_idx;
890                if is_last {
891                    ui.text(*segment).bold();
892                } else {
893                    let focused = ui.register_focusable();
894                    let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
895                    let resp = ui.interaction();
896                    let color = if resp.hovered || focused {
897                        theme.accent
898                    } else {
899                        theme.primary
900                    };
901                    ui.text(*segment).fg(color).underline();
902                    if resp.clicked || pressed {
903                        clicked_idx = Some(i);
904                    }
905                    ui.text(separator).dim();
906                }
907            }
908        });
909
910        clicked_idx
911    }
912
913    pub fn accordion(
914        &mut self,
915        title: &str,
916        open: &mut bool,
917        f: impl FnOnce(&mut Context),
918    ) -> Response {
919        let theme = self.theme;
920        let focused = self.register_focusable();
921        let old_open = *open;
922
923        if focused && self.key_code(KeyCode::Enter) {
924            *open = !*open;
925        }
926
927        let icon = if *open { "▾" } else { "▸" };
928        let title_color = if focused { theme.primary } else { theme.text };
929
930        let mut response = self.container().col(|ui| {
931            ui.line(|ui| {
932                ui.text(icon).fg(title_color);
933                let mut title_text = String::with_capacity(1 + title.len());
934                title_text.push(' ');
935                title_text.push_str(title);
936                ui.text(title_text).bold().fg(title_color);
937            });
938        });
939
940        if response.clicked {
941            *open = !*open;
942        }
943
944        if *open {
945            let _ = self.container().pl(2).col(f);
946        }
947
948        response.focused = focused;
949        response.changed = *open != old_open;
950        response
951    }
952
953    pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
954        let max_key_width = items
955            .iter()
956            .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
957            .max()
958            .unwrap_or(0);
959
960        let _ = self.col(|ui| {
961            for (key, value) in items {
962                ui.line(|ui| {
963                    let padded = format!("{:>width$}", key, width = max_key_width);
964                    ui.text(padded).dim();
965                    ui.text("  ");
966                    ui.text(*value);
967                });
968            }
969        });
970
971        Response::none()
972    }
973
974    pub fn divider_text(&mut self, label: &str) -> Response {
975        let w = self.width();
976        let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
977        let pad = 1u32;
978        let left_len = 4u32;
979        let right_len = w.saturating_sub(left_len + pad + label_len + pad);
980        let left: String = "─".repeat(left_len as usize);
981        let right: String = "─".repeat(right_len as usize);
982        let theme = self.theme;
983        self.line(|ui| {
984            ui.text(&left).fg(theme.border);
985            let mut label_text = String::with_capacity(label.len() + 2);
986            label_text.push(' ');
987            label_text.push_str(label);
988            label_text.push(' ');
989            ui.text(label_text).fg(theme.text);
990            ui.text(&right).fg(theme.border);
991        });
992
993        Response::none()
994    }
995
996    pub fn badge(&mut self, label: &str) -> Response {
997        let theme = self.theme;
998        self.badge_colored(label, theme.primary)
999    }
1000
1001    pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
1002        let fg = Color::contrast_fg(color);
1003        let mut label_text = String::with_capacity(label.len() + 2);
1004        label_text.push(' ');
1005        label_text.push_str(label);
1006        label_text.push(' ');
1007        self.text(label_text).fg(fg).bg(color);
1008
1009        Response::none()
1010    }
1011
1012    pub fn key_hint(&mut self, key: &str) -> Response {
1013        let theme = self.theme;
1014        let mut key_text = String::with_capacity(key.len() + 2);
1015        key_text.push(' ');
1016        key_text.push_str(key);
1017        key_text.push(' ');
1018        self.text(key_text).reversed().fg(theme.text_dim);
1019
1020        Response::none()
1021    }
1022
1023    pub fn stat(&mut self, label: &str, value: &str) -> Response {
1024        let _ = self.col(|ui| {
1025            ui.text(label).dim();
1026            ui.text(value).bold();
1027        });
1028
1029        Response::none()
1030    }
1031
1032    pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
1033        let _ = self.col(|ui| {
1034            ui.text(label).dim();
1035            ui.text(value).bold().fg(color);
1036        });
1037
1038        Response::none()
1039    }
1040
1041    pub fn stat_trend(
1042        &mut self,
1043        label: &str,
1044        value: &str,
1045        trend: crate::widgets::Trend,
1046    ) -> Response {
1047        let theme = self.theme;
1048        let (arrow, color) = match trend {
1049            crate::widgets::Trend::Up => ("↑", theme.success),
1050            crate::widgets::Trend::Down => ("↓", theme.error),
1051        };
1052        let _ = self.col(|ui| {
1053            ui.text(label).dim();
1054            ui.line(|ui| {
1055                ui.text(value).bold();
1056                let mut arrow_text = String::with_capacity(1 + arrow.len());
1057                arrow_text.push(' ');
1058                arrow_text.push_str(arrow);
1059                ui.text(arrow_text).fg(color);
1060            });
1061        });
1062
1063        Response::none()
1064    }
1065
1066    pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
1067        let _ = self.container().center().col(|ui| {
1068            ui.text(title).align(Align::Center);
1069            ui.text(description).dim().align(Align::Center);
1070        });
1071
1072        Response::none()
1073    }
1074
1075    pub fn empty_state_action(
1076        &mut self,
1077        title: &str,
1078        description: &str,
1079        action_label: &str,
1080    ) -> Response {
1081        let mut clicked = false;
1082        let _ = self.container().center().col(|ui| {
1083            ui.text(title).align(Align::Center);
1084            ui.text(description).dim().align(Align::Center);
1085            if ui.button(action_label).clicked {
1086                clicked = true;
1087            }
1088        });
1089
1090        Response {
1091            clicked,
1092            changed: clicked,
1093            ..Response::none()
1094        }
1095    }
1096
1097    pub fn code_block(&mut self, code: &str) -> Response {
1098        let theme = self.theme;
1099        let _ = self
1100            .bordered(Border::Rounded)
1101            .bg(theme.surface)
1102            .pad(1)
1103            .col(|ui| {
1104                for line in code.lines() {
1105                    render_highlighted_line(ui, line);
1106                }
1107            });
1108
1109        Response::none()
1110    }
1111
1112    pub fn code_block_numbered(&mut self, code: &str) -> Response {
1113        let lines: Vec<&str> = code.lines().collect();
1114        let gutter_w = format!("{}", lines.len()).len();
1115        let theme = self.theme;
1116        let _ = self
1117            .bordered(Border::Rounded)
1118            .bg(theme.surface)
1119            .pad(1)
1120            .col(|ui| {
1121                for (i, line) in lines.iter().enumerate() {
1122                    ui.line(|ui| {
1123                        ui.text(format!("{:>gutter_w$} │ ", i + 1))
1124                            .fg(theme.text_dim);
1125                        render_highlighted_line(ui, line);
1126                    });
1127                }
1128            });
1129
1130        Response::none()
1131    }
1132
1133    /// Enable word-boundary wrapping on the last rendered text element.
1134    pub fn wrap(&mut self) -> &mut Self {
1135        if let Some(idx) = self.last_text_idx {
1136            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
1137                *wrap = true;
1138            }
1139        }
1140        self
1141    }
1142
1143    /// Truncate the last rendered text with `…` when it exceeds its allocated width.
1144    /// Use with `.w()` to set a fixed width, or let the parent container constrain it.
1145    pub fn truncate(&mut self) -> &mut Self {
1146        if let Some(idx) = self.last_text_idx {
1147            if let Command::Text { truncate, .. } = &mut self.commands[idx] {
1148                *truncate = true;
1149            }
1150        }
1151        self
1152    }
1153
1154    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
1155        if let Some(idx) = self.last_text_idx {
1156            match &mut self.commands[idx] {
1157                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
1158                _ => {}
1159            }
1160        }
1161    }
1162
1163    fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
1164        if let Some(idx) = self.last_text_idx {
1165            match &mut self.commands[idx] {
1166                Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
1167                    f(constraints)
1168                }
1169                _ => {}
1170            }
1171        }
1172    }
1173
1174    fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
1175        if let Some(idx) = self.last_text_idx {
1176            match &mut self.commands[idx] {
1177                Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
1178                _ => {}
1179            }
1180        }
1181    }
1182
1183    // ── containers ───────────────────────────────────────────────────
1184
1185    /// Create a vertical (column) container.
1186    ///
1187    /// Children are stacked top-to-bottom. Returns a [`Response`] with
1188    /// click/hover state for the container area.
1189    ///
1190    /// # Example
1191    ///
1192    /// ```no_run
1193    /// # slt::run(|ui: &mut slt::Context| {
1194    /// ui.col(|ui| {
1195    ///     ui.text("line one");
1196    ///     ui.text("line two");
1197    /// });
1198    /// # });
1199    /// ```
1200    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1201        self.push_container(Direction::Column, 0, f)
1202    }
1203
1204    /// Create a vertical (column) container with a gap between children.
1205    ///
1206    /// `gap` is the number of blank rows inserted between each child.
1207    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1208        self.push_container(Direction::Column, gap, f)
1209    }
1210
1211    /// Create a horizontal (row) container.
1212    ///
1213    /// Children are placed left-to-right. Returns a [`Response`] with
1214    /// click/hover state for the container area.
1215    ///
1216    /// # Example
1217    ///
1218    /// ```no_run
1219    /// # slt::run(|ui: &mut slt::Context| {
1220    /// ui.row(|ui| {
1221    ///     ui.text("left");
1222    ///     ui.spacer();
1223    ///     ui.text("right");
1224    /// });
1225    /// # });
1226    /// ```
1227    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1228        self.push_container(Direction::Row, 0, f)
1229    }
1230
1231    /// Create a horizontal (row) container with a gap between children.
1232    ///
1233    /// `gap` is the number of blank columns inserted between each child.
1234    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1235        self.push_container(Direction::Row, gap, f)
1236    }
1237
1238    /// Render inline text with mixed styles on a single line.
1239    ///
1240    /// Unlike [`row`](Context::row), `line()` is designed for rich text —
1241    /// children are rendered as continuous inline text without gaps.
1242    ///
1243    /// # Example
1244    ///
1245    /// ```no_run
1246    /// # use slt::Color;
1247    /// # slt::run(|ui: &mut slt::Context| {
1248    /// ui.line(|ui| {
1249    ///     ui.text("Status: ");
1250    ///     ui.text("Online").bold().fg(Color::Green);
1251    /// });
1252    /// # });
1253    /// ```
1254    pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1255        let _ = self.push_container(Direction::Row, 0, f);
1256        self
1257    }
1258
1259    /// Render inline text with mixed styles, wrapping at word boundaries.
1260    ///
1261    /// Like [`line`](Context::line), but when the combined text exceeds
1262    /// the container width it wraps across multiple lines while
1263    /// preserving per-segment styles.
1264    ///
1265    /// # Example
1266    ///
1267    /// ```no_run
1268    /// # use slt::{Color, Style};
1269    /// # slt::run(|ui: &mut slt::Context| {
1270    /// ui.line_wrap(|ui| {
1271    ///     ui.text("This is a long ");
1272    ///     ui.text("important").bold().fg(Color::Red);
1273    ///     ui.text(" message that wraps across lines");
1274    /// });
1275    /// # });
1276    /// ```
1277    pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1278        let start = self.commands.len();
1279        f(self);
1280        let mut segments: Vec<(String, Style)> = Vec::new();
1281        for cmd in self.commands.drain(start..) {
1282            if let Command::Text { content, style, .. } = cmd {
1283                segments.push((content, style));
1284            }
1285        }
1286        self.commands.push(Command::RichText {
1287            segments,
1288            wrap: true,
1289            align: Align::Start,
1290            margin: Margin::default(),
1291            constraints: Constraints::default(),
1292        });
1293        self.last_text_idx = None;
1294        self
1295    }
1296
1297    /// Render content in a modal overlay with dimmed background.
1298    ///
1299    /// ```ignore
1300    /// ui.modal(|ui| {
1301    ///     ui.text("Are you sure?");
1302    ///     if ui.button("OK") { show = false; }
1303    /// });
1304    /// ```
1305    pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1306        let interaction_id = self.interaction_count;
1307        self.interaction_count += 1;
1308        self.commands.push(Command::BeginOverlay { modal: true });
1309        self.overlay_depth += 1;
1310        self.modal_active = true;
1311        f(self);
1312        self.overlay_depth = self.overlay_depth.saturating_sub(1);
1313        self.commands.push(Command::EndOverlay);
1314        self.last_text_idx = None;
1315        self.response_for(interaction_id)
1316    }
1317
1318    /// Render floating content without dimming the background.
1319    pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1320        let interaction_id = self.interaction_count;
1321        self.interaction_count += 1;
1322        self.commands.push(Command::BeginOverlay { modal: false });
1323        self.overlay_depth += 1;
1324        f(self);
1325        self.overlay_depth = self.overlay_depth.saturating_sub(1);
1326        self.commands.push(Command::EndOverlay);
1327        self.last_text_idx = None;
1328        self.response_for(interaction_id)
1329    }
1330
1331    /// Create a named group container for shared hover/focus styling.
1332    ///
1333    /// ```ignore
1334    /// ui.group("card").border(Border::Rounded)
1335    ///     .group_hover_bg(Color::Indexed(238))
1336    ///     .col(|ui| { ui.text("Hover anywhere"); });
1337    /// ```
1338    pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1339        self.group_count = self.group_count.saturating_add(1);
1340        self.group_stack.push(name.to_string());
1341        self.container().group_name(name.to_string())
1342    }
1343
1344    /// Create a container with a fluent builder.
1345    ///
1346    /// Use this for borders, padding, grow, constraints, and titles. Chain
1347    /// configuration methods on the returned [`ContainerBuilder`], then call
1348    /// `.col()` or `.row()` to finalize.
1349    ///
1350    /// # Example
1351    ///
1352    /// ```no_run
1353    /// # slt::run(|ui: &mut slt::Context| {
1354    /// use slt::Border;
1355    /// ui.container()
1356    ///     .border(Border::Rounded)
1357    ///     .pad(1)
1358    ///     .title("My Panel")
1359    ///     .col(|ui| {
1360    ///         ui.text("content");
1361    ///     });
1362    /// # });
1363    /// ```
1364    pub fn container(&mut self) -> ContainerBuilder<'_> {
1365        let border = self.theme.border;
1366        ContainerBuilder {
1367            ctx: self,
1368            gap: 0,
1369            row_gap: None,
1370            col_gap: None,
1371            align: Align::Start,
1372            align_self_value: None,
1373            justify: Justify::Start,
1374            border: None,
1375            border_sides: BorderSides::all(),
1376            border_style: Style::new().fg(border),
1377            bg: None,
1378            text_color: None,
1379            dark_bg: None,
1380            dark_border_style: None,
1381            group_hover_bg: None,
1382            group_hover_border_style: None,
1383            group_name: None,
1384            padding: Padding::default(),
1385            margin: Margin::default(),
1386            constraints: Constraints::default(),
1387            title: None,
1388            grow: 0,
1389            scroll_offset: None,
1390        }
1391    }
1392
1393    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
1394    ///
1395    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
1396    /// is updated in-place with the current scroll offset and bounds.
1397    ///
1398    /// # Example
1399    ///
1400    /// ```no_run
1401    /// # use slt::widgets::ScrollState;
1402    /// # slt::run(|ui: &mut slt::Context| {
1403    /// let mut scroll = ScrollState::new();
1404    /// ui.scrollable(&mut scroll).col(|ui| {
1405    ///     for i in 0..100 {
1406    ///         ui.text(format!("Line {i}"));
1407    ///     }
1408    /// });
1409    /// # });
1410    /// ```
1411    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1412        let index = self.scroll_count;
1413        self.scroll_count += 1;
1414        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1415            state.set_bounds(ch, vh);
1416            let max = ch.saturating_sub(vh) as usize;
1417            state.offset = state.offset.min(max);
1418        }
1419
1420        let next_id = self.interaction_count;
1421        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1422            let inner_rects: Vec<Rect> = self
1423                .prev_scroll_rects
1424                .iter()
1425                .enumerate()
1426                .filter(|&(j, sr)| {
1427                    j != index
1428                        && sr.width > 0
1429                        && sr.height > 0
1430                        && sr.x >= rect.x
1431                        && sr.right() <= rect.right()
1432                        && sr.y >= rect.y
1433                        && sr.bottom() <= rect.bottom()
1434                })
1435                .map(|(_, sr)| *sr)
1436                .collect();
1437            self.auto_scroll_nested(&rect, state, &inner_rects);
1438        }
1439
1440        self.container().scroll_offset(state.offset as u32)
1441    }
1442
1443    /// Render a scrollbar track for a [`ScrollState`].
1444    ///
1445    /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
1446    /// and position are calculated from the scroll state's content height,
1447    /// viewport height, and current offset.
1448    ///
1449    /// Typically placed beside a `scrollable()` container in a `row()`:
1450    /// ```no_run
1451    /// # use slt::widgets::ScrollState;
1452    /// # slt::run(|ui: &mut slt::Context| {
1453    /// let mut scroll = ScrollState::new();
1454    /// ui.row(|ui| {
1455    ///     ui.scrollable(&mut scroll).grow(1).col(|ui| {
1456    ///         for i in 0..100 { ui.text(format!("Line {i}")); }
1457    ///     });
1458    ///     ui.scrollbar(&scroll);
1459    /// });
1460    /// # });
1461    /// ```
1462    pub fn scrollbar(&mut self, state: &ScrollState) {
1463        let vh = state.viewport_height();
1464        let ch = state.content_height();
1465        if vh == 0 || ch <= vh {
1466            return;
1467        }
1468
1469        let track_height = vh;
1470        let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1471        let max_offset = ch.saturating_sub(vh);
1472        let thumb_pos = if max_offset == 0 {
1473            0
1474        } else {
1475            ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1476                .round() as u32
1477        };
1478
1479        let theme = self.theme;
1480        let track_char = '│';
1481        let thumb_char = '█';
1482
1483        let _ = self.container().w(1).h(track_height).col(|ui| {
1484            for i in 0..track_height {
1485                if i >= thumb_pos && i < thumb_pos + thumb_height {
1486                    ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1487                } else {
1488                    ui.styled(
1489                        track_char.to_string(),
1490                        Style::new().fg(theme.text_dim).dim(),
1491                    );
1492                }
1493            }
1494        });
1495    }
1496
1497    fn auto_scroll_nested(
1498        &mut self,
1499        rect: &Rect,
1500        state: &mut ScrollState,
1501        inner_scroll_rects: &[Rect],
1502    ) {
1503        let mut to_consume: Vec<usize> = Vec::new();
1504
1505        for (i, event) in self.events.iter().enumerate() {
1506            if self.consumed[i] {
1507                continue;
1508            }
1509            if let Event::Mouse(mouse) = event {
1510                let in_bounds = mouse.x >= rect.x
1511                    && mouse.x < rect.right()
1512                    && mouse.y >= rect.y
1513                    && mouse.y < rect.bottom();
1514                if !in_bounds {
1515                    continue;
1516                }
1517                let in_inner = inner_scroll_rects.iter().any(|sr| {
1518                    mouse.x >= sr.x
1519                        && mouse.x < sr.right()
1520                        && mouse.y >= sr.y
1521                        && mouse.y < sr.bottom()
1522                });
1523                if in_inner {
1524                    continue;
1525                }
1526                match mouse.kind {
1527                    MouseKind::ScrollUp => {
1528                        state.scroll_up(1);
1529                        to_consume.push(i);
1530                    }
1531                    MouseKind::ScrollDown => {
1532                        state.scroll_down(1);
1533                        to_consume.push(i);
1534                    }
1535                    MouseKind::Drag(MouseButton::Left) => {}
1536                    _ => {}
1537                }
1538            }
1539        }
1540
1541        for i in to_consume {
1542            self.consumed[i] = true;
1543        }
1544    }
1545
1546    /// Shortcut for `container().border(border)`.
1547    ///
1548    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
1549    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1550        self.container()
1551            .border(border)
1552            .border_sides(BorderSides::all())
1553    }
1554
1555    fn push_container(
1556        &mut self,
1557        direction: Direction,
1558        gap: u32,
1559        f: impl FnOnce(&mut Context),
1560    ) -> Response {
1561        let interaction_id = self.interaction_count;
1562        self.interaction_count += 1;
1563        let border = self.theme.border;
1564
1565        self.commands.push(Command::BeginContainer {
1566            direction,
1567            gap,
1568            align: Align::Start,
1569            align_self: None,
1570            justify: Justify::Start,
1571            border: None,
1572            border_sides: BorderSides::all(),
1573            border_style: Style::new().fg(border),
1574            bg_color: None,
1575            padding: Padding::default(),
1576            margin: Margin::default(),
1577            constraints: Constraints::default(),
1578            title: None,
1579            grow: 0,
1580            group_name: None,
1581        });
1582        self.text_color_stack.push(None);
1583        f(self);
1584        self.text_color_stack.pop();
1585        self.commands.push(Command::EndContainer);
1586        self.last_text_idx = None;
1587
1588        self.response_for(interaction_id)
1589    }
1590
1591    pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1592        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1593            return Response::none();
1594        }
1595        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1596            let clicked = self
1597                .click_pos
1598                .map(|(mx, my)| {
1599                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1600                })
1601                .unwrap_or(false);
1602            let hovered = self
1603                .mouse_pos
1604                .map(|(mx, my)| {
1605                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1606                })
1607                .unwrap_or(false);
1608            Response {
1609                clicked,
1610                hovered,
1611                changed: false,
1612                focused: false,
1613                rect: *rect,
1614            }
1615        } else {
1616            Response::none()
1617        }
1618    }
1619
1620    /// Returns true if the named group is currently hovered by the mouse.
1621    pub fn is_group_hovered(&self, name: &str) -> bool {
1622        if let Some(pos) = self.mouse_pos {
1623            self.prev_group_rects.iter().any(|(n, rect)| {
1624                n == name
1625                    && pos.0 >= rect.x
1626                    && pos.0 < rect.x + rect.width
1627                    && pos.1 >= rect.y
1628                    && pos.1 < rect.y + rect.height
1629            })
1630        } else {
1631            false
1632        }
1633    }
1634
1635    /// Returns true if the named group contains the currently focused widget.
1636    pub fn is_group_focused(&self, name: &str) -> bool {
1637        if self.prev_focus_count == 0 {
1638            return false;
1639        }
1640        let focused_index = self.focus_index % self.prev_focus_count;
1641        self.prev_focus_groups
1642            .get(focused_index)
1643            .and_then(|group| group.as_deref())
1644            .map(|group| group == name)
1645            .unwrap_or(false)
1646    }
1647
1648    /// Set the flex-grow factor of the last rendered text element.
1649    ///
1650    /// A value of `1` causes the element to expand and fill remaining space
1651    /// along the main axis.
1652    pub fn grow(&mut self, value: u16) -> &mut Self {
1653        if let Some(idx) = self.last_text_idx {
1654            if let Command::Text { grow, .. } = &mut self.commands[idx] {
1655                *grow = value;
1656            }
1657        }
1658        self
1659    }
1660
1661    /// Set the text alignment of the last rendered text element.
1662    pub fn align(&mut self, align: Align) -> &mut Self {
1663        if let Some(idx) = self.last_text_idx {
1664            if let Command::Text {
1665                align: text_align, ..
1666            } = &mut self.commands[idx]
1667            {
1668                *text_align = align;
1669            }
1670        }
1671        self
1672    }
1673
1674    /// Center-align the last rendered text element horizontally.
1675    /// Shorthand for `.align(Align::Center)`. Requires the text to have
1676    /// a width constraint (via `.w()` or parent container) to be visible.
1677    pub fn text_center(&mut self) -> &mut Self {
1678        self.align(Align::Center)
1679    }
1680
1681    /// Right-align the last rendered text element horizontally.
1682    /// Shorthand for `.align(Align::End)`.
1683    pub fn text_right(&mut self) -> &mut Self {
1684        self.align(Align::End)
1685    }
1686
1687    // ── size constraints on last text/link ──────────────────────────
1688
1689    /// Set a fixed width on the last rendered text or link element.
1690    ///
1691    /// Sets both `min_width` and `max_width` to `value`, making the element
1692    /// occupy exactly that many columns (padded with spaces or truncated).
1693    pub fn w(&mut self, value: u32) -> &mut Self {
1694        self.modify_last_constraints(|c| {
1695            c.min_width = Some(value);
1696            c.max_width = Some(value);
1697        });
1698        self
1699    }
1700
1701    /// Set a fixed height on the last rendered text or link element.
1702    ///
1703    /// Sets both `min_height` and `max_height` to `value`.
1704    pub fn h(&mut self, value: u32) -> &mut Self {
1705        self.modify_last_constraints(|c| {
1706            c.min_height = Some(value);
1707            c.max_height = Some(value);
1708        });
1709        self
1710    }
1711
1712    /// Set the minimum width on the last rendered text or link element.
1713    pub fn min_w(&mut self, value: u32) -> &mut Self {
1714        self.modify_last_constraints(|c| c.min_width = Some(value));
1715        self
1716    }
1717
1718    /// Set the maximum width on the last rendered text or link element.
1719    pub fn max_w(&mut self, value: u32) -> &mut Self {
1720        self.modify_last_constraints(|c| c.max_width = Some(value));
1721        self
1722    }
1723
1724    /// Set the minimum height on the last rendered text or link element.
1725    pub fn min_h(&mut self, value: u32) -> &mut Self {
1726        self.modify_last_constraints(|c| c.min_height = Some(value));
1727        self
1728    }
1729
1730    /// Set the maximum height on the last rendered text or link element.
1731    pub fn max_h(&mut self, value: u32) -> &mut Self {
1732        self.modify_last_constraints(|c| c.max_height = Some(value));
1733        self
1734    }
1735
1736    // ── margin on last text/link ────────────────────────────────────
1737
1738    /// Set uniform margin on all sides of the last rendered text or link element.
1739    pub fn m(&mut self, value: u32) -> &mut Self {
1740        self.modify_last_margin(|m| *m = Margin::all(value));
1741        self
1742    }
1743
1744    /// Set horizontal margin (left + right) on the last rendered text or link.
1745    pub fn mx(&mut self, value: u32) -> &mut Self {
1746        self.modify_last_margin(|m| {
1747            m.left = value;
1748            m.right = value;
1749        });
1750        self
1751    }
1752
1753    /// Set vertical margin (top + bottom) on the last rendered text or link.
1754    pub fn my(&mut self, value: u32) -> &mut Self {
1755        self.modify_last_margin(|m| {
1756            m.top = value;
1757            m.bottom = value;
1758        });
1759        self
1760    }
1761
1762    /// Set top margin on the last rendered text or link element.
1763    pub fn mt(&mut self, value: u32) -> &mut Self {
1764        self.modify_last_margin(|m| m.top = value);
1765        self
1766    }
1767
1768    /// Set right margin on the last rendered text or link element.
1769    pub fn mr(&mut self, value: u32) -> &mut Self {
1770        self.modify_last_margin(|m| m.right = value);
1771        self
1772    }
1773
1774    /// Set bottom margin on the last rendered text or link element.
1775    pub fn mb(&mut self, value: u32) -> &mut Self {
1776        self.modify_last_margin(|m| m.bottom = value);
1777        self
1778    }
1779
1780    /// Set left margin on the last rendered text or link element.
1781    pub fn ml(&mut self, value: u32) -> &mut Self {
1782        self.modify_last_margin(|m| m.left = value);
1783        self
1784    }
1785
1786    /// Render an invisible spacer that expands to fill available space.
1787    ///
1788    /// Useful for pushing siblings to opposite ends of a row or column.
1789    pub fn spacer(&mut self) -> &mut Self {
1790        self.commands.push(Command::Spacer { grow: 1 });
1791        self.last_text_idx = None;
1792        self
1793    }
1794
1795    /// Render a form that groups input fields vertically.
1796    ///
1797    /// Use [`Context::form_field`] inside the closure to render each field.
1798    pub fn form(
1799        &mut self,
1800        state: &mut FormState,
1801        f: impl FnOnce(&mut Context, &mut FormState),
1802    ) -> &mut Self {
1803        let _ = self.col(|ui| {
1804            f(ui, state);
1805        });
1806        self
1807    }
1808
1809    /// Render a single form field with label and input.
1810    ///
1811    /// Shows a validation error below the input when present.
1812    pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1813        let _ = self.col(|ui| {
1814            ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1815            let _ = ui.text_input(&mut field.input);
1816            if let Some(error) = field.error.as_deref() {
1817                ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1818            }
1819        });
1820        self
1821    }
1822
1823    /// Render a submit button.
1824    ///
1825    /// Returns `true` when the button is clicked or activated.
1826    pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1827        self.button(label)
1828    }
1829}
1830
1831const KEYWORDS: &[&str] = &[
1832    "fn",
1833    "let",
1834    "mut",
1835    "pub",
1836    "use",
1837    "impl",
1838    "struct",
1839    "enum",
1840    "trait",
1841    "type",
1842    "const",
1843    "static",
1844    "if",
1845    "else",
1846    "match",
1847    "for",
1848    "while",
1849    "loop",
1850    "return",
1851    "break",
1852    "continue",
1853    "where",
1854    "self",
1855    "super",
1856    "crate",
1857    "mod",
1858    "async",
1859    "await",
1860    "move",
1861    "ref",
1862    "in",
1863    "as",
1864    "true",
1865    "false",
1866    "Some",
1867    "None",
1868    "Ok",
1869    "Err",
1870    "Self",
1871    "def",
1872    "class",
1873    "import",
1874    "from",
1875    "pass",
1876    "lambda",
1877    "yield",
1878    "with",
1879    "try",
1880    "except",
1881    "raise",
1882    "finally",
1883    "elif",
1884    "del",
1885    "global",
1886    "nonlocal",
1887    "assert",
1888    "is",
1889    "not",
1890    "and",
1891    "or",
1892    "function",
1893    "var",
1894    "const",
1895    "export",
1896    "default",
1897    "switch",
1898    "case",
1899    "throw",
1900    "catch",
1901    "typeof",
1902    "instanceof",
1903    "new",
1904    "delete",
1905    "void",
1906    "this",
1907    "null",
1908    "undefined",
1909    "func",
1910    "package",
1911    "defer",
1912    "go",
1913    "chan",
1914    "select",
1915    "range",
1916    "map",
1917    "interface",
1918    "fallthrough",
1919    "nil",
1920];
1921
1922fn render_highlighted_line(ui: &mut Context, line: &str) {
1923    let theme = ui.theme;
1924    let is_light = matches!(
1925        theme.bg,
1926        Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1927    );
1928    let keyword_color = if is_light {
1929        Color::Rgb(166, 38, 164)
1930    } else {
1931        Color::Rgb(198, 120, 221)
1932    };
1933    let string_color = if is_light {
1934        Color::Rgb(80, 161, 79)
1935    } else {
1936        Color::Rgb(152, 195, 121)
1937    };
1938    let comment_color = theme.text_dim;
1939    let number_color = if is_light {
1940        Color::Rgb(152, 104, 1)
1941    } else {
1942        Color::Rgb(209, 154, 102)
1943    };
1944    let fn_color = if is_light {
1945        Color::Rgb(64, 120, 242)
1946    } else {
1947        Color::Rgb(97, 175, 239)
1948    };
1949    let macro_color = if is_light {
1950        Color::Rgb(1, 132, 188)
1951    } else {
1952        Color::Rgb(86, 182, 194)
1953    };
1954
1955    let trimmed = line.trim_start();
1956    let indent = &line[..line.len() - trimmed.len()];
1957    if !indent.is_empty() {
1958        ui.text(indent);
1959    }
1960
1961    if trimmed.starts_with("//") {
1962        ui.text(trimmed).fg(comment_color).italic();
1963        return;
1964    }
1965
1966    let mut pos = 0;
1967
1968    while pos < trimmed.len() {
1969        let ch = trimmed.as_bytes()[pos];
1970
1971        if ch == b'"' {
1972            if let Some(end) = trimmed[pos + 1..].find('"') {
1973                let s = &trimmed[pos..pos + end + 2];
1974                ui.text(s).fg(string_color);
1975                pos += end + 2;
1976                continue;
1977            }
1978        }
1979
1980        if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1981        {
1982            let end = trimmed[pos..]
1983                .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1984                .map_or(trimmed.len(), |e| pos + e);
1985            ui.text(&trimmed[pos..end]).fg(number_color);
1986            pos = end;
1987            continue;
1988        }
1989
1990        if ch.is_ascii_alphabetic() || ch == b'_' {
1991            let end = trimmed[pos..]
1992                .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1993                .map_or(trimmed.len(), |e| pos + e);
1994            let word = &trimmed[pos..end];
1995
1996            if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1997                ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1998                pos = end + 1;
1999            } else if end < trimmed.len()
2000                && trimmed.as_bytes()[end] == b'('
2001                && !KEYWORDS.contains(&word)
2002            {
2003                ui.text(word).fg(fn_color);
2004                pos = end;
2005            } else if KEYWORDS.contains(&word) {
2006                ui.text(word).fg(keyword_color);
2007                pos = end;
2008            } else {
2009                ui.text(word);
2010                pos = end;
2011            }
2012            continue;
2013        }
2014
2015        let end = trimmed[pos..]
2016            .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
2017            .map_or(trimmed.len(), |e| pos + e);
2018        ui.text(&trimmed[pos..end]);
2019        pos = end;
2020    }
2021}
2022
2023fn normalize_rgba(data: &[u8], width: u32, height: u32) -> Vec<u8> {
2024    let expected = (width as usize) * (height as usize) * 4;
2025    if data.len() >= expected {
2026        return data[..expected].to_vec();
2027    }
2028    let mut buf = Vec::with_capacity(expected);
2029    buf.extend_from_slice(data);
2030    buf.resize(expected, 0);
2031    buf
2032}
2033
2034fn base64_encode(data: &[u8]) -> String {
2035    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2036    let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
2037    for chunk in data.chunks(3) {
2038        let b0 = chunk[0] as u32;
2039        let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2040        let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2041        let triple = (b0 << 16) | (b1 << 8) | b2;
2042        result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
2043        result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
2044        if chunk.len() > 1 {
2045            result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
2046        } else {
2047            result.push('=');
2048        }
2049        if chunk.len() > 2 {
2050            result.push(CHARS[(triple & 0x3F) as usize] as char);
2051        } else {
2052            result.push('=');
2053        }
2054    }
2055    result
2056}
2057
2058fn split_base64(encoded: &str, chunk_size: usize) -> Vec<&str> {
2059    let mut chunks = Vec::new();
2060    let bytes = encoded.as_bytes();
2061    let mut offset = 0;
2062    while offset < bytes.len() {
2063        let end = (offset + chunk_size).min(bytes.len());
2064        chunks.push(&encoded[offset..end]);
2065        offset = end;
2066    }
2067    if chunks.is_empty() {
2068        chunks.push("");
2069    }
2070    chunks
2071}