use rosace_core::types::{Point, Rect, Size};
use rosace_layout::Constraints;
use rosace_render::{Color, DrawCommand};
use rosace_shader::ShaderMaterial;
use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget, avail_w, avail_h};
use super::padding::EdgeInsets;
use super::material::{resolve_material, ContainerMaterial};
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum BoxShape {
#[default]
Rect,
Circle,
Stadium,
}
pub struct Container {
pub background: Option<Color>,
pub gradient: Option<(Color, Color, bool)>, pub border_color: Option<Color>,
pub border_width: f32,
pub border_radius: f32,
pub shape: BoxShape,
pub shadow_blur: f32,
pub shadow_color: Color,
pub padding: EdgeInsets,
pub margin: EdgeInsets,
pub width: Option<f32>,
pub height: Option<f32>,
pub min_width: f32,
pub min_height: f32,
pub clip: bool,
pub align: Option<super::Alignment>,
pub material: Option<ShaderMaterial>,
pub child: Option<BoxedWidget>,
}
impl Container {
pub fn new() -> Self {
Self {
background: None,
gradient: None,
border_color: None,
border_width: 1.0,
border_radius: 0.0,
shape: BoxShape::Rect,
shadow_blur: 0.0,
shadow_color: Color::rgba(0, 0, 0, 0),
padding: EdgeInsets::default(),
margin: EdgeInsets::default(),
width: None,
height: None,
min_width: 0.0,
min_height: 0.0,
clip: false,
align: None,
material: None,
child: None,
}
}
fn radius_for(&self, size: Size) -> f32 {
match self.shape {
BoxShape::Rect => self.border_radius,
BoxShape::Circle => size.width.min(size.height) / 2.0,
BoxShape::Stadium => size.height / 2.0,
}
}
pub fn align(mut self, a: super::Alignment) -> Self { self.align = Some(a); self }
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn gradient(mut self, from: Color, to: Color) -> Self { self.gradient = Some((from, to, true)); self }
pub fn gradient_h(mut self, from: Color, to: Color) -> Self { self.gradient = Some((from, to, false)); self }
pub fn border(mut self, c: Color, w: f32) -> Self { self.border_color = Some(c); self.border_width = w; self }
pub fn radius(mut self, r: f32) -> Self { self.border_radius = r; self }
pub fn shape(mut self, s: BoxShape) -> Self { self.shape = s; self }
pub fn circle(mut self) -> Self { self.shape = BoxShape::Circle; self }
pub fn stadium(mut self) -> Self { self.shape = BoxShape::Stadium; self }
pub fn shadow(mut self, color: Color, blur: f32) -> Self { self.shadow_color = color; self.shadow_blur = blur; self }
pub fn elevation(mut self, e: f32) -> Self { self.shadow_color = Color::rgba(0, 0, 0, 90); self.shadow_blur = e; self }
pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
pub fn margin(mut self, m: EdgeInsets) -> Self { self.margin = m; self }
pub fn clip(mut self) -> Self { self.clip = true; self }
pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
pub fn height(mut self, h: f32) -> Self { self.height = Some(h); self }
pub fn size(mut self, w: f32, h: f32) -> Self { self.width = Some(w); self.height = Some(h); self }
pub fn min_size(mut self, w: f32, h: f32) -> Self { self.min_width = w; self.min_height = h; self }
pub fn child(mut self, w: impl Widget + 'static) -> Self { self.child = Some(Box::new(w)); self }
pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
}
impl Default for Container {
fn default() -> Self { Self::new() }
}
impl Widget for Container {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let constraints = ctx.constraints;
let child_size = self.child.as_ref().map(|c| {
let avail_w = self.width.unwrap_or_else(|| avail_w(constraints));
let avail_h = self.height.unwrap_or_else(|| avail_h(constraints));
let inner_c = Constraints::loose(
(avail_w - self.padding.total_h()).max(0.0),
(avail_h - self.padding.total_v()).max(0.0),
);
self.padding.grow(c.layout(&ctx.with_constraints(inner_c)))
}).unwrap_or(Size { width: 0.0, height: 0.0 });
let (fill_w, fill_h) = if self.align.is_some() {
(avail_w(constraints), avail_h(constraints))
} else {
(f32::INFINITY, f32::INFINITY) };
let w = self.width.unwrap_or_else(|| {
if self.align.is_some() && fill_w.is_finite() { fill_w }
else { child_size.width.max(self.min_width) }
});
let h = self.height.unwrap_or_else(|| {
if self.align.is_some() && fill_h.is_finite() { fill_h }
else { child_size.height.max(self.min_height) }
});
constraints.constrain(Size {
width: w.max(self.min_width) + self.margin.total_h(),
height: h.max(self.min_height) + self.margin.total_v(),
})
}
fn paint(&self, ctx: &mut PaintCtx) {
let rect = self.margin.shrink(ctx.rect);
let radius = self.radius_for(rect.size);
if self.shadow_blur > 0.5 {
ctx.fill_shadow_rrect(rect, radius, self.shadow_color, self.shadow_blur);
}
let material = resolve_material::<ContainerMaterial>(&ctx.theme, self.material.as_ref());
if let Some(m) = material {
if let Some(fallback) = m.fallback {
if radius > 0.5 { ctx.fill_rrect(rect, radius, fallback); }
else { ctx.fill_rect(rect, fallback); }
}
ctx.shader_fill(rect, m.pipeline, m.uniforms);
} else if let Some((from, to, vertical)) = self.gradient {
ctx.fill_gradient(rect, radius, from, to, vertical);
} else if let Some(bg) = self.background {
if radius > 0.5 { ctx.fill_rrect(rect, radius, bg); }
else { ctx.fill_rect(rect, bg); }
}
if let Some(bc) = self.border_color {
if radius > 0.5 { ctx.stroke_rrect(rect, radius, bc, self.border_width); }
else { ctx.stroke_rect(rect, bc, self.border_width); }
}
if let Some(child) = &self.child {
let inner = self.padding.shrink(rect);
let child_rect = if let Some(align) = self.align {
let inner_c = Constraints::loose(inner.size.width, inner.size.height);
let child_size = child.layout(&ctx.layout_ctx(inner_c));
let off = align.offset(inner.size, child_size);
Rect {
origin: Point { x: inner.origin.x + off.x, y: inner.origin.y + off.y },
size: child_size,
}
} else {
inner
};
if self.clip {
ctx.record(DrawCommand::PushClip { rect });
child.paint(&mut ctx.child(child_rect));
ctx.record(DrawCommand::PopClip);
} else {
child.paint(&mut ctx.child(child_rect));
}
}
}
}
pub(super) fn draw_rounded_rect_pub(ctx: &mut PaintCtx, rect: Rect, color: Color, radius: f32) {
ctx.fill_rrect(rect, radius, color);
}
#[cfg(test)]
mod material_cascade_tests {
use super::*;
use rosace_shader::PipelineId;
fn mat(id: u64) -> ShaderMaterial {
ShaderMaterial::new(PipelineId::user(0x2000 + id), vec![id as u8])
}
fn paint_and_check(container: Container, theme: rosace_theme::ThemeData) -> bool {
let font = rosace_render::FontCache::embedded();
let mut recorder = rosace_render::PictureRecorder::new();
let tree = std::rc::Rc::new(std::cell::RefCell::new(super::super::render_tree::RenderTree::new()));
let rect = Rect {
origin: Point { x: 0.0, y: 0.0 },
size: Size { width: 100.0, height: 100.0 },
};
let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
container.paint(&mut ctx);
let picture = recorder.finish();
picture.commands.iter().any(|c| matches!(c, DrawCommand::ShaderFill { .. }))
}
#[test]
fn instance_material_paints_shader_fill() {
let theme = rosace_theme::built_in::dark_theme();
assert!(paint_and_check(Container::new().material(mat(1)), theme));
}
#[test]
fn theme_material_used_when_no_instance() {
let theme = rosace_theme::built_in::dark_theme().with_ext(super::super::material::ContainerMaterial(mat(2)));
assert!(paint_and_check(Container::new(), theme));
}
#[test]
fn no_material_renders_as_before() {
let theme = rosace_theme::built_in::dark_theme();
assert!(!paint_and_check(Container::new().background(Color::rgb(10, 10, 10)), theme));
}
}