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