Skip to main content

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](https://github.com/kihyun1998/justerm/issues/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](https://github.com/kihyun1998/justerm/blob/master/docs/adr/0003-damage-model-incremental-bounds.md)).
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; 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](https://github.com/kihyun1998/justerm/issues/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](https://github.com/kihyun1998/justerm/issues/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}