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}