Skip to main content

slt/context/widgets_display/
layout.rs

1use super::*;
2
3impl Context {
4    /// Conditionally render content when the named screen is active.
5    pub fn screen(&mut self, name: &str, screens: &ScreenState, f: impl FnOnce(&mut Context)) {
6        if screens.current() == name {
7            f(self);
8        }
9    }
10
11    /// Create a vertical (column) container.
12    ///
13    /// Children are stacked top-to-bottom. Returns a [`Response`] with
14    /// click/hover state for the container area.
15    ///
16    /// # Example
17    ///
18    /// ```no_run
19    /// # slt::run(|ui: &mut slt::Context| {
20    /// ui.col(|ui| {
21    ///     ui.text("line one");
22    ///     ui.text("line two");
23    /// });
24    /// # });
25    /// ```
26    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
27        self.push_container(Direction::Column, 0, f)
28    }
29
30    /// Create a vertical (column) container with a gap between children.
31    ///
32    /// `gap` is the number of blank rows inserted between each child.
33    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
34        self.push_container(Direction::Column, gap, f)
35    }
36
37    /// Create a horizontal (row) container.
38    ///
39    /// Children are placed left-to-right. Returns a [`Response`] with
40    /// click/hover state for the container area.
41    ///
42    /// # Example
43    ///
44    /// ```no_run
45    /// # slt::run(|ui: &mut slt::Context| {
46    /// ui.row(|ui| {
47    ///     ui.text("left");
48    ///     ui.spacer();
49    ///     ui.text("right");
50    /// });
51    /// # });
52    /// ```
53    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
54        self.push_container(Direction::Row, 0, f)
55    }
56
57    /// Create a horizontal (row) container with a gap between children.
58    ///
59    /// `gap` is the number of blank columns inserted between each child.
60    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
61        self.push_container(Direction::Row, gap, f)
62    }
63
64    /// Render inline text with mixed styles on a single line.
65    ///
66    /// Unlike [`row`](Context::row), `line()` is designed for rich text —
67    /// children are rendered as continuous inline text without gaps.
68    ///
69    /// It intentionally returns `&mut Self` instead of [`Response`] so you can
70    /// keep chaining display-oriented modifiers after composing the inline run.
71    ///
72    /// # Example
73    ///
74    /// ```no_run
75    /// # use slt::Color;
76    /// # slt::run(|ui: &mut slt::Context| {
77    /// ui.line(|ui| {
78    ///     ui.text("Status: ");
79    ///     ui.text("Online").bold().fg(Color::Green);
80    /// });
81    /// # });
82    /// ```
83    pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
84        let _ = self.push_container(Direction::Row, 0, f);
85        self
86    }
87
88    /// Render inline text with mixed styles, wrapping at word boundaries.
89    ///
90    /// Like [`line`](Context::line), but when the combined text exceeds
91    /// the container width it wraps across multiple lines while
92    /// preserving per-segment styles.
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// # use slt::{Color, Style};
98    /// # slt::run(|ui: &mut slt::Context| {
99    /// ui.line_wrap(|ui| {
100    ///     ui.text("This is a long ");
101    ///     ui.text("important").bold().fg(Color::Red);
102    ///     ui.text(" message that wraps across lines");
103    /// });
104    /// # });
105    /// ```
106    pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
107        let start = self.commands.len();
108        f(self);
109        let has_link = self.commands[start..]
110            .iter()
111            .any(|cmd| matches!(cmd, Command::Link { .. }));
112
113        if has_link {
114            self.commands.insert(
115                start,
116                Command::BeginContainer {
117                    direction: Direction::Row,
118                    gap: 0,
119                    align: Align::Start,
120                    align_self: None,
121                    justify: Justify::Start,
122                    border: None,
123                    border_sides: BorderSides::all(),
124                    border_style: Style::new(),
125                    bg_color: None,
126                    padding: Padding::default(),
127                    margin: Margin::default(),
128                    constraints: Constraints::default(),
129                    title: None,
130                    grow: 0,
131                    group_name: None,
132                },
133            );
134            self.commands.push(Command::EndContainer);
135            self.last_text_idx = None;
136            return self;
137        }
138
139        let mut segments: Vec<(String, Style)> = Vec::new();
140        for cmd in self.commands.drain(start..) {
141            match cmd {
142                Command::Text { content, style, .. } => {
143                    segments.push((content, style));
144                }
145                Command::Link { text, style, .. } => {
146                    // Preserve link text with underline styling (URL lost in RichText,
147                    // but text is visible and wraps correctly)
148                    segments.push((text, style));
149                }
150                _ => {}
151            }
152        }
153        self.commands.push(Command::RichText {
154            segments,
155            wrap: true,
156            align: Align::Start,
157            margin: Margin::default(),
158            constraints: Constraints::default(),
159        });
160        self.last_text_idx = None;
161        self
162    }
163
164    /// Render content in a modal overlay with dimmed background.
165    ///
166    /// ```ignore
167    /// ui.modal(|ui| {
168    ///     ui.text("Are you sure?");
169    ///     if ui.button("OK") { show = false; }
170    /// });
171    /// ```
172    pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
173        let interaction_id = self.next_interaction_id();
174        self.commands.push(Command::BeginOverlay { modal: true });
175        self.overlay_depth += 1;
176        self.modal_active = true;
177        self.modal_focus_start = self.focus_count;
178        f(self);
179        self.modal_focus_count = self.focus_count.saturating_sub(self.modal_focus_start);
180        self.overlay_depth = self.overlay_depth.saturating_sub(1);
181        self.commands.push(Command::EndOverlay);
182        self.last_text_idx = None;
183        self.response_for(interaction_id)
184    }
185
186    /// Render floating content without dimming the background.
187    pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
188        let interaction_id = self.next_interaction_id();
189        self.commands.push(Command::BeginOverlay { modal: false });
190        self.overlay_depth += 1;
191        f(self);
192        self.overlay_depth = self.overlay_depth.saturating_sub(1);
193        self.commands.push(Command::EndOverlay);
194        self.last_text_idx = None;
195        self.response_for(interaction_id)
196    }
197
198    /// Render a hover tooltip for the previously rendered interactive widget.
199    ///
200    /// Call this right after a widget or container response:
201    /// ```ignore
202    /// if ui.button("Save").clicked { save(); }
203    /// ui.tooltip("Save the current document to disk");
204    /// ```
205    pub fn tooltip(&mut self, text: impl Into<String>) {
206        let tooltip_text = text.into();
207        if tooltip_text.is_empty() {
208            return;
209        }
210        let last_interaction_id = self.interaction_count.saturating_sub(1);
211        let last_response = self.response_for(last_interaction_id);
212        if !last_response.hovered || last_response.rect.width == 0 || last_response.rect.height == 0
213        {
214            return;
215        }
216        let lines = wrap_tooltip_text(&tooltip_text, 38);
217        self.pending_tooltips.push(PendingTooltip {
218            anchor_rect: last_response.rect,
219            lines,
220        });
221    }
222
223    pub(crate) fn emit_pending_tooltips(&mut self) {
224        let tooltips = std::mem::take(&mut self.pending_tooltips);
225        if tooltips.is_empty() {
226            return;
227        }
228        let area_w = self.area_width;
229        let area_h = self.area_height;
230        let surface = self.theme.surface;
231        let border_color = self.theme.border;
232        let text_color = self.theme.surface_text;
233
234        for tooltip in tooltips {
235            let content_w = tooltip
236                .lines
237                .iter()
238                .map(|l| UnicodeWidthStr::width(l.as_str()) as u32)
239                .max()
240                .unwrap_or(0);
241            let box_w = content_w.saturating_add(4).min(area_w);
242            let box_h = (tooltip.lines.len() as u32).saturating_add(4).min(area_h);
243
244            let tooltip_x = tooltip.anchor_rect.x.min(area_w.saturating_sub(box_w));
245            let below_y = tooltip.anchor_rect.bottom();
246            let tooltip_y = if below_y.saturating_add(box_h) <= area_h {
247                below_y
248            } else {
249                tooltip.anchor_rect.y.saturating_sub(box_h)
250            };
251
252            let lines = tooltip.lines;
253            let _ = self.overlay(|ui| {
254                let _ = ui.container().w(area_w).h(area_h).col(|ui| {
255                    let _ = ui
256                        .container()
257                        .ml(tooltip_x)
258                        .mt(tooltip_y)
259                        .max_w(box_w)
260                        .border(Border::Rounded)
261                        .border_fg(border_color)
262                        .bg(surface)
263                        .p(1)
264                        .col(|ui| {
265                            for line in &lines {
266                                ui.text(line.as_str()).fg(text_color);
267                            }
268                        });
269                });
270            });
271        }
272    }
273
274    /// Create a named group container for shared hover/focus styling.
275    ///
276    /// ```ignore
277    /// ui.group("card").border(Border::Rounded)
278    ///     .group_hover_bg(Color::Indexed(238))
279    ///     .col(|ui| { ui.text("Hover anywhere"); });
280    /// ```
281    pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
282        self.group_count = self.group_count.saturating_add(1);
283        self.group_stack.push(name.to_string());
284        self.container().group_name(name.to_string())
285    }
286
287    /// Create a container with a fluent builder.
288    ///
289    /// Use this for borders, padding, grow, constraints, and titles. Chain
290    /// configuration methods on the returned [`ContainerBuilder`], then call
291    /// `.col()` or `.row()` to finalize.
292    ///
293    /// # Example
294    ///
295    /// ```no_run
296    /// # slt::run(|ui: &mut slt::Context| {
297    /// use slt::Border;
298    /// ui.container()
299    ///     .border(Border::Rounded)
300    ///     .pad(1)
301    ///     .title("My Panel")
302    ///     .col(|ui| {
303    ///         ui.text("content");
304    ///     });
305    /// # });
306    /// ```
307    pub fn container(&mut self) -> ContainerBuilder<'_> {
308        let border = self.theme.border;
309        ContainerBuilder {
310            ctx: self,
311            gap: 0,
312            row_gap: None,
313            col_gap: None,
314            align: Align::Start,
315            align_self_value: None,
316            justify: Justify::Start,
317            border: None,
318            border_sides: BorderSides::all(),
319            border_style: Style::new().fg(border),
320            bg: None,
321            text_color: None,
322            dark_bg: None,
323            dark_border_style: None,
324            group_hover_bg: None,
325            group_hover_border_style: None,
326            group_name: None,
327            padding: Padding::default(),
328            margin: Margin::default(),
329            constraints: Constraints::default(),
330            title: None,
331            grow: 0,
332            scroll_offset: None,
333        }
334    }
335
336    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
337    ///
338    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
339    /// is updated in-place with the current scroll offset and bounds.
340    ///
341    /// # Example
342    ///
343    /// ```no_run
344    /// # use slt::widgets::ScrollState;
345    /// # slt::run(|ui: &mut slt::Context| {
346    /// let mut scroll = ScrollState::new();
347    /// ui.scrollable(&mut scroll).col(|ui| {
348    ///     for i in 0..100 {
349    ///         ui.text(format!("Line {i}"));
350    ///     }
351    /// });
352    /// # });
353    /// ```
354    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
355        let index = self.scroll_count;
356        self.scroll_count += 1;
357        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
358            state.set_bounds(ch, vh);
359            let max = ch.saturating_sub(vh) as usize;
360            state.offset = state.offset.min(max);
361        }
362
363        let next_id = self.interaction_count;
364        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
365            let inner_rects: Vec<Rect> = self
366                .prev_scroll_rects
367                .iter()
368                .enumerate()
369                .filter(|&(j, sr)| {
370                    j != index
371                        && sr.width > 0
372                        && sr.height > 0
373                        && sr.x >= rect.x
374                        && sr.right() <= rect.right()
375                        && sr.y >= rect.y
376                        && sr.bottom() <= rect.bottom()
377                })
378                .map(|(_, sr)| *sr)
379                .collect();
380            self.auto_scroll_nested(&rect, state, &inner_rects);
381        }
382
383        self.container().scroll_offset(state.offset as u32)
384    }
385
386    /// Render a scrollbar track for a [`ScrollState`].
387    ///
388    /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
389    /// and position are calculated from the scroll state's content height,
390    /// viewport height, and current offset.
391    ///
392    /// Typically placed beside a `scrollable()` container in a `row()`:
393    /// ```no_run
394    /// # use slt::widgets::ScrollState;
395    /// # slt::run(|ui: &mut slt::Context| {
396    /// let mut scroll = ScrollState::new();
397    /// ui.row(|ui| {
398    ///     ui.scrollable(&mut scroll).grow(1).col(|ui| {
399    ///         for i in 0..100 { ui.text(format!("Line {i}")); }
400    ///     });
401    ///     ui.scrollbar(&scroll);
402    /// });
403    /// # });
404    /// ```
405    pub fn scrollbar(&mut self, state: &ScrollState) {
406        let vh = state.viewport_height();
407        let ch = state.content_height();
408        if vh == 0 || ch <= vh {
409            return;
410        }
411
412        let track_height = vh;
413        let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
414        let max_offset = ch.saturating_sub(vh);
415        let thumb_pos = if max_offset == 0 {
416            0
417        } else {
418            ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
419                .round() as u32
420        };
421
422        let theme = self.theme;
423        let track_char = '│';
424        let thumb_char = '█';
425
426        let _ = self.container().w(1).h(track_height).col(|ui| {
427            for i in 0..track_height {
428                if i >= thumb_pos && i < thumb_pos + thumb_height {
429                    ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
430                } else {
431                    ui.styled(
432                        track_char.to_string(),
433                        Style::new().fg(theme.text_dim).dim(),
434                    );
435                }
436            }
437        });
438    }
439
440    fn auto_scroll_nested(
441        &mut self,
442        rect: &Rect,
443        state: &mut ScrollState,
444        inner_scroll_rects: &[Rect],
445    ) {
446        let mut to_consume: Vec<usize> = Vec::new();
447
448        for (i, event) in self.events.iter().enumerate() {
449            if self.consumed[i] {
450                continue;
451            }
452            if let Event::Mouse(mouse) = event {
453                let in_bounds = mouse.x >= rect.x
454                    && mouse.x < rect.right()
455                    && mouse.y >= rect.y
456                    && mouse.y < rect.bottom();
457                if !in_bounds {
458                    continue;
459                }
460                let in_inner = inner_scroll_rects.iter().any(|sr| {
461                    mouse.x >= sr.x
462                        && mouse.x < sr.right()
463                        && mouse.y >= sr.y
464                        && mouse.y < sr.bottom()
465                });
466                if in_inner {
467                    continue;
468                }
469                let delta = self.scroll_lines_per_event as usize;
470                match mouse.kind {
471                    MouseKind::ScrollUp => {
472                        state.scroll_up(delta);
473                        to_consume.push(i);
474                    }
475                    MouseKind::ScrollDown => {
476                        state.scroll_down(delta);
477                        to_consume.push(i);
478                    }
479                    MouseKind::Drag(MouseButton::Left) => {}
480                    _ => {}
481                }
482            }
483        }
484
485        for i in to_consume {
486            self.consumed[i] = true;
487        }
488    }
489
490    /// Shortcut for `container().border(border)`.
491    ///
492    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
493    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
494        self.container()
495            .border(border)
496            .border_sides(BorderSides::all())
497    }
498
499    fn push_container(
500        &mut self,
501        direction: Direction,
502        gap: u32,
503        f: impl FnOnce(&mut Context),
504    ) -> Response {
505        let interaction_id = self.next_interaction_id();
506        let border = self.theme.border;
507
508        self.commands.push(Command::BeginContainer {
509            direction,
510            gap,
511            align: Align::Start,
512            align_self: None,
513            justify: Justify::Start,
514            border: None,
515            border_sides: BorderSides::all(),
516            border_style: Style::new().fg(border),
517            bg_color: None,
518            padding: Padding::default(),
519            margin: Margin::default(),
520            constraints: Constraints::default(),
521            title: None,
522            grow: 0,
523            group_name: None,
524        });
525        self.text_color_stack.push(None);
526        f(self);
527        self.text_color_stack.pop();
528        self.commands.push(Command::EndContainer);
529        self.last_text_idx = None;
530
531        self.response_for(interaction_id)
532    }
533
534    pub(crate) fn response_for(&self, interaction_id: usize) -> Response {
535        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
536            return Response::none();
537        }
538        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
539            let clicked = self
540                .click_pos
541                .map(|(mx, my)| {
542                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
543                })
544                .unwrap_or(false);
545            let hovered = self
546                .mouse_pos
547                .map(|(mx, my)| {
548                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
549                })
550                .unwrap_or(false);
551            Response {
552                clicked,
553                hovered,
554                changed: false,
555                focused: false,
556                rect: *rect,
557            }
558        } else {
559            Response::none()
560        }
561    }
562
563    /// Returns true if the named group is currently hovered by the mouse.
564    pub fn is_group_hovered(&self, name: &str) -> bool {
565        if let Some(pos) = self.mouse_pos {
566            self.prev_group_rects.iter().any(|(n, rect)| {
567                n == name
568                    && pos.0 >= rect.x
569                    && pos.0 < rect.x + rect.width
570                    && pos.1 >= rect.y
571                    && pos.1 < rect.y + rect.height
572            })
573        } else {
574            false
575        }
576    }
577
578    /// Returns true if the named group contains the currently focused widget.
579    pub fn is_group_focused(&self, name: &str) -> bool {
580        if self.prev_focus_count == 0 {
581            return false;
582        }
583        let focused_index = self.focus_index % self.prev_focus_count;
584        self.prev_focus_groups
585            .get(focused_index)
586            .and_then(|group| group.as_deref())
587            .map(|group| group == name)
588            .unwrap_or(false)
589    }
590
591    /// Render a form that groups input fields vertically.
592    ///
593    /// Use [`Context::form_field`] inside the closure to render each field.
594    pub fn form(
595        &mut self,
596        state: &mut FormState,
597        f: impl FnOnce(&mut Context, &mut FormState),
598    ) -> &mut Self {
599        let _ = self.col(|ui| {
600            f(ui, state);
601        });
602        self
603    }
604
605    /// Render a single form field with label and input.
606    ///
607    /// Shows a validation error below the input when present.
608    pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
609        let _ = self.col(|ui| {
610            ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
611            let _ = ui.text_input(&mut field.input);
612            if let Some(error) = field.error.as_deref() {
613                ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
614            }
615        });
616        self
617    }
618
619    /// Render a submit button.
620    ///
621    /// Returns `true` when the button is clicked or activated.
622    pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
623        self.button(label)
624    }
625}