Skip to main content

long_screen/engine/
overscroll.rs

1//! The render pass's side of elastic overscroll (docs/ARCHITECTURE.md §十 "合成与瞬态像素"): the page edges of every
2//! moving canvas, worked out once from all stored plans before the first frame is painted, and per placement the
3//! lines past those edges that the compositor must leave out.
4//!
5//! A frame whose pose runs past an edge shows the window's background in the strip beyond it. That strip becomes
6//! extra occlusion rectangles on the placement: the compositor, the temporal records and reveal already leave
7//! occluded pixels out, and the observation row carries them to reveal. The canvas bounds leave the strip out too.
8
9use crate::engine::render::{FramePlan, PlanPlacement};
10use crate::kernels::geometry::{js_round, Rect};
11use crate::kernels::overscroll::{background_lines, page_edges, Edges, Lines, Pose, SIDES};
12use std::collections::HashMap;
13
14/// Lines next to the edge a strip may still show something other than background in: an edge read from the motion
15/// can sit a pixel or two outside where the background really ends (the spring stopped short).
16const SLACK: i64 = 3;
17
18/// A placement resolved to its canvas and pose.
19pub struct Resolved<'a> {
20    pub canvas: &'a str,
21    pub x: f64,
22    pub y: f64,
23}
24
25#[derive(Default)]
26pub struct Overscroll {
27    /// Painted moving-region placements of every stored plan, kept until the edges are worked out.
28    plans: Vec<(u32, PlanPlacement)>,
29    /// Page edges by canvas.
30    edges: Option<HashMap<String, Edges>>,
31    /// The lines last left out per layer, with the canvas and pose they were measured at.
32    last: HashMap<String, (String, f64, f64, [i64; 4])>,
33}
34
35impl Overscroll {
36    /// Keeps a stored plan's placements of moving regions (`moving` says which layers are).
37    pub fn add_plan(&mut self, index: u32, plan: &FramePlan, moving: impl Fn(&str) -> bool) {
38        if self.edges.is_some() {
39            return;
40        }
41        for p in &plan.placements {
42            if p.skip != Some(true) && moving(&p.layer) {
43                self.plans.push((index, p.clone()));
44            }
45        }
46    }
47
48    /// Works the edges out on first use. `resolve` gives a placement's final canvas and pose (`None` when it cannot
49    /// be placed), `extent` its layer's region size.
50    pub fn prepare<E>(
51        &mut self,
52        mut resolve: impl FnMut(&PlanPlacement, u32) -> Result<Option<(String, f64, f64)>, E>,
53        extent: impl Fn(&str) -> Option<(i64, i64)>,
54    ) -> Result<(), E> {
55        if self.edges.is_some() {
56            return Ok(());
57        }
58        let mut tracks: HashMap<(String, String), Vec<Pose>> = HashMap::new();
59        for (frame, p) in &self.plans {
60            if let Some((canvas, x, y)) = resolve(p, *frame)? {
61                tracks
62                    .entry((canvas, p.layer.clone()))
63                    .or_default()
64                    .push(Pose {
65                        frame: *frame,
66                        x: js_round(x) as i64,
67                        y: js_round(y) as i64,
68                    });
69            }
70        }
71        let mut edges = HashMap::new();
72        for ((canvas, layer), mut track) in tracks {
73            let Some((w, h)) = extent(&layer) else {
74                continue;
75            };
76            track.sort_by_key(|p| p.frame);
77            let found = page_edges(&track, w, h);
78            if !found.is_empty() {
79                edges.insert(canvas, found);
80            }
81        }
82        self.plans = Vec::new();
83        self.edges = Some(edges);
84        Ok(())
85    }
86
87    /// The lines of each side ([`SIDES`] order) a placement leaves out: on a side whose pose runs past the page's
88    /// edge, the background lines counted from the region's outer side, at most as many as the pose runs past it,
89    /// and only when they reach to within [`SLACK`] lines of the edge. A strip with something else in it further
90    /// out is left whole: leaving out only its outer part made test_8's bounce frames paint the input bar pinned in
91    /// the moving layer at shifted places, and the page end came out torn.
92    /// `lines` holds the frame; its `region` is the placement's region.
93    pub fn measure(&mut self, layer: &str, at: Resolved, lines: &Lines) -> [i64; 4] {
94        let mut out = [0; 4];
95        let edges = self.edges.as_ref().and_then(|e| e.get(at.canvas));
96        if let Some(edges) = edges {
97            let over = edges.overshoot(js_round(at.x) as i64, js_round(at.y) as i64);
98            for (side, (&over, out)) in SIDES.iter().zip(over.iter().zip(out.iter_mut())) {
99                if over <= 0 {
100                    continue;
101                }
102                let counted = background_lines(lines, *side, over as usize) as i64;
103                if counted + SLACK >= over {
104                    *out = counted;
105                }
106            }
107        }
108        self.last
109            .insert(layer.to_string(), (at.canvas.to_string(), at.x, at.y, out));
110        out
111    }
112
113    /// What the last measured placement of `layer` left out, when it was at this canvas and pose (a frame the
114    /// duplicate shortcut does not paint).
115    pub fn reuse(&self, layer: &str, at: Resolved) -> [i64; 4] {
116        match self.last.get(layer) {
117            Some((canvas, x, y, out)) if canvas == at.canvas && *x == at.x && *y == at.y => *out,
118            _ => [0; 4],
119        }
120    }
121}
122
123/// The region's rect in whole screen pixels: x, y, width, height.
124pub fn pixel_rect(region: Rect) -> (i64, i64, i64, i64) {
125    let (x0, y0) = (region.x.floor() as i64, region.y.floor() as i64);
126    let (x1, y1) = (
127        (region.x + region.width).ceil() as i64,
128        (region.y + region.height).ceil() as i64,
129    );
130    (x0, y0, x1 - x0, y1 - y0)
131}
132
133/// The screen rectangles the left-out lines cover.
134pub fn strips(region: Rect, out: [i64; 4]) -> Vec<Rect> {
135    let (x, y, w, h) = pixel_rect(region);
136    let [top, bottom, left, right] = out;
137    let rect = |x: i64, y: i64, w: i64, h: i64| Rect {
138        x: x as f64,
139        y: y as f64,
140        width: w as f64,
141        height: h as f64,
142    };
143    let mut rects = Vec::new();
144    if top > 0 {
145        rects.push(rect(x, y, w, top));
146    }
147    if bottom > 0 {
148        rects.push(rect(x, y + h - bottom, w, bottom));
149    }
150    if left > 0 {
151        rects.push(rect(x, y, left, h));
152    }
153    if right > 0 {
154        rects.push(rect(x + w - right, y, right, h));
155    }
156    rects
157}
158
159/// A placed rect on the canvas without the left-out lines; empty when nothing is left.
160pub fn trim(bounds: Rect, out: [i64; 4]) -> Rect {
161    let [top, bottom, left, right] = out.map(|v| v as f64);
162    let width = bounds.width - left - right;
163    let height = bounds.height - top - bottom;
164    if width <= 0.0 || height <= 0.0 {
165        return Rect {
166            x: 0.0,
167            y: 0.0,
168            width: 0.0,
169            height: 0.0,
170        };
171    }
172    Rect {
173        x: bounds.x + left,
174        y: bounds.y + top,
175        width,
176        height,
177    }
178}