1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::{Color, DrawCommand};
4use rosace_shader::ShaderMaterial;
5use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w, avail_h};
6use super::padding::EdgeInsets;
7use super::material::{resolve_material, ContainerMaterial};
8
9#[derive(Clone, Copy, Debug, PartialEq, Default)]
11pub enum BoxShape {
12 #[default]
14 Rect,
15 Circle,
17 Stadium,
19}
20
21pub struct Container {
28 pub background: Option<Color>,
29 pub gradient: Option<(Color, Color, bool)>, pub border_color: Option<Color>,
31 pub border_width: f32,
32 pub border_radius: f32,
33 pub shape: BoxShape,
34 pub shadow_blur: f32,
35 pub shadow_color: Color,
36 pub padding: EdgeInsets,
37 pub margin: EdgeInsets,
38 pub width: Option<f32>,
39 pub height: Option<f32>,
40 pub min_width: f32,
41 pub min_height: f32,
42 pub clip: bool,
43 pub align: Option<super::Alignment>,
44 pub material: Option<ShaderMaterial>,
45 pub child: Option<BoxedWidget>,
46}
47
48impl Container {
49 pub fn new() -> Self {
50 Self {
51 background: None,
52 gradient: None,
53 border_color: None,
54 border_width: 1.0,
55 border_radius: 0.0,
56 shape: BoxShape::Rect,
57 shadow_blur: 0.0,
58 shadow_color: Color::rgba(0, 0, 0, 0),
59 padding: EdgeInsets::default(),
60 margin: EdgeInsets::default(),
61 width: None,
62 height: None,
63 min_width: 0.0,
64 min_height: 0.0,
65 clip: false,
66 align: None,
67 material: None,
68 child: None,
69 }
70 }
71
72 fn radius_for(&self, size: Size) -> f32 {
74 match self.shape {
75 BoxShape::Rect => self.border_radius,
76 BoxShape::Circle => size.width.min(size.height) / 2.0,
77 BoxShape::Stadium => size.height / 2.0,
78 }
79 }
80
81 pub fn align(mut self, a: super::Alignment) -> Self { self.align = Some(a); self }
82 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
83 pub fn gradient(mut self, from: Color, to: Color) -> Self { self.gradient = Some((from, to, true)); self }
85 pub fn gradient_h(mut self, from: Color, to: Color) -> Self { self.gradient = Some((from, to, false)); self }
86 pub fn border(mut self, c: Color, w: f32) -> Self { self.border_color = Some(c); self.border_width = w; self }
87 pub fn radius(mut self, r: f32) -> Self { self.border_radius = r; self }
88 pub fn shape(mut self, s: BoxShape) -> Self { self.shape = s; self }
89 pub fn circle(mut self) -> Self { self.shape = BoxShape::Circle; self }
90 pub fn stadium(mut self) -> Self { self.shape = BoxShape::Stadium; self }
91 pub fn shadow(mut self, color: Color, blur: f32) -> Self { self.shadow_color = color; self.shadow_blur = blur; self }
92 pub fn elevation(mut self, e: f32) -> Self { self.shadow_color = Color::rgba(0, 0, 0, 90); self.shadow_blur = e; self }
94 pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
95 pub fn margin(mut self, m: EdgeInsets) -> Self { self.margin = m; self }
96 pub fn clip(mut self) -> Self { self.clip = true; self }
98 pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
99 pub fn height(mut self, h: f32) -> Self { self.height = Some(h); self }
100 pub fn size(mut self, w: f32, h: f32) -> Self { self.width = Some(w); self.height = Some(h); self }
101 pub fn min_size(mut self, w: f32, h: f32) -> Self { self.min_width = w; self.min_height = h; self }
102 pub fn child(mut self, w: impl Widget + 'static) -> Self { self.child = Some(Box::new(w)); self }
103 pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
108}
109
110impl Default for Container {
111 fn default() -> Self { Self::new() }
112}
113
114impl Widget for Container {
115 fn layout(&self, ctx: &LayoutCtx) -> Size {
116 let constraints = ctx.constraints;
117 let child_size = self.child.as_ref().map(|c| {
118 let avail_w = self.width.unwrap_or_else(|| avail_w(constraints));
121 let avail_h = self.height.unwrap_or_else(|| avail_h(constraints));
122 let inner_c = Constraints::loose(
123 (avail_w - self.padding.total_h()).max(0.0),
124 (avail_h - self.padding.total_v()).max(0.0),
125 );
126 self.padding.grow(c.layout(&ctx.with_constraints(inner_c)))
127 }).unwrap_or(Size { width: 0.0, height: 0.0 });
128
129 let (fill_w, fill_h) = if self.align.is_some() {
132 (avail_w(constraints), avail_h(constraints))
133 } else {
134 (f32::INFINITY, f32::INFINITY) };
136 let w = self.width.unwrap_or_else(|| {
137 if self.align.is_some() && fill_w.is_finite() { fill_w }
138 else { child_size.width.max(self.min_width) }
139 });
140 let h = self.height.unwrap_or_else(|| {
141 if self.align.is_some() && fill_h.is_finite() { fill_h }
142 else { child_size.height.max(self.min_height) }
143 });
144
145 constraints.constrain(Size {
148 width: w.max(self.min_width) + self.margin.total_h(),
149 height: h.max(self.min_height) + self.margin.total_v(),
150 })
151 }
152
153 fn paint(&self, ctx: &mut PaintCtx) {
154 let rect = self.margin.shrink(ctx.rect);
156 let radius = self.radius_for(rect.size);
157
158 if self.shadow_blur > 0.5 {
160 ctx.fill_shadow_rrect(rect, radius, self.shadow_color, self.shadow_blur);
161 }
162
163 let material = resolve_material::<ContainerMaterial>(&ctx.theme, self.material.as_ref());
166 if let Some(m) = material {
167 if let Some(fallback) = m.fallback {
168 if radius > 0.5 { ctx.fill_rrect(rect, radius, fallback); }
169 else { ctx.fill_rect(rect, fallback); }
170 }
171 ctx.shader_fill(rect, m.pipeline, m.uniforms);
172 } else if let Some((from, to, vertical)) = self.gradient {
173 ctx.fill_gradient(rect, radius, from, to, vertical);
174 } else if let Some(bg) = self.background {
175 if radius > 0.5 { ctx.fill_rrect(rect, radius, bg); }
176 else { ctx.fill_rect(rect, bg); }
177 }
178
179 if let Some(bc) = self.border_color {
181 if radius > 0.5 { ctx.stroke_rrect(rect, radius, bc, self.border_width); }
182 else { ctx.stroke_rect(rect, bc, self.border_width); }
183 }
184
185 if let Some(child) = &self.child {
187 let inner = self.padding.shrink(rect);
188 let child_rect = if let Some(align) = self.align {
189 let inner_c = Constraints::loose(inner.size.width, inner.size.height);
190 let child_size = child.layout(&ctx.layout_ctx(inner_c));
191 let off = align.offset(inner.size, child_size);
192 Rect {
193 origin: Point { x: inner.origin.x + off.x, y: inner.origin.y + off.y },
194 size: child_size,
195 }
196 } else {
197 inner
198 };
199 if self.clip {
200 ctx.record(DrawCommand::PushClip { rect });
201 child.paint(&mut ctx.child(child_rect));
202 ctx.record(DrawCommand::PopClip);
203 } else {
204 child.paint(&mut ctx.child(child_rect));
205 }
206 }
207 }
208}
209
210pub(super) fn draw_rounded_rect_pub(ctx: &mut PaintCtx, rect: Rect, color: Color, radius: f32) {
213 ctx.fill_rrect(rect, radius, color);
214}
215
216#[cfg(test)]
217mod material_cascade_tests {
218 use super::*;
219 use rosace_shader::PipelineId;
220
221 fn mat(id: u64) -> ShaderMaterial {
222 ShaderMaterial::new(PipelineId::user(0x2000 + id), vec![id as u8])
223 }
224
225 fn paint_and_check(container: Container, theme: rosace_theme::ThemeData) -> bool {
226 let font = rosace_render::FontCache::embedded();
227 let mut recorder = rosace_render::PictureRecorder::new();
228 let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
229 let rect = Rect {
230 origin: Point { x: 0.0, y: 0.0 },
231 size: Size { width: 100.0, height: 100.0 },
232 };
233 let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
234 container.paint(&mut ctx);
235 let picture = recorder.finish();
236 picture.commands.iter().any(|c| matches!(c, DrawCommand::ShaderFill { .. }))
237 }
238
239 #[test]
240 fn instance_material_paints_shader_fill() {
241 let theme = rosace_theme::built_in::dark_theme();
242 assert!(paint_and_check(Container::new().material(mat(1)), theme));
243 }
244
245 #[test]
246 fn theme_material_used_when_no_instance() {
247 let theme = rosace_theme::built_in::dark_theme().with_ext(super::super::material::ContainerMaterial(mat(2)));
248 assert!(paint_and_check(Container::new(), theme));
249 }
250
251 #[test]
252 fn no_material_renders_as_before() {
253 let theme = rosace_theme::built_in::dark_theme();
254 assert!(!paint_and_check(Container::new().background(Color::rgb(10, 10, 10)), theme));
255 }
256}