← luna

Commit 9306ce2

3
files
+624
added
-0
deleted
@@ -0,0 +1,333 @@
1 +//! Pomocniki układu i „chrome”: `ToolbarView`, `WindowTitle`, `Clamp`, `WrapBox`,
2 +//! `BreakpointBin`, `ButtonContent`.
3 +
4 +use libluna_core::style::weight;
5 +use libluna_core::{Rect, Size, Theme};
6 +use libluna_render::TextEngine;
7 +
8 +use crate::ctx::PaintCtx;
9 +
10 +/// Pasek górny + treść + pasek dolny.
11 +#[derive(Debug, Clone, Copy, Default)]
12 +pub struct ToolbarView {
13 + top: f64,
14 + bottom: f64,
15 +}
16 +
17 +/// Podział na pasek górny, treść i pasek dolny.
18 +#[derive(Debug, Clone, Copy, PartialEq)]
19 +pub struct ToolbarRects {
20 + pub top: Option<Rect>,
21 + pub content: Rect,
22 + pub bottom: Option<Rect>,
23 +}
24 +
25 +impl ToolbarView {
26 + pub fn new() -> Self {
27 + Self::default()
28 + }
29 +
30 + pub fn top(mut self, height: f64) -> Self {
31 + self.top = height;
32 + self
33 + }
34 +
35 + pub fn bottom(mut self, height: f64) -> Self {
36 + self.bottom = height;
37 + self
38 + }
39 +
40 + /// Prostokąty pasków (o ile mają dodatnią wysokość) i treści.
41 + pub fn rects(&self, rect: Rect) -> ToolbarRects {
42 + let top =
43 + (self.top > 0.0).then(|| Rect::from_xywh(rect.x(), rect.y(), rect.width(), self.top));
44 + let bottom = (self.bottom > 0.0).then(|| {
45 + Rect::from_xywh(
46 + rect.x(),
47 + rect.bottom() - self.bottom,
48 + rect.width(),
49 + self.bottom,
50 + )
51 + });
52 + let y = rect.y() + self.top;
53 + let h = (rect.height() - self.top - self.bottom).max(0.0);
54 + ToolbarRects {
55 + top,
56 + content: Rect::from_xywh(rect.x(), y, rect.width(), h),
57 + bottom,
58 + }
59 + }
60 +}
61 +
62 +/// Tytuł okna (z opcjonalnym podtytułem), wyśrodkowany.
63 +#[derive(Debug, Clone, Default)]
64 +pub struct WindowTitle {
65 + title: String,
66 + subtitle: Option<String>,
67 +}
68 +
69 +/// Prostokąty tytułu i podtytułu.
70 +#[derive(Debug, Clone, Copy, PartialEq)]
71 +pub struct WindowTitleRects {
72 + pub title: Rect,
73 + pub subtitle: Option<Rect>,
74 +}
75 +
76 +impl WindowTitle {
77 + pub fn new(title: impl Into<String>) -> Self {
78 + Self {
79 + title: title.into(),
80 + subtitle: None,
81 + }
82 + }
83 +
84 + pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
85 + self.subtitle = Some(subtitle.into());
86 + self
87 + }
88 +
89 + pub fn rects(&self, rect: Rect, theme: &Theme) -> WindowTitleRects {
90 + let cx = rect.center().x;
91 + let half = rect.width().min(360.0) / 2.0;
92 + let body = theme.typography.body.line_height;
93 + let caption = theme.typography.caption.line_height;
94 + let (title, subtitle) = match self.subtitle.is_some() {
95 + true => (
96 + Rect::from_xywh(cx - half, rect.center().y - body, half * 2.0, body),
97 + Some(Rect::from_xywh(
98 + cx - half,
99 + rect.center().y,
100 + half * 2.0,
101 + caption,
102 + )),
103 + ),
104 + false => (
105 + Rect::from_xywh(cx - half, rect.center().y - body / 2.0, half * 2.0, body),
106 + None,
107 + ),
108 + };
109 + WindowTitleRects { title, subtitle }
110 + }
111 +
112 + pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
113 + let theme = *ctx.theme;
114 + let r = self.rects(rect, &theme);
115 + ctx.label_center(
116 + r.title.center().x,
117 + r.title.center().y,
118 + &self.title,
119 + theme.typography.body.size,
120 + weight::BOLD,
121 + theme.colors.text,
122 + );
123 + if let (Some(subtitle), Some(sr)) = (&self.subtitle, r.subtitle) {
124 + ctx.label_center(
125 + sr.center().x,
126 + sr.center().y,
127 + subtitle,
128 + theme.typography.caption.size,
129 + weight::NORMAL,
130 + theme.colors.dim_text,
131 + );
132 + }
133 + }
134 +}
135 +
136 +/// Ograniczenie szerokości treści (wyśrodkowane).
137 +#[derive(Debug, Clone, Copy)]
138 +pub struct Clamp {
139 + max_width: f64,
140 +}
141 +
142 +impl Default for Clamp {
143 + fn default() -> Self {
144 + Self { max_width: 600.0 }
145 + }
146 +}
147 +
148 +impl Clamp {
149 + pub fn new(max_width: f64) -> Self {
150 + Self { max_width }
151 + }
152 +
153 + /// Prostokąt treści: pełna wysokość, szerokość ograniczona i wyśrodkowana.
154 + pub fn content_rect(&self, rect: Rect) -> Rect {
155 + let w = rect.width().min(self.max_width);
156 + Rect::from_xywh(
157 + rect.x() + (rect.width() - w) / 2.0,
158 + rect.y(),
159 + w,
160 + rect.height(),
161 + )
162 + }
163 +}
164 +
165 +/// Zawijanie elementów w kolejne linie (flow).
166 +#[derive(Debug, Clone, Copy, Default)]
167 +pub struct WrapBox {
168 + spacing: f64,
169 + line_spacing: f64,
170 +}
171 +
172 +impl WrapBox {
173 + pub fn new() -> Self {
174 + Self::default()
175 + }
176 +
177 + pub fn spacing(mut self, spacing: f64) -> Self {
178 + self.spacing = spacing;
179 + self
180 + }
181 +
182 + pub fn line_spacing(mut self, spacing: f64) -> Self {
183 + self.line_spacing = spacing;
184 + self
185 + }
186 +
187 + /// Rozmieszcza elementy o zadanych naturalnych rozmiarach, zawijając do szerokości `width`.
188 + pub fn layout(&self, rect: Rect, sizes: &[Size]) -> Vec<Rect> {
189 + let mut out = Vec::with_capacity(sizes.len());
190 + let mut x = rect.x();
191 + let mut y = rect.y();
192 + let mut line_h = 0.0f64;
193 + for &size in sizes {
194 + if x > rect.x() && x + size.width > rect.right() {
195 + x = rect.x();
196 + y += line_h + self.line_spacing;
197 + line_h = 0.0;
198 + }
199 + out.push(Rect::from_xywh(x, y, size.width, size.height));
200 + x += size.width + self.spacing;
201 + line_h = line_h.max(size.height);
202 + }
203 + out
204 + }
205 +
206 + /// Wysokość potrzebna na zawinięcie elementów w szerokości `width`.
207 + pub fn height(&self, width: f64, sizes: &[Size]) -> f64 {
208 + let mut x = 0.0;
209 + let mut y = 0.0;
210 + let mut line_h = 0.0f64;
211 + for &size in sizes {
212 + if x > 0.0 && x + size.width > width {
213 + x = 0.0;
214 + y += line_h + self.line_spacing;
215 + line_h = 0.0;
216 + }
217 + x += size.width + self.spacing;
218 + line_h = line_h.max(size.height);
219 + }
220 + y + line_h
221 + }
222 +}
223 +
224 +/// Punkt przełamania (breakpoint) — responsywny układ wg szerokości.
225 +#[derive(Debug, Clone, Default)]
226 +pub struct BreakpointBin {
227 + breakpoints: Vec<f64>,
228 +}
229 +
230 +impl BreakpointBin {
231 + pub fn new(breakpoints: Vec<f64>) -> Self {
232 + let mut breakpoints = breakpoints;
233 + breakpoints.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
234 + Self { breakpoints }
235 + }
236 +
237 + pub fn breakpoints(&self) -> &[f64] {
238 + &self.breakpoints
239 + }
240 +
241 + /// Indeks aktywnego progu (liczba progów <= szerokości).
242 + pub fn active(&self, width: f64) -> usize {
243 + self.breakpoints.iter().filter(|b| width >= **b).count()
244 + }
245 +}
246 +
247 +/// Układ zawartości przycisku: ikona (opcjonalna) + etykieta, wyśrodkowane.
248 +#[derive(Debug, Clone, Copy, Default)]
249 +pub struct ButtonContent {
250 + icon: bool,
251 +}
252 +
253 +impl ButtonContent {
254 + pub fn label() -> Self {
255 + Self { icon: false }
256 + }
257 +
258 + pub fn with_icon() -> Self {
259 + Self { icon: true }
260 + }
261 +
262 + /// Zwraca `(opcjonalny prostokąt ikony, prostokąt etykiety)`.
263 + pub fn layout(
264 + &self,
265 + rect: Rect,
266 + theme: &Theme,
267 + text: &TextEngine,
268 + label: &str,
269 + ) -> (Option<Rect>, Rect) {
270 + let body = theme.typography.body.size;
271 + let label_w = text.measure(label, body, weight::NORMAL).width;
272 + let icon = theme.metrics.entry.icon;
273 + let gap = if self.icon { theme.spacing.sm } else { 0.0 };
274 + let icon_w = if self.icon { icon + gap } else { 0.0 };
275 + let total = icon_w + label_w;
276 + let start = rect.center().x - total / 2.0;
277 + let icon_rect = self
278 + .icon
279 + .then(|| Rect::from_xywh(start, rect.center().y - icon / 2.0, icon, icon));
280 + let label_rect = Rect::from_xywh(start + icon_w, rect.y(), label_w, rect.height());
281 + (icon_rect, label_rect)
282 + }
283 +}
284 +
285 +#[cfg(test)]
286 +mod tests {
287 + use super::*;
288 +
289 + fn rect() -> Rect {
290 + Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
291 + }
292 +
293 + #[test]
294 + fn toolbar_partitions() {
295 + let tv = ToolbarView::new().top(46.0).bottom(28.0);
296 + let r = tv.rects(rect());
297 + assert_eq!(r.top.unwrap().height(), 46.0);
298 + assert_eq!(r.bottom.unwrap().height(), 28.0);
299 + assert_eq!(r.content.y(), 46.0);
300 + assert_eq!(r.content.height(), 600.0 - 46.0 - 28.0);
301 + }
302 +
303 + #[test]
304 + fn clamp_centers_and_limits() {
305 + let clamp = Clamp::new(400.0);
306 + let r = clamp.content_rect(rect());
307 + assert_eq!(r.width(), 400.0);
308 + assert_eq!(r.x(), 200.0);
309 + let small = clamp.content_rect(Rect::from_xywh(0.0, 0.0, 300.0, 100.0));
310 + assert_eq!(small.width(), 300.0);
311 + }
312 +
313 + #[test]
314 + fn wrap_box_wraps_and_measures() {
315 + let wrap = WrapBox::new().spacing(10.0).line_spacing(5.0);
316 + let sizes = [Size::new(120.0, 30.0); 5];
317 + let rects = wrap.layout(rect(), &sizes);
318 + assert_eq!(rects.len(), 5);
319 + // Zmieściło się 6 na linię w 800 px? (5*120 + 4*10 = 640 < 800) → jedna linia.
320 + assert_eq!(rects[0].y(), rects[4].y());
321 + let h = wrap.height(400.0, &sizes);
322 + // 400 px: 3 na linię (3*120+2*10=380), więc 2 linie.
323 + assert!((h - (30.0 + 5.0 + 30.0)).abs() < 1e-9);
324 + }
325 +
326 + #[test]
327 + fn breakpoint_active() {
328 + let bp = BreakpointBin::new(vec![400.0, 700.0]);
329 + assert_eq!(bp.active(300.0), 0);
330 + assert_eq!(bp.active(500.0), 1);
331 + assert_eq!(bp.active(900.0), 2);
332 + }
333 +}
@@ -42,6 +42,9 @@
42 42 //! - [`Row`] — wiersz listy (switch/combo/expander/entry/spin…) — rodzina „boxed list”.
43 43 //! - [`PreferencesGroup`] / [`PreferencesPage`] — grupa i strona ustawień.
44 44 //! - [`Dialog`] (+ [`AlertDialog`], [`AboutDialog`], [`ShortcutsDialog`], [`PreferencesDialog`]) — okna modalne.
45 +//! - [`NavigationView`] / [`NavigationSplitView`] / [`OverlaySplitView`] / [`Sidebar`] — nawigacja.
46 +//! - [`Carousel`] (alias [`Pager`]), [`ToolbarView`], [`WindowTitle`], [`Clamp`], [`WrapBox`],
47 +//! [`BreakpointBin`], [`ButtonContent`] — pomocniki.
45 48 //!
46 49 //! Układ (retencyjne drzewo, `measure`/`allocate`): [`layout`] ([`Node`], [`Layout`], [`Constraints`]).
47 50 //!
@@ -66,11 +69,13 @@ pub mod dialogs;
66 69 pub mod dropdown;
67 70 pub mod entry;
68 71 pub mod header_bar;
72 +pub mod helpers;
69 73 pub mod icon;
70 74 pub mod item_view;
71 75 pub mod layout;
72 76 pub mod menu;
73 77 pub mod nav_list;
78 +pub mod navigation;
74 79 pub mod pager;
75 80 pub mod path_bar;
76 81 pub mod preferences;
@@ -107,11 +112,18 @@ pub use dialogs::{
107 112 pub use dropdown::DropDown;
108 113 pub use entry::{Entry, EntryIcon, TextInput};
109 114 pub use header_bar::{ControlsLayout, ControlsSide, HeaderBar, TitleAlign, WindowControl};
115 +pub use helpers::{
116 + BreakpointBin, ButtonContent, Clamp, ToolbarRects, ToolbarView, WindowTitle, WindowTitleRects,
117 + WrapBox,
118 +};
110 119 pub use item_view::{Item, ItemKind, ItemView, ViewMode};
111 120 pub use layout::{Constraints, Layout, LayoutCtx, Node, Stack};
112 121 pub use menu::{Menu, MenuEntry, MenuRow};
113 122 pub use nav_list::{NavEntry, NavList, NavRow};
123 +pub use navigation::{NavigationSplitView, NavigationView, OverlaySplitView, Sidebar};
114 124 pub use pager::Pager;
125 +/// `Carousel` — nazwa [`Pager`] w duchu libadwaita.
126 +pub use pager::Pager as Carousel;
115 127 pub use path_bar::PathBar;
116 128 pub use preferences::{PreferencesGroup, PreferencesPage};
117 129 pub use radio_group::RadioGroup;
118 130 new file mode 100644
@@ -0,0 +1,279 @@
1 +//! `NavigationView` / `NavigationSplitView` / `OverlaySplitView` / `Sidebar` — nawigacja.
2 +//!
3 +//! Pomocniki układu i „chrome” nawigacji (jak `AdwNavigationView` i widoki dzielone):
4 +//! wyznaczają prostokąty paneli i rysują pasek z przyciskiem „wstecz”.
5 +
6 +use libluna_core::{Insets, Point, Rect, Theme};
7 +
8 +use crate::ctx::PaintCtx;
9 +use crate::icon;
10 +
11 +/// Pasek nawigacji: „wstecz” + obszar tytułu + treść strony.
12 +#[derive(Debug, Clone, Default)]
13 +pub struct NavigationView {
14 + depth: usize,
15 +}
16 +
17 +impl NavigationView {
18 + pub fn new() -> Self {
19 + Self::default()
20 + }
21 +
22 + /// Głębokość stosu (0 = strona główna).
23 + pub fn depth(&self) -> usize {
24 + self.depth
25 + }
26 +
27 + pub fn can_pop(&self) -> bool {
28 + self.depth > 0
29 + }
30 +
31 + pub fn push(&mut self) {
32 + self.depth += 1;
33 + }
34 +
35 + /// Zdejmuje stronę; zwraca `true`, jeśli coś zdjęto.
36 + pub fn pop(&mut self) -> bool {
37 + if self.depth > 0 {
38 + self.depth -= 1;
39 + true
40 + } else {
41 + false
42 + }
43 + }
44 +
45 + fn bar_height(theme: &Theme) -> f64 {
46 + theme.metrics.headerbar.height
47 + }
48 +
49 + /// Pasek nawigacji (nad treścią strony).
50 + pub fn bar_rect(&self, rect: Rect, theme: &Theme) -> Rect {
51 + Rect::from_xywh(rect.x(), rect.y(), rect.width(), Self::bar_height(theme))
52 + }
53 +
54 + /// Prostokąt treści strony (poniżej paska).
55 + pub fn content_rect(&self, rect: Rect, theme: &Theme) -> Rect {
56 + let h = Self::bar_height(theme);
57 + Rect::from_xywh(
58 + rect.x(),
59 + rect.y() + h,
60 + rect.width(),
61 + (rect.height() - h).max(0.0),
62 + )
63 + }
64 +
65 + /// Prostokąt przycisku „wstecz” (gdy jest co zdjąć).
66 + pub fn back_rect(&self, rect: Rect, theme: &Theme) -> Option<Rect> {
67 + if !self.can_pop() {
68 + return None;
69 + }
70 + let bar = self.bar_rect(rect, theme);
71 + let control = theme.metrics.headerbar.control;
72 + Some(Rect::from_xywh(
73 + rect.x() + theme.spacing.md,
74 + bar.center().y - control / 2.0,
75 + control,
76 + control,
77 + ))
78 + }
79 +
80 + pub fn back_at(&self, rect: Rect, theme: &Theme, p: Point) -> bool {
81 + self.back_rect(rect, theme).is_some_and(|r| r.contains(p))
82 + }
83 +
84 + /// Rysuje pasek nawigacji (tło + „wstecz”). Tytuł rysuje aplikacja (np. `WindowTitle`).
85 + pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
86 + let theme = *ctx.theme;
87 + let bar = self.bar_rect(rect, &theme);
88 + ctx.scene.fill_rect(bar, theme.colors.headerbar_bg);
89 + ctx.scene.fill_rect(
90 + Rect::from_xywh(bar.x(), bar.bottom() - 1.0, bar.width(), 1.0),
91 + theme.colors.border,
92 + );
93 + if let Some(b) = self.back_rect(rect, &theme) {
94 + icon::chevron_left(ctx, theme.colors.text, b.center(), 4.0, 2.0);
95 + }
96 + }
97 +}
98 +
99 +/// Widok dzielony: panel boczny + treść (panel można schować).
100 +#[derive(Debug, Clone)]
101 +pub struct NavigationSplitView {
102 + show_sidebar: bool,
103 + sidebar_width: f64,
104 +}
105 +
106 +impl Default for NavigationSplitView {
107 + fn default() -> Self {
108 + Self {
109 + show_sidebar: true,
110 + sidebar_width: 220.0,
111 + }
112 + }
113 +}
114 +
115 +impl NavigationSplitView {
116 + pub fn new() -> Self {
117 + Self::default()
118 + }
119 +
120 + pub fn sidebar_width(mut self, width: f64) -> Self {
121 + self.sidebar_width = width;
122 + self
123 + }
124 +
125 + pub fn show_sidebar(&self) -> bool {
126 + self.show_sidebar
127 + }
128 +
129 + pub fn set_show_sidebar(&mut self, show: bool) {
130 + self.show_sidebar = show;
131 + }
132 +
133 + pub fn toggle(&mut self) {
134 + self.show_sidebar = !self.show_sidebar;
135 + }
136 +
137 + /// Prostokąt panelu bocznego (gdy widoczny).
138 + pub fn sidebar_rect(&self, rect: Rect) -> Option<Rect> {
139 + self.show_sidebar
140 + .then(|| Rect::from_xywh(rect.x(), rect.y(), self.sidebar_width, rect.height()))
141 + }
142 +
143 + /// Prostokąt treści (obok panelu, albo cały gdy schowany).
144 + pub fn content_rect(&self, rect: Rect) -> Rect {
145 + let x = if self.show_sidebar {
146 + self.sidebar_width
147 + } else {
148 + 0.0
149 + };
150 + Rect::from_xywh(
151 + rect.x() + x,
152 + rect.y(),
153 + (rect.width() - x).max(0.0),
154 + rect.height(),
155 + )
156 + }
157 +}
158 +
159 +/// Widok dzielony z panelem **nakładanym** (na wąskich oknach).
160 +#[derive(Debug, Clone, Default)]
161 +pub struct OverlaySplitView {
162 + open: bool,
163 + sidebar_width: f64,
164 +}
165 +
166 +impl OverlaySplitView {
167 + pub fn new() -> Self {
168 + Self {
169 + open: false,
170 + sidebar_width: 220.0,
171 + }
172 + }
173 +
174 + pub fn sidebar_width(mut self, width: f64) -> Self {
175 + self.sidebar_width = width;
176 + self
177 + }
178 +
179 + pub fn is_open(&self) -> bool {
180 + self.open
181 + }
182 +
183 + pub fn set_open(&mut self, open: bool) {
184 + self.open = open;
185 + }
186 +
187 + pub fn toggle(&mut self) {
188 + self.open = !self.open;
189 + }
190 +
191 + /// Prostokąt panelu (przyklejony do lewej).
192 + pub fn sidebar_rect(&self, rect: Rect) -> Rect {
193 + Rect::from_xywh(rect.x(), rect.y(), self.sidebar_width, rect.height())
194 + }
195 +
196 + /// Czy punkt trafia w tło poza panelem (klik zamykający; tylko gdy otwarty).
197 + pub fn scrim_at(&self, rect: Rect, p: Point) -> bool {
198 + self.open && !self.sidebar_rect(rect).contains(p)
199 + }
200 +
201 + pub fn paint_scrim(&self, ctx: &mut PaintCtx, rect: Rect) {
202 + if self.open {
203 + let theme = *ctx.theme;
204 + use libluna_core::Color;
205 + ctx.scene.fill_rect(
206 + rect,
207 + Color::rgba(0.0, 0.0, 0.0, theme.metrics.sheet.scrim as f32),
208 + );
209 + }
210 + }
211 +}
212 +
213 +/// Panel boczny: tło + krawędź, obszar zawartości na listę ([`NavList`](crate::NavList)).
214 +#[derive(Debug, Clone, Default)]
215 +pub struct Sidebar;
216 +
217 +impl Sidebar {
218 + pub fn new() -> Self {
219 + Self
220 + }
221 +
222 + /// Prostokąt zawartości (bez wewnętrznych wcięć).
223 + pub fn content_rect(&self, rect: Rect, theme: &Theme) -> Rect {
224 + rect.deflate(Insets::symmetric(theme.spacing.sm, 0.0))
225 + }
226 +
227 + /// Rysuje tło panelu i krawędź po prawej.
228 + pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
229 + let theme = *ctx.theme;
230 + ctx.scene.fill_rect(rect, theme.colors.sidebar_bg);
231 + ctx.scene.fill_rect(
232 + Rect::from_xywh(rect.right() - 1.0, rect.y(), 1.0, rect.height()),
233 + theme.colors.border,
234 + );
235 + }
236 +}
237 +
238 +#[cfg(test)]
239 +mod tests {
240 + use super::*;
241 +
242 + fn rect() -> Rect {
243 + Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
244 + }
245 +
246 + #[test]
247 + fn navigation_view_back_only_after_push() {
248 + let theme = Theme::light();
249 + let mut nav = NavigationView::new();
250 + assert!(nav.back_rect(rect(), &theme).is_none());
251 + assert!(!nav.can_pop());
252 + nav.push();
253 + assert!(nav.can_pop());
254 + let back = nav.back_rect(rect(), &theme).unwrap();
255 + assert!(nav.back_at(rect(), &theme, back.center()));
256 + assert!(nav.pop());
257 + assert!(!nav.pop());
258 + }
259 +
260 + #[test]
261 + fn split_view_content_and_sidebar() {
262 + let v = NavigationSplitView::new().sidebar_width(200.0);
263 + assert_eq!(v.sidebar_rect(rect()).unwrap().width(), 200.0);
264 + assert_eq!(v.content_rect(rect()).x(), 200.0);
265 + let mut v = v;
266 + v.set_show_sidebar(false);
267 + assert!(v.sidebar_rect(rect()).is_none());
268 + assert_eq!(v.content_rect(rect()), rect());
269 + }
270 +
271 + #[test]
272 + fn overlay_scrim_closes_only_when_open() {
273 + let mut o = OverlaySplitView::new().sidebar_width(200.0);
274 + assert!(!o.scrim_at(rect(), Point::new(500.0, 300.0)));
275 + o.set_open(true);
276 + assert!(o.scrim_at(rect(), Point::new(500.0, 300.0)));
277 + assert!(!o.scrim_at(rect(), Point::new(50.0, 300.0)));
278 + }
279 +}