rosace_widgets/tree/
card.rs1use rosace_core::types::Size;
2use rosace_layout::Constraints;
3use rosace_render::Color;
4use rosace_shader::ShaderMaterial;
5use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget};
6use super::padding::EdgeInsets;
7use super::container::draw_rounded_rect_pub;
8use super::material::{resolve_material, CardMaterial};
9
10pub struct Card {
14 pub background: Color,
15 pub border_color: Option<Color>,
16 pub radius: f32,
17 pub elevation: f32,
18 pub padding: EdgeInsets,
19 pub width: Option<f32>,
20 pub material: Option<ShaderMaterial>,
21 pub child: BoxedWidget,
22}
23
24impl Card {
25 pub fn new(child: impl Widget + 'static) -> Self {
26 Self {
27 background: Color::rgba(0, 0, 0, 0), border_color: Some(Color::rgba(0, 0, 0, 0)), radius: 8.0,
30 elevation: 4.0,
31 padding: EdgeInsets::all(12.0),
32 width: None,
33 material: None,
34 child: Box::new(child),
35 }
36 }
37
38 pub fn background(mut self, c: Color) -> Self { self.background = c; self }
39 pub fn border(mut self, c: Color) -> Self { self.border_color = Some(c); self }
40 pub fn no_border(mut self) -> Self { self.border_color = None; self }
41 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
42 pub fn elevation(mut self, e: f32) -> Self { self.elevation = e; self }
43 pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
44 pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
45 pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
49}
50
51impl Widget for Card {
52 fn layout(&self, ctx: &LayoutCtx) -> Size {
53 let constraints = ctx.constraints;
54 let avail_w = self.width.unwrap_or_else(|| constraints.max_width_f32());
56 let inner_c = Constraints::loose(
57 (avail_w - self.padding.total_h()).max(0.0),
58 (constraints.max_height_f32() - self.padding.total_v()).max(0.0),
59 );
60 let child_size = self.child.layout(&ctx.with_constraints(inner_c));
61 let total = self.padding.grow(child_size);
62 constraints.constrain(Size {
63 width: self.width.unwrap_or(total.width),
64 height: total.height,
65 })
66 }
67
68 fn paint(&self, ctx: &mut PaintCtx) {
69 let r = ctx.rect;
70
71 if self.elevation > 0.5 {
72 ctx.fill_shadow_rrect(r, self.radius, Color::rgba(0, 0, 0, 80), self.elevation);
73 }
74
75 let material = resolve_material::<CardMaterial>(&ctx.theme, self.material.as_ref());
76 if let Some(m) = &material {
77 if let Some(fallback) = m.fallback {
83 draw_rounded_rect_pub(ctx, r, fallback, self.radius);
84 }
85 ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
86 } else {
87 let bg = if self.background.a == 0 {
88 ctx.tc(ctx.theme.colors.surface_variant)
89 } else {
90 self.background
91 };
92 draw_rounded_rect_pub(ctx, r, bg, self.radius);
93 }
94
95 if let Some(bc) = self.border_color {
96 let bc = if bc.a == 0 { ctx.tc(ctx.theme.colors.outline) } else { bc };
97 ctx.stroke_rrect(r, self.radius, bc, 1.0);
98 }
99
100 let inner = self.padding.shrink(r);
102 self.child.paint(&mut ctx.child(inner));
103 }
104}
105
106#[cfg(test)]
107mod material_cascade_tests {
108 use super::*;
109 use rosace_shader::PipelineId;
110 use rosace_core::types::{Point, Rect, Size};
111
112 fn mat(id: u64) -> ShaderMaterial {
113 ShaderMaterial::new(PipelineId::user(0x3000 + id), vec![id as u8])
114 }
115
116 fn paint_and_check(card: Card, theme: rosace_theme::ThemeData) -> bool {
117 let font = rosace_render::FontCache::embedded();
118 let mut recorder = rosace_render::PictureRecorder::new();
119 let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
120 let rect = Rect {
121 origin: Point { x: 0.0, y: 0.0 },
122 size: Size { width: 100.0, height: 100.0 },
123 };
124 let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
125 card.paint(&mut ctx);
126 let picture = recorder.finish();
127 picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. }))
128 }
129
130 #[test]
131 fn instance_material_paints_shader_fill() {
132 let theme = rosace_theme::built_in::dark_theme();
133 assert!(paint_and_check(Card::new(super::super::spacer::Spacer::new(0.0)).material(mat(1)), theme));
134 }
135
136 #[test]
137 fn theme_material_used_when_no_instance() {
138 let theme = rosace_theme::built_in::dark_theme().with_ext(super::super::material::CardMaterial(mat(2)));
139 assert!(paint_and_check(Card::new(super::super::spacer::Spacer::new(0.0)), theme));
140 }
141
142 #[test]
143 fn no_material_renders_as_before() {
144 let theme = rosace_theme::built_in::dark_theme();
145 assert!(!paint_and_check(Card::new(super::super::spacer::Spacer::new(0.0)), theme));
146 }
147}