Skip to main content

slt/context/
widgets_display.rs

1use super::*;
2
3impl Context {
4    // ── text ──────────────────────────────────────────────────────────
5
6    /// Render a text element. Returns `&mut Self` for style chaining.
7    ///
8    /// # Example
9    ///
10    /// ```no_run
11    /// # slt::run(|ui: &mut slt::Context| {
12    /// use slt::Color;
13    /// ui.text("hello").bold().fg(Color::Cyan);
14    /// # });
15    /// ```
16    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
17        let content = s.into();
18        self.commands.push(Command::Text {
19            content,
20            style: Style::new(),
21            grow: 0,
22            align: Align::Start,
23            wrap: false,
24            margin: Margin::default(),
25            constraints: Constraints::default(),
26        });
27        self.last_text_idx = Some(self.commands.len() - 1);
28        self
29    }
30
31    /// Render a clickable hyperlink.
32    ///
33    /// The link is interactive: clicking it (or pressing Enter/Space when
34    /// focused) opens the URL in the system browser. OSC 8 is also emitted
35    /// for terminals that support native hyperlinks.
36    pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
37        let url_str = url.into();
38        let focused = self.register_focusable();
39        let interaction_id = self.interaction_count;
40        self.interaction_count += 1;
41        let response = self.response_for(interaction_id);
42
43        let mut activated = response.clicked;
44        if focused {
45            for (i, event) in self.events.iter().enumerate() {
46                if let Event::Key(key) = event {
47                    if key.kind != KeyEventKind::Press {
48                        continue;
49                    }
50                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
51                        activated = true;
52                        self.consumed[i] = true;
53                    }
54                }
55            }
56        }
57
58        if activated {
59            let _ = open_url(&url_str);
60        }
61
62        let style = if focused {
63            Style::new()
64                .fg(self.theme.primary)
65                .bg(self.theme.surface_hover)
66                .underline()
67                .bold()
68        } else if response.hovered {
69            Style::new()
70                .fg(self.theme.accent)
71                .bg(self.theme.surface_hover)
72                .underline()
73        } else {
74            Style::new().fg(self.theme.primary).underline()
75        };
76
77        self.commands.push(Command::Link {
78            text: text.into(),
79            url: url_str,
80            style,
81            margin: Margin::default(),
82            constraints: Constraints::default(),
83        });
84        self.last_text_idx = Some(self.commands.len() - 1);
85        self
86    }
87
88    /// Render a text element with word-boundary wrapping.
89    ///
90    /// Long lines are broken at word boundaries to fit the container width.
91    /// Style chaining works the same as [`Context::text`].
92    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
93        let content = s.into();
94        self.commands.push(Command::Text {
95            content,
96            style: Style::new(),
97            grow: 0,
98            align: Align::Start,
99            wrap: true,
100            margin: Margin::default(),
101            constraints: Constraints::default(),
102        });
103        self.last_text_idx = Some(self.commands.len() - 1);
104        self
105    }
106
107    // ── style chain (applies to last text) ───────────────────────────
108
109    /// Apply bold to the last rendered text element.
110    pub fn bold(&mut self) -> &mut Self {
111        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
112        self
113    }
114
115    /// Apply dim styling to the last rendered text element.
116    ///
117    /// Also sets the foreground color to the theme's `text_dim` color if no
118    /// explicit foreground has been set.
119    pub fn dim(&mut self) -> &mut Self {
120        let text_dim = self.theme.text_dim;
121        self.modify_last_style(|s| {
122            s.modifiers |= Modifiers::DIM;
123            if s.fg.is_none() {
124                s.fg = Some(text_dim);
125            }
126        });
127        self
128    }
129
130    /// Apply italic to the last rendered text element.
131    pub fn italic(&mut self) -> &mut Self {
132        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
133        self
134    }
135
136    /// Apply underline to the last rendered text element.
137    pub fn underline(&mut self) -> &mut Self {
138        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
139        self
140    }
141
142    /// Apply reverse-video to the last rendered text element.
143    pub fn reversed(&mut self) -> &mut Self {
144        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
145        self
146    }
147
148    /// Apply strikethrough to the last rendered text element.
149    pub fn strikethrough(&mut self) -> &mut Self {
150        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
151        self
152    }
153
154    /// Set the foreground color of the last rendered text element.
155    pub fn fg(&mut self, color: Color) -> &mut Self {
156        self.modify_last_style(|s| s.fg = Some(color));
157        self
158    }
159
160    /// Set the background color of the last rendered text element.
161    pub fn bg(&mut self, color: Color) -> &mut Self {
162        self.modify_last_style(|s| s.bg = Some(color));
163        self
164    }
165
166    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
167        let apply_group_style = self
168            .group_stack
169            .last()
170            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
171            .unwrap_or(false);
172        if apply_group_style {
173            self.modify_last_style(|s| s.fg = Some(color));
174        }
175        self
176    }
177
178    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
179        let apply_group_style = self
180            .group_stack
181            .last()
182            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
183            .unwrap_or(false);
184        if apply_group_style {
185            self.modify_last_style(|s| s.bg = Some(color));
186        }
187        self
188    }
189
190    /// Render a text element with an explicit [`Style`] applied immediately.
191    ///
192    /// Equivalent to calling `text(s)` followed by style-chain methods, but
193    /// more concise when you already have a `Style` value.
194    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
195        self.commands.push(Command::Text {
196            content: s.into(),
197            style,
198            grow: 0,
199            align: Align::Start,
200            wrap: false,
201            margin: Margin::default(),
202            constraints: Constraints::default(),
203        });
204        self.last_text_idx = Some(self.commands.len() - 1);
205        self
206    }
207
208    /// Render a half-block image in the terminal.
209    ///
210    /// Each terminal cell displays two vertical pixels using the `▀` character
211    /// with foreground (upper pixel) and background (lower pixel) colors.
212    ///
213    /// Create a [`HalfBlockImage`] from a file (requires `image` feature):
214    /// ```ignore
215    /// let img = image::open("photo.png").unwrap();
216    /// let half = HalfBlockImage::from_dynamic(&img, 40, 20);
217    /// ui.image(&half);
218    /// ```
219    ///
220    /// Or from raw RGB data (no feature needed):
221    /// ```no_run
222    /// # use slt::{Context, HalfBlockImage};
223    /// # slt::run(|ui: &mut Context| {
224    /// let rgb = vec![255u8; 30 * 20 * 3];
225    /// let half = HalfBlockImage::from_rgb(&rgb, 30, 10);
226    /// ui.image(&half);
227    /// # });
228    /// ```
229    pub fn image(&mut self, img: &HalfBlockImage) {
230        let width = img.width;
231        let height = img.height;
232
233        self.container().w(width).h(height).gap(0).col(|ui| {
234            for row in 0..height {
235                ui.container().gap(0).row(|ui| {
236                    for col in 0..width {
237                        let idx = (row * width + col) as usize;
238                        if let Some(&(upper, lower)) = img.pixels.get(idx) {
239                            ui.styled("▀", Style::new().fg(upper).bg(lower));
240                        }
241                    }
242                });
243            }
244        });
245    }
246
247    /// Render streaming text with a typing cursor indicator.
248    ///
249    /// Displays the accumulated text content. While `streaming` is true,
250    /// shows a blinking cursor (`▌`) at the end.
251    ///
252    /// ```no_run
253    /// # use slt::widgets::StreamingTextState;
254    /// # slt::run(|ui: &mut slt::Context| {
255    /// let mut stream = StreamingTextState::new();
256    /// stream.start();
257    /// stream.push("Hello from ");
258    /// stream.push("the AI!");
259    /// ui.streaming_text(&mut stream);
260    /// # });
261    /// ```
262    pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
263        if state.streaming {
264            state.cursor_tick = state.cursor_tick.wrapping_add(1);
265            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
266        }
267
268        if state.content.is_empty() && state.streaming {
269            let cursor = if state.cursor_visible { "▌" } else { " " };
270            let primary = self.theme.primary;
271            self.text(cursor).fg(primary);
272            return;
273        }
274
275        if !state.content.is_empty() {
276            if state.streaming && state.cursor_visible {
277                self.text_wrap(format!("{}▌", state.content));
278            } else {
279                self.text_wrap(&state.content);
280            }
281        }
282    }
283
284    /// Render a tool approval widget with approve/reject buttons.
285    ///
286    /// Shows the tool name, description, and two action buttons.
287    /// Returns the updated [`ApprovalAction`] each frame.
288    ///
289    /// ```no_run
290    /// # use slt::widgets::{ApprovalAction, ToolApprovalState};
291    /// # slt::run(|ui: &mut slt::Context| {
292    /// let mut tool = ToolApprovalState::new("read_file", "Read contents of config.toml");
293    /// ui.tool_approval(&mut tool);
294    /// if tool.action == ApprovalAction::Approved {
295    /// }
296    /// # });
297    /// ```
298    pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
299        let theme = self.theme;
300        self.bordered(Border::Rounded).col(|ui| {
301            ui.row(|ui| {
302                ui.text("⚡").fg(theme.warning);
303                ui.text(&state.tool_name).bold().fg(theme.primary);
304            });
305            ui.text(&state.description).dim();
306
307            if state.action == ApprovalAction::Pending {
308                ui.row(|ui| {
309                    if ui.button("✓ Approve") {
310                        state.action = ApprovalAction::Approved;
311                    }
312                    if ui.button("✗ Reject") {
313                        state.action = ApprovalAction::Rejected;
314                    }
315                });
316            } else {
317                let (label, color) = match state.action {
318                    ApprovalAction::Approved => ("✓ Approved", theme.success),
319                    ApprovalAction::Rejected => ("✗ Rejected", theme.error),
320                    ApprovalAction::Pending => unreachable!(),
321                };
322                ui.text(label).fg(color).bold();
323            }
324        });
325    }
326
327    /// Render a context bar showing active context items with token counts.
328    ///
329    /// Displays a horizontal bar of context sources (files, URLs, etc.)
330    /// with their token counts, useful for AI chat interfaces.
331    ///
332    /// ```no_run
333    /// # use slt::widgets::ContextItem;
334    /// # slt::run(|ui: &mut slt::Context| {
335    /// let items = vec![ContextItem::new("main.rs", 1200), ContextItem::new("lib.rs", 800)];
336    /// ui.context_bar(&items);
337    /// # });
338    /// ```
339    pub fn context_bar(&mut self, items: &[ContextItem]) {
340        if items.is_empty() {
341            return;
342        }
343
344        let theme = self.theme;
345        let total: usize = items.iter().map(|item| item.tokens).sum();
346
347        self.container().row(|ui| {
348            ui.text("📎").dim();
349            for item in items {
350                ui.text(format!(
351                    "{} ({})",
352                    item.label,
353                    format_token_count(item.tokens)
354                ))
355                .fg(theme.secondary);
356            }
357            ui.spacer();
358            ui.text(format!("Σ {}", format_token_count(total))).dim();
359        });
360    }
361
362    /// Enable word-boundary wrapping on the last rendered text element.
363    pub fn wrap(&mut self) -> &mut Self {
364        if let Some(idx) = self.last_text_idx {
365            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
366                *wrap = true;
367            }
368        }
369        self
370    }
371
372    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
373        if let Some(idx) = self.last_text_idx {
374            match &mut self.commands[idx] {
375                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
376                _ => {}
377            }
378        }
379    }
380
381    // ── containers ───────────────────────────────────────────────────
382
383    /// Create a vertical (column) container.
384    ///
385    /// Children are stacked top-to-bottom. Returns a [`Response`] with
386    /// click/hover state for the container area.
387    ///
388    /// # Example
389    ///
390    /// ```no_run
391    /// # slt::run(|ui: &mut slt::Context| {
392    /// ui.col(|ui| {
393    ///     ui.text("line one");
394    ///     ui.text("line two");
395    /// });
396    /// # });
397    /// ```
398    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
399        self.push_container(Direction::Column, 0, f)
400    }
401
402    /// Create a vertical (column) container with a gap between children.
403    ///
404    /// `gap` is the number of blank rows inserted between each child.
405    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
406        self.push_container(Direction::Column, gap, f)
407    }
408
409    /// Create a horizontal (row) container.
410    ///
411    /// Children are placed left-to-right. Returns a [`Response`] with
412    /// click/hover state for the container area.
413    ///
414    /// # Example
415    ///
416    /// ```no_run
417    /// # slt::run(|ui: &mut slt::Context| {
418    /// ui.row(|ui| {
419    ///     ui.text("left");
420    ///     ui.spacer();
421    ///     ui.text("right");
422    /// });
423    /// # });
424    /// ```
425    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
426        self.push_container(Direction::Row, 0, f)
427    }
428
429    /// Create a horizontal (row) container with a gap between children.
430    ///
431    /// `gap` is the number of blank columns inserted between each child.
432    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
433        self.push_container(Direction::Row, gap, f)
434    }
435
436    /// Render inline text with mixed styles on a single line.
437    ///
438    /// Unlike [`row`](Context::row), `line()` is designed for rich text —
439    /// children are rendered as continuous inline text without gaps.
440    ///
441    /// # Example
442    ///
443    /// ```no_run
444    /// # use slt::Color;
445    /// # slt::run(|ui: &mut slt::Context| {
446    /// ui.line(|ui| {
447    ///     ui.text("Status: ");
448    ///     ui.text("Online").bold().fg(Color::Green);
449    /// });
450    /// # });
451    /// ```
452    pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
453        let _ = self.push_container(Direction::Row, 0, f);
454        self
455    }
456
457    /// Render inline text with mixed styles, wrapping at word boundaries.
458    ///
459    /// Like [`line`](Context::line), but when the combined text exceeds
460    /// the container width it wraps across multiple lines while
461    /// preserving per-segment styles.
462    ///
463    /// # Example
464    ///
465    /// ```no_run
466    /// # use slt::{Color, Style};
467    /// # slt::run(|ui: &mut slt::Context| {
468    /// ui.line_wrap(|ui| {
469    ///     ui.text("This is a long ");
470    ///     ui.text("important").bold().fg(Color::Red);
471    ///     ui.text(" message that wraps across lines");
472    /// });
473    /// # });
474    /// ```
475    pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
476        let start = self.commands.len();
477        f(self);
478        let mut segments: Vec<(String, Style)> = Vec::new();
479        for cmd in self.commands.drain(start..) {
480            if let Command::Text { content, style, .. } = cmd {
481                segments.push((content, style));
482            }
483        }
484        self.commands.push(Command::RichText {
485            segments,
486            wrap: true,
487            align: Align::Start,
488            margin: Margin::default(),
489            constraints: Constraints::default(),
490        });
491        self.last_text_idx = None;
492        self
493    }
494
495    /// Render content in a modal overlay with dimmed background.
496    ///
497    /// ```ignore
498    /// ui.modal(|ui| {
499    ///     ui.text("Are you sure?");
500    ///     if ui.button("OK") { show = false; }
501    /// });
502    /// ```
503    pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
504        self.commands.push(Command::BeginOverlay { modal: true });
505        self.overlay_depth += 1;
506        self.modal_active = true;
507        f(self);
508        self.overlay_depth = self.overlay_depth.saturating_sub(1);
509        self.commands.push(Command::EndOverlay);
510        self.last_text_idx = None;
511    }
512
513    /// Render floating content without dimming the background.
514    pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
515        self.commands.push(Command::BeginOverlay { modal: false });
516        self.overlay_depth += 1;
517        f(self);
518        self.overlay_depth = self.overlay_depth.saturating_sub(1);
519        self.commands.push(Command::EndOverlay);
520        self.last_text_idx = None;
521    }
522
523    /// Create a named group container for shared hover/focus styling.
524    ///
525    /// ```ignore
526    /// ui.group("card").border(Border::Rounded)
527    ///     .group_hover_bg(Color::Indexed(238))
528    ///     .col(|ui| { ui.text("Hover anywhere"); });
529    /// ```
530    pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
531        self.group_count = self.group_count.saturating_add(1);
532        self.group_stack.push(name.to_string());
533        self.container().group_name(name.to_string())
534    }
535
536    /// Create a container with a fluent builder.
537    ///
538    /// Use this for borders, padding, grow, constraints, and titles. Chain
539    /// configuration methods on the returned [`ContainerBuilder`], then call
540    /// `.col()` or `.row()` to finalize.
541    ///
542    /// # Example
543    ///
544    /// ```no_run
545    /// # slt::run(|ui: &mut slt::Context| {
546    /// use slt::Border;
547    /// ui.container()
548    ///     .border(Border::Rounded)
549    ///     .pad(1)
550    ///     .title("My Panel")
551    ///     .col(|ui| {
552    ///         ui.text("content");
553    ///     });
554    /// # });
555    /// ```
556    pub fn container(&mut self) -> ContainerBuilder<'_> {
557        let border = self.theme.border;
558        ContainerBuilder {
559            ctx: self,
560            gap: 0,
561            align: Align::Start,
562            justify: Justify::Start,
563            border: None,
564            border_sides: BorderSides::all(),
565            border_style: Style::new().fg(border),
566            bg: None,
567            dark_bg: None,
568            dark_border_style: None,
569            group_hover_bg: None,
570            group_hover_border_style: None,
571            group_name: None,
572            padding: Padding::default(),
573            margin: Margin::default(),
574            constraints: Constraints::default(),
575            title: None,
576            grow: 0,
577            scroll_offset: None,
578        }
579    }
580
581    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
582    ///
583    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
584    /// is updated in-place with the current scroll offset and bounds.
585    ///
586    /// # Example
587    ///
588    /// ```no_run
589    /// # use slt::widgets::ScrollState;
590    /// # slt::run(|ui: &mut slt::Context| {
591    /// let mut scroll = ScrollState::new();
592    /// ui.scrollable(&mut scroll).col(|ui| {
593    ///     for i in 0..100 {
594    ///         ui.text(format!("Line {i}"));
595    ///     }
596    /// });
597    /// # });
598    /// ```
599    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
600        let index = self.scroll_count;
601        self.scroll_count += 1;
602        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
603            state.set_bounds(ch, vh);
604            let max = ch.saturating_sub(vh) as usize;
605            state.offset = state.offset.min(max);
606        }
607
608        let next_id = self.interaction_count;
609        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
610            let inner_rects: Vec<Rect> = self
611                .prev_scroll_rects
612                .iter()
613                .enumerate()
614                .filter(|&(j, sr)| {
615                    j != index
616                        && sr.width > 0
617                        && sr.height > 0
618                        && sr.x >= rect.x
619                        && sr.right() <= rect.right()
620                        && sr.y >= rect.y
621                        && sr.bottom() <= rect.bottom()
622                })
623                .map(|(_, sr)| *sr)
624                .collect();
625            self.auto_scroll_nested(&rect, state, &inner_rects);
626        }
627
628        self.container().scroll_offset(state.offset as u32)
629    }
630
631    /// Render a scrollbar track for a [`ScrollState`].
632    ///
633    /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
634    /// and position are calculated from the scroll state's content height,
635    /// viewport height, and current offset.
636    ///
637    /// Typically placed beside a `scrollable()` container in a `row()`:
638    /// ```no_run
639    /// # use slt::widgets::ScrollState;
640    /// # slt::run(|ui: &mut slt::Context| {
641    /// let mut scroll = ScrollState::new();
642    /// ui.row(|ui| {
643    ///     ui.scrollable(&mut scroll).grow(1).col(|ui| {
644    ///         for i in 0..100 { ui.text(format!("Line {i}")); }
645    ///     });
646    ///     ui.scrollbar(&scroll);
647    /// });
648    /// # });
649    /// ```
650    pub fn scrollbar(&mut self, state: &ScrollState) {
651        let vh = state.viewport_height();
652        let ch = state.content_height();
653        if vh == 0 || ch <= vh {
654            return;
655        }
656
657        let track_height = vh;
658        let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
659        let max_offset = ch.saturating_sub(vh);
660        let thumb_pos = if max_offset == 0 {
661            0
662        } else {
663            ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
664                .round() as u32
665        };
666
667        let theme = self.theme;
668        let track_char = '│';
669        let thumb_char = '█';
670
671        self.container().w(1).h(track_height).col(|ui| {
672            for i in 0..track_height {
673                if i >= thumb_pos && i < thumb_pos + thumb_height {
674                    ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
675                } else {
676                    ui.styled(
677                        track_char.to_string(),
678                        Style::new().fg(theme.text_dim).dim(),
679                    );
680                }
681            }
682        });
683    }
684
685    fn auto_scroll_nested(
686        &mut self,
687        rect: &Rect,
688        state: &mut ScrollState,
689        inner_scroll_rects: &[Rect],
690    ) {
691        let mut to_consume: Vec<usize> = Vec::new();
692
693        for (i, event) in self.events.iter().enumerate() {
694            if self.consumed[i] {
695                continue;
696            }
697            if let Event::Mouse(mouse) = event {
698                let in_bounds = mouse.x >= rect.x
699                    && mouse.x < rect.right()
700                    && mouse.y >= rect.y
701                    && mouse.y < rect.bottom();
702                if !in_bounds {
703                    continue;
704                }
705                let in_inner = inner_scroll_rects.iter().any(|sr| {
706                    mouse.x >= sr.x
707                        && mouse.x < sr.right()
708                        && mouse.y >= sr.y
709                        && mouse.y < sr.bottom()
710                });
711                if in_inner {
712                    continue;
713                }
714                match mouse.kind {
715                    MouseKind::ScrollUp => {
716                        state.scroll_up(1);
717                        to_consume.push(i);
718                    }
719                    MouseKind::ScrollDown => {
720                        state.scroll_down(1);
721                        to_consume.push(i);
722                    }
723                    MouseKind::Drag(MouseButton::Left) => {}
724                    _ => {}
725                }
726            }
727        }
728
729        for i in to_consume {
730            self.consumed[i] = true;
731        }
732    }
733
734    /// Shortcut for `container().border(border)`.
735    ///
736    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
737    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
738        self.container()
739            .border(border)
740            .border_sides(BorderSides::all())
741    }
742
743    fn push_container(
744        &mut self,
745        direction: Direction,
746        gap: u32,
747        f: impl FnOnce(&mut Context),
748    ) -> Response {
749        let interaction_id = self.interaction_count;
750        self.interaction_count += 1;
751        let border = self.theme.border;
752
753        self.commands.push(Command::BeginContainer {
754            direction,
755            gap,
756            align: Align::Start,
757            justify: Justify::Start,
758            border: None,
759            border_sides: BorderSides::all(),
760            border_style: Style::new().fg(border),
761            bg_color: None,
762            padding: Padding::default(),
763            margin: Margin::default(),
764            constraints: Constraints::default(),
765            title: None,
766            grow: 0,
767            group_name: None,
768        });
769        f(self);
770        self.commands.push(Command::EndContainer);
771        self.last_text_idx = None;
772
773        self.response_for(interaction_id)
774    }
775
776    pub(super) fn response_for(&self, interaction_id: usize) -> Response {
777        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
778            return Response::default();
779        }
780        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
781            let clicked = self
782                .click_pos
783                .map(|(mx, my)| {
784                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
785                })
786                .unwrap_or(false);
787            let hovered = self
788                .mouse_pos
789                .map(|(mx, my)| {
790                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
791                })
792                .unwrap_or(false);
793            Response { clicked, hovered }
794        } else {
795            Response::default()
796        }
797    }
798
799    /// Returns true if the named group is currently hovered by the mouse.
800    pub fn is_group_hovered(&self, name: &str) -> bool {
801        if let Some(pos) = self.mouse_pos {
802            self.prev_group_rects.iter().any(|(n, rect)| {
803                n == name
804                    && pos.0 >= rect.x
805                    && pos.0 < rect.x + rect.width
806                    && pos.1 >= rect.y
807                    && pos.1 < rect.y + rect.height
808            })
809        } else {
810            false
811        }
812    }
813
814    /// Returns true if the named group contains the currently focused widget.
815    pub fn is_group_focused(&self, name: &str) -> bool {
816        if self.prev_focus_count == 0 {
817            return false;
818        }
819        let focused_index = self.focus_index % self.prev_focus_count;
820        self.prev_focus_groups
821            .get(focused_index)
822            .and_then(|group| group.as_deref())
823            .map(|group| group == name)
824            .unwrap_or(false)
825    }
826
827    /// Set the flex-grow factor of the last rendered text element.
828    ///
829    /// A value of `1` causes the element to expand and fill remaining space
830    /// along the main axis.
831    pub fn grow(&mut self, value: u16) -> &mut Self {
832        if let Some(idx) = self.last_text_idx {
833            if let Command::Text { grow, .. } = &mut self.commands[idx] {
834                *grow = value;
835            }
836        }
837        self
838    }
839
840    /// Set the text alignment of the last rendered text element.
841    pub fn align(&mut self, align: Align) -> &mut Self {
842        if let Some(idx) = self.last_text_idx {
843            if let Command::Text {
844                align: text_align, ..
845            } = &mut self.commands[idx]
846            {
847                *text_align = align;
848            }
849        }
850        self
851    }
852
853    /// Render an invisible spacer that expands to fill available space.
854    ///
855    /// Useful for pushing siblings to opposite ends of a row or column.
856    pub fn spacer(&mut self) -> &mut Self {
857        self.commands.push(Command::Spacer { grow: 1 });
858        self.last_text_idx = None;
859        self
860    }
861
862    /// Render a form that groups input fields vertically.
863    ///
864    /// Use [`Context::form_field`] inside the closure to render each field.
865    pub fn form(
866        &mut self,
867        state: &mut FormState,
868        f: impl FnOnce(&mut Context, &mut FormState),
869    ) -> &mut Self {
870        self.col(|ui| {
871            f(ui, state);
872        });
873        self
874    }
875
876    /// Render a single form field with label and input.
877    ///
878    /// Shows a validation error below the input when present.
879    pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
880        self.col(|ui| {
881            ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
882            ui.text_input(&mut field.input);
883            if let Some(error) = field.error.as_deref() {
884                ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
885            }
886        });
887        self
888    }
889
890    /// Render a submit button.
891    ///
892    /// Returns `true` when the button is clicked or activated.
893    pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
894        self.button(label)
895    }
896}