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