use rosace_core::types::{Point, Rect, Size};
use rosace_layout::Constraints;
use rosace_render::Color;
use rosace_scroll::ScrollController;
use rosace_shader::ShaderMaterial;
use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget};
use super::container::draw_rounded_rect_pub;
use super::material::{resolve_material, SheetMaterial};
use super::padding::EdgeInsets;
use super::scroll_view::ScrollView;
use super::spacer::Spacer;
#[derive(Clone, Copy, Debug, PartialEq)]
enum SheetHeight {
Content,
Fixed(f32),
Detent(f32),
Full,
}
pub struct Sheet {
pub child: BoxedWidget,
pub radius: f32,
pub padding: EdgeInsets,
pub show_handle: bool,
height_mode: SheetHeight,
background: Option<Color>,
handle_color: Option<Color>,
material: Option<ShaderMaterial>,
scrollable: bool,
}
impl Sheet {
pub fn new(child: impl Widget + 'static) -> Self {
Self {
child: Box::new(child),
radius: 16.0,
padding: EdgeInsets::all(20.0),
show_handle: true,
height_mode: SheetHeight::Content,
background: None,
handle_color: None,
material: None,
scrollable: false,
}
}
pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
pub fn padding(mut self, p: EdgeInsets) -> Self { self.padding = p; self }
pub fn no_handle(mut self) -> Self { self.show_handle = false; self }
pub fn height(mut self, h: f32) -> Self { self.height_mode = SheetHeight::Fixed(h); self }
pub fn detent(mut self, fraction: f32) -> Self {
self.height_mode = SheetHeight::Detent(fraction);
self
}
pub fn full_screen(mut self) -> Self { self.height_mode = SheetHeight::Full; self }
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn handle_color(mut self, c: Color) -> Self { self.handle_color = Some(c); self }
pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
pub fn scrollable(self) -> Self {
self.scrollable_with(ScrollController::new())
}
pub fn scrollable_with(mut self, controller: ScrollController) -> Self {
let child = std::mem::replace(&mut self.child, Box::new(Spacer::new(0.0)));
self.child = Box::new(ScrollView::new(child).controller(controller));
self.scrollable = true;
self
}
fn handle_space(&self) -> f32 {
if self.show_handle { 16.0 } else { 0.0 }
}
}
impl Widget for Sheet {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let width = ctx.constraints.max_width_f32();
let height = match self.height_mode {
SheetHeight::Content => {
let inner_c = Constraints::loose(
(width - self.padding.total_h()).max(0.0),
f32::INFINITY,
);
let child_size = self.child.layout(&ctx.with_constraints(inner_c));
child_size.height + self.padding.total_v() + self.handle_space()
}
SheetHeight::Fixed(h) => h,
SheetHeight::Detent(f) => {
ctx.constraints.max_height_f32() * f.clamp(0.0, 1.0)
}
SheetHeight::Full => ctx.constraints.max_height_f32(),
};
ctx.constraints.constrain(Size { width, height })
}
fn paint(&self, ctx: &mut PaintCtx) {
ctx.semantics(super::Semantics::new(rosace_core::Role::Dialog));
let (surface, handle_color) = {
let t = &ctx.theme.colors;
(
self.background.unwrap_or_else(|| ctx.tc(t.surface)),
self.handle_color.unwrap_or_else(|| ctx.tc(t.outline)),
)
};
let r = ctx.rect;
let material = resolve_material::<SheetMaterial>(&ctx.theme, self.material.as_ref());
let fill = match &material {
Some(m) => m.fallback,
None => Some(surface),
};
if let Some(fill) = fill {
draw_rounded_rect_pub(ctx, r, fill, self.radius);
ctx.fill_rect(Rect {
origin: Point { x: r.origin.x, y: r.origin.y + r.size.height - self.radius },
size: Size { width: r.size.width, height: self.radius },
}, fill);
}
if let Some(m) = &material {
ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
}
if self.show_handle {
let handle_w = 36.0;
ctx.fill_rrect(Rect {
origin: Point {
x: r.origin.x + (r.size.width - handle_w) / 2.0,
y: r.origin.y + 6.0,
},
size: Size { width: handle_w, height: 4.0 },
}, 2.0, handle_color);
}
let content = Rect {
origin: Point { x: r.origin.x, y: r.origin.y + self.handle_space() },
size: Size { width: r.size.width, height: r.size.height - self.handle_space() },
};
self.child.paint(&mut ctx.child(self.padding.shrink(content)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::spacer::Spacer;
use rosace_layout::Constraints;
#[test]
fn instance_material_paints_a_shader_fill() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
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: 400.0, height: 300.0 } };
let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
let m = ShaderMaterial::new(rosace_shader::PipelineId::user(0x4001), vec![0u8; 16]);
Sheet::new(Spacer::new(0.0)).material(m).paint(&mut ctx);
let picture = recorder.finish();
assert!(picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. })));
}
fn ctx_800x600<'a>(
font: &'a rosace_render::FontCache,
theme: &'a rosace_theme::ThemeData,
) -> LayoutCtx<'a> {
LayoutCtx::new(Constraints::loose(800.0, 600.0), font, theme)
}
#[test]
fn content_mode_takes_the_natural_child_height_plus_chrome() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = ctx_800x600(&font, &theme);
let size = Sheet::new(Spacer::gap(0.0, 100.0)).layout(&ctx);
assert_eq!(size.width, 800.0);
assert_eq!(size.height, 156.0);
}
#[test]
fn fixed_detent_and_full_screen_heights_resolve_against_the_window() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = ctx_800x600(&font, &theme);
let fixed = Sheet::new(Spacer::gap(0.0, 100.0)).height(220.0).layout(&ctx);
assert_eq!(fixed.height, 220.0);
let detent = Sheet::new(Spacer::gap(0.0, 100.0)).detent(0.5).layout(&ctx);
assert_eq!(detent.height, 300.0);
let full = Sheet::new(Spacer::gap(0.0, 100.0)).full_screen().layout(&ctx);
assert_eq!(full.height, 600.0);
}
#[test]
fn fixed_height_is_capped_at_the_available_height() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = ctx_800x600(&font, &theme);
let size = Sheet::new(Spacer::gap(0.0, 100.0)).height(10_000.0).layout(&ctx);
assert_eq!(size.height, 600.0);
}
#[test]
fn scrollable_without_an_explicit_height_takes_the_full_height() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = ctx_800x600(&font, &theme);
let size = Sheet::new(Spacer::gap(0.0, 5_000.0)).scrollable().layout(&ctx);
assert_eq!(size.height, 600.0, "a scrollable sheet has no natural height");
}
#[test]
fn scrollable_with_a_detent_keeps_the_detent_height() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = ctx_800x600(&font, &theme);
let size = Sheet::new(Spacer::gap(0.0, 5_000.0))
.scrollable()
.detent(0.5)
.layout(&ctx);
assert_eq!(size.height, 300.0);
}
}