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