Skip to main content

tessera_ui/
clip.rs

1//! Shape clipping for rendered content and pointer input.
2//!
3//! ## Usage
4//!
5//! Restrict a subtree to rounded corners or an elliptical viewport.
6
7use crate::{PxPosition, PxRect};
8
9/// A shape expressed in physical pixels, relative to a node's bounds.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum ClipShape {
12    /// A rounded rectangle with top-left, top-right, bottom-right, and
13    /// bottom-left radii and curvature exponents.
14    RoundedRectangle {
15        /// Corner radii in physical pixels.
16        radii: [f32; 4],
17        /// Corner curvature exponents; two produces circular corners.
18        exponents: [f32; 4],
19    },
20    /// An ellipse inscribed in the node's bounds.
21    Ellipse,
22}
23
24/// A clipping shape in physical coordinates.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct ClipRegion {
27    /// Origin in physical pixels, including fractional placement.
28    pub position: PxPosition,
29    /// Width and height in physical pixels, including fractional sizes.
30    pub size: [f32; 2],
31    /// Shape inscribed in the bounds.
32    pub shape: ClipShape,
33}
34
35impl ClipRegion {
36    /// Returns an enclosing integer rectangle, rounded outwards.
37    pub fn bounds(self) -> PxRect {
38        PxRect::enclosing_f32(self.position, self.size)
39    }
40
41    /// Tests whether an absolute point lies inside the clipping shape.
42    pub fn contains(self, position: PxPosition) -> bool {
43        let local = position - self.position;
44        if !(local.x >= 0.0 && local.y >= 0.0 && local.x < self.size[0] && local.y < self.size[1]) {
45            return false;
46        }
47        let half = [self.size[0] * 0.5, self.size[1] * 0.5];
48        let point = [local.x - half[0], local.y - half[1]];
49        match self.shape {
50            ClipShape::Ellipse => {
51                (point[0] / half[0]).powi(2) + (point[1] / half[1]).powi(2) <= 1.0
52            }
53            ClipShape::RoundedRectangle { radii, exponents } => {
54                let corner = match (point[0] < 0.0, point[1] < 0.0) {
55                    (true, true) => 0,
56                    (false, true) => 1,
57                    (false, false) => 2,
58                    (true, false) => 3,
59                };
60                let radius = finite_radius(radii[corner], half[0].min(half[1]));
61                if radius == 0.0 {
62                    return true;
63                }
64                let exponent = finite_exponent(exponents[corner]);
65                let q = [
66                    (point[0].abs() - half[0] + radius).max(0.0) / radius,
67                    (point[1].abs() - half[1] + radius).max(0.0) / radius,
68                ];
69                q[0].powf(exponent) + q[1].powf(exponent) <= 1.0
70            }
71        }
72    }
73}
74
75pub(crate) fn finite_radius(radius: f32, max: f32) -> f32 {
76    if radius.is_finite() {
77        radius.clamp(0.0, max.max(0.0))
78    } else {
79        0.0
80    }
81}
82
83pub(crate) fn finite_exponent(exponent: f32) -> f32 {
84    if exponent.is_finite() {
85        exponent.clamp(1.0, 32.0)
86    } else {
87        2.0
88    }
89}
90
91/// Depth and stencil configuration required by UI draw pipelines.
92///
93/// Set `RenderPipelineDescriptor::depth_stencil` to this value for pipelines
94/// submitted through the UI renderer. Independent texture-generation passes
95/// may use their own attachment configuration.
96pub fn ui_stencil_state() -> Option<wgpu::DepthStencilState> {
97    Some(wgpu::DepthStencilState {
98        format: wgpu::TextureFormat::Stencil8,
99        depth_write_enabled: None,
100        depth_compare: None,
101        stencil: wgpu::StencilState {
102            front: wgpu::StencilFaceState {
103                compare: wgpu::CompareFunction::Equal,
104                ..Default::default()
105            },
106            back: wgpu::StencilFaceState {
107                compare: wgpu::CompareFunction::Equal,
108                ..Default::default()
109            },
110            read_mask: 0xff,
111            write_mask: 0,
112        },
113        bias: Default::default(),
114    })
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::Px;
120
121    use super::*;
122
123    #[test]
124    fn shape_hit_testing_matches_mask_geometry() {
125        let _runtime_session = crate::RuntimeSession::new();
126        let _runtime_scope = _runtime_session.enter();
127        let mut region = ClipRegion {
128            position: PxPosition::from_f32_xy(10.0, 20.0),
129            size: [100.0, 60.0],
130            shape: ClipShape::Ellipse,
131        };
132        assert!(region.contains(PxPosition::from_f32_xy(60.0, 50.0)));
133        assert!(!region.contains(PxPosition::from_f32_xy(11.0, 21.0)));
134        region.shape = ClipShape::RoundedRectangle {
135            radii: [20.0; 4],
136            exponents: [2.0; 4],
137        };
138        assert!(!region.contains(PxPosition::from_f32_xy(11.0, 21.0)));
139        assert!(region.contains(PxPosition::from_f32_xy(30.0, 21.0)));
140        assert!(!region.contains(PxPosition::from_f32_xy(109.0, 79.0)));
141    }
142
143    #[test]
144    fn invalid_radii_and_exponents_are_sanitized() {
145        let _runtime_session = crate::RuntimeSession::new();
146        let _runtime_scope = _runtime_session.enter();
147        assert_eq!(finite_radius(f32::NAN, 10.0), 0.0);
148        assert_eq!(finite_radius(100.0, 10.0), 10.0);
149        assert_eq!(finite_exponent(f32::INFINITY), 2.0);
150        assert_eq!(finite_exponent(-1.0), 1.0);
151    }
152
153    #[test]
154    fn fractional_masks_keep_geometry_and_round_scissors_outwards() {
155        let _runtime_session = crate::RuntimeSession::new();
156        let _runtime_scope = _runtime_session.enter();
157        let region = ClipRegion {
158            position: PxPosition::from_f32_xy(-0.25, 0.75),
159            size: [20.0, 10.0],
160            shape: ClipShape::RoundedRectangle {
161                radii: [0.0; 4],
162                exponents: [2.0; 4],
163            },
164        };
165        assert_eq!(region.bounds(), PxRect::new(Px(-1), Px(0), Px(21), Px(11)));
166        assert!(!region.contains(PxPosition::from_f32_xy(0.0, 0.5)));
167        assert!(region.contains(PxPosition::from_f32_xy(0.0, 0.8)));
168        assert!(!region.contains(PxPosition::from_f32_xy(19.8, 5.0)));
169    }
170}