Skip to main content

retex_engine/
pack.rs

1//! Packaging: `hpack` and `vpackage` (*TeX: The Program* §644–§679).
2//!
3//! The over/underfull box messages follow TeX82; LuaTeX's box display adds
4//! the box direction (see [`crate::showbox`]).
5
6use crate::Engine;
7use crate::build::PackSpec;
8use crate::nodes::{BoxData, NodeData, NodeRef, glue_sub, id};
9use crate::primitives_gen::{assign_dimen as dp, assign_int as ip};
10
11/// Totals of stretch and shrink by glue order (`total_stretch`,
12/// `total_shrink`).
13#[derive(Default, Clone, Copy)]
14struct Totals {
15    stretch: [i32; 5],
16    shrink: [i32; 5],
17}
18
19impl Totals {
20    #[inline]
21    fn add(&mut self, g: &crate::glue::Glue) {
22        let so = (g.stretch_order as usize).min(4);
23        let ho = (g.shrink_order as usize).min(4);
24        self.stretch[so] = self.stretch[so].wrapping_add(g.stretch);
25        self.shrink[ho] = self.shrink[ho].wrapping_add(g.shrink);
26    }
27
28    fn stretch_order(&self) -> u8 {
29        (1..5).rev().find(|&o| self.stretch[o] != 0).unwrap_or(0) as u8
30    }
31
32    fn shrink_order(&self) -> u8 {
33        (1..5).rev().find(|&o| self.shrink[o] != 0).unwrap_or(0) as u8
34    }
35}
36
37impl Engine {
38    /// With `\tracinglostchars` 4 or more (LaTeX's setting), reports glyph
39    /// `p` if its font lacks its character, as LuaTeX does each time it
40    /// measures the glyph (when packing a box and in each line-breaking
41    /// pass): `Missing character: There is no ペ (U+30DA) in font cmr10!`
42    /// on its own line, whatever `\tracingonline` (observed).
43    #[inline]
44    pub fn check_lost_char(&mut self, p: NodeRef) {
45        if self.eqtb.int(ip::TRACINGLOSTCHARS) < 4 {
46            return;
47        }
48        let NodeData::Glyph(g) = self.nodes.data(p) else {
49            return;
50        };
51        let (f, c) = (g.font, g.chr);
52        if self.fonts.char_metrics(f, c).is_some() {
53            return;
54        }
55        self.printer.print_nl("Missing character: There is no ");
56        self.printer.print_char(c);
57        self.printer.print_str(&format!(" (U+{c:04X}) in font "));
58        let name = self.font(f).name.clone();
59        self.printer.print_bytes(&name);
60        self.printer.print_char('!' as u32);
61        self.printer.print_nl("");
62    }
63
64    /// Reports character `c` of font `f`, which the font lacks, when it is
65    /// shipped out (observed): with `\tracinglostchars` 1 in the log only
66    /// (unless `\tracingonline`), from 2 on the terminal too, and with 3
67    /// as an error (context, no help).
68    pub fn shipout_lost_char(&mut self, f: u32, c: u32) {
69        let level = self.eqtb.int(ip::TRACINGLOSTCHARS);
70        if level <= 0 {
71            return;
72        }
73        if level == 1 {
74            self.begin_diagnostic();
75        }
76        self.printer.print_nl("Missing character: There is no ");
77        self.printer.print_char(c);
78        self.printer.print_str(&format!(" (U+{c:04X}) in font "));
79        let name = self.font(f).name.clone();
80        self.printer.print_bytes(&name);
81        self.printer.print_char('!' as u32);
82        if level == 1 {
83            self.end_diagnostic(false);
84        } else {
85            self.printer.print_nl("");
86        }
87        if level == 3 {
88            self.help(&[]);
89            self.error();
90        }
91    }
92
93    /// The dimensions (width, height, depth) of a glyph node. Observed:
94    /// the `yoffset` raises or lowers the height, but only a raised glyph
95    /// has a smaller depth; a lowered one keeps its depth.
96    #[inline]
97    pub fn glyph_dimensions(&self, p: NodeRef) -> (i32, i32, i32) {
98        match self.nodes.data(p) {
99            NodeData::Glyph(g) => match self.fonts.char_metrics(g.font, g.chr) {
100                Some(m) => (
101                    crate::expansion::expanded_width(m.width, g.expansion_factor),
102                    m.height + g.yoffset,
103                    m.depth - g.yoffset.max(0),
104                ),
105                None => (0, 0, 0),
106            },
107            _ => (0, 0, 0),
108        }
109    }
110
111    /// Gives box `r` the direction in [`Engine::pack_dir`], if any.
112    fn take_pack_dir(&mut self, r: NodeRef) {
113        let dir = std::mem::replace(&mut self.pack_dir, -1);
114        if dir >= 0
115            && let Some(b) = self.nodes.box_data_mut(r)
116        {
117            b.dir = dir;
118        }
119    }
120
121    /// Packs the hlist `p` into an hbox (`hpack`).
122    pub fn hpack(&mut self, p: NodeRef, spec: PackSpec) -> NodeRef {
123        self.fonts.sync_dense();
124        self.last_badness = 0;
125        let r = self.new_null_box_node(true);
126        self.take_pack_dir(r);
127        let (mut h, mut d, mut x) = (0i32, 0i32, 0i32);
128        let mut tot = Totals::default();
129        // `q` trails behind `p`; a temporary head holds the list so that
130        // nodes can be moved to the adjustment lists.
131        let head = self.nodes.new_temp();
132        self.nodes.link(head, p);
133        let mut q = head;
134        let mut p = p;
135        while !p.is_null() {
136            let next = self.nodes.next(p);
137            match self.nodes.data(p) {
138                NodeData::Glyph(_) => {
139                    self.check_lost_char(p);
140                    let (w, gh, gd) = self.glyph_dimensions(p);
141                    x += w;
142                    if gh > h {
143                        h = gh;
144                    }
145                    if gd > d {
146                        d = gd;
147                    }
148                }
149                NodeData::Box(b) => {
150                    x += b.width;
151                    let s = b.shift;
152                    if b.height - s > h {
153                        h = b.height - s;
154                    }
155                    if b.depth + s > d {
156                        d = b.depth + s;
157                    }
158                }
159                NodeData::Unset { b, .. } => {
160                    x += b.width;
161                    if b.height > h {
162                        h = b.height;
163                    }
164                    if b.depth > d {
165                        d = b.depth;
166                    }
167                }
168                NodeData::Rule {
169                    width,
170                    height,
171                    depth,
172                    ..
173                } => {
174                    x += *width;
175                    if *height > h {
176                        h = *height;
177                    }
178                    if *depth > d {
179                        d = *depth;
180                    }
181                }
182                NodeData::Ins { .. } | NodeData::Mark { .. } | NodeData::Adjust { .. } => {
183                    if let Some(at) = self.ts.adjust_tail {
184                        // Transfer node p to the adjustment list.
185                        self.nodes.set_next(q, next);
186                        if !next.is_null() {
187                            self.nodes.set_prev(next, q);
188                        }
189                        let is_pre = self.nodes.id(p) == id::ADJUST && self.nodes.subtype(p) == 1;
190                        if self.nodes.id(p) == id::ADJUST {
191                            let l = self.nodes.list_ptr(p);
192                            self.nodes.set_list_ptr(p, NodeRef::NULL);
193                            self.nodes.free(p);
194                            if !l.is_null() {
195                                let target = if is_pre {
196                                    self.ts.pre_adjust_tail.unwrap_or(at)
197                                } else {
198                                    at
199                                };
200                                self.nodes.link(target, l);
201                                let t = self.nodes.tail_of(l);
202                                if is_pre && self.ts.pre_adjust_tail.is_some() {
203                                    self.ts.pre_adjust_tail = Some(t);
204                                } else {
205                                    self.ts.adjust_tail = Some(t);
206                                }
207                            }
208                        } else {
209                            self.nodes.link(at, p);
210                            self.nodes.set_next(p, NodeRef::NULL);
211                            self.ts.adjust_tail = Some(p);
212                        }
213                        p = next;
214                        continue;
215                    }
216                }
217                NodeData::Glue { spec, leader } => {
218                    x += spec.width;
219                    tot.add(spec);
220                    let (leader, sub) = (*leader, self.nodes.subtype(p));
221                    if sub >= glue_sub::A_LEADERS && !leader.is_null() {
222                        let (lh, ld) = (self.node_height(leader), self.node_depth(leader));
223                        if lh > h {
224                            h = lh;
225                        }
226                        if ld > d {
227                            d = ld;
228                        }
229                    }
230                }
231                NodeData::Kern {
232                    kern,
233                    expansion_factor,
234                } => x += *kern + *expansion_factor,
235                NodeData::MarginKern { width, .. } => x += *width,
236                NodeData::Math { surround, glue } => {
237                    if glue.width != 0 || glue.stretch != 0 || glue.shrink != 0 {
238                        x += glue.width;
239                        let g = *glue;
240                        tot.add(&g);
241                    } else {
242                        x += *surround;
243                    }
244                }
245                NodeData::Disc { replace, .. } => {
246                    // The replacement text is typeset when there is no break.
247                    let mut s = *replace;
248                    while !s.is_null() {
249                        self.check_lost_char(s);
250                        let (w, sh, sd) = self.simple_dimensions(s);
251                        x += w;
252                        if sh > h {
253                            h = sh;
254                        }
255                        if sd > d {
256                            d = sd;
257                        }
258                        s = self.nodes.next(s);
259                    }
260                }
261                _ => {}
262            }
263            q = p;
264            p = next;
265        }
266        if let Some(at) = self.ts.adjust_tail {
267            self.nodes.set_next(at, NodeRef::NULL);
268        }
269        if let Some(at) = self.ts.pre_adjust_tail {
270            self.nodes.set_next(at, NodeRef::NULL);
271        }
272        let first = self.nodes.next(head);
273        self.nodes.free(head);
274        if !first.is_null() {
275            self.nodes.set_prev(first, NodeRef::NULL);
276        }
277        let w = match spec {
278            PackSpec::Exactly(w) => w,
279            PackSpec::Additional(w) => x + w,
280        };
281        {
282            let b = self.box_mut(r);
283            b.head = first;
284            b.height = h;
285            b.depth = d;
286            b.width = w;
287        }
288        self.ts.last_totals = (tot.stretch, tot.shrink);
289        let x = w - x;
290        // With the hpack_quality callback, it gets the incident instead
291        // of the warning and the overfull rule.
292        let quality = self.has_callback("hpack_quality");
293        let mut incident: Option<(&str, i32)> = None;
294        let report: Option<()> = if x == 0 {
295            None
296        } else if x > 0 {
297            let o = tot.stretch_order();
298            let ts = tot.stretch[o as usize];
299            {
300                let b = self.box_mut(r);
301                b.glue_order = o;
302                b.glue_sign = 1;
303                if ts != 0 {
304                    b.glue_set = crate::nodes::glue_ratio(x, ts);
305                } else {
306                    b.glue_sign = 0;
307                    b.glue_set = 0.0;
308                }
309            }
310            if o == 0 && !first.is_null() {
311                self.last_badness = retex_arith::badness(x, tot.stretch[0]);
312                if self.last_badness > self.eqtb.int(ip::HBADNESS) && quality {
313                    let kind = if self.last_badness > 100 {
314                        "underfull"
315                    } else {
316                        "loose"
317                    };
318                    incident = Some((kind, self.last_badness));
319                    None
320                } else if self.last_badness > self.eqtb.int(ip::HBADNESS) {
321                    self.printer.print_ln();
322                    if self.last_badness > 100 {
323                        self.diag_capture_start();
324                        self.printer.print_nl("Underfull");
325                    } else {
326                        self.printer.print_nl("Loose");
327                    }
328                    self.printer.print_str(" \\hbox (badness ");
329                    self.printer.print_int(i64::from(self.last_badness));
330                    Some(())
331                } else {
332                    None
333                }
334            } else {
335                None
336            }
337        } else {
338            let o = tot.shrink_order();
339            let ts = tot.shrink[o as usize];
340            {
341                let b = self.box_mut(r);
342                b.glue_order = o;
343                b.glue_sign = 2;
344                if ts != 0 {
345                    b.glue_set = crate::nodes::glue_ratio(-x, ts);
346                } else {
347                    b.glue_sign = 0;
348                    b.glue_set = 0.0;
349                }
350            }
351            if ts < -x && o == 0 && !first.is_null() {
352                self.last_badness = 1_000_000;
353                self.box_mut(r).glue_set = 1.0;
354                let excess = -x - tot.shrink[0];
355                let hfuzz = self.eqtb.dimen(dp::HFUZZ);
356                if (excess > hfuzz || self.eqtb.int(ip::HBADNESS) < 100) && quality {
357                    incident = Some(("overfull", excess));
358                    None
359                } else if excess > hfuzz || self.eqtb.int(ip::HBADNESS) < 100 {
360                    let ofr = self.eqtb.dimen(dp::OVERFULLRULE);
361                    if ofr > 0 && excess > hfuzz {
362                        let rule = self.nodes.new_rule(ofr, NULL_RULE, NULL_RULE);
363                        if first.is_null() {
364                            self.box_mut(r).head = rule;
365                        } else {
366                            let t = self.nodes.tail_of(first);
367                            self.nodes.link(t, rule);
368                        }
369                    }
370                    self.printer.print_ln();
371                    self.diag_capture_start();
372                    self.printer.print_nl("Overfull \\hbox (");
373                    self.printer.print_scaled(excess);
374                    self.printer.print_str("pt too wide");
375                    Some(())
376                } else {
377                    None
378                }
379            } else if o == 0 && !first.is_null() {
380                self.last_badness = retex_arith::badness(-x, tot.shrink[0]);
381                if self.last_badness > self.eqtb.int(ip::HBADNESS) && quality {
382                    incident = Some(("tight", self.last_badness));
383                    None
384                } else if self.last_badness > self.eqtb.int(ip::HBADNESS) {
385                    self.printer.print_ln();
386                    self.diag_capture_start();
387                    self.printer.print_nl("Tight \\hbox (badness ");
388                    self.printer.print_int(i64::from(self.last_badness));
389                    Some(())
390                } else {
391                    None
392                }
393            } else {
394                None
395            }
396        };
397        if report.is_some() {
398            self.finish_hbox_diagnostic(r);
399        }
400        if let Some((kind, detail)) = incident {
401            // Observed: a node returned goes at the end of the box's list.
402            let p = self.call_pack_quality("hpack_quality", kind, detail, r);
403            if !p.is_null() {
404                let head = self.nodes.list_ptr(r);
405                if head.is_null() {
406                    self.box_mut(r).head = p;
407                } else {
408                    let t = self.nodes.tail_of(head);
409                    self.nodes.link(t, p);
410                }
411            }
412        }
413        r
414    }
415
416    /// Calls `hpack_quality` or `vpack_quality` for box `r` (observed:
417    /// with the box, `\pack_begin_line` and the current line).
418    fn call_pack_quality(&mut self, cb: &str, kind: &str, detail: i32, r: NodeRef) -> NodeRef {
419        let (first, last) = (self.ts.pack_begin_line, self.input.line());
420        self.figure_callback(cb);
421        match self.lua.clone() {
422            Some(h) => h.pack_quality(self, cb, kind, detail, r, first, last),
423            None => NodeRef::NULL,
424        }
425    }
426
427    /// The natural width, height and depth of the hlist from `p` up to
428    /// `stop` (exclusive), as `hpack` measures it (LuaTeX's
429    /// `node.dimensions`).
430    pub fn natural_dimensions(&self, mut p: NodeRef, stop: NodeRef) -> (i32, i32, i32) {
431        let (mut w, mut h, mut d) = (0i32, 0i32, 0i32);
432        while !p.is_null() && p != stop {
433            let (pw, ph, pd) = match self.nodes.data(p) {
434                NodeData::Glyph(_) => self.glyph_dimensions(p),
435                NodeData::Box(b) => (b.width, b.height - b.shift, b.depth + b.shift),
436                NodeData::Rule {
437                    width,
438                    height,
439                    depth,
440                    ..
441                } => (*width, *height, *depth),
442                NodeData::Disc { replace, .. } => {
443                    let r = *replace;
444                    self.natural_dimensions(r, NodeRef::NULL)
445                }
446                _ => (self.nodes.width(p), 0, 0),
447            };
448            w += pw;
449            h = h.max(ph);
450            d = d.max(pd);
451            p = self.nodes.next(p);
452        }
453        (w, h, d)
454    }
455
456    /// Like [`natural_dimensions`](Self::natural_dimensions), with the
457    /// glue set as in a box with `glue_set`, `sign` (1 stretching, 2
458    /// shrinking) and `order` (`node.dimensions` with glue settings).
459    /// Observed: the glue widths are rounded as TeX outputs a box
460    /// (§625), from the running total of the stretch or shrink.
461    pub fn set_dimensions(
462        &self,
463        mut p: NodeRef,
464        stop: NodeRef,
465        glue_set: f64,
466        sign: i32,
467        order: i32,
468    ) -> (i32, i32, i32) {
469        let (mut w, mut h, mut d) = (0i64, 0i32, 0i32);
470        let (mut cur_glue, mut cur_g) = (0f64, 0i64);
471        while !p.is_null() && p != stop {
472            if let NodeData::Glue { spec, .. } = self.nodes.data(p) {
473                let mut wd = i64::from(spec.width) - cur_g;
474                let set = match sign {
475                    1 if i32::from(spec.stretch_order) == order => {
476                        cur_glue += f64::from(spec.stretch);
477                        true
478                    }
479                    2 if i32::from(spec.shrink_order) == order => {
480                        cur_glue -= f64::from(spec.shrink);
481                        true
482                    }
483                    _ => false,
484                };
485                if set {
486                    cur_g = (glue_set * cur_glue).clamp(-1e9, 1e9).round() as i64;
487                }
488                wd += cur_g;
489                w += wd;
490            } else {
491                let (pw, ph, pd) = self.natural_dimensions(p, self.nodes.next(p));
492                w += i64::from(pw);
493                h = h.max(ph);
494                d = d.max(pd);
495            }
496            p = self.nodes.next(p);
497        }
498        (w as i32, h, d)
499    }
500
501    /// Width, height and depth of a node in a discretionary text.
502    fn simple_dimensions(&self, p: NodeRef) -> (i32, i32, i32) {
503        match self.nodes.data(p) {
504            NodeData::Glyph(_) => self.glyph_dimensions(p),
505            NodeData::Box(b) => (b.width, b.height - b.shift, b.depth + b.shift),
506            NodeData::Rule {
507                width,
508                height,
509                depth,
510                ..
511            } => (*width, *height, *depth),
512            NodeData::Kern {
513                kern,
514                expansion_factor,
515            } => (*kern + *expansion_factor, 0, 0),
516            _ => (0, 0, 0),
517        }
518    }
519
520    fn box_mut(&mut self, r: NodeRef) -> &mut BoxData {
521        match self.nodes.data_mut(r) {
522            NodeData::Box(b) => b,
523            _ => unreachable!("box_mut on a non-box node"),
524        }
525    }
526
527    /// Prints the location and the box for an over/underfull hbox message.
528    fn finish_hbox_diagnostic(&mut self, r: NodeRef) {
529        if self.ts.output_active {
530            self.printer
531                .print_str(") has occurred while \\output is active");
532        } else {
533            let pbl = self.ts.pack_begin_line;
534            if pbl != 0 {
535                if pbl > 0 {
536                    self.printer.print_str(") in paragraph at lines ");
537                } else {
538                    self.printer.print_str(") in alignment at lines ");
539                }
540                self.printer.print_int(i64::from(pbl.abs()));
541                self.printer.print_str("--");
542            } else {
543                self.printer.print_str(") detected at line ");
544            }
545            self.printer.print_int(i64::from(self.input.line()));
546        }
547        let at = (self.ts.pack_begin_line != 0).then(|| self.ts.pack_begin_line.unsigned_abs());
548        self.diag_capture_warning(at);
549        if self.diagnostics.is_some() {
550            let facts = self.hbox_facts(r);
551            if let Some(d) = self.diagnostics.as_mut().and_then(|l| l.last_mut()) {
552                d.facts = facts;
553            }
554        }
555        self.printer.print_ln();
556        self.ts.font_in_short_display = 0;
557        let l = self.nodes.list_ptr(r);
558        self.short_display(l);
559        self.printer.print_ln();
560        self.begin_diagnostic();
561        self.show_box(r);
562        self.end_diagnostic(true);
563    }
564
565    /// Packs a vlist with unlimited depth (`vpack`).
566    pub fn vpack(&mut self, p: NodeRef, spec: PackSpec) -> NodeRef {
567        self.vpackage(p, spec, retex_arith::MAX_DIMEN)
568    }
569
570    /// Packs the vlist `p` into a vbox with maximum depth `l`
571    /// (`vpackage`).
572    pub fn vpackage(&mut self, p: NodeRef, spec: PackSpec, l: i32) -> NodeRef {
573        self.last_badness = 0;
574        let r = self.new_null_box_node(false);
575        self.take_pack_dir(r);
576        let (mut w, mut d, mut x) = (0i32, 0i32, 0i32);
577        let mut tot = Totals::default();
578        let mut q = p;
579        while !q.is_null() {
580            match self.nodes.data(q) {
581                NodeData::Box(b) => {
582                    x += d + b.height;
583                    d = b.depth;
584                    if b.width + b.shift > w {
585                        w = b.width + b.shift;
586                    }
587                }
588                NodeData::Unset { b, .. } => {
589                    x += d + b.height;
590                    d = b.depth;
591                    if b.width > w {
592                        w = b.width;
593                    }
594                }
595                NodeData::Rule {
596                    width,
597                    height,
598                    depth,
599                    ..
600                } => {
601                    x += d + *height;
602                    d = *depth;
603                    if *width > w {
604                        w = *width;
605                    }
606                }
607                NodeData::Glue { spec, leader } => {
608                    x += d;
609                    d = 0;
610                    x += spec.width;
611                    tot.add(spec);
612                    let leader = *leader;
613                    if self.nodes.subtype(q) >= glue_sub::A_LEADERS && !leader.is_null() {
614                        let lw = self.nodes.width(leader);
615                        if lw > w {
616                            w = lw;
617                        }
618                    }
619                }
620                NodeData::Kern { kern, .. } => {
621                    x += d + *kern;
622                    d = 0;
623                }
624                // Observed: LuaTeX stops at a glyph in a vertical list
625                // (Lua code can give `node.vpack` one).
626                NodeData::Glyph(_) => self.confusion("vpack"),
627                _ => {}
628            }
629            q = self.nodes.next(q);
630        }
631        {
632            let b = self.box_mut(r);
633            b.width = w;
634            b.head = p;
635        }
636        if d > l {
637            x += d - l;
638            self.box_mut(r).depth = l;
639        } else {
640            self.box_mut(r).depth = d;
641        }
642        let h = match spec {
643            PackSpec::Exactly(h) => h,
644            PackSpec::Additional(h) => x + h,
645        };
646        self.box_mut(r).height = h;
647        self.ts.last_totals = (tot.stretch, tot.shrink);
648        let x = h - x;
649        let quality = self.has_callback("vpack_quality");
650        let mut incident: Option<(&str, i32)> = None;
651        let report = if x == 0 {
652            false
653        } else if x > 0 {
654            let o = tot.stretch_order();
655            let ts = tot.stretch[o as usize];
656            {
657                let b = self.box_mut(r);
658                b.glue_order = o;
659                b.glue_sign = 1;
660                if ts != 0 {
661                    b.glue_set = crate::nodes::glue_ratio(x, ts);
662                } else {
663                    b.glue_sign = 0;
664                    b.glue_set = 0.0;
665                }
666            }
667            if o == 0 && !p.is_null() {
668                self.last_badness = retex_arith::badness(x, tot.stretch[0]);
669                if self.last_badness > self.eqtb.int(ip::VBADNESS) && quality {
670                    let kind = if self.last_badness > 100 {
671                        "underfull"
672                    } else {
673                        "loose"
674                    };
675                    incident = Some((kind, self.last_badness));
676                    false
677                } else if self.last_badness > self.eqtb.int(ip::VBADNESS) {
678                    self.printer.print_ln();
679                    if self.last_badness > 100 {
680                        self.diag_capture_start();
681                        self.printer.print_nl("Underfull");
682                    } else {
683                        self.printer.print_nl("Loose");
684                    }
685                    self.printer.print_str(" \\vbox (badness ");
686                    self.printer.print_int(i64::from(self.last_badness));
687                    true
688                } else {
689                    false
690                }
691            } else {
692                false
693            }
694        } else {
695            let o = tot.shrink_order();
696            let ts = tot.shrink[o as usize];
697            {
698                let b = self.box_mut(r);
699                b.glue_order = o;
700                b.glue_sign = 2;
701                if ts != 0 {
702                    b.glue_set = crate::nodes::glue_ratio(-x, ts);
703                } else {
704                    b.glue_sign = 0;
705                    b.glue_set = 0.0;
706                }
707            }
708            if ts < -x && o == 0 && !p.is_null() {
709                self.last_badness = 1_000_000;
710                self.box_mut(r).glue_set = 1.0;
711                let excess = -x - tot.shrink[0];
712                if (excess > self.eqtb.dimen(dp::VFUZZ) || self.eqtb.int(ip::VBADNESS) < 100)
713                    && quality
714                {
715                    incident = Some(("overfull", excess));
716                    false
717                } else if excess > self.eqtb.dimen(dp::VFUZZ) || self.eqtb.int(ip::VBADNESS) < 100 {
718                    self.printer.print_ln();
719                    self.diag_capture_start();
720                    self.printer.print_nl("Overfull \\vbox (");
721                    self.printer.print_scaled(excess);
722                    self.printer.print_str("pt too high");
723                    true
724                } else {
725                    false
726                }
727            } else if o == 0 && !p.is_null() {
728                self.last_badness = retex_arith::badness(-x, tot.shrink[0]);
729                if self.last_badness > self.eqtb.int(ip::VBADNESS) && quality {
730                    incident = Some(("tight", self.last_badness));
731                    false
732                } else if self.last_badness > self.eqtb.int(ip::VBADNESS) {
733                    self.printer.print_ln();
734                    self.diag_capture_start();
735                    self.printer.print_nl("Tight \\vbox (badness ");
736                    self.printer.print_int(i64::from(self.last_badness));
737                    true
738                } else {
739                    false
740                }
741            } else {
742                false
743            }
744        };
745        if let Some((kind, detail)) = incident {
746            // The callback's return value is not used (observed).
747            self.call_pack_quality("vpack_quality", kind, detail, r);
748        }
749        if report {
750            if self.ts.output_active {
751                self.printer
752                    .print_str(") has occurred while \\output is active");
753            } else {
754                let pbl = self.ts.pack_begin_line;
755                if pbl != 0 {
756                    self.printer.print_str(") in alignment at lines ");
757                    self.printer.print_int(i64::from(pbl.abs()));
758                    self.printer.print_str("--");
759                } else {
760                    self.printer.print_str(") detected at line ");
761                }
762                self.printer.print_int(i64::from(self.input.line()));
763            }
764            let at = (self.ts.pack_begin_line != 0).then(|| self.ts.pack_begin_line.unsigned_abs());
765            self.diag_capture_warning(at);
766            // An underfull page: explain why (see `pagenote`).
767            if self.ts.output_active
768                && self.last_badness > 100
769                && self.last_badness < 1_000_000
770                && let Some((message, notes, help)) = self.underfull_page_note(self.last_badness)
771                && let Some(d) = self.diagnostics.as_mut().and_then(|l| l.last_mut())
772                && d.message.starts_with("Underfull \\vbox")
773            {
774                d.message = message;
775                d.notes = notes;
776                d.help = help;
777            }
778            if !self.ts.output_active {
779                self.printer.print_ln();
780            }
781            self.begin_diagnostic();
782            self.show_box(r);
783            self.end_diagnostic(true);
784        }
785        r
786    }
787}
788
789/// The running dimension of rules (`null_flag`).
790const NULL_RULE: i32 = crate::typeset::NULL_FLAG;