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