use std::sync::Arc;
use rosace_core::types::Size;
use rosace_state::Atom;
use rosace_render::Color;
use rosace_shader::ShaderMaterial;
use super::{Widget, LayoutCtx, PaintCtx, BoxedWidget};
use super::material::{resolve_material, DrawerMaterial};
use super::overlay::{OverlayEntry, LayerPosition, InputBehavior, FocusBehavior, ScrimConfig, push_overlay};
pub struct Drawer {
open: Atom<bool>,
width: f32,
full_screen: bool,
background: Option<Color>,
scrim_color: Color,
material: Option<ShaderMaterial>,
panel: Arc<dyn Fn() -> BoxedWidget + Send + Sync>,
}
impl Drawer {
pub fn new(open: Atom<bool>, panel: impl Fn() -> BoxedWidget + Send + Sync + 'static) -> Self {
Self {
open,
width: 280.0,
full_screen: false,
background: None,
scrim_color: Color::rgba(0, 0, 0, 120),
material: None,
panel: Arc::new(panel),
}
}
pub fn width(mut self, w: f32) -> Self { self.width = w; self }
pub fn full_screen(mut self) -> Self { self.full_screen = true; self }
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn scrim_color(mut self, c: Color) -> Self { self.scrim_color = c; self }
pub fn material(mut self, m: ShaderMaterial) -> Self { self.material = Some(m); self }
pub fn emit(&self) {
if !self.open.get() { return; }
let close = self.open.clone();
let panel = (self.panel)();
push_overlay(
OverlayEntry::new(LayerPosition::Fill, DrawerPanel {
width: self.width,
full_screen: self.full_screen,
background: self.background,
material: self.material.clone(),
panel,
})
.input(InputBehavior::Block)
.focus(FocusBehavior::Trap)
.scrim(ScrimConfig { color: self.scrim_color, on_tap: Some(Arc::new(move || close.set(false))), exclude_rect: None }),
);
}
}
struct DrawerPanel {
width: f32,
full_screen: bool,
background: Option<Color>,
material: Option<ShaderMaterial>,
panel: BoxedWidget,
}
impl Widget for DrawerPanel {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let avail_w = super::avail_w(ctx.constraints);
let w = if self.full_screen { avail_w } else { self.width.min(avail_w) };
Size { width: w, height: super::avail_h(ctx.constraints) }
}
fn paint(&self, ctx: &mut PaintCtx) {
let bg = self.background.unwrap_or_else(|| ctx.tc(ctx.theme.colors.surface));
let r = ctx.rect;
let material = resolve_material::<DrawerMaterial>(&ctx.theme, self.material.as_ref());
match &material {
Some(m) => {
if let Some(fallback) = m.fallback {
ctx.fill_rect(r, fallback);
}
ctx.shader_fill(r, m.pipeline, m.uniforms.clone());
}
None => ctx.fill_rect(r, bg),
}
self.panel.paint(&mut ctx.child(r));
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::overlay::{clear_overlays, drain_overlays};
use super::super::spacer::Spacer;
use rosace_layout::Constraints;
fn emit_one(drawer: &Drawer) -> OverlayEntry {
clear_overlays();
drawer.emit();
let mut entries = drain_overlays();
assert_eq!(entries.len(), 1);
entries.pop().unwrap()
}
#[test]
fn instance_material_paints_a_shader_fill() {
let open = rosace_state::use_atom(true);
let m = ShaderMaterial::new(rosace_shader::PipelineId::user(0x4002), vec![0u8; 16]);
let drawer = Drawer::new(open, || Box::new(Spacer::new(0.0))).material(m);
let entry = emit_one(&drawer);
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 = rosace_core::types::Rect {
origin: rosace_core::types::Point { x: 0.0, y: 0.0 },
size: Size { width: 280.0, height: 600.0 },
};
let mut ctx = PaintCtx::root(&mut recorder, rect, &font, theme, tree);
entry.widget.paint(&mut ctx);
let picture = recorder.finish();
assert!(picture.commands.iter().any(|c| matches!(c, rosace_render::DrawCommand::ShaderFill { .. })));
}
#[test]
fn emit_pushes_nothing_while_closed() {
clear_overlays();
let open = rosace_state::use_atom(false);
Drawer::new(open, || Box::new(Spacer::new(0.0))).emit();
assert!(drain_overlays().is_empty());
}
#[test]
fn emit_maps_to_fill_block_trap_with_dismissable_scrim() {
let open = rosace_state::use_atom(true);
let drawer = Drawer::new(open.clone(), || Box::new(Spacer::new(0.0)));
let e = emit_one(&drawer);
assert!(matches!(e.position, LayerPosition::Fill));
assert_eq!(e.input, InputBehavior::Block);
assert_eq!(e.focus, FocusBehavior::Trap);
let scrim = e.scrim.expect("drawer must have a scrim");
let on_tap = scrim.on_tap.expect("scrim must dismiss on tap");
on_tap();
assert!(!open.get(), "scrim tap must close the drawer");
}
#[test]
fn panel_is_side_width_by_default_and_window_width_when_full_screen() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = LayoutCtx::new(Constraints::loose(800.0, 600.0), &font, &theme);
let open = rosace_state::use_atom(true);
let side = emit_one(&Drawer::new(open.clone(), || Box::new(Spacer::new(0.0))));
let size = side.widget.layout(&ctx);
assert_eq!((size.width, size.height), (280.0, 600.0));
let full = emit_one(&Drawer::new(open, || Box::new(Spacer::new(0.0))).full_screen());
let size = full.widget.layout(&ctx);
assert_eq!((size.width, size.height), (800.0, 600.0));
}
}