justerm_core/damage.rs
1//! Damage tracking — what changed since the last reset, as line + column spans.
2//! See ADR-0003 for the model (incremental bounds, ack-gated reset).
3
4/// The damaged column span of a single line.
5///
6/// **No `#[non_exhaustive]` (#844): nothing outside this crate has a reason to build one.** No
7/// public function accepts it — the engine hands it out — and there are zero out-of-crate literal
8/// sites, so the attribute would bind nothing it does not already bind.
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub struct LineDamage {
11 pub line: usize,
12 pub left: usize,
13 pub right: usize,
14}
15
16/// A first-class scroll: rows `[top..=bottom]` shifted by `count` lines
17/// (positive = up, negative = down). The renderer moves the rows instead of
18/// redrawing them. Recorded by the engine — which executes the scroll — rather
19/// than diff-detected (ADR-0003).
20///
21/// **A reported `count` never exceeds `bottom - top + 1`** — see
22/// [`crate::Engine::scroll_delta`], which caps it. `count` is `isize` here and
23/// `i16` on the wire, so an uncapped accumulation overflowed the field and
24/// reversed the shift's direction (#661); the bound is also the point past which
25/// the value stops meaning anything, since every source row is then outside the
26/// region. The cap is applied when the op is *read*, not while it accumulates, so
27/// a region that scrolls far and returns still reports its true small net.
28///
29/// **No `#[non_exhaustive]` (#844).** 7 out-of-crate literal sites. `{top, bottom, count}` is the
30/// whole of a region shift, so nothing outside this crate can add to it.
31#[derive(Clone, Copy, PartialEq, Eq, Debug)]
32pub struct ScrollOp {
33 pub top: usize,
34 pub bottom: usize,
35 pub count: isize,
36}
37
38/// What changed since the last `reset_damage()`.
39///
40/// **Deliberately exhaustive (#843).** A consumer that ignored a new damage kind
41/// would render stale content with no error anywhere, so a new member is one the
42/// compiler must make it look at. Left exhaustive on purpose, not by omission.
43#[derive(Clone, PartialEq, Eq, Debug)]
44pub enum TermDamage {
45 /// The whole screen must be redrawn (flood / resize / alt-screen clear).
46 Full,
47 /// Only these lines changed, each carrying its damaged column span.
48 Partial(Vec<LineDamage>),
49}
50
51/// Per-line damage bounds. "Undamaged" is encoded as `left > right`, so an
52/// untouched line never reports as damaged and the first `expand` sets a real
53/// span. (Mirrors Alacritty's `LineDamageBounds`.)
54#[derive(Clone, Copy)]
55pub(crate) struct LineBounds {
56 left: usize,
57 right: usize,
58 cols: usize,
59}
60
61impl LineBounds {
62 pub(crate) fn undamaged(cols: usize) -> Self {
63 LineBounds {
64 left: cols,
65 right: 0,
66 cols,
67 }
68 }
69
70 /// A line damaged across its full width (a newly exposed scroll line).
71 pub(crate) fn fully_damaged(cols: usize) -> Self {
72 LineBounds {
73 left: 0,
74 right: cols.saturating_sub(1),
75 cols,
76 }
77 }
78
79 /// Widen the span to include columns `[left, right]`.
80 pub(crate) fn expand(&mut self, left: usize, right: usize) {
81 self.left = self.left.min(left);
82 self.right = self.right.max(right);
83 }
84
85 pub(crate) fn is_damaged(&self) -> bool {
86 self.left <= self.right
87 }
88
89 pub(crate) fn reset(&mut self) {
90 self.left = self.cols;
91 self.right = 0;
92 }
93
94 pub(crate) fn span(&self) -> (usize, usize) {
95 (self.left, self.right)
96 }
97}