rosace_widgets/tree/
drawer.rs1use std::sync::Arc;
2use rosace_core::types::Size;
3use rosace_state::Atom;
4use rosace_render::Color;
5use rosace_shader::ShaderMaterial;
6use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget};
7use super::material::{resolve_material, DrawerMaterial};
8use super::overlay::{OverlayEntry, LayerPosition, InputBehavior, FocusBehavior, ScrimConfig, push_overlay};
9
10pub struct Drawer {
18 open: Atom<bool>,
19 width: f32,
20 full_screen: bool,
21 background: Option<Color>,
22 scrim_color: Color,
23 material: Option<ShaderMaterial>,
24 panel: Arc<dyn Fn() -> BoxedWidget + Send + Sync>,
25}
26
27impl Drawer {
28 pub fn new(open: Atom<bool>, panel: impl Fn() -> BoxedWidget + Send + Sync + 'static) -> Self {
29 Self {
30 open,
31 width: 280.0,
32 full_screen: false,
33 background: None,
34 scrim_color: Color::rgba(0, 0, 0, 120),
35 material: None,
36 panel: Arc::new(panel),
37 }
38 }
39 pub fn width(mut self, w: f32) -> Self { self.width = w; self }
40
41 pub fn full_screen(mut self) -> Self { self.full_screen = true; self }
45
46 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
48
49 pub fn scrim_color(mut self, c: Color) -> Self { self.scrim_color = c; self }
52 pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
55
56 pub fn emit(&self) {
59 if !self.open.get() { return; }
60 let close = self.open.clone();
61 let panel = (self.panel)();
62 push_overlay(
63 OverlayEntry::new(LayerPosition::Fill, DrawerPanel {
64 width: self.width,
65 full_screen: self.full_screen,
66 background: self.background,
67 material: self.material.clone(),
68 panel,
69 })
70 .input(InputBehavior::Block)
71 .focus(FocusBehavior::Trap)
72 .scrim(ScrimConfig { color: self.scrim_color, on_tap: Some(Arc::new(move || close.set(false))), exclude_rect: None }),
73 );
74 }
75}
76
77struct DrawerPanel {
78 width: f32,
79 full_screen: bool,
80 background: Option<Color>,
81 material: Option<ShaderMaterial>,
82 panel: BoxedWidget,
83}
84
85impl Widget for DrawerPanel {
86 fn layout(&self, ctx: &LayoutCtx) -> Size {
87 let avail_w = super::avail_w(ctx.constraints);
94 let w = if self.full_screen { avail_w } else { self.width.min(avail_w) };
95 Size { width: w, height: super::avail_h(ctx.constraints) }
96 }
97 fn paint(&self, ctx: &mut PaintCtx) {
98 let bg = self.background.unwrap_or_else(|| ctx.tc(ctx.theme.colors.surface));
99 let r = ctx.rect;
100 let material = resolve_material::<DrawerMaterial>(&ctx.theme, self.material.as_ref());
105 match &material {
106 Some(m) => {
107 if let Some(fallback) = m.fallback {
108 ctx.fill_rect(r, fallback);
109 }
110 ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
111 }
112 None => ctx.fill_rect(r, bg),
113 }
114 self.panel.paint(&mut ctx.child(r));
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use super::super::overlay::{clear_overlays, drain_overlays};
122 use super::super::spacer::Spacer;
123 use rosace_layout::Constraints;
124
125 fn emit_one(drawer: &Drawer) -> OverlayEntry {
126 clear_overlays();
127 drawer.emit();
128 let mut entries = drain_overlays();
129 assert_eq!(entries.len(), 1);
130 entries.pop().unwrap()
131 }
132
133 #[test]
134 fn instance_material_paints_a_shader_fill() {
135 let open = rosace_state::use_atom(true);
136 let m = ShaderMaterial::new(rosace_shader::PipelineId::user(0x4002), vec![0u8; 16]);
137 let drawer = Drawer::new(open, || Box::new(Spacer::new(0.0))).material(m);
138 let entry = emit_one(&drawer);
139
140 let font = rosace_render::FontCache::embedded();
141 let theme = rosace_theme::built_in::dark_theme();
142 let mut recorder = rosace_render::PictureRecorder::new();
143 let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
144 let rect = rosace_core::types::Rect {
145 origin: rosace_core::types::Point { x: 0.0, y: 0.0 },
146 size: Size { width: 280.0, height: 600.0 },
147 };
148 let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
149 entry.widget.paint(&mut ctx);
150 let picture = recorder.finish();
151 assert!(picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. })));
152 }
153
154 #[test]
155 fn emit_pushes_nothing_while_closed() {
156 clear_overlays();
157 let open = rosace_state::use_atom(false);
158 Drawer::new(open, || Box::new(Spacer::new(0.0))).emit();
159 assert!(drain_overlays().is_empty());
160 }
161
162 #[test]
163 fn emit_maps_to_fill_block_trap_with_dismissable_scrim() {
164 let open = rosace_state::use_atom(true);
165 let drawer = Drawer::new(open.clone(), || Box::new(Spacer::new(0.0)));
166 let e = emit_one(&drawer);
167 assert!(matches!(e.position, LayerPosition::Fill));
168 assert_eq!(e.input, InputBehavior::Block);
169 assert_eq!(e.focus, FocusBehavior::Trap);
170 let scrim = e.scrim.expect("drawer must have a scrim");
171 let on_tap = scrim.on_tap.expect("scrim must dismiss on tap");
172 on_tap();
173 assert!(!open.get(), "scrim tap must close the drawer");
174 }
175
176 #[test]
177 fn panel_is_side_width_by_default_and_window_width_when_full_screen() {
178 let font = rosace_render::FontCache::embedded();
179 let theme = rosace_theme::built_in::dark_theme();
180 let ctx = LayoutCtx::new(Constraints::loose(800.0, 600.0), &font, &theme);
181
182 let open = rosace_state::use_atom(true);
183 let side = emit_one(&Drawer::new(open.clone(), || Box::new(Spacer::new(0.0))));
184 let size = side.widget.layout(&ctx);
185 assert_eq!((size.width, size.height), (280.0, 600.0));
186
187 let full = emit_one(&Drawer::new(open, || Box::new(Spacer::new(0.0))).full_screen());
188 let size = full.widget.layout(&ctx);
189 assert_eq!((size.width, size.height), (800.0, 600.0));
190 }
191}