use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use crate::base::{Point, Rect, Size};
use crate::reactive::{Scope, Signal};
use crate::ui::View;
use super::overlays::{LayerHandle, Overlays};
use super::viewport::use_viewport;
pub const DRAWER_Z: i32 = 800;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DrawerEdge {
Left,
Right,
Top,
Bottom,
}
impl DrawerEdge {
fn index(self) -> usize {
match self {
DrawerEdge::Left => 0,
DrawerEdge::Right => 1,
DrawerEdge::Top => 2,
DrawerEdge::Bottom => 3,
}
}
fn panel_z(self) -> i32 {
DRAWER_Z + (self.index() as i32) * 2 + 1
}
fn scrim_z(self) -> i32 {
self.panel_z() - 1
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum DrawerSize {
Cells(i32),
Percent(f32),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DrawerFocus {
Modal,
Passive,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DrawerCloseReason {
Api,
Escape,
OutsidePress,
Replaced,
HostGone,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct Geometry {
rect: Rect,
closed: Point,
}
fn solve_rect(viewport: Size, edge: DrawerEdge, size: DrawerSize) -> Rect {
let axis = match edge {
DrawerEdge::Left | DrawerEdge::Right => viewport.w,
DrawerEdge::Top | DrawerEdge::Bottom => viewport.h,
};
let want = match size {
DrawerSize::Cells(n) => n,
DrawerSize::Percent(p) => ((axis as f32) * p.clamp(0.0, 1.0)).round() as i32,
}
.clamp(1, axis.max(1));
match edge {
DrawerEdge::Left => Rect::new(0, 0, want, viewport.h),
DrawerEdge::Right => Rect::new(viewport.w - want, 0, want, viewport.h),
DrawerEdge::Top => Rect::new(0, 0, viewport.w, want),
DrawerEdge::Bottom => Rect::new(0, viewport.h - want, viewport.w, want),
}
}
fn solve_geometry(viewport: Size, edge: DrawerEdge, size: DrawerSize) -> Geometry {
let rect = solve_rect(viewport, edge, size);
let closed = match edge {
DrawerEdge::Left => Point::new(-rect.w, rect.y),
DrawerEdge::Right => Point::new(viewport.w, rect.y),
DrawerEdge::Top => Point::new(rect.x, -rect.h),
DrawerEdge::Bottom => Point::new(rect.x, viewport.h),
};
Geometry { rect, closed }
}
fn origin_at(geo: Geometry, t: f32) -> Point {
let lerp = |a: i32, b: i32| a as f32 + (b - a) as f32 * t;
Point::new(
lerp(geo.closed.x, geo.rect.x).round() as i32,
lerp(geo.closed.y, geo.rect.y).round() as i32,
)
}
#[path = "drawer_open.rs"]
mod machinery;
use machinery::{begin_close, close_now, open_now, reclamp};
pub(super) struct DrawerConfig {
pub edge: DrawerEdge,
pub size: DrawerSize,
pub focus: DrawerFocus,
pub scrim: bool,
pub close_on_outside: bool,
pub title: Option<String>,
pub motion: Duration,
}
pub struct Drawer {
cfg: DrawerConfig,
overlays: Option<Overlays>,
bound: Option<Signal<bool>>,
on_close: Option<Box<dyn FnMut(DrawerCloseReason)>>,
}
impl Drawer {
pub fn new(edge: DrawerEdge) -> Drawer {
Drawer {
cfg: DrawerConfig {
edge,
size: DrawerSize::Percent(0.4),
focus: DrawerFocus::Modal,
scrim: true,
close_on_outside: true,
title: None,
motion: Duration::from_millis(160),
},
overlays: None,
bound: None,
on_close: None,
}
}
pub fn size(mut self, size: DrawerSize) -> Drawer {
self.cfg.size = size;
self
}
pub fn focus(mut self, focus: DrawerFocus) -> Drawer {
self.cfg.focus = focus;
self
}
pub fn scrim(mut self, on: bool) -> Drawer {
self.cfg.scrim = on;
self
}
pub fn close_on_outside(mut self, on: bool) -> Drawer {
self.cfg.close_on_outside = on;
self
}
pub fn title(mut self, title: impl Into<String>) -> Drawer {
self.cfg.title = Some(title.into());
self
}
pub fn motion(mut self, motion: Duration) -> Drawer {
self.cfg.motion = motion;
self
}
pub fn overlays(mut self, overlays: &Overlays) -> Drawer {
self.overlays = Some(overlays.clone());
self
}
pub fn bind(mut self, open: Signal<bool>) -> Drawer {
self.bound = Some(open);
self
}
pub fn on_close(mut self, f: impl FnMut(DrawerCloseReason) + 'static) -> Drawer {
self.on_close = Some(Box::new(f));
self
}
pub fn install(self, cx: Scope, build: impl Fn(Scope) -> View + 'static) -> DrawerHandle {
let overlays = self
.overlays
.or_else(|| cx.use_context::<Overlays>())
.unwrap_or_else(|| {
debug_assert!(
false,
"abstracttui drawer: no overlay store — install inside an App \
(context) or pass Drawer::overlays(..) explicitly"
);
Overlays::new() });
let host = cx.child();
let inner = Rc::new(RefCell::new(Inner {
cfg: self.cfg,
overlays,
host,
build: Rc::new(build),
bound: self.bound,
on_close: self.on_close,
mount: None,
desired_open: false,
opening: false,
pending_reason: None,
}));
let weak = Rc::downgrade(&inner);
host.on_cleanup(move || {
if let Some(inner) = weak.upgrade() {
close_now(&inner, DrawerCloseReason::HostGone, false);
}
});
let bound = inner.borrow().bound;
if let Some(sig) = bound {
let weak = Rc::downgrade(&inner);
host.effect_labeled("drawer-bound", move || {
let want = sig.get();
let Some(inner) = weak.upgrade() else { return };
let desired = inner.borrow().desired_open;
if want && !desired {
open_now(&inner);
} else if !want && desired {
begin_close(&inner, DrawerCloseReason::Api);
}
});
}
let weak = Rc::downgrade(&inner);
let viewport = use_viewport(host);
host.effect_labeled("drawer-reclamp", move || {
let vp = viewport.get(); let Some(inner) = weak.upgrade() else { return };
reclamp(&inner, vp);
});
DrawerHandle { inner }
}
}
struct Mount {
scope: Scope,
panel: LayerHandle,
scrim: Option<LayerHandle>,
progress: Signal<f32>,
closing: Signal<bool>,
geometry: Signal<Geometry>,
veil: crate::render::Cell,
}
struct Inner {
cfg: DrawerConfig,
overlays: Overlays,
host: Scope,
build: Rc<dyn Fn(Scope) -> View>,
bound: Option<Signal<bool>>,
on_close: Option<Box<dyn FnMut(DrawerCloseReason)>>,
mount: Option<Mount>,
desired_open: bool,
opening: bool,
pending_reason: Option<DrawerCloseReason>,
}
#[derive(Clone)]
pub struct DrawerHandle {
inner: Rc<RefCell<Inner>>,
}
impl DrawerHandle {
pub fn open(&self) {
open_now(&self.inner);
}
pub fn close(&self) {
begin_close(&self.inner, DrawerCloseReason::Api);
}
pub fn toggle(&self) {
if self.is_open() {
self.close();
} else {
self.open();
}
}
pub fn is_open(&self) -> bool {
self.inner.borrow().desired_open
}
pub fn edge(&self) -> DrawerEdge {
self.inner.borrow().cfg.edge
}
pub fn layer(&self) -> Option<LayerHandle> {
self.inner.borrow().mount.as_ref().map(|m| m.panel.clone())
}
pub(super) fn close_with(&self, reason: DrawerCloseReason) {
begin_close(&self.inner, reason);
}
}
#[cfg(test)]
#[path = "drawer_tests.rs"]
mod tests;