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}