Skip to main content

frust_engine/compile/
layers.rs

1//! Opacity layers, retained brackets, and the bracket stack they share with
2//! clips.
3//!
4//! Three of the display list's commands open a *group* — a bracket whose
5//! matching pop has to undo exactly what it did, and inside which a hoisted
6//! clear is confined (see [`clear`](super::clear)). [`GroupStack`] is that one
7//! stack. It is deliberately one stack and not three: `frust_scene` records
8//! `PopClip`, `PopLayer` and `PopRetained` as distinct commands, but a widget
9//! tree can emit them unbalanced or interleaved, and three stacks would let one
10//! kind of pop lift a bracket another kind still relies on. One stack means the
11//! innermost open bracket is always what closes, whichever pop closes it.
12//!
13//! ## Opacity layers
14//!
15//! A `PushLayer` at `alpha >= 1.0` composites source-over at full opacity,
16//! which is precisely what drawing its contents into the parent does — so it
17//! lowers to nothing but its rectangle's clip, exactly as `PushClip` does
18//! (`frust_scene` documents the two as the same command with `alpha` fixed).
19//! Below full opacity the layer has to be rendered in isolation and composited
20//! as a whole, which is a page and a pass in the scheduler
21//! ([`schedule`](crate::schedule)) — so it lowers to a recorded layer *plus*
22//! that same rectangular clip.
23//!
24//! The rectangle goes through the clip stack rather than through
25//! [`LayerProps::clip_path`]: frust lowers every clip to a scissor or a
26//! coverage mask and never to an intermediate texture, and the scheduler
27//! refuses a recorded layer carrying a clip path for that exact reason. Setting
28//! both would clip the layer twice.
29//!
30//! ## Retained brackets
31//!
32//! `frust_scene` states what a renderer that does not retain must do with a
33//! `PushRetained` bracket: paint the body inline, wrapped exactly as if the
34//! recorder had emitted `push_transform(scale_about(scale, rect.center()))`
35//! when `scale != 1.0` and `push_layer(rect, alpha)` when `alpha < 1.0`.
36//! [`RetainedStack`] is that emulation, and it is also the whole of what a
37//! compiler with retention off does (see `SceneCompiler::set_retained_layers`).
38//!
39//! A compiler with retention on lowers the outermost bracket the same way and
40//! additionally records it as a [`RetainedBracket`]: the body becomes a
41//! recorded layer of its own *whatever* its alpha — at full opacity one with no
42//! clip of its own ([`GroupStack::push_retained`]), so the inline lowering's
43//! geometry is unchanged — which the renderer's retained-page store
44//! (`crate::cache::retained`) either admits, rendering the layer into a page it
45//! keeps across frames, or refuses, in which case the scheduler serves it as
46//! the ordinary layer the inline emulation describes. A `DrawRetained` becomes
47//! an empty recorded layer standing at its painter-order position, which the
48//! store resolves to a composite of the resident page. The page holds the body
49//! *with* the presentation scale baked in, exactly as the inline lowering
50//! rasterizes it, and *without* the alpha, so a page is reusable under any
51//! alpha and any whole-pixel move of the same transform ([`RetainedPlacement`]).
52//!
53//! The scale is applied as a *correction* composed ahead of every subsequent
54//! command's own transform rather than by rewriting the body's commands,
55//! because the body was recorded in the ordinary composed transform space: the
56//! bracket's presentation parameters are deliberately not baked into it. The
57//! correction that reproduces `push_transform` around a body already carrying
58//! `transform` is `transform * scale_about(..) * transform.inverse()` — see
59//! [`retained_correction`].
60//!
61//! Only the outermost bracket is honoured, which the display list explicitly
62//! permits: an inner bracket's own `alpha` and `scale` are ignored, and its
63//! depth is tracked only so the matching pop can be identified. With retention
64//! on, an inner bracket's key — and that of any bracket that records no
65//! [`RetainedBracket`] — is listed in `CompiledFrame::retained_superseded`
66//! instead: its body is painted as part of the outermost one, so whatever page
67//! the key held is older than this frame's recording and the store evicts it.
68
69use kurbo::{Affine, Rect};
70use peniko::BlendMode;
71use vello_common::geometry::RectU16;
72use vello_common::record::LayerProps;
73
74/// One open bracket: what its matching pop has to undo, and the device-space
75/// bounds a clear hoisted past it is confined to.
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct Group {
78    clip: bool,
79    layer: bool,
80    bounds: Rect,
81}
82
83impl Group {
84    /// Whether closing this bracket pops the clip stack.
85    #[must_use]
86    pub fn closes_clip(&self) -> bool {
87        self.clip
88    }
89
90    /// Whether closing this bracket pops a recorded layer.
91    #[must_use]
92    pub fn closes_layer(&self) -> bool {
93        self.layer
94    }
95
96    /// The bracket's device-space bounds.
97    ///
98    /// Exact for the axis-aligned transforms frust records (translate and
99    /// scale); a rotated or skewed bracket bounds conservatively, which widens
100    /// a hoisted clear's confinement rather than narrowing it.
101    #[must_use]
102    pub fn bounds(&self) -> Rect {
103        self.bounds
104    }
105}
106
107/// The compiler's stack of open clip, layer and retained brackets.
108///
109/// Retained across frames alongside the clip stack it mirrors —
110/// [`reset`](Self::reset) is what keeps it from carrying state between frames.
111///
112/// Every entry here has a counterpart on the clip stack, and the two are
113/// pushed and popped together: a bracket is opened by pushing both, and closed
114/// by popping both. That is the invariant that lets an unbalanced pop be
115/// ignored safely rather than underflowing either one.
116#[derive(Debug, Default)]
117pub struct GroupStack {
118    entries: Vec<Group>,
119}
120
121impl GroupStack {
122    /// An empty stack, with no bracket open.
123    #[must_use]
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Drop every open bracket, keeping the buffer.
129    pub fn reset(&mut self) {
130        self.entries.clear();
131    }
132
133    /// Open a clip bracket covering `bounds`.
134    pub fn push_clip(&mut self, bounds: Rect) {
135        self.entries.push(Group {
136            clip: true,
137            layer: false,
138            bounds,
139        });
140    }
141
142    /// Open a layer bracket covering `bounds`.
143    ///
144    /// `isolated` says whether the layer was recorded as a layer of its own —
145    /// a full-opacity one lowered to its clip alone, and has nothing in the
146    /// recording for its pop to close.
147    pub fn push_layer(&mut self, bounds: Rect, isolated: bool) {
148        self.entries.push(Group {
149            clip: true,
150            layer: isolated,
151            bounds,
152        });
153    }
154
155    /// Open the bracket a retained body at full opacity records: a layer of
156    /// its own with no clip behind it.
157    ///
158    /// The inline lowering of such a bracket clips nothing, so neither does
159    /// this — the body draws exactly the pixels it would have drawn unwrapped,
160    /// and the layer only gathers them for the retained page. Its bounds are
161    /// unbounded for the same reason: nothing hoisted past it is confined by
162    /// it, and nothing drawn inside it is windowed by it.
163    pub fn push_retained(&mut self) {
164        self.entries.push(Group {
165            clip: false,
166            layer: true,
167            bounds: Rect::new(
168                f64::NEG_INFINITY,
169                f64::NEG_INFINITY,
170                f64::INFINITY,
171                f64::INFINITY,
172            ),
173        });
174    }
175
176    /// Close the innermost open bracket, or `None` when none is open.
177    pub fn pop(&mut self) -> Option<Group> {
178        self.entries.pop()
179    }
180
181    /// Whether no bracket is open — a command here is at the frame root.
182    #[must_use]
183    pub fn is_empty(&self) -> bool {
184        self.entries.is_empty()
185    }
186
187    /// How many brackets are open.
188    #[must_use]
189    pub fn depth(&self) -> usize {
190        self.entries.len()
191    }
192
193    /// The device-space bounds of every open bracket, outermost first.
194    pub fn bounds(&self) -> impl Iterator<Item = Rect> + '_ {
195        self.entries.iter().map(Group::bounds)
196    }
197}
198
199/// How a `PushLayer` bracket lowers.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum LayerLowering {
202    /// Its rectangle's clip and nothing else.
203    Clip,
204    /// A recorded layer of its own, plus that same clip.
205    Isolated,
206}
207
208/// How a layer at `alpha` lowers.
209///
210/// A non-finite `alpha` answers [`LayerLowering::Isolated`], the same as any
211/// alpha below one. It never reaches here from the frame path — the compiler's
212/// up-front walk refuses such a layer outright — and the scheduler escalates a
213/// recorded layer whose opacity is not finite, so the conservative answer is
214/// the isolating one either way.
215#[must_use]
216pub fn lower_layer(alpha: f32) -> LayerLowering {
217    if alpha >= 1.0 {
218        LayerLowering::Clip
219    } else {
220        LayerLowering::Isolated
221    }
222}
223
224/// The recorded properties an isolated opacity layer carries.
225///
226/// Everything but the opacity is left at its default deliberately: a blend
227/// mode, a mask or a clip path on a recorded layer each need a route the
228/// scheduler does not serve, and frust records none of them — the layer's own
229/// rectangle is lowered through the clip stack instead (see the module
230/// header).
231#[must_use]
232pub fn layer_props(opacity: f32) -> LayerProps {
233    LayerProps {
234        blend_mode: BlendMode::default(),
235        opacity,
236        mask: None,
237        clip_path: None,
238    }
239}
240
241/// The correction that reproduces a retained bracket's presentation scale.
242///
243/// The body was recorded under `transform` without the scale baked in, so
244/// scaling it about `rect`'s centre means conjugating the scale by that
245/// transform: move into the body's own space, scale about the centre there,
246/// and move back. Composed ahead of the frame root, the result applies to
247/// every command inside the bracket without any of them being rewritten.
248///
249/// Answers the identity in the two cases where there is nothing to correct: a
250/// scale of exactly one, and a `transform` whose inverse is not finite. A
251/// singular transform (a zero scale, a collapsed axis) has no usable inverse,
252/// and the conjugation would carry infinities into every subsequent command's
253/// transform; dropping the presentation scale draws the body unscaled, where
254/// propagating them would refuse a frame the rest of which is perfectly
255/// drawable.
256#[must_use]
257pub fn retained_correction(rect: Rect, scale: f64, transform: Affine) -> Affine {
258    if scale == 1.0 {
259        return Affine::IDENTITY;
260    }
261
262    let correction = transform * Affine::scale_about(scale, rect.center()) * transform.inverse();
263    if correction.as_coeffs().iter().all(|c| c.is_finite()) {
264        correction
265    } else {
266        Affine::IDENTITY
267    }
268}
269
270/// Where a retained body lands on the device grid: the composed transform
271/// its local coordinates are mapped through, split into the linear part and
272/// the translation.
273///
274/// A page holds the body rasterized under one placement. Another placement
275/// can be served from it only when the linear parts agree — the same scale,
276/// rotation and skew, so the page's pixels are the pixels the body would draw
277/// there — and the translations differ by a move a 1:1 composite can make
278/// ([`Self::offset_to`]).
279#[derive(Debug, Clone, Copy, PartialEq)]
280pub struct RetainedPlacement {
281    /// The transform's linear part, `[a, b, c, d]` in `kurbo`'s coefficient
282    /// order.
283    pub linear: [f64; 4],
284    /// The transform's translation, in device pixels.
285    pub translation: (f64, f64),
286}
287
288impl RetainedPlacement {
289    /// How far apart two linear parts may be and still be read as one: well
290    /// below anything a scale animation or a device-scale change produces, and
291    /// well above the rounding a transform recomposed from the same factors
292    /// accumulates.
293    pub const LINEAR_EPSILON: f64 = 1e-6;
294
295    /// The placement `transform` describes.
296    #[must_use]
297    pub fn of(transform: Affine) -> Self {
298        let [a, b, c, d, e, f] = transform.as_coeffs();
299        Self {
300            linear: [a, b, c, d],
301            translation: (e, f),
302        }
303    }
304
305    /// The whole-pixel move from `self` to `other`, or `None` when the two
306    /// differ in anything but their translation.
307    ///
308    /// A fractional move is rounded to the nearest whole pixel: a composite is
309    /// a 1:1 copy of the page's texels and cannot land between them, so a body
310    /// sliding by a fraction of a pixel per frame is drawn at most half a pixel
311    /// from where an inline lowering would rasterize it. A whole-pixel move
312    /// (the common case of a scroll or a pixel-snapped transition) is exact.
313    #[must_use]
314    pub fn offset_to(&self, other: &Self) -> Option<(i32, i32)> {
315        let same_linear = self
316            .linear
317            .iter()
318            .zip(other.linear.iter())
319            .all(|(a, b)| (a - b).abs() <= Self::LINEAR_EPSILON);
320        if !same_linear {
321            return None;
322        }
323        let dx = (other.translation.0 - self.translation.0).round();
324        let dy = (other.translation.1 - self.translation.1).round();
325        let representable = |value: f64| value.is_finite() && value.abs() <= f64::from(u16::MAX);
326        (representable(dx) && representable(dy)).then_some((dx as i32, dy as i32))
327    }
328
329    /// The device extent `rect` covers under this placement, per axis, rounded
330    /// to whole pixels — the extent the resident-key ledger reports.
331    #[must_use]
332    pub fn extent(&self, rect: Rect) -> (u32, u32) {
333        let [a, b, c, d] = self.linear;
334        let x_scale = a.hypot(b);
335        let y_scale = c.hypot(d);
336        let pixels = |value: f64| {
337            if value.is_finite() {
338                value.round().clamp(0.0, f64::from(u32::MAX)) as u32
339            } else {
340                0
341            }
342        };
343        (
344            pixels(rect.width() * x_scale),
345            pixels(rect.height() * y_scale),
346        )
347    }
348
349    /// The uniform scale this placement rasterizes at — the square root of the
350    /// linear part's determinant, which is the device scale for an
351    /// axis-aligned body and the presentation scale folded into it.
352    #[must_use]
353    pub fn scale(&self) -> f32 {
354        let [a, b, c, d] = self.linear;
355        let scale = (a * d - b * c).abs().sqrt();
356        if scale.is_finite() { scale as f32 } else { 0.0 }
357    }
358}
359
360/// One retained bracket a compiler with retention on recorded this frame,
361/// for the renderer's store (`crate::cache::retained`) to resolve.
362#[derive(Debug, Clone, PartialEq)]
363pub struct RetainedBracket {
364    /// The key the bracket carried.
365    pub key: u64,
366    /// The recorded layer standing for the bracket, indexed into
367    /// [`CommandRecorder::layers`](vello_common::record::CommandRecorder::layers).
368    pub layer: u32,
369    /// The bracket's alpha — what the page is composited at. Never baked into
370    /// the page itself.
371    pub opacity: f32,
372    /// Where the body's local space lands on the device grid, presentation
373    /// scale included.
374    pub placement: RetainedPlacement,
375    /// The device extent the bracket's `rect` covers under
376    /// [`placement`](Self::placement) — the ledger's `extent_px`.
377    pub extent_px: (u32, u32),
378    /// Which of the two commands this is, with what each one needs.
379    pub kind: RetainedKind,
380}
381
382/// The two retained commands, as the store needs to see them.
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum RetainedKind {
385    /// A `PushRetained` body: rendered into a page when the store admits it.
386    Record {
387        /// The bracket's `rect` on the device grid, rounded out to whole tiles
388        /// and clamped to the viewport — the most a page of this body may
389        /// hold. A body whose own drawn bounds leave it painted outside its
390        /// rectangle, which a page holding `rect` cannot carry anywhere else.
391        rect: RectU16,
392        /// Whether the body can be retained at all: it lay wholly inside the
393        /// viewport and every scissor around it under no coverage mask (so
394        /// nothing of it was clipped away on the way in), it drew no hole
395        /// punch and no externally bound texture (whose pixels change with no
396        /// command changing), and its bracket closed where it was opened.
397        retainable: bool,
398    },
399    /// A `DrawRetained`: a composite of a resident page.
400    Draw {
401        /// The painter-order index the composite takes — the slot the command
402        /// was recorded at.
403        depth: u32,
404        /// The device rectangle the composite may write: the viewport
405        /// intersected with every scissor open around the command.
406        window: RectU16,
407        /// Whether a coverage-mask clip is open around the command, which a
408        /// 1:1 composite cannot honour.
409        masked: bool,
410    },
411}
412
413impl RetainedBracket {
414    /// Whether this is a `DrawRetained`.
415    #[must_use]
416    pub fn is_draw(&self) -> bool {
417        matches!(self.kind, RetainedKind::Draw { .. })
418    }
419}
420
421/// The retained bracket's inline emulation: how deep the walk is inside one,
422/// and what the outermost bracket installed.
423#[derive(Debug)]
424pub struct RetainedStack {
425    depth: usize,
426    correction: Affine,
427    layer_bracket: Option<usize>,
428}
429
430impl Default for RetainedStack {
431    fn default() -> Self {
432        Self::new()
433    }
434}
435
436impl RetainedStack {
437    /// A stack with no bracket open and nothing corrected.
438    #[must_use]
439    pub fn new() -> Self {
440        Self {
441            depth: 0,
442            correction: Affine::IDENTITY,
443            layer_bracket: None,
444        }
445    }
446
447    /// Drop every open bracket and its correction.
448    pub fn reset(&mut self) {
449        *self = Self::new();
450    }
451
452    /// The affine to compose ahead of the frame root for every command inside
453    /// the open bracket — the identity when none is open, or when the
454    /// outermost one asked for no scale.
455    #[must_use]
456    pub fn correction(&self) -> Affine {
457        self.correction
458    }
459
460    /// Whether a bracket is open.
461    #[must_use]
462    pub fn is_open(&self) -> bool {
463        self.depth > 0
464    }
465
466    /// Enter a bracket, returning whether it is the outermost one.
467    ///
468    /// The outermost bracket installs its presentation scale as the
469    /// correction; an inner one installs nothing, which is the display list's
470    /// own rule that only the outermost bracket needs honouring.
471    pub fn enter(&mut self, rect: Rect, scale: f64, transform: Affine) -> bool {
472        let outermost = self.depth == 0;
473        if outermost {
474            self.correction = retained_correction(rect, scale, transform);
475        }
476        self.depth = self.depth.saturating_add(1);
477        outermost
478    }
479
480    /// Record that the outermost bracket opened a layer group, so the matching
481    /// pop closes it.
482    ///
483    /// Separate from [`enter`](Self::enter) because opening the layer is the
484    /// compiler's to do and can be declined — a bracket whose corrected
485    /// transform does not survive composition opens none, and its pop must not
486    /// then close a bracket it never opened. `bracket` is the group stack's
487    /// depth just after the layer's own push — the handle [`leave`](Self::leave)
488    /// uses to tell whether that group is still the one it would close.
489    pub fn record_layer(&mut self, bracket: usize) {
490        self.layer_bracket = Some(bracket);
491    }
492
493    /// Leave a bracket, returning whether the outermost one just closed
494    /// *having opened a layer group* — that is, whether the caller now has a
495    /// group to close.
496    ///
497    /// A pop with no bracket open is ignored, the policy the display list
498    /// states for an unbalanced `PopRetained`. `open_brackets` is the group
499    /// stack's current depth: when a stray pop inside the bracket's body has
500    /// already closed the group the bracket opened, the recorded depth is no
501    /// longer reachable and no group is reported — closing one anyway would
502    /// lift an ancestor bracket the display list still holds open.
503    pub fn leave(&mut self, open_brackets: usize) -> bool {
504        if self.depth == 0 {
505            return false;
506        }
507        self.depth -= 1;
508        if self.depth > 0 {
509            return false;
510        }
511
512        self.correction = Affine::IDENTITY;
513        self.layer_bracket
514            .take()
515            .is_some_and(|bracket| bracket <= open_brackets)
516    }
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    #[test]
524    fn a_full_opacity_layer_lowers_to_its_clip_alone() {
525        assert_eq!(lower_layer(1.0), LayerLowering::Clip);
526        assert_eq!(lower_layer(2.0), LayerLowering::Clip);
527        assert_eq!(lower_layer(0.5), LayerLowering::Isolated);
528        // Zero contributes nothing, but that is the scheduler's call to make
529        // from the recorded opacity, not the compiler's to make by omission.
530        assert_eq!(lower_layer(0.0), LayerLowering::Isolated);
531    }
532
533    #[test]
534    fn a_recorded_layer_carries_only_its_opacity() {
535        let props = layer_props(0.25);
536        assert_eq!(props.opacity, 0.25);
537        assert_eq!(props.blend_mode, BlendMode::default());
538        assert!(props.mask.is_none());
539        assert!(props.clip_path.is_none());
540    }
541
542    #[test]
543    fn the_innermost_bracket_is_what_closes_whichever_pop_closes_it() {
544        let mut stack = GroupStack::new();
545        stack.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
546        stack.push_layer(Rect::new(2.0, 2.0, 8.0, 8.0), true);
547        assert_eq!(stack.depth(), 2);
548
549        let inner = stack.pop().expect("two brackets are open");
550        assert!(inner.closes_clip() && inner.closes_layer());
551        let outer = stack.pop().expect("one bracket is still open");
552        assert!(outer.closes_clip() && !outer.closes_layer());
553
554        assert!(stack.is_empty());
555        assert!(stack.pop().is_none());
556    }
557
558    #[test]
559    fn a_full_opacity_layer_bracket_has_no_recorded_layer_to_close() {
560        let mut stack = GroupStack::new();
561        stack.push_layer(Rect::new(0.0, 0.0, 4.0, 4.0), false);
562        let group = stack.pop().expect("a bracket is open");
563        assert!(group.closes_clip());
564        assert!(!group.closes_layer());
565    }
566
567    #[test]
568    fn open_bracket_bounds_are_listed_outermost_first() {
569        let mut stack = GroupStack::new();
570        stack.push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
571        stack.push_layer(Rect::new(10.0, 10.0, 20.0, 20.0), true);
572
573        let bounds: Vec<Rect> = stack.bounds().collect();
574        assert_eq!(
575            bounds,
576            vec![
577                Rect::new(0.0, 0.0, 40.0, 40.0),
578                Rect::new(10.0, 10.0, 20.0, 20.0),
579            ]
580        );
581    }
582
583    #[test]
584    fn the_correction_scales_the_body_about_the_rect_centre() {
585        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
586        let correction = retained_correction(rect, 0.5, Affine::IDENTITY);
587
588        // The centre is the scale's fixed point; a corner moves halfway to it.
589        assert_eq!(correction * rect.center(), rect.center());
590        assert_eq!(correction * rect.origin(), (5.0, 5.0).into());
591    }
592
593    #[test]
594    fn the_correction_conjugates_by_the_body_transform() {
595        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
596        let transform = Affine::translate((100.0, 0.0));
597        let correction = retained_correction(rect, 0.5, transform);
598
599        // The centre in the body's own space is what stays put, so under the
600        // composed transform the body's centre is still where it was.
601        let composed = correction * transform;
602        assert_eq!(composed * rect.center(), transform * rect.center());
603    }
604
605    #[test]
606    fn a_scale_of_one_and_a_singular_transform_both_correct_nothing() {
607        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
608        assert_eq!(
609            retained_correction(rect, 1.0, Affine::translate((3.0, 4.0))),
610            Affine::IDENTITY
611        );
612        assert_eq!(
613            retained_correction(rect, 0.5, Affine::scale(0.0)),
614            Affine::IDENTITY
615        );
616    }
617
618    #[test]
619    fn only_the_outermost_bracket_installs_its_presentation() {
620        let outer = Rect::new(0.0, 0.0, 20.0, 20.0);
621        let inner = Rect::new(0.0, 0.0, 4.0, 4.0);
622        let mut stack = RetainedStack::new();
623
624        assert!(stack.enter(outer, 0.5, Affine::IDENTITY));
625        let installed = stack.correction();
626        assert_ne!(installed, Affine::IDENTITY);
627
628        assert!(!stack.enter(inner, 4.0, Affine::IDENTITY));
629        assert_eq!(stack.correction(), installed);
630
631        // The inner pop restores nothing; the outer one restores everything.
632        assert!(!stack.leave(0));
633        assert_eq!(stack.correction(), installed);
634        assert!(!stack.leave(0));
635        assert_eq!(stack.correction(), Affine::IDENTITY);
636        assert!(!stack.is_open());
637    }
638
639    #[test]
640    fn a_bracket_that_opened_a_layer_reports_it_once_at_its_own_pop() {
641        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
642        let mut stack = RetainedStack::new();
643
644        stack.enter(rect, 1.0, Affine::IDENTITY);
645        stack.record_layer(1);
646        stack.enter(rect, 1.0, Affine::IDENTITY);
647
648        assert!(!stack.leave(1), "the inner pop closes no group");
649        assert!(stack.leave(1), "the outer pop closes the group it opened");
650        assert!(!stack.leave(1), "an unbalanced pop closes nothing");
651    }
652
653    #[test]
654    fn an_unbalanced_pop_retained_is_ignored() {
655        let mut stack = RetainedStack::new();
656        assert!(!stack.leave(0));
657        assert!(!stack.leave(0));
658        assert!(!stack.is_open());
659        assert_eq!(stack.correction(), Affine::IDENTITY);
660    }
661
662    #[test]
663    fn a_stray_pop_inside_a_retained_body_does_not_hand_its_group_to_pop_retained() {
664        let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
665        let mut stack = RetainedStack::new();
666
667        stack.enter(rect, 1.0, Affine::IDENTITY);
668        stack.record_layer(2);
669
670        // A stray pop inside the body already closed the bracket's own group:
671        // the group stack is back below the recorded depth, so the bracket's
672        // pop must not report a group — closing one would lift an ancestor.
673        assert!(!stack.leave(1), "a lost bracket reports no group to close");
674        assert!(!stack.leave(1), "an unbalanced pop still closes nothing");
675    }
676
677    #[test]
678    fn a_retained_group_closes_a_layer_and_no_clip_and_confines_nothing() {
679        let mut stack = GroupStack::new();
680        stack.push_retained();
681        let group = stack.pop().expect("a bracket is open");
682        assert!(group.closes_layer());
683        assert!(!group.closes_clip());
684        assert!(group.bounds().x0.is_infinite() && group.bounds().x1.is_infinite());
685    }
686
687    #[test]
688    fn a_placement_moves_by_whole_pixels_only_under_the_same_linear_part() {
689        let page = RetainedPlacement::of(Affine::translate((8.0, 8.0)) * Affine::scale(2.0));
690        let moved = RetainedPlacement::of(Affine::translate((20.4, 7.6)) * Affine::scale(2.0));
691        assert_eq!(page.offset_to(&moved), Some((12, 0)));
692        assert_eq!(page.offset_to(&page), Some((0, 0)));
693
694        let rescaled = RetainedPlacement::of(Affine::translate((8.0, 8.0)) * Affine::scale(1.9));
695        assert_eq!(
696            page.offset_to(&rescaled),
697            None,
698            "another scale is not a move"
699        );
700        let rotated = RetainedPlacement::of(Affine::rotate(0.1));
701        assert_eq!(page.offset_to(&rotated), None);
702        let far = RetainedPlacement::of(Affine::translate((1e9, 0.0)) * Affine::scale(2.0));
703        assert_eq!(
704            page.offset_to(&far),
705            None,
706            "a move off the grid is not representable"
707        );
708    }
709
710    #[test]
711    fn a_placement_reports_its_extent_and_scale_as_rasterized() {
712        let rect = Rect::new(0.0, 0.0, 50.0, 20.0);
713        let placement = RetainedPlacement::of(Affine::translate((3.0, 4.0)) * Affine::scale(2.5));
714        assert_eq!(placement.extent(rect), (125, 50));
715        assert_eq!(placement.scale(), 2.5);
716
717        let collapsed = RetainedPlacement::of(Affine::scale(0.0));
718        assert_eq!(collapsed.extent(rect), (0, 0));
719        assert_eq!(collapsed.scale(), 0.0);
720    }
721}