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
+}