use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_render::Color;
use super::{avail_w, BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DismissDirection {
Horizontal,
EndToStart,
StartToEnd,
}
const DEFAULT_THRESHOLD: f32 = 0.35;
pub struct Dismissible {
child: BoxedWidget,
background: Option<BoxedWidget>,
direction: DismissDirection,
threshold: f32,
on_dismissed: Option<Arc<dyn Fn() + Send + Sync>>,
}
impl Dismissible {
pub fn new(child: impl Widget + 'static) -> Self {
Self {
child: Box::new(child),
background: None,
direction: DismissDirection::Horizontal,
threshold: DEFAULT_THRESHOLD,
on_dismissed: None,
}
}
pub fn background(mut self, w: impl Widget + 'static) -> Self {
self.background = Some(Box::new(w));
self
}
pub fn direction(mut self, d: DismissDirection) -> Self {
self.direction = d;
self
}
pub fn threshold(mut self, t: f32) -> Self {
self.threshold = t.clamp(0.05, 0.95);
self
}
pub fn on_dismissed(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.on_dismissed = Some(Arc::new(f));
self
}
fn allows(&self, dx: f32) -> bool {
match self.direction {
DismissDirection::Horizontal => true,
DismissDirection::EndToStart => dx < 0.0,
DismissDirection::StartToEnd => dx > 0.0,
}
}
}
impl Widget for Dismissible {
fn children(&self) -> Children<'_> {
Children::One(&*self.child)
}
fn layout(&self, ctx: &LayoutCtx) -> Size {
let child_size = self.child.layout(ctx);
Size { width: avail_w(ctx.constraints), height: child_size.height }
}
fn paint(&self, ctx: &mut PaintCtx) {
let r = ctx.rect;
let ctrl = ctx.scroll_controller();
let drag_ctrl = ctrl.clone();
ctx.on_press_at(move |x, y| {
let (dx, _) = drag_ctrl.drag_delta(x, y);
if dx != 0.0 {
let o = drag_ctrl.offset.get();
drag_ctrl.offset.set([o[0] + dx, o[1]]);
}
});
let is_pressed = ctx.pressed();
let was_pressed = ctrl.was_pressed();
let mut dx = ctrl.offset.get()[0];
if !is_pressed && was_pressed {
let commit = dx.abs() >= r.size.width * self.threshold && self.allows(dx);
if commit {
let target = if dx < 0.0 { -r.size.width } else { r.size.width };
ctrl.offset.set([target, 0.0]);
dx = target;
if let Some(cb) = &self.on_dismissed {
cb();
}
} else {
ctrl.offset.set([0.0, 0.0]);
dx = 0.0;
}
ctrl.end_drag();
}
ctrl.set_was_pressed(is_pressed);
if dx.abs() > 0.001 {
ctx.request_animation();
}
if dx.abs() > 0.001 {
match &self.background {
Some(bg) => bg.paint(&mut ctx.child(r)),
None => draw_default_background(ctx, r, dx),
}
}
let child_rect = Rect { origin: Point { x: r.origin.x + dx, y: r.origin.y }, size: r.size };
ctx.record(rosace_render::DrawCommand::PushClip { rect: r });
self.child.paint(&mut ctx.child(child_rect));
ctx.record(rosace_render::DrawCommand::PopClip);
}
}
fn draw_default_background(ctx: &mut PaintCtx, r: Rect, dx: f32) {
let red = Color::rgb(220, 62, 54);
ctx.fill_rect(r, red);
const ICON: f32 = 22.0;
let cy = r.origin.y + (r.size.height - ICON) / 2.0;
let cx = if dx < 0.0 {
r.origin.x + r.size.width - ICON - 18.0 } else {
r.origin.x + 18.0 };
let icon = super::Icon::new(super::IconKind::Trash).size(ICON).color(Color::rgb(255, 255, 255));
icon.paint(&mut ctx.child(Rect { origin: Point { x: cx, y: cy }, size: Size { width: ICON, height: ICON } }));
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
struct Row;
impl Widget for Row {
fn layout(&self, _ctx: &LayoutCtx) -> Size {
Size { width: 300.0, height: 56.0 }
}
fn paint(&self, _ctx: &mut PaintCtx) {}
}
fn test_env() -> (rosace_render::FontCache, rosace_theme::ThemeData) {
(rosace_render::FontCache::embedded(), rosace_theme::built_in::dark_theme())
}
#[test]
fn height_matches_child_width_fills_parent() {
let d = Dismissible::new(Row);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::loose(390.0, 800.0), &font, &theme);
let size = d.layout(&ctx);
assert_eq!((size.width, size.height), (390.0, 56.0));
}
#[test]
fn allows_respects_direction() {
let both = Dismissible::new(Row);
assert!(both.allows(-50.0) && both.allows(50.0));
let end_to_start = Dismissible::new(Row).direction(DismissDirection::EndToStart);
assert!(end_to_start.allows(-50.0) && !end_to_start.allows(50.0));
let start_to_end = Dismissible::new(Row).direction(DismissDirection::StartToEnd);
assert!(!start_to_end.allows(-50.0) && start_to_end.allows(50.0));
}
#[test]
fn threshold_clamps_to_sane_range() {
let d = Dismissible::new(Row).threshold(5.0);
assert!(d.threshold <= 0.95);
let d2 = Dismissible::new(Row).threshold(-1.0);
assert!(d2.threshold >= 0.05);
}
}