1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::Color;
4use rosace_scroll::ScrollController;
5use rosace_shader::ShaderMaterial;
6use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget};
7use super::container::draw_rounded_rect_pub;
8use super::material::{resolve_material, SheetMaterial};
9use super::padding::EdgeInsets;
10use super::scroll_view::ScrollView;
11use super::spacer::Spacer;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
15enum SheetHeight {
16 Content,
19 Fixed(f32),
21 Detent(f32),
23 Full,
25}
26
27pub struct Sheet {
40 pub child: BoxedWidget,
41 pub radius: f32,
42 pub padding: EdgeInsets,
43 pub show_handle: bool,
44 height_mode: SheetHeight,
45 background: Option<Color>,
46 handle_color: Option<Color>,
47 material: Option<ShaderMaterial>,
48 scrollable: bool,
49}
50
51impl Sheet {
52 pub fn new(child: impl Widget + 'static) -> Self {
53 Self {
54 child: Box::new(child),
55 radius: 16.0,
56 padding: EdgeInsets::all(20.0),
57 show_handle: true,
58 height_mode: SheetHeight::Content,
59 background: None,
60 handle_color: None,
61 material: None,
62 scrollable: false,
63 }
64 }
65
66 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
67 pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
68 pub fn no_handle(mut self) -> Self { self.show_handle = false; self }
69
70 pub fn height(mut self, h: f32) -> Self { self.height_mode = SheetHeight::Fixed(h); self }
72
73 pub fn detent(mut self, fraction: f32) -> Self {
76 self.height_mode = SheetHeight::Detent(fraction);
77 self
78 }
79
80 pub fn full_screen(mut self) -> Self { self.height_mode = SheetHeight::Full; self }
83
84 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
86
87 pub fn handle_color(mut self, c: Color) -> Self { self.handle_color = Some(c); self }
89 pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
92
93 pub fn scrollable(self) -> Self {
104 self.scrollable_with(ScrollController::new())
105 }
106
107 pub fn scrollable_with(mut self, controller: ScrollController) -> Self {
112 let child = std::mem::replace(&mut self.child, Box::new(Spacer::new(0.0)));
113 self.child = Box::new(ScrollView::new(child).controller(controller));
114 self.scrollable = true;
115 self
116 }
117
118 fn handle_space(&self) -> f32 {
119 if self.show_handle { 16.0 } else { 0.0 }
120 }
121}
122
123impl Widget for Sheet {
124 fn layout(&self, ctx: &LayoutCtx) -> Size {
125 let width = ctx.constraints.max_width_f32();
126 let height = match self.height_mode {
127 SheetHeight::Content => {
128 let inner_c = Constraints::loose(
134 (width - self.padding.total_h()).max(0.0),
135 f32::INFINITY,
136 );
137 let child_size = self.child.layout(&ctx.with_constraints(inner_c));
138 child_size.height + self.padding.total_v() + self.handle_space()
139 }
140 SheetHeight::Fixed(h) => h,
141 SheetHeight::Detent(f) => {
142 ctx.constraints.max_height_f32() * f.clamp(0.0, 1.0)
143 }
144 SheetHeight::Full => ctx.constraints.max_height_f32(),
145 };
146 ctx.constraints.constrain(Size { width, height })
147 }
148
149 fn paint(&self, ctx: &mut PaintCtx) {
150 ctx.semantics(super::Semantics::new(rosace_core::Role::Dialog));
154 let (surface, handle_color) = {
156 let t = &ctx.theme.colors;
157 (
158 self.background.unwrap_or_else(|| ctx.tc(t.surface)),
159 self.handle_color.unwrap_or_else(|| ctx.tc(t.outline)),
160 )
161 };
162 let r = ctx.rect;
163
164 let material = resolve_material::<SheetMaterial>(&ctx.theme, self.material.as_ref());
171 let fill = match &material {
172 Some(m) => m.fallback,
173 None => Some(surface),
174 };
175 if let Some(fill) = fill {
176 draw_rounded_rect_pub(ctx, r, fill, self.radius);
177 ctx.fill_rect(Rect {
178 origin: Point { x: r.origin.x, y: r.origin.y + r.size.height - self.radius },
179 size: Size { width: r.size.width, height: self.radius },
180 }, fill);
181 }
182 if let Some(m) = &material {
183 ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
184 }
185
186 if self.show_handle {
187 let handle_w = 36.0;
188 ctx.fill_rrect(Rect {
189 origin: Point {
190 x: r.origin.x + (r.size.width - handle_w) / 2.0,
191 y: r.origin.y + 6.0,
192 },
193 size: Size { width: handle_w, height: 4.0 },
194 }, 2.0, handle_color);
195 }
196
197 let content = Rect {
198 origin: Point { x: r.origin.x, y: r.origin.y + self.handle_space() },
199 size: Size { width: r.size.width, height: r.size.height - self.handle_space() },
200 };
201 self.child.paint(&mut ctx.child(self.padding.shrink(content)));
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use super::super::spacer::Spacer;
209 use rosace_layout::Constraints;
210
211 #[test]
212 fn instance_material_paints_a_shader_fill() {
213 let font = rosace_render::FontCache::embedded();
214 let theme = rosace_theme::built_in::dark_theme();
215 let mut recorder = rosace_render::PictureRecorder::new();
216 let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
217 let rect = Rect { origin: Point { x: 0.0, y: 0.0 }, size: Size { width: 400.0, height: 300.0 } };
218 let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
219 let m = ShaderMaterial::new(rosace_shader::PipelineId::user(0x4001), vec![0u8; 16]);
220 Sheet::new(Spacer::new(0.0)).material(m).paint(&mut ctx);
221 let picture = recorder.finish();
222 assert!(picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. })));
223 }
224
225 fn ctx_800x600<'a>(
226 font: &'a rosace_render::FontCache,
227 theme: &'a rosace_theme::ThemeData,
228 ) -> LayoutCtx<'a> {
229 LayoutCtx::new(Constraints::loose(800.0, 600.0), font, theme)
230 }
231
232 #[test]
233 fn content_mode_takes_the_natural_child_height_plus_chrome() {
234 let font = rosace_render::FontCache::embedded();
235 let theme = rosace_theme::built_in::dark_theme();
236 let ctx = ctx_800x600(&font, &theme);
237 let size = Sheet::new(Spacer::gap(0.0, 100.0)).layout(&ctx);
238 assert_eq!(size.width, 800.0);
239 assert_eq!(size.height, 156.0);
241 }
242
243 #[test]
244 fn fixed_detent_and_full_screen_heights_resolve_against_the_window() {
245 let font = rosace_render::FontCache::embedded();
246 let theme = rosace_theme::built_in::dark_theme();
247 let ctx = ctx_800x600(&font, &theme);
248
249 let fixed = Sheet::new(Spacer::gap(0.0, 100.0)).height(220.0).layout(&ctx);
250 assert_eq!(fixed.height, 220.0);
251
252 let detent = Sheet::new(Spacer::gap(0.0, 100.0)).detent(0.5).layout(&ctx);
253 assert_eq!(detent.height, 300.0);
254
255 let full = Sheet::new(Spacer::gap(0.0, 100.0)).full_screen().layout(&ctx);
256 assert_eq!(full.height, 600.0);
257 }
258
259 #[test]
260 fn fixed_height_is_capped_at_the_available_height() {
261 let font = rosace_render::FontCache::embedded();
262 let theme = rosace_theme::built_in::dark_theme();
263 let ctx = ctx_800x600(&font, &theme);
264 let size = Sheet::new(Spacer::gap(0.0, 100.0)).height(10_000.0).layout(&ctx);
265 assert_eq!(size.height, 600.0);
266 }
267
268 #[test]
269 fn scrollable_without_an_explicit_height_takes_the_full_height() {
270 let font = rosace_render::FontCache::embedded();
271 let theme = rosace_theme::built_in::dark_theme();
272 let ctx = ctx_800x600(&font, &theme);
273 let size = Sheet::new(Spacer::gap(0.0, 5_000.0)).scrollable().layout(&ctx);
274 assert_eq!(size.height, 600.0, "a scrollable sheet has no natural height");
275 }
276
277 #[test]
278 fn scrollable_with_a_detent_keeps_the_detent_height() {
279 let font = rosace_render::FontCache::embedded();
280 let theme = rosace_theme::built_in::dark_theme();
281 let ctx = ctx_800x600(&font, &theme);
282 let size = Sheet::new(Spacer::gap(0.0, 5_000.0))
283 .scrollable()
284 .detent(0.5)
285 .layout(&ctx);
286 assert_eq!(size.height, 300.0);
287 }
288}