Skip to main content

crabshell_ui/
region.rs

1//! [`Region`], the part of a window that takes pointer input.
2
3/// An area of a window in logical pixels, built from rectangles, rounded
4/// rectangles and ellipses joined with [`union`](Self::union),
5/// [`subtract`](Self::subtract), [`intersect`](Self::intersect) and
6/// [`xor`](Self::xor), applied in the order they are called.
7///
8/// [`PanelWindow::mask`](crate::PanelWindow::mask) takes one: pointer
9/// input outside it goes to whatever is below the window.
10///
11/// ```
12/// use crabshell_ui::Region;
13///
14/// // A 200 by 30 bar that only takes clicks at its two rounded ends.
15/// let ends = Region::rect(0, 0, 40, 30)
16///     .radius(8.0)
17///     .union(Region::rect(160, 0, 40, 30).radius(8.0));
18/// ```
19#[derive(Clone, Debug, PartialEq)]
20pub struct Region {
21  shape: Shape,
22  ops: Vec<(Op, Region)>,
23}
24
25#[derive(Clone, Debug, PartialEq)]
26enum Shape {
27  Empty,
28  /// Corner radii clockwise from the top left.
29  Rect([i32; 4], [f32; 4]),
30  Ellipse([i32; 4]),
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
34enum Op {
35  Add,
36  Subtract,
37  Intersect,
38  Xor,
39}
40
41/// One row's covered columns: sorted, disjoint, half-open `(start, end)`.
42type Spans = Vec<(i32, i32)>;
43
44impl Region {
45  /// No area at all: a window masked with it lets every click through.
46  pub fn empty() -> Region {
47    Region {
48      shape: Shape::Empty,
49      ops: Vec::new(),
50    }
51  }
52
53  /// The rectangle at `x`, `y`, relative to the window's top left.
54  pub fn rect(x: i32, y: i32, width: u32, height: u32) -> Region {
55    Region {
56      shape: Shape::Rect(bounds(x, y, width, height), [0.0; 4]),
57      ops: Vec::new(),
58    }
59  }
60
61  /// The ellipse that fills the rectangle at `x`, `y`.
62  pub fn ellipse(x: i32, y: i32, width: u32, height: u32) -> Region {
63    Region {
64      shape: Shape::Ellipse(bounds(x, y, width, height)),
65      ops: Vec::new(),
66    }
67  }
68
69  /// Rounds every corner of a [`rect`](Self::rect). It does nothing to
70  /// other shapes.
71  pub fn radius(self, radius: f32) -> Region {
72    self.radii(radius, radius, radius, radius)
73  }
74
75  /// Rounds each corner of a [`rect`](Self::rect) on its own. It does
76  /// nothing to other shapes.
77  pub fn radii(
78    mut self,
79    top_left: f32,
80    top_right: f32,
81    bottom_right: f32,
82    bottom_left: f32,
83  ) -> Region {
84    if let Shape::Rect(_, radii) = &mut self.shape {
85      *radii = [top_left, top_right, bottom_right, bottom_left];
86    }
87    self
88  }
89
90  /// Adds `other`'s area.
91  pub fn union(self, other: Region) -> Region {
92    self.then(Op::Add, other)
93  }
94
95  /// Removes `other`'s area.
96  pub fn subtract(self, other: Region) -> Region {
97    self.then(Op::Subtract, other)
98  }
99
100  /// Keeps only the area also in `other`.
101  pub fn intersect(self, other: Region) -> Region {
102    self.then(Op::Intersect, other)
103  }
104
105  /// Keeps the area in exactly one of the two.
106  pub fn xor(self, other: Region) -> Region {
107    self.then(Op::Xor, other)
108  }
109
110  fn then(mut self, op: Op, other: Region) -> Region {
111    self.ops.push((op, other));
112    self
113  }
114
115  /// The region inside a `width` by `height` window as rectangles
116  /// `[x, y, width, height]`, which don't overlap. Curves are followed to
117  /// the nearest whole pixel at each row's center.
118  pub fn rects(&self, width: u32, height: u32) -> Vec<[i32; 4]> {
119    let width = i32::try_from(width).unwrap_or(i32::MAX);
120    let height = i32::try_from(height).unwrap_or(i32::MAX);
121    let mut rects = Vec::new();
122    // Rows with the same spans as the row above grow its rectangles.
123    let mut band: Option<(i32, Spans)> = None;
124    for y in 0..=height {
125      let spans = if y < height {
126        clip(self.spans(y as f32 + 0.5), width)
127      } else {
128        Spans::new()
129      };
130      if band.as_ref().is_some_and(|(_, above)| *above == spans) {
131        continue;
132      }
133      if let Some((top, above)) = band.take() {
134        rects.extend(
135          above
136            .iter()
137            .map(|&(start, end)| [start, top, end - start, y - top]),
138        );
139      }
140      band = Some((y, spans));
141    }
142    rects
143  }
144
145  /// The columns covered on the row whose center is at `y`.
146  fn spans(&self, y: f32) -> Spans {
147    let mut spans = match self.shape {
148      Shape::Empty => Spans::new(),
149      Shape::Rect(rect, radii) => rect_spans(rect, radii, y),
150      Shape::Ellipse(rect) => ellipse_spans(rect, y),
151    };
152    for (op, other) in &self.ops {
153      let keep = match op {
154        Op::Add => |a, b| a || b,
155        Op::Subtract => |a, b: bool| a && !b,
156        Op::Intersect => |a, b| a && b,
157        Op::Xor => |a, b| a != b,
158      };
159      spans = combine(&spans, &other.spans(y), keep);
160    }
161    spans
162  }
163}
164
165/// `[left, top, right, bottom]`, saturating instead of overflowing.
166fn bounds(x: i32, y: i32, width: u32, height: u32) -> [i32; 4] {
167  let side = |side: u32| i32::try_from(side).unwrap_or(i32::MAX);
168  [
169    x,
170    y,
171    x.saturating_add(side(width)),
172    y.saturating_add(side(height)),
173  ]
174}
175
176fn rect_spans([left, top, right, bottom]: [i32; 4], radii: [f32; 4], y: f32) -> Spans {
177  if y < top as f32 || y >= bottom as f32 {
178    return Spans::new();
179  }
180  // A radius can't be more than half the shorter side.
181  let most = (right as f32 - left as f32).min(bottom as f32 - top as f32) / 2.0;
182  let [top_left, top_right, bottom_right, bottom_left] = radii.map(|r| r.clamp(0.0, most));
183  // How far a corner of radius `r` cuts into a row `dy` inside its edge.
184  let inset = |r: f32, dy: f32| {
185    if dy >= r {
186      0.0
187    } else {
188      r - (r * r - (r - dy) * (r - dy)).sqrt()
189    }
190  };
191  let (from_top, from_bottom) = (y - top as f32, bottom as f32 - y);
192  let left_inset = inset(top_left, from_top).max(inset(bottom_left, from_bottom));
193  let right_inset = inset(top_right, from_top).max(inset(bottom_right, from_bottom));
194  span(left as f32 + left_inset, right as f32 - right_inset)
195}
196
197fn ellipse_spans([left, top, right, bottom]: [i32; 4], y: f32) -> Spans {
198  let rx = (right as f32 - left as f32) / 2.0;
199  let ry = (bottom as f32 - top as f32) / 2.0;
200  if rx <= 0.0 || ry <= 0.0 {
201    return Spans::new();
202  }
203  let t = (y - (top as f32 + ry)) / ry;
204  if t.abs() >= 1.0 {
205    return Spans::new();
206  }
207  let (cx, dx) = (left as f32 + rx, rx * (1.0 - t * t).sqrt());
208  span(cx - dx, cx + dx)
209}
210
211/// The columns from `start` to `end`, rounded to whole pixels.
212fn span(start: f32, end: f32) -> Spans {
213  let (start, end) = (start.round() as i32, end.round() as i32);
214  if start < end {
215    vec![(start, end)]
216  } else {
217    Spans::new()
218  }
219}
220
221/// `spans` cut to the columns from 0 to `width`.
222fn clip(spans: Spans, width: i32) -> Spans {
223  spans
224    .into_iter()
225    .map(|(start, end)| (start.max(0), end.min(width)))
226    .filter(|(start, end)| start < end)
227    .collect()
228}
229
230/// The columns where `keep` holds for whether they are in `a` and in `b`.
231fn combine(a: &[(i32, i32)], b: &[(i32, i32)], keep: fn(bool, bool) -> bool) -> Spans {
232  let mut edges: Vec<i32> = a
233    .iter()
234    .chain(b)
235    .flat_map(|&(start, end)| [start, end])
236    .collect();
237  edges.sort_unstable();
238  edges.dedup();
239  let inside =
240    |spans: &[(i32, i32)], x: i32| spans.iter().any(|&(start, end)| start <= x && x < end);
241  let mut out = Spans::new();
242  for pair in edges.windows(2) {
243    let (start, end) = (pair[0], pair[1]);
244    if !keep(inside(a, start), inside(b, start)) {
245      continue;
246    }
247    match out.last_mut() {
248      Some(last) if last.1 == start => last.1 = end,
249      _ => out.push((start, end)),
250    }
251  }
252  out
253}