Skip to main content

blitz_dom_api/
geometry.rs

1//! `getBoundingClientRect`, the one operation in scope that reads layout.
2//!
3//! Upstream: `blitz-script/src/dom/element.rs`. See MAPPING.md.
4
5use blitz_dom::{BaseDocument, NodeId};
6
7use crate::Result;
8
9/// A viewport-space rectangle, in zoomed (device-independent) CSS pixels.
10///
11/// This crate's own type rather than `blitz_dom`'s `BoundingRect`, which
12/// `blitz-dom` does not re-export and so cannot be named from outside it.
13#[derive(Debug, Clone, Copy, PartialEq, Default)]
14pub struct Rect {
15    /// Distance from the left of the viewport.
16    pub x: f64,
17    /// Distance from the top of the viewport.
18    pub y: f64,
19    /// Border-box width.
20    pub width: f64,
21    /// Border-box height.
22    pub height: f64,
23}
24
25impl Rect {
26    /// An all-zero rectangle, which is what a node with no box reports.
27    pub const ZERO: Self = Self {
28        x: 0.0,
29        y: 0.0,
30        width: 0.0,
31        height: 0.0,
32    };
33
34    /// `rect.left`.
35    pub fn left(&self) -> f64 {
36        self.x
37    }
38
39    /// `rect.top`.
40    pub fn top(&self) -> f64 {
41        self.y
42    }
43
44    /// `rect.right`.
45    pub fn right(&self) -> f64 {
46        self.x + self.width
47    }
48
49    /// `rect.bottom`.
50    pub fn bottom(&self) -> f64 {
51        self.y + self.height
52    }
53}
54
55/// `element.getBoundingClientRect()`.
56///
57/// **Layout must already be current.** This is the one operation in the crate
58/// whose answer depends on resolved layout rather than on tree state, and it
59/// is the one place where the facade cannot carry upstream's whole behaviour:
60/// `blitz-script` calls `DomCtx::flush_layout` first, which resolves the
61/// document if script has mutated it since the last frame. That dirty flag
62/// belongs to the binding, not to an operation over a borrowed document, and a
63/// facade that resolved unconditionally would turn a cheap read into a full
64/// layout pass on every call.
65///
66/// So the contract is inverted: the caller flushes, then reads. A binding
67/// reparenting onto this keeps its `ctx.flush_layout()` line and replaces only
68/// the read below it. A caller that forgets gets the geometry from before its
69/// own mutations, silently.
70///
71/// Zeros for a node with no box, matching upstream.
72pub fn bounding_client_rect(doc: &BaseDocument, node: NodeId) -> Result<Rect> {
73    Ok(match doc.get_client_bounding_rect(node) {
74        Some(rect) => Rect {
75            x: rect.x,
76            y: rect.y,
77            width: rect.width,
78            height: rect.height,
79        },
80        None => Rect::ZERO,
81    })
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::document;
88    use crate::element;
89    use crate::node;
90    use crate::test_support::viewport_skeleton;
91
92    #[test]
93    fn bounding_client_rect_reports_the_laid_out_box() {
94        let (mut doc, _html, _head, body) = viewport_skeleton(400, 300);
95        let id = document::create_element(&mut doc, "div").unwrap();
96        element::set_attribute(&mut doc, id, "style", "width: 120px; height: 40px").unwrap();
97        node::append_child(&mut doc, body, id).unwrap();
98        doc.resolve(0.0);
99
100        let rect = bounding_client_rect(&doc, id).unwrap();
101        assert_eq!(rect.width, 120.0);
102        assert_eq!(rect.height, 40.0);
103        assert_eq!(rect.right(), rect.x + 120.0);
104        assert_eq!(rect.bottom(), rect.y + 40.0);
105    }
106
107    /// The documented contract, as a test: the read does not flush, so a
108    /// mutation made after the last resolve is not visible until the caller
109    /// resolves. This is the behaviour a binding has to compensate for.
110    #[test]
111    fn the_read_does_not_flush_layout() {
112        let (mut doc, _html, _head, body) = viewport_skeleton(400, 300);
113        let id = document::create_element(&mut doc, "div").unwrap();
114        element::set_attribute(&mut doc, id, "style", "width: 120px; height: 40px").unwrap();
115        node::append_child(&mut doc, body, id).unwrap();
116        doc.resolve(0.0);
117
118        element::set_attribute(&mut doc, id, "style", "width: 200px; height: 40px").unwrap();
119        assert_eq!(bounding_client_rect(&doc, id).unwrap().width, 120.0);
120        doc.resolve(0.0);
121        assert_eq!(bounding_client_rect(&doc, id).unwrap().width, 200.0);
122    }
123
124    #[test]
125    fn a_detached_node_reports_zeros() {
126        let (mut doc, _html, _head, _body) = viewport_skeleton(400, 300);
127        let id = document::create_element(&mut doc, "div").unwrap();
128        doc.resolve(0.0);
129        assert_eq!(bounding_client_rect(&doc, id).unwrap(), Rect::ZERO);
130    }
131}