use std::rc::Rc;
use std::time::Duration;
use geometry_core::{Rect, Transform};
use layout_core::{
AlignItems, AvailableSpace, JustifyContent, LayoutError, LayoutStyle, NodeId, SizeDimension,
};
use motion_core::{Animated, Easing, tween};
use platform_core::{Event, Key, NamedKey, PointerButton};
use reactive_core::RwSignal;
use renderer_core::{Color, RectStyle};
use ui_tree::{Component, EventResult, RenderNode};
use crate::context::{compute_layout, mark_dirty, new_container, track_layout};
use crate::layout_item::LayoutItem;
pub const DEFAULT_SCRIM: Color = Color::rgba(0.0, 0.0, 0.0, 0.35);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Edge {
Top,
Bottom,
Left,
Right,
Center,
}
const ENTER_MS: u64 = 200;
const SLIDE_DISTANCE: f32 = 24.0;
pub(crate) const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
#[derive(Clone, Copy)]
pub(crate) enum EnterMotion {
Slide(Edge),
Fade,
}
pub(crate) fn enter_transform(motion: EnterMotion, progress: f32) -> ([f32; 6], f32) {
let p = progress.clamp(0.0, 1.0);
let opacity = p;
match motion {
EnterMotion::Fade => (IDENTITY, opacity),
EnterMotion::Slide(anchor) => {
let d = SLIDE_DISTANCE * (1.0 - p);
let (dx, dy) = match anchor {
Edge::Top => (0.0, -d),
Edge::Bottom => (0.0, d),
Edge::Left => (-d, 0.0),
Edge::Right => (d, 0.0),
Edge::Center => (0.0, 0.0),
};
(Transform::translate(dx, dy).to_array(), opacity)
}
}
}
pub(crate) fn apply_enter(node: RenderNode, matrix: [f32; 6], opacity: f32) -> RenderNode {
let faded = if opacity < 1.0 {
RenderNode::layer(opacity, 0.0, [node])
} else {
node
};
if matrix == IDENTITY {
faded
} else {
RenderNode::transform_with(matrix, [faded])
}
}
#[derive(Clone)]
pub struct SurfaceTransition {
progress: Animated<f32>,
duration: Duration,
}
impl SurfaceTransition {
pub fn enter() -> Self {
let duration = Duration::from_millis(ENTER_MS);
let progress = Animated::new(0.0, tween(duration, Easing::EaseOut));
progress.retarget(1.0);
Self { progress, duration }
}
pub fn leave(&self) {
self.progress.retarget(0.0);
}
pub fn duration(&self) -> Duration {
self.duration
}
pub(crate) fn get(&self) -> f32 {
self.progress.get()
}
}
pub struct SurfaceScaffold {
root: NodeId,
panel_rect: Option<RwSignal<Rect>>,
root_rect: Option<RwSignal<Rect>>,
content: Box<dyn LayoutItem>,
scrim: Option<Color>,
dismiss: Option<Rc<dyn Fn()>>,
edge: Edge,
transition: Option<SurfaceTransition>,
}
impl SurfaceScaffold {
pub fn new(
edge: Edge,
align: AlignItems,
margin: (i32, i32, i32, i32),
scrim: Option<Color>,
dismiss: Option<Rc<dyn Fn()>>,
content: Box<dyn LayoutItem>,
) -> Result<Self, LayoutError> {
let panel_node = content.layout_node();
let (mt, mr, mb, ml) = margin;
let base = LayoutStyle::new()
.width(SizeDimension::Percent(1.0))
.height(SizeDimension::Percent(1.0))
.padding_top(mt as f32)
.padding_right(mr as f32)
.padding_bottom(mb as f32)
.padding_left(ml as f32);
let style = match edge {
Edge::Top => base
.flex_column()
.justify_content(JustifyContent::START)
.align_items(align),
Edge::Bottom => base
.flex_column()
.justify_content(JustifyContent::END)
.align_items(align),
Edge::Left => base
.flex_row()
.justify_content(JustifyContent::START)
.align_items(align),
Edge::Right => base
.flex_row()
.justify_content(JustifyContent::END)
.align_items(align),
Edge::Center => base
.flex_column()
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::CENTER),
};
let root = new_container(style, &[panel_node])?;
let panel_rect = track_layout(panel_node);
let root_rect = track_layout(root);
Ok(Self {
root,
panel_rect,
root_rect,
content,
scrim,
dismiss,
edge,
transition: None,
})
}
pub fn animate_in(self) -> Self {
self.animate(SurfaceTransition::enter())
}
pub fn animate(mut self, transition: SurfaceTransition) -> Self {
self.transition = Some(transition);
self
}
}
impl LayoutItem for SurfaceScaffold {
fn layout_node(&self) -> NodeId {
self.root
}
}
impl Component for SurfaceScaffold {
fn view(&self) -> RenderNode {
let content = self.content.view();
let (matrix, opacity) = match &self.transition {
Some(transition) => enter_transform(EnterMotion::Slide(self.edge), transition.get()),
None => (IDENTITY, 1.0),
};
let panel = apply_enter(content, matrix, opacity);
match (self.scrim, self.root_rect.as_ref()) {
(Some(color), Some(rect)) => {
let scrim = apply_enter(
RenderNode::rect(rect.get(), RectStyle::filled(color, 0.0)),
IDENTITY,
opacity,
);
RenderNode::group([scrim, panel])
}
_ => panel,
}
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::WindowResized { width, height } => {
mark_dirty(self.root).ok();
compute_layout(
self.root,
AvailableSpace::Definite(*width as f32),
AvailableSpace::Definite(*height as f32),
)
.ok();
EventResult::Handled
}
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
..
} => {
let inside = self
.panel_rect
.as_ref()
.map(|r| r.get().contains(*x as f32, *y as f32))
.unwrap_or(false);
match (inside, &self.dismiss) {
(false, Some(dismiss)) => {
dismiss();
EventResult::Handled
}
_ => self.content.on_event(event),
}
}
Event::KeyPressed {
key: Key::Named(NamedKey::Escape),
..
} => match (self.content.on_event(event), &self.dismiss) {
(EventResult::Ignored, Some(dismiss)) => {
dismiss();
EventResult::Handled
}
(result, _) => result,
},
_ => self.content.on_event(event),
}
}
fn debug_name(&self) -> &'static str {
"SurfaceScaffold"
}
}
#[cfg(test)]
mod tests {
use super::*;
use renderer_core::TextStyle;
use std::cell::Cell;
use platform_core::PointerSource;
use renderer_core::RectStyle;
use crate::StyledContainer;
use crate::context::reset_layout_runtime;
use crate::layout_item::box_item;
fn panel() -> Box<dyn LayoutItem> {
box_item(
StyledContainer::new(
LayoutStyle::new().width(100.0).height(40.0),
|_r| RectStyle::default(),
vec![],
)
.unwrap(),
)
}
fn press(x: f64, y: f64) -> Event {
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
#[test]
fn dismiss_fires_on_outside_press_only() {
reset_layout_runtime();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
let mut scaffold = SurfaceScaffold::new(
Edge::Top,
AlignItems::CENTER,
(20, 20, 20, 20),
Some(DEFAULT_SCRIM),
Some(Rc::new(move || f.set(f.get() + 1))),
panel(),
)
.unwrap();
scaffold.on_event(&Event::WindowResized {
width: 200,
height: 200,
});
scaffold.on_event(&press(100.0, 40.0));
assert_eq!(fired.get(), 0, "a press inside the panel must not dismiss");
scaffold.on_event(&press(10.0, 190.0));
assert_eq!(fired.get(), 1, "a press outside the panel dismisses");
}
#[test]
fn escape_dismisses_only_what_the_panel_left_alone() {
reset_layout_runtime();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
let mut scaffold = SurfaceScaffold::new(
Edge::Top,
AlignItems::CENTER,
(20, 20, 20, 20),
Some(DEFAULT_SCRIM),
Some(Rc::new(move || f.set(f.get() + 1))),
panel(),
)
.unwrap();
scaffold.on_event(&Event::WindowResized {
width: 200,
height: 200,
});
let escape = Event::KeyPressed {
key: Key::Named(NamedKey::Escape),
modifiers: Default::default(),
};
assert_eq!(scaffold.on_event(&escape), EventResult::Handled);
assert_eq!(
fired.get(),
1,
"a plain panel lets Escape close the surface"
);
reset_layout_runtime();
let untouched = Rc::new(Cell::new(0u32));
let u = untouched.clone();
let field = crate::input::Input::new(
reactive_core::signal(String::new()),
LayoutStyle::new().width(100.0).height(30.0),
|| TextStyle::new(14.0, Color::BLACK),
)
.unwrap()
.autofocus();
let mut focused = SurfaceScaffold::new(
Edge::Top,
AlignItems::CENTER,
(20, 20, 20, 20),
Some(DEFAULT_SCRIM),
Some(Rc::new(move || u.set(u.get() + 1))),
box_item(field),
)
.unwrap();
focused.on_event(&Event::WindowResized {
width: 200,
height: 200,
});
focused.on_event(&press(100.0, 40.0));
focused.on_event(&escape);
assert_eq!(
untouched.get(),
0,
"a field that claims Escape to release its own focus keeps the surface up"
);
}
#[test]
fn cross_axis_margin_insets_the_panel_from_the_side_edges() {
reset_layout_runtime();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
let mut scaffold = SurfaceScaffold::new(
Edge::Top,
AlignItems::START,
(30, 10, 10, 10),
Some(DEFAULT_SCRIM),
Some(Rc::new(move || f.set(f.get() + 1))),
panel(),
)
.unwrap();
scaffold.on_event(&Event::WindowResized {
width: 200,
height: 200,
});
scaffold.on_event(&press(50.0, 40.0));
assert_eq!(
fired.get(),
0,
"a press on the inset panel must not dismiss"
);
scaffold.on_event(&press(4.0, 40.0));
assert_eq!(
fired.get(),
1,
"a press in the left gap (before the inset) dismisses"
);
scaffold.on_event(&press(150.0, 40.0));
assert_eq!(fired.get(), 2, "a press in the right gap dismisses");
}
#[test]
fn no_dismiss_when_not_configured() {
reset_layout_runtime();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
let mut scaffold = SurfaceScaffold::new(
Edge::Top,
AlignItems::CENTER,
(0, 0, 0, 0),
Some(DEFAULT_SCRIM),
None,
panel(),
)
.unwrap();
let _ = &f;
scaffold.on_event(&Event::WindowResized {
width: 200,
height: 200,
});
scaffold.on_event(&press(10.0, 190.0));
assert_eq!(
fired.get(),
0,
"no dismiss must fire when dismiss_on_outside is off"
);
}
#[test]
fn enter_transform_fade_is_opacity_only() {
assert_eq!(enter_transform(EnterMotion::Fade, 0.0), (IDENTITY, 0.0));
assert_eq!(enter_transform(EnterMotion::Fade, 0.5), (IDENTITY, 0.5));
assert_eq!(enter_transform(EnterMotion::Fade, 1.0), (IDENTITY, 1.0));
}
#[test]
fn enter_transform_slide_offsets_from_edge_then_settles() {
let (m, o) = enter_transform(EnterMotion::Slide(Edge::Top), 0.0);
assert_eq!(o, 0.0);
assert_eq!(m[5], -SLIDE_DISTANCE, "top slides down from above");
assert_eq!(
enter_transform(EnterMotion::Slide(Edge::Top), 1.0),
(IDENTITY, 1.0)
);
assert_eq!(
enter_transform(EnterMotion::Slide(Edge::Bottom), 0.0).0[5],
SLIDE_DISTANCE
);
assert_eq!(
enter_transform(EnterMotion::Slide(Edge::Left), 0.0).0[4],
-SLIDE_DISTANCE
);
assert_eq!(
enter_transform(EnterMotion::Slide(Edge::Right), 0.0).0[4],
SLIDE_DISTANCE
);
}
}