Skip to main content

retroglyph_ui/
ui.rs

1//! [`Ui`]: one frame's [`Surface`] and [`Interaction`] paired, so a call site names an `area`/`id`
2//! once and gets both hit-testing and drawing from it.
3//!
4//! You rarely build a `Ui` yourself. [`Interaction::frame`](crate::Interaction::frame) runs one
5//! frame's `begin_frame`/`end_frame` lifecycle and hands its closure a ready `Ui`; [`Ui::new`] is
6//! the escape hatch for callers driving that lifecycle by hand. From there [`Ui::show`] hit-tests
7//! and draws an [`InteractiveWidget`] from one `area`/`id`, [`Ui::draw`]
8//! renders a plain [`Widget`], and [`Ui::vertical`]/[`Ui::horizontal`] open a cursor
9//! that allocates areas for [`show_sized`](Ui::show_sized)/[`draw_sized`](Ui::draw_sized) so a call
10//! site never computes a child `Rect` by hand.
11//!
12//! ```
13//! use retroglyph_core::backend::Headless;
14//! use retroglyph_core::grid::Rect;
15//! use retroglyph_core::terminal::Terminal;
16//! use retroglyph_ui::{Interaction, Sense};
17//!
18//! #[derive(Clone, Copy, PartialEq, Eq)]
19//! enum WidgetId {
20//!     Save,
21//! }
22//!
23//! let mut term = Terminal::new(Headless::new(20, 10));
24//! let mut interaction = Interaction::<WidgetId>::new();
25//! let clicked = interaction.frame(&mut term.surface(), |ui| {
26//!     let area = Rect::new(0, 0, 10, 1);
27//!     ui.interaction().interact(area, WidgetId::Save, Sense::click()).clicked()
28//! });
29//! assert!(!clicked); // nothing clicked yet: no input was fed in
30//! ```
31
32use retroglyph_core::grid::Rect;
33use retroglyph_core::surface::Surface;
34
35use crate::interact::{Interaction, Response, Sense};
36use crate::widget::{InteractiveWidget, Measure, StatefulWidget, Widget};
37
38/// Which way a cursor-based child `Ui` (see [`Ui::vertical`]/[`Ui::horizontal`]) stacks the
39/// areas it allocates.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41enum Axis {
42    Vertical,
43    Horizontal,
44}
45
46/// The mutable allocation state behind a cursor-based child `Ui`: which axis it stacks along,
47/// and how much of its area is still unclaimed.
48struct Cursor {
49    axis: Axis,
50    remaining: Rect,
51}
52
53impl Cursor {
54    /// Carve `primary` cells off the leading edge of `remaining` along `axis` (top for
55    /// [`Axis::Vertical`], left for [`Axis::Horizontal`]), clipped to whatever's left (like
56    /// [`split_v`](crate::split_v)/[`split_h`](crate::split_h), this never overflows `remaining`)
57    /// and shrinks `remaining` by the same amount.
58    fn allocate(&mut self, primary: u16) -> Rect {
59        match self.axis {
60            Axis::Vertical => {
61                let h = primary.min(self.remaining.height());
62                let rect = Rect::new(
63                    self.remaining.left(),
64                    self.remaining.top(),
65                    self.remaining.width(),
66                    h,
67                );
68                self.remaining = Rect::new(
69                    self.remaining.left(),
70                    self.remaining.top().saturating_add(h),
71                    self.remaining.width(),
72                    self.remaining.height() - h,
73                );
74                rect
75            }
76            Axis::Horizontal => {
77                let w = primary.min(self.remaining.width());
78                let rect = Rect::new(
79                    self.remaining.left(),
80                    self.remaining.top(),
81                    w,
82                    self.remaining.height(),
83                );
84                self.remaining = Rect::new(
85                    self.remaining.left().saturating_add(w),
86                    self.remaining.top(),
87                    self.remaining.width() - w,
88                    self.remaining.height(),
89                );
90                rect
91            }
92        }
93    }
94
95    /// Take everything left, leaving nothing for a later allocation from this cursor.
96    fn allocate_rest(&mut self) -> Rect {
97        let primary = match self.axis {
98            Axis::Vertical => self.remaining.height(),
99            Axis::Horizontal => self.remaining.width(),
100        };
101        self.allocate(primary)
102    }
103}
104
105/// One frame's drawing surface and interaction state, together, so a call site names an
106/// `area`/`id` once and gets both hit-testing and drawing from it: see [`show`](Self::show).
107///
108/// # Why two lifetimes
109///
110/// `Surface<'g>` holds a `&'g mut Grid`, which makes `Surface` invariant in `'g`: nothing can
111/// shrink or otherwise reinterpret that lifetime once it is fixed. The surface borrow and the
112/// grid borrow are therefore kept as two separate lifetime parameters here, `'s` (how long this
113/// `Ui` itself, and the `&'s mut Surface` it holds, lives) and `'g` (how long the underlying grid
114/// is borrowed for). Collapsing them into one, e.g. writing the field as `&'a mut Surface<'a>`,
115/// forces `'a` to cover both uses at once: the invariance in `'g` then makes the borrow of the
116/// surface last exactly as long as the grid borrow it is invariant over, so the surface (and the
117/// grid behind it) stay borrowed, and therefore unusable, for the rest of `'a` even after the
118/// `Ui` that held them is dropped. Two parameters let `'s` end (releasing the `Ui`'s borrow of
119/// the surface) while `'g` keeps going, which is exactly what [`Interaction::frame`] relies on:
120/// the surface passed in is usable again once the closure returns.
121pub struct Ui<'s, 'g, Id> {
122    surface: &'s mut Surface<'g>,
123    interaction: &'s mut Interaction<Id>,
124    enabled: bool,
125    /// The active [`vertical`](Self::vertical)/[`horizontal`](Self::horizontal) allocation
126    /// cursor, if any. `None` for a `Ui` fresh from [`new`](Self::new), or a child of
127    /// [`enabled`](Self::enabled)/[`modal`](Self::modal): those two deliberately don't inherit
128    /// an active cursor (see their docs), so [`show_sized`](Self::show_sized)/
129    /// [`draw_sized`](Self::draw_sized)/[`show_auto`](Self::show_auto)/
130    /// [`draw_auto`](Self::draw_auto)/[`vertical_sized`](Self::vertical_sized)/
131    /// [`horizontal_sized`](Self::horizontal_sized) must be called on the `Ui`
132    /// `vertical`/`horizontal` hand to your closure, not through those two.
133    cursor: Option<&'s mut Cursor>,
134}
135
136impl<'s, 'g, Id> Ui<'s, 'g, Id> {
137    /// A `Ui` pairing `surface` with `interaction` for one frame, enabled.
138    ///
139    /// The low-level constructor; prefer [`Interaction::frame`](crate::Interaction::frame), which
140    /// builds this and runs the frame lifecycle for you.
141    #[must_use]
142    pub const fn new(surface: &'s mut Surface<'g>, interaction: &'s mut Interaction<Id>) -> Self {
143        Self {
144            surface,
145            interaction,
146            enabled: true,
147            cursor: None,
148        }
149    }
150
151    /// Whether [`show`](Ui::show)/[`show_stateful`](Ui::show_stateful)/[`region`](Ui::region)
152    /// calls through this context report their widgets as enabled: `retroglyph#602`.
153    #[must_use]
154    pub const fn is_enabled(&self) -> bool {
155        self.enabled
156    }
157
158    /// The surface, for drawing that no widget in this crate covers.
159    #[must_use]
160    pub const fn surface(&mut self) -> &mut Surface<'g> {
161        self.surface
162    }
163
164    /// The interaction context, for hit-testing/focus queries no method here covers.
165    #[must_use]
166    pub const fn interaction(&mut self) -> &mut Interaction<Id> {
167        self.interaction
168    }
169
170    /// The region this `Ui`'s surface represents; see [`Surface::area`].
171    #[must_use]
172    pub const fn area(&self) -> Rect {
173        self.surface.area()
174    }
175}
176
177impl<'g, Id: Copy + PartialEq> Ui<'_, 'g, Id> {
178    /// A child context whose [`show`](Self::show)/[`show_stateful`](Self::show_stateful)/
179    /// [`region`](Self::region) calls report `enabled`: `retroglyph#602`.
180    ///
181    /// Nesting only ever tightens: a child of a context already disabled via `enabled(false)`
182    /// stays disabled regardless of what `enabled` this call passes, matching how egui's and
183    /// `Dear ImGui`'s disabled scopes compose.
184    ///
185    /// The returned `Ui` never inherits an active [`vertical`](Self::vertical)/
186    /// [`horizontal`](Self::horizontal) cursor, even if `self` has one: call
187    /// [`show_sized`](Self::show_sized)/[`draw_sized`](Self::draw_sized)/etc. directly on the
188    /// cursor `Ui`, and reach for `enabled` on the widget-level `show`/`show_stateful` instead
189    /// if you need both.
190    #[must_use]
191    pub const fn enabled(&mut self, enabled: bool) -> Ui<'_, 'g, Id> {
192        Ui {
193            surface: self.surface,
194            interaction: self.interaction,
195            enabled: self.enabled && enabled,
196            cursor: None,
197        }
198    }
199
200    /// Hit-test `area` for `id` with `widget`'s own [`Sense`], then draw `widget` into `area`.
201    ///
202    /// This is the one-`id`-one-`area` guarantee the [`InteractiveWidget`]/[`Ui`] split exists
203    /// for: `area` is registered for hit-testing and used to scope the surface the widget draws
204    /// into from the same value, so the two cannot disagree.
205    ///
206    /// If this context is [`disabled`](Self::enabled), the returned [`Response`] still reports
207    /// [`hovered`](Response::hovered) but never an activation: see
208    /// [`Sense::DISABLED`](crate::Sense::DISABLED).
209    #[must_use]
210    pub fn show(
211        &mut self,
212        area: Rect,
213        id: Id,
214        widget: &impl InteractiveWidget<Id, State = ()>,
215    ) -> Response<Id> {
216        self.show_stateful(area, id, widget, &mut ())
217    }
218
219    /// Like [`show`](Self::show), for an [`InteractiveWidget`] with externally owned `state`.
220    #[must_use]
221    pub fn show_stateful<W: InteractiveWidget<Id> + ?Sized>(
222        &mut self,
223        area: Rect,
224        id: Id,
225        widget: &W,
226        state: &mut W::State,
227    ) -> Response<Id> {
228        let sense = widget.sense().disabled_if(!self.enabled);
229        let response = self.interaction.interact(area, id, sense);
230        widget.render(&mut self.surface.scope(area), state, response);
231        response
232    }
233
234    /// Draw a non-interactive `widget` into `area`.
235    pub fn draw(&mut self, area: Rect, widget: &impl Widget) {
236        widget.render(&mut self.surface.scope(area));
237    }
238
239    /// Like [`draw`](Self::draw), for a [`StatefulWidget`] with externally owned `state`.
240    pub fn draw_stateful<W: StatefulWidget + ?Sized>(
241        &mut self,
242        area: Rect,
243        widget: &W,
244        state: &mut W::State,
245    ) {
246        widget.render(&mut self.surface.scope(area), state);
247    }
248
249    /// Register `area` for `id` with `sense`, and hand back both the resolved [`Response`] and a
250    /// surface scoped to `area`, for drawing this crate has no widget for.
251    ///
252    /// Like [`show`](Self::show), `area` is committed once, by this call, for both hit-testing
253    /// and drawing, so the two cannot disagree.
254    #[must_use]
255    pub fn region(&mut self, area: Rect, id: Id, sense: Sense) -> (Response<Id>, Surface<'_>) {
256        let sense = sense.disabled_if(!self.enabled);
257        let response = self.interaction.interact(area, id, sense);
258        (response, self.surface.scope(area))
259    }
260
261    /// Run `f` with a [`Ui`] whose widgets sit above everything shown so far, and whose pointer
262    /// hits inside `area` never reach a widget registered *before* `f` (a menu bar, the screen
263    /// behind a dropdown, ...), so an app doesn't have to answer "is the thing under this overlay
264    /// still supposed to see this event" with its own bookkeeping. A widget registered *after*
265    /// `f` still wins inside `area`, so an overlay has to be shown after the content it covers,
266    /// not before it.
267    ///
268    /// The barrier is scoped to `area`: a pointer *outside* it reaches whatever's registered
269    /// outside `f` exactly as if `modal` hadn't been called. A modal claims the region it covers,
270    /// not the whole screen; pass a full-screen `area` for a blocking overlay. Drawing is
271    /// unaffected either way, `f`'s `Ui` still draws to this `Ui`'s full surface unless it also
272    /// narrows with [`show`](Self::show)/[`draw`](Self::draw)/[`scope`](retroglyph_core::surface::Surface::scope)
273    /// itself: `modal` only changes hit-testing.
274    pub fn modal<R>(&mut self, area: Rect, f: impl FnOnce(&mut Ui<'_, 'g, Id>) -> R) -> R {
275        self.interaction.push_barrier(area);
276        let mut inner = Ui {
277            surface: self.surface,
278            interaction: self.interaction,
279            enabled: self.enabled,
280            cursor: None,
281        };
282        f(&mut inner)
283    }
284
285    /// Allocate `primary` cells from this `Ui`'s own cursor; see [`show_sized`](Self::show_sized).
286    ///
287    /// # Panics
288    ///
289    /// If this `Ui` has no active cursor: see [`vertical`](Self::vertical)/
290    /// [`horizontal`](Self::horizontal).
291    fn allocate(&mut self, primary: u16) -> Rect {
292        self.cursor
293            .as_deref_mut()
294            .expect(
295                "Ui::show_sized/draw_sized/vertical_sized/horizontal_sized require an active \
296                 cursor: call them on the `Ui` that `vertical`/`horizontal` hands to your \
297                 closure, not on a `Ui` fresh from `new`/`enabled`/`modal`",
298            )
299            .allocate(primary)
300    }
301
302    /// Take everything left in this `Ui`'s own cursor, or [`area`](Self::area) if it has none.
303    fn allocate_rest(&mut self) -> Rect {
304        self.cursor
305            .as_deref_mut()
306            .map_or_else(|| self.surface.area(), Cursor::allocate_rest)
307    }
308
309    /// The remaining width of this `Ui`'s own [`vertical`](Self::vertical) cursor, for
310    /// [`show_auto`](Self::show_auto)/[`draw_auto`](Self::draw_auto) to measure against.
311    ///
312    /// # Panics
313    ///
314    /// If this `Ui` has no active cursor, or its cursor is [`horizontal`](Self::horizontal):
315    /// [`Measure`] reports a height for a given width, not the other way around, so a
316    /// horizontal cursor has nothing to size a column by.
317    fn vertical_cursor_width(&self) -> u16 {
318        match &self.cursor {
319            Some(cursor) if cursor.axis == Axis::Vertical => cursor.remaining.width(),
320            Some(_) => panic!(
321                "Ui::show_auto/draw_auto require a vertical cursor: `Measure::height_for` takes \
322                 a width, so a horizontal cursor has nothing to size a column by"
323            ),
324            None => panic!(
325                "Ui::show_auto/draw_auto require an active cursor: call them on the `Ui` that \
326                 `vertical`/`horizontal` hands to your closure, not on a `Ui` fresh from \
327                 `new`/`enabled`/`modal`"
328            ),
329        }
330    }
331
332    /// Run `f` with a fresh vertical cursor claiming whatever's left: the rest of this `Ui`'s
333    /// own cursor if it has one (so a `vertical` nested inside a `horizontal` row claims that
334    /// row's remaining width), or the whole of [`area`](Self::area) if it doesn't (a `vertical`
335    /// called directly inside [`Interaction::frame`], say).
336    ///
337    /// Widgets shown/drawn through the closure's `Ui` via [`show_sized`](Self::show_sized)/
338    /// [`draw_sized`](Self::draw_sized)/[`show_auto`](Self::show_auto)/
339    /// [`draw_auto`](Self::draw_auto) stack top-to-bottom, each claiming a horizontal strip of
340    /// the cursor's remaining area sized by an explicit height or, for `show_auto`/`draw_auto`,
341    /// by [`Measure::height_for`], and advancing the cursor by that strip's height, so the
342    /// call site never computes a `Rect` by hand. Content past the bottom of the cursor's area
343    /// clips, the same way [`split_v`](crate::split_v) clips a pane that overflows `area`.
344    ///
345    /// Nests with [`horizontal`](Self::horizontal): the `Ui` handed to `f` is a plain `Ui`, so
346    /// it can call `vertical`/`horizontal` again to start a flow along the other axis, scoped to
347    /// whatever this cursor has left at that point.
348    pub fn vertical<R>(&mut self, f: impl FnOnce(&mut Ui<'_, 'g, Id>) -> R) -> R {
349        let area = self.allocate_rest();
350        let mut cursor = Cursor {
351            axis: Axis::Vertical,
352            remaining: area,
353        };
354        let mut inner = Ui {
355            surface: self.surface,
356            interaction: self.interaction,
357            enabled: self.enabled,
358            cursor: Some(&mut cursor),
359        };
360        f(&mut inner)
361    }
362
363    /// Like [`vertical`](Self::vertical), stacking left-to-right instead of top-to-bottom.
364    pub fn horizontal<R>(&mut self, f: impl FnOnce(&mut Ui<'_, 'g, Id>) -> R) -> R {
365        let area = self.allocate_rest();
366        let mut cursor = Cursor {
367            axis: Axis::Horizontal,
368            remaining: area,
369        };
370        let mut inner = Ui {
371            surface: self.surface,
372            interaction: self.interaction,
373            enabled: self.enabled,
374            cursor: Some(&mut cursor),
375        };
376        f(&mut inner)
377    }
378
379    /// Like [`vertical`](Self::vertical), but claims only `height` rows of this `Ui`'s own
380    /// cursor (clipped to whatever's left) instead of everything remaining, so a fixed-size
381    /// nested flow (a one-row-tall horizontal button bar inside a vertical column, say)
382    /// leaves the rest of the outer cursor for whatever comes after it.
383    ///
384    /// # Panics
385    ///
386    /// If this `Ui` has no active cursor: see [`vertical`](Self::vertical).
387    pub fn vertical_sized<R>(
388        &mut self,
389        height: u16,
390        f: impl FnOnce(&mut Ui<'_, 'g, Id>) -> R,
391    ) -> R {
392        let area = self.allocate(height);
393        let mut cursor = Cursor {
394            axis: Axis::Vertical,
395            remaining: area,
396        };
397        let mut inner = Ui {
398            surface: self.surface,
399            interaction: self.interaction,
400            enabled: self.enabled,
401            cursor: Some(&mut cursor),
402        };
403        f(&mut inner)
404    }
405
406    /// Like [`vertical_sized`](Self::vertical_sized), claiming `width` columns instead of
407    /// `height` rows.
408    ///
409    /// # Panics
410    ///
411    /// If this `Ui` has no active cursor: see [`vertical`](Self::vertical).
412    pub fn horizontal_sized<R>(
413        &mut self,
414        width: u16,
415        f: impl FnOnce(&mut Ui<'_, 'g, Id>) -> R,
416    ) -> R {
417        let area = self.allocate(width);
418        let mut cursor = Cursor {
419            axis: Axis::Horizontal,
420            remaining: area,
421        };
422        let mut inner = Ui {
423            surface: self.surface,
424            interaction: self.interaction,
425            enabled: self.enabled,
426            cursor: Some(&mut cursor),
427        };
428        f(&mut inner)
429    }
430
431    /// Like [`show`](Self::show), but allocates the area from this `Ui`'s own cursor instead of
432    /// taking one explicitly: `size` is a height on a [`vertical`](Self::vertical) cursor, a
433    /// width on a [`horizontal`](Self::horizontal) one, clipped to whatever's left (like
434    /// [`split_v`](crate::split_v)/[`split_h`](crate::split_h)), and the cursor advances by that
435    /// amount so the next `show_sized`/`draw_sized`/`show_auto`/`draw_auto` call claims the
436    /// space right after it.
437    ///
438    /// # Panics
439    ///
440    /// If this `Ui` has no active cursor: see [`vertical`](Self::vertical).
441    #[must_use]
442    pub fn show_sized(
443        &mut self,
444        id: Id,
445        widget: &impl InteractiveWidget<Id, State = ()>,
446        size: u16,
447    ) -> Response<Id> {
448        let area = self.allocate(size);
449        self.show(area, id, widget)
450    }
451
452    /// Like [`draw`](Self::draw), sized from this `Ui`'s own cursor; see
453    /// [`show_sized`](Self::show_sized).
454    ///
455    /// # Panics
456    ///
457    /// If this `Ui` has no active cursor: see [`vertical`](Self::vertical).
458    pub fn draw_sized(&mut self, widget: &impl Widget, size: u16) {
459        let area = self.allocate(size);
460        self.draw(area, widget);
461    }
462
463    /// Like [`show_sized`](Self::show_sized), sized by [`Measure::height_for`] instead of an
464    /// explicit size, for a `widget` that can report its own height.
465    ///
466    /// # Panics
467    ///
468    /// If this `Ui` has no active cursor, or its cursor is [`horizontal`](Self::horizontal):
469    /// [`Measure::height_for`] takes a width, so a horizontal cursor has nothing to size a
470    /// column by.
471    #[must_use]
472    pub fn show_auto(
473        &mut self,
474        id: Id,
475        widget: &(impl InteractiveWidget<Id, State = ()> + Measure),
476    ) -> Response<Id> {
477        let height = widget.height_for(self.vertical_cursor_width());
478        self.show_sized(id, widget, height)
479    }
480
481    /// Like [`draw_sized`](Self::draw_sized), sized by [`Measure::height_for`] instead of an
482    /// explicit size; see [`show_auto`](Self::show_auto).
483    ///
484    /// # Panics
485    ///
486    /// If this `Ui` has no active cursor, or its cursor is [`horizontal`](Self::horizontal).
487    pub fn draw_auto(&mut self, widget: &(impl Widget + Measure)) {
488        let height = widget.height_for(self.vertical_cursor_width());
489        self.draw_sized(widget, height);
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use alloc::string::String;
496
497    use retroglyph_core::color::Style;
498    use retroglyph_core::event::{Event, KeyModifiers, MouseEvent, MouseEventKind};
499    use retroglyph_core::grid::{Grid, Pos};
500
501    use super::*;
502    use crate::widget::Widget;
503
504    fn move_pointer(interaction: &mut Interaction<Id>, pos: Pos) {
505        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
506            MouseEventKind::Moved,
507            pos,
508            KeyModifiers::NONE,
509        )));
510    }
511
512    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
513    enum Id {
514        Button,
515        Behind,
516        InModal,
517    }
518
519    struct Dot;
520
521    impl<Id> InteractiveWidget<Id> for Dot {
522        type State = ();
523
524        fn sense(&self) -> Sense {
525            Sense::click()
526        }
527
528        fn render(
529            &self,
530            surface: &mut Surface<'_>,
531            _state: &mut Self::State,
532            response: Response<Id>,
533        ) {
534            let glyph = if response.hovered() { '*' } else { '.' };
535            surface.put((0, 0), glyph, Style::new());
536        }
537    }
538
539    struct Fill(char);
540
541    impl Widget for Fill {
542        fn render(&self, surface: &mut Surface<'_>) {
543            let area = surface.area();
544            for y in 0..area.height() {
545                for x in 0..area.width() {
546                    surface.put((x, y), self.0, Style::new());
547                }
548            }
549        }
550    }
551
552    /// `Ui::show` registers the same rect it draws into: a pointer landing inside the shown area
553    /// hits it on the *next* frame's hit-test, one outside it does not.
554    #[test]
555    fn show_registers_the_area_it_draws_into() {
556        let mut grid = Grid::new(10, 10);
557        let mut interaction = Interaction::<Id>::new();
558        let area = Rect::new(2, 2, 3, 3);
559
560        // Frame 1 registers `area` for `Id::Button`.
561        interaction.frame(
562            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
563            |ui| {
564                let _ = ui.show(area, Id::Button, &Dot);
565            },
566        );
567
568        // The pointer moves inside `area` between frames.
569        move_pointer(&mut interaction, Pos::new(3, 3));
570
571        // Frame 2 resolves hover against frame 1's registration.
572        let response = interaction.frame(
573            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
574            |ui| ui.show(area, Id::Button, &Dot),
575        );
576        assert!(response.hovered());
577
578        // The pointer moves outside `area` between frames.
579        move_pointer(&mut interaction, Pos::new(8, 8));
580        let response = interaction.frame(
581            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
582            |ui| ui.show(area, Id::Button, &Dot),
583        );
584        assert!(!response.hovered());
585    }
586
587    /// `Ui::region` likewise commits one rect for both hit-testing and the surface it hands
588    /// back.
589    #[test]
590    fn region_registers_the_area_it_scopes_the_surface_to() {
591        let mut grid = Grid::new(10, 10);
592        let mut interaction = Interaction::<Id>::new();
593        let area = Rect::new(2, 2, 3, 3);
594
595        interaction.frame(
596            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
597            |ui| {
598                let (_response, mut surface) = ui.region(area, Id::Button, Sense::hover());
599                assert_eq!(surface.area(), area);
600                // `put` addresses this surface's own local coordinates: (0, 0) is `area`'s own
601                // top-left, grid-absolute (2, 2).
602                surface.put((0, 0), 'x', Style::new());
603            },
604        );
605
606        assert_eq!(grid[Pos::new(2, 2)].glyph(), 'x');
607
608        move_pointer(&mut interaction, Pos::new(3, 3));
609        let response = interaction.frame(
610            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
611            |ui| ui.region(area, Id::Button, Sense::hover()).0,
612        );
613        assert!(response.hovered());
614    }
615
616    /// `Interaction::frame` calls `begin_frame`/`end_frame` exactly once around the closure.
617    #[test]
618    fn frame_calls_begin_and_end_exactly_once() {
619        let mut grid = Grid::new(4, 4);
620        let mut interaction = Interaction::<Id>::new();
621        let area = Rect::new(0, 0, 4, 4);
622
623        // Frame 1 registers `area` for `Id::Button`.
624        interaction.frame(&mut Surface::new(&mut grid, area, 0), |ui| {
625            let _ = ui.show(area, Id::Button, &Dot);
626        });
627
628        // A press-then-release inside `area`, fed in between frames.
629        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
630            MouseEventKind::Down(retroglyph_core::event::MouseButton::Left),
631            Pos::new(1, 1),
632            KeyModifiers::NONE,
633        )));
634        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
635            MouseEventKind::Up(retroglyph_core::event::MouseButton::Left),
636            Pos::new(1, 1),
637            KeyModifiers::NONE,
638        )));
639
640        // Frame 2, if `frame` ran begin/end exactly once, resolves the click against frame 1's
641        // registration and reports it.
642        let clicked = interaction.frame(&mut Surface::new(&mut grid, area, 0), |ui| {
643            ui.show(area, Id::Button, &Dot).clicked()
644        });
645        assert!(clicked);
646    }
647
648    /// `Ui::enabled` only ever tightens: an `enabled(true)` child of an `enabled(false)`
649    /// context stays disabled, matching egui's/Dear `ImGui`'s disabled-scope composition.
650    #[test]
651    fn enabled_nesting_only_tightens() {
652        let mut grid = Grid::new(4, 4);
653        let mut interaction = Interaction::<Id>::new();
654        let area = Rect::new(0, 0, 4, 4);
655        move_pointer(&mut interaction, Pos::new(0, 0));
656
657        // Register once so the second frame has something to resolve against.
658        interaction.frame(&mut Surface::new(&mut grid, area, 0), |ui| {
659            let mut disabled = ui.enabled(false);
660            let mut re_enabled = disabled.enabled(true);
661            assert!(!re_enabled.is_enabled());
662            let _ = re_enabled.show(area, Id::Button, &Dot);
663        });
664
665        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
666            MouseEventKind::Down(retroglyph_core::event::MouseButton::Left),
667            Pos::new(0, 0),
668            KeyModifiers::NONE,
669        )));
670        let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
671            MouseEventKind::Up(retroglyph_core::event::MouseButton::Left),
672            Pos::new(0, 0),
673            KeyModifiers::NONE,
674        )));
675
676        let response = interaction.frame(&mut Surface::new(&mut grid, area, 0), |ui| {
677            let mut disabled = ui.enabled(false);
678            let mut re_enabled = disabled.enabled(true);
679            re_enabled.show(area, Id::Button, &Dot)
680        });
681        assert!(response.disabled());
682        assert!(!response.clicked());
683        assert!(response.hovered());
684    }
685
686    /// A `Ui` borrow released at the end of `Interaction::frame` leaves the surface usable
687    /// afterwards: this is the two-lifetime property `Ui` exists for, checked at compile time by
688    /// the fact that this test compiles at all.
689    #[test]
690    fn surface_is_usable_after_frame_returns() {
691        let mut grid = Grid::new(4, 4);
692        let mut interaction = Interaction::<Id>::new();
693        let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0);
694
695        interaction.frame(&mut surface, |ui| {
696            ui.draw(Rect::new(0, 0, 4, 4), &Fill('.'));
697        });
698
699        // `surface` is still a live `&mut Surface` here, not moved or borrowed by `frame`.
700        surface.put((0, 0), 'x', Style::new());
701        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'x');
702    }
703
704    /// A widget shown inside `Ui::modal` wins a hit over a widget registered earlier at the same
705    /// position, even though the earlier one alone would otherwise win by being drawn under the
706    /// pointer (the usual topmost-wins rule): the modal's barrier makes the earlier registration
707    /// unreachable at that position for the rest of this frame.
708    #[test]
709    fn modal_wins_a_hit_over_an_earlier_widget_at_the_same_position() {
710        let mut grid = Grid::new(10, 10);
711        let mut interaction = Interaction::<Id>::new();
712        let area = Rect::new(2, 2, 3, 3);
713
714        interaction.frame(
715            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
716            |ui| {
717                let _ = ui.show(area, Id::Behind, &Dot);
718                ui.modal(area, |ui| {
719                    let _ = ui.show(area, Id::InModal, &Dot);
720                });
721            },
722        );
723
724        move_pointer(&mut interaction, Pos::new(3, 3));
725
726        let (behind, in_modal) = interaction.frame(
727            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
728            |ui| {
729                let behind = ui.show(area, Id::Behind, &Dot);
730                let in_modal = ui.modal(area, |ui| ui.show(area, Id::InModal, &Dot));
731                (behind, in_modal)
732            },
733        );
734        assert!(!behind.hovered()); // the barrier shadows it
735        assert!(in_modal.hovered());
736    }
737
738    /// A widget registered outside `Ui::modal`'s `area` is unaffected by the barrier and still
739    /// wins hits at its own position: a modal claims the region it covers, not the whole screen.
740    #[test]
741    fn a_widget_outside_the_modal_area_still_wins_hits_at_its_own_position() {
742        let mut grid = Grid::new(10, 10);
743        let mut interaction = Interaction::<Id>::new();
744        let modal_area = Rect::new(2, 2, 3, 3);
745        let outside_area = Rect::new(7, 7, 2, 2);
746
747        interaction.frame(
748            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
749            |ui| {
750                let _ = ui.show(outside_area, Id::Behind, &Dot);
751                ui.modal(modal_area, |ui| {
752                    let _ = ui.show(modal_area, Id::InModal, &Dot);
753                });
754            },
755        );
756
757        move_pointer(&mut interaction, Pos::new(7, 7));
758
759        let behind = interaction.frame(
760            &mut Surface::new(&mut grid, Rect::new(0, 0, 10, 10), 0),
761            |ui| {
762                let behind = ui.show(outside_area, Id::Behind, &Dot);
763                let _ = ui.modal(modal_area, |ui| ui.show(modal_area, Id::InModal, &Dot));
764                behind
765            },
766        );
767        assert!(behind.hovered()); // outside the modal's own rect: unaffected by its barrier
768    }
769
770    /// A widget that reports a fixed height regardless of width, for [`show_auto`]/[`draw_auto`]
771    /// tests: real `Measure` widgets (e.g. `Paragraph`) wrap at their given width instead, but
772    /// that wrapping behavior isn't what these tests are checking.
773    struct FixedHeight(u16);
774
775    impl Widget for FixedHeight {
776        fn render(&self, surface: &mut Surface<'_>) {
777            let area = surface.area();
778            for y in 0..area.height() {
779                for x in 0..area.width() {
780                    surface.put((x, y), '#', Style::new());
781                }
782            }
783        }
784    }
785
786    impl<Id> InteractiveWidget<Id> for FixedHeight {
787        type State = ();
788
789        fn sense(&self) -> Sense {
790            Sense::click()
791        }
792
793        fn render(
794            &self,
795            surface: &mut Surface<'_>,
796            _state: &mut Self::State,
797            _response: Response<Id>,
798        ) {
799            Widget::render(self, surface);
800        }
801    }
802
803    impl Measure for FixedHeight {
804        fn height_for(&self, _width: u16) -> u16 {
805            self.0
806        }
807    }
808
809    fn glyphs_in_row(grid: &Grid, y: u16, width: u16) -> String {
810        (0..width).map(|x| grid[Pos::new(x, y)].glyph()).collect()
811    }
812
813    /// `Ui::vertical` stacks `draw_sized` calls top-to-bottom, each claiming a horizontal strip
814    /// the height of its `size` argument and advancing the cursor by that much.
815    #[test]
816    fn vertical_stacks_draw_sized_rows_top_to_bottom() {
817        let mut grid = Grid::new(4, 4);
818        let mut interaction = Interaction::<Id>::new();
819
820        interaction.frame(
821            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
822            |ui| {
823                ui.vertical(|ui| {
824                    ui.draw_sized(&Fill('a'), 1);
825                    ui.draw_sized(&Fill('b'), 1);
826                });
827            },
828        );
829
830        assert_eq!(glyphs_in_row(&grid, 0, 4), "aaaa");
831        assert_eq!(glyphs_in_row(&grid, 1, 4), "bbbb");
832        assert_eq!(glyphs_in_row(&grid, 2, 4), "    "); // untouched: nothing claimed it
833    }
834
835    /// `Ui::horizontal` stacks `draw_sized` calls left-to-right instead of top-to-bottom.
836    #[test]
837    fn horizontal_stacks_draw_sized_columns_left_to_right() {
838        let mut grid = Grid::new(4, 4);
839        let mut interaction = Interaction::<Id>::new();
840
841        interaction.frame(
842            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
843            |ui| {
844                ui.horizontal(|ui| {
845                    ui.draw_sized(&Fill('a'), 1);
846                    ui.draw_sized(&Fill('b'), 1);
847                });
848            },
849        );
850
851        assert_eq!(glyphs_in_row(&grid, 0, 4), "ab  ");
852        assert_eq!(glyphs_in_row(&grid, 3, 4), "ab  ");
853    }
854
855    /// `Ui::horizontal_sized` claims only `width` columns of this `Ui`'s own cursor (here, a
856    /// vertical one), leaving the rest for whatever comes after it; the mirror image of
857    /// `vertical_sized`'s own test.
858    #[test]
859    fn horizontal_sized_claims_only_its_own_width_from_the_outer_cursor() {
860        let mut grid = Grid::new(4, 4);
861        let mut interaction = Interaction::<Id>::new();
862
863        interaction.frame(
864            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
865            |ui| {
866                ui.horizontal(|ui| {
867                    // Claims 2 columns from the outer cursor regardless of how much of them
868                    // this nested flow itself draws into.
869                    ui.horizontal_sized(2, |ui| {
870                        ui.draw_sized(&Fill('a'), 2);
871                    });
872                    ui.draw_sized(&Fill('b'), 1);
873                });
874            },
875        );
876
877        assert_eq!(glyphs_in_row(&grid, 0, 4), "aab ");
878    }
879
880    /// A cursor clips content that overflows its remaining space, the same way `split_v` clips a
881    /// pane that overflows `area`: the third row here would run past the 2-row-tall area, so it
882    /// gets zero height instead of drawing out of bounds.
883    #[test]
884    fn vertical_clips_when_rows_overflow_the_cursor_area() {
885        let mut grid = Grid::new(4, 2);
886        let mut interaction = Interaction::<Id>::new();
887
888        interaction.frame(
889            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 2), 0),
890            |ui| {
891                ui.vertical(|ui| {
892                    ui.draw_sized(&Fill('a'), 1);
893                    ui.draw_sized(&Fill('b'), 1);
894                    // Nothing left: this claims a zero-height area and draws nothing.
895                    ui.draw_sized(&Fill('c'), 1);
896                });
897            },
898        );
899
900        assert_eq!(glyphs_in_row(&grid, 0, 4), "aaaa");
901        assert_eq!(glyphs_in_row(&grid, 1, 4), "bbbb");
902    }
903
904    /// `Ui::show_sized` commits the same allocated rect for both hit-testing and drawing, just
905    /// like `Ui::show` does for an explicit `area`.
906    #[test]
907    fn show_sized_registers_the_area_it_allocates() {
908        let mut grid = Grid::new(4, 4);
909        let mut interaction = Interaction::<Id>::new();
910
911        interaction.frame(
912            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
913            |ui| {
914                ui.vertical(|ui| {
915                    let _ = ui.show_sized(Id::Button, &Dot, 1);
916                });
917            },
918        );
919
920        // `show_sized`'s row is the first row, (0, 0)-(4, 1): the pointer inside it hits.
921        move_pointer(&mut interaction, Pos::new(1, 0));
922        let response = interaction.frame(
923            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
924            |ui| ui.vertical(|ui| ui.show_sized(Id::Button, &Dot, 1)),
925        );
926        assert!(response.hovered());
927
928        // Outside the allocated row: no hit.
929        move_pointer(&mut interaction, Pos::new(1, 2));
930        let response = interaction.frame(
931            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
932            |ui| ui.vertical(|ui| ui.show_sized(Id::Button, &Dot, 1)),
933        );
934        assert!(!response.hovered());
935    }
936
937    /// `Ui::show_auto`/`Ui::draw_auto` size their row by `Measure::height_for` instead of an
938    /// explicit size.
939    #[test]
940    fn draw_auto_sizes_by_measure_height_for() {
941        let mut grid = Grid::new(4, 4);
942        let mut interaction = Interaction::<Id>::new();
943
944        interaction.frame(
945            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
946            |ui| {
947                ui.vertical(|ui| {
948                    ui.draw_auto(&FixedHeight(2));
949                    ui.draw_sized(&Fill('x'), 1); // right after the 2-row-tall widget above
950                });
951            },
952        );
953
954        assert_eq!(glyphs_in_row(&grid, 0, 4), "####");
955        assert_eq!(glyphs_in_row(&grid, 1, 4), "####");
956        assert_eq!(glyphs_in_row(&grid, 2, 4), "xxxx");
957    }
958
959    /// `Ui::show_auto` sizes by `Measure::height_for` like `draw_auto`, and, like `show_sized`,
960    /// commits the same allocated rect for both hit-testing and drawing.
961    #[test]
962    fn show_auto_sizes_by_measure_and_registers_the_area_it_allocates() {
963        let mut grid = Grid::new(4, 4);
964        let mut interaction = Interaction::<Id>::new();
965
966        interaction.frame(
967            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
968            |ui| {
969                ui.vertical(|ui| {
970                    let _ = ui.show_auto(Id::Button, &FixedHeight(2));
971                });
972            },
973        );
974
975        assert_eq!(glyphs_in_row(&grid, 0, 4), "####");
976        assert_eq!(glyphs_in_row(&grid, 1, 4), "####");
977        assert_eq!(glyphs_in_row(&grid, 2, 4), "    "); // untouched: past the 2-row-tall widget
978
979        // `show_auto`'s row is (0, 0)-(4, 2): the pointer inside it hits.
980        move_pointer(&mut interaction, Pos::new(1, 1));
981        let response = interaction.frame(
982            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
983            |ui| ui.vertical(|ui| ui.show_auto(Id::Button, &FixedHeight(2))),
984        );
985        assert!(response.hovered());
986    }
987
988    /// `Ui::show_auto`/`Ui::draw_auto` panic when called on a `Ui` with no active cursor at all
989    /// (not just the wrong axis; see `draw_auto_on_a_horizontal_cursor_panics`).
990    #[test]
991    #[should_panic(expected = "require an active cursor")]
992    fn draw_auto_without_an_active_cursor_panics() {
993        let mut grid = Grid::new(4, 4);
994        let mut interaction = Interaction::<Id>::new();
995
996        interaction.frame(
997            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
998            |ui| {
999                ui.draw_auto(&FixedHeight(1));
1000            },
1001        );
1002    }
1003
1004    /// `vertical`/`horizontal` nest: a `horizontal` row started inside a `vertical` column is
1005    /// scoped to that column's remaining area (its full width, whatever height is left), and
1006    /// stacking within it advances a cursor along the other axis.
1007    #[test]
1008    fn horizontal_nested_in_vertical_is_scoped_to_the_outer_cursors_remaining_area() {
1009        let mut grid = Grid::new(4, 4);
1010        let mut interaction = Interaction::<Id>::new();
1011
1012        interaction.frame(
1013            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
1014            |ui| {
1015                ui.vertical(|ui| {
1016                    ui.draw_sized(&Fill('t'), 1); // a title row
1017                    ui.vertical_sized(1, |ui| {
1018                        ui.horizontal(|ui| {
1019                            ui.draw_sized(&Fill('a'), 1);
1020                            ui.draw_sized(&Fill('b'), 1);
1021                        });
1022                    });
1023                });
1024            },
1025        );
1026
1027        assert_eq!(glyphs_in_row(&grid, 0, 4), "tttt");
1028        assert_eq!(glyphs_in_row(&grid, 1, 4), "ab  ");
1029        assert_eq!(glyphs_in_row(&grid, 2, 4), "    "); // untouched: nothing claimed it
1030    }
1031
1032    /// The other direction of nesting: a `vertical` claiming everything left of a `horizontal`
1033    /// cursor gets that row's remaining width, not the whole screen.
1034    #[test]
1035    fn vertical_nested_in_horizontal_claims_the_outer_cursors_remaining_width() {
1036        let mut grid = Grid::new(4, 4);
1037        let mut interaction = Interaction::<Id>::new();
1038
1039        interaction.frame(
1040            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
1041            |ui| {
1042                ui.horizontal(|ui| {
1043                    ui.draw_sized(&Fill('l'), 1); // a left-hand column
1044                    ui.vertical(|ui| {
1045                        ui.draw_sized(&Fill('a'), 1);
1046                        ui.draw_sized(&Fill('b'), 1);
1047                    });
1048                });
1049            },
1050        );
1051
1052        // The left-hand column spans the full height (a `draw_sized` on a horizontal cursor
1053        // claims the whole remaining height), so `l` persists into both rows.
1054        assert_eq!(glyphs_in_row(&grid, 0, 4), "laaa");
1055        assert_eq!(glyphs_in_row(&grid, 1, 4), "lbbb");
1056    }
1057
1058    /// `Ui::show_sized` panics with a clear message when called on a `Ui` with no active cursor
1059    /// (fresh from `new`/`enabled`/`modal`), instead of silently misbehaving.
1060    #[test]
1061    #[should_panic(expected = "require an active cursor")]
1062    fn show_sized_without_an_active_cursor_panics() {
1063        let mut grid = Grid::new(4, 4);
1064        let mut interaction = Interaction::<Id>::new();
1065
1066        interaction.frame(
1067            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
1068            |ui| {
1069                let _ = ui.show_sized(Id::Button, &Dot, 1);
1070            },
1071        );
1072    }
1073
1074    /// `Ui::enabled`'s child doesn't inherit an active cursor, even from a `vertical`/
1075    /// `horizontal` parent: it's a fresh `Ui` like `new`'s, by design (see `Ui::enabled`'s docs).
1076    #[test]
1077    #[should_panic(expected = "require an active cursor")]
1078    fn enabled_child_of_a_cursor_ui_has_no_active_cursor() {
1079        let mut grid = Grid::new(4, 4);
1080        let mut interaction = Interaction::<Id>::new();
1081
1082        interaction.frame(
1083            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
1084            |ui| {
1085                ui.vertical(|ui| {
1086                    let _ = ui.enabled(true).show_sized(Id::Button, &Dot, 1);
1087                });
1088            },
1089        );
1090    }
1091
1092    /// `Ui::show_auto`/`Ui::draw_auto` panic on a `horizontal` cursor: `Measure::height_for`
1093    /// takes a width, so a horizontal cursor has nothing to size a column by.
1094    #[test]
1095    #[should_panic(expected = "require a vertical cursor")]
1096    fn draw_auto_on_a_horizontal_cursor_panics() {
1097        let mut grid = Grid::new(4, 4);
1098        let mut interaction = Interaction::<Id>::new();
1099
1100        interaction.frame(
1101            &mut Surface::new(&mut grid, Rect::new(0, 0, 4, 4), 0),
1102            |ui| {
1103                ui.horizontal(|ui| {
1104                    ui.draw_auto(&FixedHeight(1));
1105                });
1106            },
1107        );
1108    }
1109}