1
//! Break tokens — the value-type "resume here" state for true fragmentation.
2
//!
3
//! Design: `scripts/BREAK_TOKENS_DESIGN.md` (K30a). The layout contract is
4
//! LayoutNG-shaped and PURE:
5
//!
6
//! ```text
7
//! (node, constraint_space { remaining_extent }, break_token?) -> (fragment, break_token?)
8
//! ```
9
//!
10
//! Tokens are OWNED, comparable value types — nothing about pagination is
11
//! ever written into the node tree, the layout tree, or any cache during a
12
//! pass (the fossil at `paged_layout.rs:1-11` documents why the mutable
13
//! alternative failed). Two laws every consumer may rely on:
14
//!
15
//! 1. **Determinism**: identical input (content + constraints + incoming
16
//!    token) produces an identical outgoing token, comparable with `==`.
17
//!    Equality is structural; float fields inherit text3's rounding-tolerant
18
//!    `Rect` comparison, which is safe in the conservative direction — a
19
//!    false *inequality* merely re-lays one extra page, a false *equality*
20
//!    cannot arise from tolerant comparison of identical-bits passes.
21
//! 2. **Progress**: an outgoing token never equals the incoming token of
22
//!    the same fragmentainer (the page loop asserts this; violating it is
23
//!    the NG infinite-loop class).
24
//!
25
//! `token_fingerprint` is a FAST-PATH REJECTOR for K34 convergence checks:
26
//! `a == b ⇒ fingerprint(a) == fingerprint(b)` (it hashes a subset of the
27
//! compared fields). Convergence must NEVER be decided on fingerprints
28
//! alone — equal fingerprints require the full `==` before stopping
29
//! repagination (a collision that stopped early would ship stale pages).
30
//!
31
//! Provenance note: the token SHAPE follows public architecture prose
32
//! (css-break-3, the `RenderingNG` fragmentation article, the `LayoutNG`
33
//! README); no engine implementation source was consulted. See the design
34
//! doc §9.
35

            
36
use alloc::boxed::Box;
37
use alloc::vec::Vec;
38

            
39
use crate::text3::cache::{BreakCursor, Hyphens, LineBreakStrictness, ShapedItem, WordBreak};
40

            
41
/// The resume state a fragmentainer boundary produced. `None` anywhere a
42
/// token could appear means "finished — nothing to resume".
43
#[derive(Debug, Clone, PartialEq)]
44
pub enum BreakToken {
45
    /// Resume a block-level box (its unfinished/unstarted children carry
46
    /// their own tokens).
47
    Block(BlockBreakToken),
48
    /// Resume an inline formatting context mid-flow.
49
    Inline(InlineBreakToken),
50
}
51

            
52
/// Resume state for one BLOCK box. Invariant (asserted by consumers, not
53
/// trusted): every sibling BEFORE the first entry in `children` is FINISHED
54
/// — `children` is the unfinished tail, in document order.
55
#[derive(Debug, Clone, PartialEq)]
56
pub struct BlockBreakToken {
57
    /// Layout-tree index of the box this token resumes. Tokens never
58
    /// outlive their layout generation (they are regenerated per pass), so
59
    /// the index is same-generation by construction; `generation` exists to
60
    /// assert that in debug builds.
61
    pub node: usize,
62
    /// Block-size of this box already consumed by previous fragmentainers.
63
    /// Drives `box-decoration-break: slice` (default: no re-emitted top
64
    /// decoration on resume) and monolith overflow resumption.
65
    pub consumed_block_size: f32,
66
    /// The unfinished tail of this box's children, document order.
67
    pub children: Vec<ChildBreakEntry>,
68
    /// Layout-generation stamp for debug assertions (see `node`).
69
    pub generation: u64,
70
}
71

            
72
/// One unfinished child in a [`BlockBreakToken`].
73
#[derive(Debug, Clone, PartialEq)]
74
pub enum ChildBreakEntry {
75
    /// The child started in an earlier fragmentainer; resume it with this
76
    /// token.
77
    ResumeIn {
78
        child: usize,
79
        token: Box<BreakToken>,
80
    },
81
    /// The child has not started yet — a break landed before it.
82
    /// `forced` = the break came from `break-before: page` (or a
83
    /// `<pagebreak/>` node): css-break-3 §5.2 truncates margins adjoining
84
    /// UNFORCED breaks only, so the resume side keeps this child's top
85
    /// margin iff the break was forced.
86
    BreakBefore { child: usize, forced: bool },
87
}
88

            
89
/// Owned snapshot of text3's [`BreakCursor`] — the inline resume state.
90
///
91
/// `BreakCursor` borrows `&'a [ShapedItem]` and therefore cannot be stored
92
/// across passes or compared as a value; this snapshot owns exactly the
93
/// STATE (resume index + hyphenation remainder). The style knobs on the
94
/// cursor (`word_break` / `hyphens` / `line_break`) are deliberately NOT
95
/// part of the token: they derive from style, not from layout progress —
96
/// [`InlineBreakToken::resume`] takes them from the caller, who reads them
97
/// from the same style the original cursor did.
98
#[derive(Debug, Clone, PartialEq)]
99
pub struct InlineBreakToken {
100
    /// Index of the next *full* item to process in the IFC's shaped-item
101
    /// sequence.
102
    pub next_item_index: usize,
103
    /// The remainder of an item split by hyphenation on the boundary line —
104
    /// the very first content of the resumed fragment.
105
    pub partial_remainder: Vec<ShapedItem>,
106
}
107

            
108
impl InlineBreakToken {
109
    /// Snapshot a live cursor's resume state (pure; the cursor is untouched).
110
    #[must_use]
111
2
    pub fn from_cursor(cursor: &BreakCursor<'_>) -> Self {
112
2
        Self {
113
2
            next_item_index: cursor.next_item_index,
114
2
            partial_remainder: cursor.partial_remainder.clone(),
115
2
        }
116
2
    }
117

            
118
    /// Reconstruct a cursor over `items` positioned exactly where the
119
    /// snapshotted one stopped. The style knobs come from the caller (they
120
    /// are style-derived, not layout state — see the type docs).
121
    #[must_use]
122
1
    pub fn resume<'a>(
123
1
        &self,
124
1
        items: &'a [ShapedItem],
125
1
        word_break: WordBreak,
126
1
        hyphens: Hyphens,
127
1
        line_break: LineBreakStrictness,
128
1
    ) -> BreakCursor<'a> {
129
1
        BreakCursor {
130
1
            items,
131
1
            next_item_index: self.next_item_index,
132
1
            partial_remainder: self.partial_remainder.clone(),
133
1
            word_break,
134
1
            hyphens,
135
1
            line_break,
136
1
        }
137
1
    }
138

            
139
    /// True when resuming would start from the very beginning — such a
140
    /// token should not exist (it encodes "no progress"); the page loop's
141
    /// progress guard treats it as a hard stop.
142
    #[must_use]
143
3
    pub const fn is_degenerate_start(&self) -> bool {
144
3
        self.next_item_index == 0 && self.partial_remainder.is_empty()
145
3
    }
146
}
147

            
148
// ---------------------------------------------------------------------------
149
// K30b decision helpers — pure, exhaustively unit-tested; `layout_bfc` only
150
// wires them (design §4.4)
151
// ---------------------------------------------------------------------------
152

            
153
/// Verdict for one normal-flow child against the fragmentainer.
154
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155
pub enum FitDecision {
156
    /// The child ends within the remaining extent: place it here.
157
    Fits,
158
    /// Break BEFORE this child — it (and its later siblings) resume in the
159
    /// next fragmentainer.
160
    BreakBeforeHere,
161
    /// The child is the first content of this fragmentainer and no
162
    /// fragmentainer can ever hold it: place it OVERFLOWING (css-break
163
    /// monolith rule — never tear, never loop).
164
    MonolithOverflow,
165
}
166

            
167
/// Geometry tolerance for fit checks: sub-1/100-px overshoot is float noise,
168
/// not a page break.
169
const FIT_EPS: f32 = 0.01;
170

            
171
/// Decide whether a child whose border-box starts at `pen` (margins already
172
/// resolved) and spans `child_block_size` fits the remaining extent.
173
///
174
/// `placed_any_content`: whether this fragmentainer already holds content
175
/// from this box — if it does, breaking before the child always makes
176
/// progress; if it does not, breaking only helps when a FRESH fragmentainer
177
/// is actually bigger than what remains here (otherwise the child is a
178
/// monolith and overflows).
179
#[must_use]
180
727
pub fn fragment_fit(
181
727
    pen: f32,
182
727
    child_block_size: f32,
183
727
    remaining_block_extent: f32,
184
727
    next_fragmentainer_extent: f32,
185
727
    placed_any_content: bool,
186
727
) -> FitDecision {
187
727
    if pen + child_block_size <= remaining_block_extent + FIT_EPS {
188
596
        return FitDecision::Fits;
189
131
    }
190
131
    if placed_any_content {
191
118
        return FitDecision::BreakBeforeHere;
192
13
    }
193
    // First content of the fragmentainer overflows on its own. Progress
194
    // guarantee: only defer to the next fragmentainer if it is genuinely
195
    // roomier than what is left here AND can hold the child.
196
13
    if child_block_size <= next_fragmentainer_extent + FIT_EPS
197
1
        && next_fragmentainer_extent > remaining_block_extent + FIT_EPS
198
    {
199
1
        FitDecision::BreakBeforeHere
200
    } else {
201
12
        FitDecision::MonolithOverflow
202
    }
203
727
}
204

            
205
/// Build the outgoing token when a break lands before `breaking_child`:
206
/// the unfinished tail is that child plus every later in-flow sibling, all
207
/// as `BreakBefore` entries (block-granular v1 — `ResumeIn` entries appear
208
/// when nested resume lands, K30b part 2).
209
#[must_use]
210
16
pub fn tail_token(
211
16
    node: usize,
212
16
    consumed_block_size: f32,
213
16
    breaking_child: usize,
214
16
    later_in_flow_siblings: impl Iterator<Item = usize>,
215
16
) -> BreakToken {
216
16
    let mut children = alloc::vec![ChildBreakEntry::BreakBefore {
217
16
        child: breaking_child,
218
16
        forced: false,
219
16
    }];
220
16
    children.extend(
221
26
        later_in_flow_siblings.map(|child| ChildBreakEntry::BreakBefore { child, forced: false }),
222
    );
223
16
    BreakToken::Block(BlockBreakToken {
224
16
        node,
225
16
        consumed_block_size,
226
16
        children,
227
16
        generation: 0,
228
16
    })
229
16
}
230

            
231
/// The consumer-side resume plan for a block token: which child is the
232
/// FIRST unfinished one (everything before it is finished and must be
233
/// skipped with zero side effects). `None` for a childless token —
234
/// defensive: such a token encodes no work and resuming from it is a
235
/// no-op, which the page loop's progress guard turns into a stop.
236
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237
pub struct ResumePlan {
238
    pub first_unfinished: usize,
239
}
240

            
241
#[must_use]
242
326
pub fn resume_plan(token: &BlockBreakToken) -> Option<ResumePlan> {
243
326
    token.children.first().map(|entry| ResumePlan {
244
37
        first_unfinished: match entry {
245
20
            ChildBreakEntry::ResumeIn { child, .. }
246
37
            | ChildBreakEntry::BreakBefore { child, .. } => *child,
247
        },
248
37
    })
249
326
}
250

            
251
// ---------------------------------------------------------------------------
252
// Fingerprints — fast-path rejector for K34 convergence
253
// ---------------------------------------------------------------------------
254

            
255
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
256
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
257

            
258
#[inline]
259
10582
fn fnv(hash: u64, byte: u8) -> u64 {
260
10582
    (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
261
10582
}
262

            
263
#[inline]
264
1235
fn fnv_u64(mut hash: u64, value: u64) -> u64 {
265
9880
    for b in value.to_le_bytes() {
266
9880
        hash = fnv(hash, b);
267
9880
    }
268
1235
    hash
269
1235
}
270

            
271
/// 64-bit structural fingerprint. Law: `a == b ⇒ fingerprint(a) ==
272
/// fingerprint(b)` — guaranteed because it hashes a SUBSET of the fields
273
/// `PartialEq` compares (float geometry inside `partial_remainder` items is
274
/// summarized by count + source indices, never by the tolerant-compared
275
/// floats themselves, so the law survives the rounding tolerance).
276
/// Convergence checks use it to reject fast and MUST confirm with `==`.
277
#[must_use]
278
122
pub fn token_fingerprint(token: &BreakToken) -> u64 {
279
122
    fingerprint_into(FNV_OFFSET, token)
280
122
}
281

            
282
268
fn fingerprint_into(mut h: u64, token: &BreakToken) -> u64 {
283
268
    match token {
284
257
        BreakToken::Block(b) => {
285
257
            h = fnv(h, 0x01);
286
257
            h = fnv_u64(h, b.node as u64);
287
            // consumed_block_size participates in PartialEq as an exact
288
            // f32 compare, so its bits are a valid fingerprint component.
289
257
            h = fnv_u64(h, u64::from(b.consumed_block_size.to_bits()));
290
257
            h = fnv_u64(h, b.children.len() as u64);
291
691
            for entry in &b.children {
292
434
                match entry {
293
146
                    ChildBreakEntry::ResumeIn { child, token } => {
294
146
                        h = fnv(h, 0x02);
295
146
                        h = fnv_u64(h, *child as u64);
296
146
                        h = fingerprint_into(h, token);
297
146
                    }
298
288
                    ChildBreakEntry::BreakBefore { child, .. } => {
299
288
                        h = fnv(h, 0x03);
300
288
                        h = fnv_u64(h, *child as u64);
301
288
                    }
302
                }
303
            }
304
257
            h
305
        }
306
11
        BreakToken::Inline(t) => {
307
11
            h = fnv(h, 0x04);
308
11
            h = fnv_u64(h, t.next_item_index as u64);
309
11
            h = fnv_u64(h, t.partial_remainder.len() as u64);
310
            // Source indices are integer-exact fields of the compared items
311
            // (Rect floats are deliberately excluded — they compare with a
312
            // rounding tolerance, hashing them would break the law).
313
15
            for item in &t.partial_remainder {
314
4
                if let Some(src) = shaped_item_source(item) {
315
4
                    h = fnv_u64(h, u64::from(src.run_index));
316
4
                    h = fnv_u64(h, u64::from(src.item_index));
317
4
                }
318
            }
319
11
            h
320
        }
321
    }
322
268
}
323

            
324
// Returns Option to mirror the other `*_source` accessors and to stay
325
// source-compatible if a future `ShapedItem` variant has no content index.
326
#[allow(clippy::unnecessary_wraps)]
327
4
const fn shaped_item_source(item: &ShapedItem) -> Option<azul_core::selection::ContentIndex> {
328
4
    match item {
329
        ShapedItem::Cluster(c) => Some(c.source_content_index),
330
        ShapedItem::CombinedBlock { source, .. }
331
        | ShapedItem::Object { source, .. }
332
4
        | ShapedItem::Tab { source, .. }
333
4
        | ShapedItem::Break { source, .. } => Some(*source),
334
    }
335
4
}
336

            
337
// ---------------------------------------------------------------------------
338
// Property tests (K30a exit gate; see design doc §6.2)
339
// ---------------------------------------------------------------------------
340

            
341
#[cfg(test)]
342
mod break_token_laws {
343
    use azul_core::selection::ContentIndex;
344

            
345
    use super::*;
346
    use crate::text3::cache::Rect;
347

            
348
11
    fn tab(run: u32, item: u32, w: f32) -> ShapedItem {
349
11
        ShapedItem::Tab {
350
11
            source: ContentIndex {
351
11
                run_index: run,
352
11
                item_index: item,
353
11
            },
354
11
            bounds: Rect {
355
11
                x: 0.0,
356
11
                y: 0.0,
357
11
                width: w,
358
11
                height: 16.0,
359
11
            },
360
11
        }
361
11
    }
362

            
363
12
    fn inline(next: usize, remainder: Vec<ShapedItem>) -> BreakToken {
364
12
        BreakToken::Inline(InlineBreakToken {
365
12
            next_item_index: next,
366
12
            partial_remainder: remainder,
367
12
        })
368
12
    }
369

            
370
6
    fn block(node: usize, consumed: f32, children: Vec<ChildBreakEntry>) -> BreakToken {
371
6
        BreakToken::Block(BlockBreakToken {
372
6
            node,
373
6
            consumed_block_size: consumed,
374
6
            children,
375
6
            generation: 1,
376
6
        })
377
6
    }
378

            
379
    // -- Eq laws ----------------------------------------------------------
380

            
381
    #[test]
382
1
    fn equality_is_structural_and_reflexive() {
383
1
        let t = block(
384
            7,
385
            120.5,
386
1
            vec![
387
1
                ChildBreakEntry::BreakBefore { child: 3, forced: false },
388
1
                ChildBreakEntry::ResumeIn {
389
1
                    child: 2,
390
1
                    token: Box::new(inline(4, vec![tab(0, 9, 12.0)])),
391
1
                },
392
            ],
393
        );
394
1
        assert_eq!(t, t.clone());
395
        // Any structural difference breaks equality: node…
396
1
        let mut o = t.clone();
397
1
        if let BreakToken::Block(b) = &mut o {
398
1
            b.node = 8;
399
1
        }
400
1
        assert_ne!(t, o);
401
        // …consumed size…
402
1
        let mut o = t.clone();
403
1
        if let BreakToken::Block(b) = &mut o {
404
1
            b.consumed_block_size += 0.5;
405
1
        }
406
1
        assert_ne!(t, o);
407
        // …child order (document order is semantic)…
408
1
        let mut o = t.clone();
409
1
        if let BreakToken::Block(b) = &mut o {
410
1
            b.children.reverse();
411
1
        }
412
1
        assert_ne!(t, o);
413
        // …and nested inline state.
414
1
        let mut o = t.clone();
415
1
        if let BreakToken::Block(b) = &mut o {
416
1
            if let ChildBreakEntry::ResumeIn { token, .. } = &mut b.children[1] {
417
1
                **token = inline(5, vec![tab(0, 9, 12.0)]);
418
1
            }
419
        }
420
1
        assert_ne!(t, o);
421
1
    }
422

            
423
    #[test]
424
1
    fn fingerprint_law_equal_tokens_have_equal_fingerprints() {
425
1
        let cases = [
426
1
            inline(0, vec![]),
427
1
            inline(3, vec![tab(1, 2, 8.0)]),
428
1
            block(0, 0.0, vec![]),
429
1
            block(
430
1
                5,
431
1
                33.25,
432
1
                vec![ChildBreakEntry::ResumeIn {
433
1
                    child: 1,
434
1
                    token: Box::new(inline(2, vec![])),
435
1
                }],
436
1
            ),
437
1
        ];
438
5
        for t in &cases {
439
4
            assert_eq!(
440
4
                token_fingerprint(t),
441
4
                token_fingerprint(&t.clone()),
442
                "fingerprint must be a pure function of compared fields: {t:?}"
443
            );
444
        }
445
        // And it actually discriminates the obvious cases (not a constant).
446
1
        assert_ne!(
447
1
            token_fingerprint(&cases[0]),
448
1
            token_fingerprint(&cases[2]),
449
            "inline(0) vs block(0) must not collide on the variant tag"
450
        );
451
1
        assert_ne!(
452
1
            token_fingerprint(&inline(1, vec![])),
453
1
            token_fingerprint(&inline(2, vec![]))
454
        );
455
1
    }
456

            
457
    #[test]
458
1
    fn fingerprint_survives_the_tolerant_rect_compare() {
459
        // text3's Rect PartialEq is rounding-tolerant: two tokens whose
460
        // remainder Rects differ inside the tolerance are EQUAL — the
461
        // fingerprint must agree (law: a == b ⇒ fp(a) == fp(b)). This is
462
        // exactly why Rect floats are excluded from the fingerprint.
463
1
        let a = inline(3, vec![tab(1, 2, 8.0)]);
464
1
        let b = inline(3, vec![tab(1, 2, 8.000001)]);
465
1
        if a == b {
466
1
            assert_eq!(token_fingerprint(&a), token_fingerprint(&b));
467
        } else {
468
            // If the tolerance ever tightens to bit-exact this branch keeps
469
            // the test meaningful instead of vacuous.
470
            assert_ne!(a, b);
471
        }
472
1
    }
473

            
474
    // -- Cursor bridge ----------------------------------------------------
475

            
476
    #[test]
477
1
    fn cursor_snapshot_resume_round_trips() {
478
1
        let items = vec![tab(0, 0, 10.0), tab(0, 1, 10.0), tab(0, 2, 10.0)];
479
1
        let mut cursor = BreakCursor::new(&items);
480
1
        cursor.next_item_index = 2;
481
1
        cursor.partial_remainder = vec![tab(0, 1, 4.0)];
482

            
483
1
        let token = InlineBreakToken::from_cursor(&cursor);
484
1
        let resumed = token.resume(
485
1
            &items,
486
1
            cursor.word_break,
487
1
            cursor.hyphens,
488
1
            cursor.line_break,
489
        );
490

            
491
1
        assert_eq!(resumed.next_item_index, cursor.next_item_index);
492
1
        assert_eq!(resumed.partial_remainder, cursor.partial_remainder);
493
1
        assert!(!resumed.is_at_start());
494
        // And the snapshot round-trips through the snapshot again.
495
1
        assert_eq!(InlineBreakToken::from_cursor(&resumed), token);
496
1
    }
497

            
498
    #[test]
499
1
    fn degenerate_start_token_is_detected() {
500
1
        assert!(InlineBreakToken {
501
1
            next_item_index: 0,
502
1
            partial_remainder: vec![],
503
1
        }
504
1
        .is_degenerate_start());
505
1
        assert!(!InlineBreakToken {
506
1
            next_item_index: 0,
507
1
            partial_remainder: vec![tab(0, 0, 1.0)],
508
1
        }
509
1
        .is_degenerate_start());
510
1
        assert!(!InlineBreakToken {
511
1
            next_item_index: 1,
512
1
            partial_remainder: vec![],
513
1
        }
514
1
        .is_degenerate_start());
515
1
    }
516

            
517
    // -- K30b decision helpers ---------------------------------------------
518

            
519
    #[test]
520
1
    fn fragment_fit_truth_table() {
521
        use FitDecision::*;
522
        // Fits exactly / with epsilon slack.
523
1
        assert_eq!(fragment_fit(0.0, 100.0, 100.0, 100.0, false), Fits);
524
1
        assert_eq!(fragment_fit(50.0, 50.005, 100.0, 100.0, true), Fits);
525
        // Mid-page overflow with content already placed: break before.
526
1
        assert_eq!(fragment_fit(80.0, 40.0, 100.0, 100.0, true), BreakBeforeHere);
527
        // First content, uniform pages, taller than a page: monolith.
528
1
        assert_eq!(
529
1
            fragment_fit(0.0, 250.0, 100.0, 100.0, false),
530
            MonolithOverflow
531
        );
532
        // First content, but the NEXT page is roomier and holds it: defer.
533
1
        assert_eq!(
534
1
            fragment_fit(0.0, 250.0, 100.0, 300.0, false),
535
            BreakBeforeHere
536
        );
537
        // First content, next page roomier but STILL too small: monolith
538
        // (deferring would just move the overflow, not fix it).
539
1
        assert_eq!(
540
1
            fragment_fit(0.0, 400.0, 100.0, 300.0, false),
541
            MonolithOverflow
542
        );
543
        // Progress guarantee: never break-before on first content when the
544
        // next fragmentainer is the same size (would loop forever).
545
1
        assert_eq!(
546
1
            fragment_fit(0.0, 100.02, 100.0, 100.0, false),
547
            MonolithOverflow
548
        );
549
1
    }
550

            
551
    #[test]
552
1
    fn tail_token_lists_the_breaking_child_then_later_siblings_in_order() {
553
1
        let t = tail_token(4, 320.0, 7, [9, 12].into_iter());
554
1
        let BreakToken::Block(b) = &t else {
555
            panic!("block token expected")
556
        };
557
1
        assert_eq!(b.node, 4);
558
1
        assert_eq!(b.consumed_block_size, 320.0);
559
1
        assert_eq!(
560
            b.children,
561
1
            vec![
562
1
                ChildBreakEntry::BreakBefore { child: 7, forced: false },
563
1
                ChildBreakEntry::BreakBefore { child: 9, forced: false },
564
1
                ChildBreakEntry::BreakBefore { child: 12, forced: false },
565
            ]
566
        );
567
        // Consumer side: resume starts exactly at the breaking child.
568
1
        assert_eq!(resume_plan(b).unwrap().first_unfinished, 7);
569
1
    }
570

            
571
    #[test]
572
1
    fn resume_plan_is_none_for_a_childless_token() {
573
1
        let empty = BlockBreakToken {
574
1
            node: 1,
575
1
            consumed_block_size: 0.0,
576
1
            children: vec![],
577
1
            generation: 0,
578
1
        };
579
1
        assert!(resume_plan(&empty).is_none());
580
1
    }
581

            
582
    // -- Progress guard shape ----------------------------------------------
583

            
584
    #[test]
585
1
    fn progress_is_observable_via_equality() {
586
        // The page loop's no-progress guard is `outgoing == incoming`; pin
587
        // that "one more child finished" and "one more item consumed" are
588
        // both visible to it.
589
1
        let before = block(
590
            0,
591
            0.0,
592
1
            vec![
593
1
                ChildBreakEntry::ResumeIn {
594
1
                    child: 1,
595
1
                    token: Box::new(inline(2, vec![])),
596
1
                },
597
1
                ChildBreakEntry::BreakBefore { child: 2, forced: false },
598
            ],
599
        );
600
1
        let after_child_finished = block(
601
            0,
602
            0.0,
603
1
            vec![ChildBreakEntry::ResumeIn {
604
1
                child: 2,
605
1
                token: Box::new(inline(0, vec![tab(0, 0, 1.0)])),
606
1
            }],
607
        );
608
1
        assert_ne!(before, after_child_finished);
609

            
610
1
        let after_items_consumed = block(
611
            0,
612
            0.0,
613
1
            vec![
614
1
                ChildBreakEntry::ResumeIn {
615
1
                    child: 1,
616
1
                    token: Box::new(inline(3, vec![])),
617
1
                },
618
1
                ChildBreakEntry::BreakBefore { child: 2, forced: false },
619
            ],
620
        );
621
1
        assert_ne!(before, after_items_consumed);
622
1
    }
623
}