use smol_str::SmolStr;
use std::marker::PhantomData;
use crate::animation::{Curve, Spring};
use crate::debugger::SourceLocation;
use crate::ui::event::EventResult;
use crate::ui::{Event, View};
use crate::{Context, Node};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MorphId(pub SmolStr);
impl MorphId {
pub fn new(id: impl Into<SmolStr>) -> Self {
Self(id.into())
}
}
impl From<&'static str> for MorphId {
fn from(s: &'static str) -> Self {
Self(SmolStr::new_static(s))
}
}
impl From<String> for MorphId {
fn from(s: String) -> Self {
Self(SmolStr::new(s))
}
}
impl From<SmolStr> for MorphId {
fn from(s: SmolStr) -> Self {
Self(s)
}
}
impl std::fmt::Display for MorphId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MorphTransition {
pub curve: Curve,
pub cross_fade_start: f32,
pub cross_fade_end: f32,
pub morph_effects: bool,
}
impl Default for MorphTransition {
fn default() -> Self {
Self {
curve: Curve::spring(Spring::gentle()),
cross_fade_start: 0.35,
cross_fade_end: 0.65,
morph_effects: true,
}
}
}
impl MorphTransition {
pub fn new() -> Self {
Self::default()
}
pub fn spring(mut self, spring: Spring) -> Self {
self.curve = Curve::spring(spring);
self
}
pub fn curve(mut self, curve: Curve) -> Self {
self.curve = curve;
self
}
pub fn cross_fade_window(mut self, start: f32, end: f32) -> Self {
self.cross_fade_start = start.clamp(0.0, 1.0);
self.cross_fade_end = end.clamp(self.cross_fade_start, 1.0);
self
}
pub fn morph_effects(mut self, morph: bool) -> Self {
self.morph_effects = morph;
self
}
}
#[derive(Clone, Debug)]
pub struct MorphInfo {
pub node: Node,
pub id: MorphId,
pub transition: MorphTransition,
}
#[derive(Clone, Debug)]
pub struct MorphPair {
pub id: MorphId,
pub source: MorphInfo,
pub target: MorphInfo,
}
pub struct Morphable<Id, V> {
pub(crate) id: Id,
pub(crate) view: V,
pub(crate) transition: MorphTransition,
pub(crate) source_loc: Option<SourceLocation>,
}
#[track_caller]
pub fn morphable<Id: Into<MorphId>, V>(id: Id, view: V) -> Morphable<MorphId, V> {
Morphable {
id: id.into(),
view,
transition: MorphTransition::default(),
source_loc: Some(SourceLocation::here("Morphable")),
}
}
impl<Id, V> Morphable<Id, V> {
pub fn spring(mut self, spring: Spring) -> Self {
self.transition.curve = Curve::spring(spring);
self
}
pub fn curve(mut self, curve: Curve) -> Self {
self.transition.curve = curve;
self
}
pub fn cross_fade_window(mut self, start: f32, end: f32) -> Self {
self.transition = self.transition.cross_fade_window(start, end);
self
}
pub fn transition(mut self, transition: MorphTransition) -> Self {
self.transition = transition;
self
}
}
pub struct MorphElement<V: View<State>, State> {
pub(crate) node: Node,
pub(crate) inner_el: V::Element,
pub(crate) id: MorphId,
_marker: PhantomData<State>,
}
impl<State, V: View<State>> View<State> for Morphable<MorphId, V> {
type Element = MorphElement<V, State>;
type Message = V::Message;
fn build(&self, ctx: &mut Context) -> Self::Element {
let inner_el = self.view.build(ctx);
let node = self.view.get_node(&inner_el);
if let Some(loc) = self.source_loc {
ctx.set_node_source(node, loc);
}
ctx.register_morph_node(node, self.id.clone(), self.transition.clone());
MorphElement {
node,
inner_el,
id: self.id.clone(),
_marker: PhantomData,
}
}
fn rebuild(&self, prev: &Self, ctx: &mut Context, element: &mut Self::Element) {
self.view.rebuild(&prev.view, ctx, &mut element.inner_el);
let updated_node = self.view.get_node(&element.inner_el);
if self.id != element.id || updated_node != element.node {
ctx.unregister_morph_node(element.node);
element.id = self.id.clone();
element.node = updated_node;
ctx.register_morph_node(element.node, self.id.clone(), self.transition.clone());
}
}
fn rebuild_with_parent(
&self,
prev: &Self,
ctx: &mut Context,
element: &mut Self::Element,
parent: Node,
next_sibling: Option<Node>,
) {
self.view
.rebuild_with_parent(&prev.view, ctx, &mut element.inner_el, parent, next_sibling);
let updated_node = self.view.get_node(&element.inner_el);
if self.id != element.id || updated_node != element.node {
ctx.unregister_morph_node(element.node);
element.id = self.id.clone();
element.node = updated_node;
ctx.register_morph_node(element.node, self.id.clone(), self.transition.clone());
}
}
fn teardown(&self, ctx: &mut Context, element: &mut Self::Element) {
ctx.unregister_morph_node(element.node);
self.view.teardown(ctx, &mut element.inner_el);
}
fn get_node(&self, element: &Self::Element) -> Node {
element.node
}
fn handle_event(
&self,
element: &mut Self::Element,
state: &State,
event: Event,
ctx: &mut Context,
) -> (EventResult, Option<Self::Message>) {
self.view
.handle_event(&mut element.inner_el, state, event, ctx)
}
}
pub trait MorphViewExt: Sized {
fn morph<Id: Into<MorphId>>(self, id: Id) -> Morphable<MorphId, Self> {
morphable(id, self)
}
}
impl<V> MorphViewExt for V {}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::Spring;
use crate::ui::widgets::text;
#[test]
fn test_morph_id_and_transition_builder() {
let id1: MorphId = "hero-image".into();
let id2: MorphId = String::from("hero-image").into();
let id3: MorphId = SmolStr::new("hero-image").into();
assert_eq!(id1, id2);
assert_eq!(id2, id3);
let trans = MorphTransition::default()
.spring(Spring::bouncy())
.cross_fade_window(0.2, 0.8)
.morph_effects(false);
assert_eq!(trans.cross_fade_start, 0.2);
assert_eq!(trans.cross_fade_end, 0.8);
assert!(!trans.morph_effects);
}
#[test]
fn test_morphable_registration_and_pair_discovery() {
let mut ctx = Context::new();
let v1 = text::<_, ()>("Card").morph("shared-card");
let v2 = text::<_, ()>("Other").morph("unique-1");
let v3 = text::<_, ()>("Card Expanded").morph("shared-card");
let v4 = text::<_, ()>("Another").morph("unique-2");
let mut el1 = View::<()>::build(&v1, &mut ctx);
let mut el2 = View::<()>::build(&v2, &mut ctx);
let mut el3 = View::<()>::build(&v3, &mut ctx);
let mut el4 = View::<()>::build(&v4, &mut ctx);
let root_a = ctx.create_node();
root_a.append(&mut ctx, el1.node);
root_a.append(&mut ctx, el2.node);
let root_b = ctx.create_node();
root_b.append(&mut ctx, el3.node);
root_b.append(&mut ctx, el4.node);
let pairs = ctx.find_morph_pairs(root_a, root_b);
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0].id, MorphId::new("shared-card"));
assert_eq!(pairs[0].source.node, el1.node);
assert_eq!(pairs[0].target.node, el3.node);
View::<()>::teardown(&v1, &mut ctx, &mut el1);
View::<()>::teardown(&v2, &mut ctx, &mut el2);
View::<()>::teardown(&v3, &mut ctx, &mut el3);
View::<()>::teardown(&v4, &mut ctx, &mut el4);
let pairs_after = ctx.find_morph_pairs(root_a, root_b);
assert_eq!(pairs_after.len(), 0);
}
#[test]
fn test_morphable_zero_overhead_and_constraint_preservation() {
use crate::ui::style::ViewStyleExt;
use crate::{Size, Style};
let mut ctx = Context::new();
let styled_box = text::<_, ()>("Album Cover")
.style(
Style::new()
.width(Size::Fill)
.aspect_ratio(1.0)
.bg_color(crate::rgb!(255, 0, 0)),
)
.morph("album-cover");
let el = View::<()>::build(&styled_box, &mut ctx);
let node = View::<()>::get_node(&styled_box, &el);
let cons = node.get_constraints(&ctx).unwrap();
assert_eq!(cons.width, Size::Fill);
assert_eq!(cons.aspect_ratio, 1.0);
let eff = ctx.effects.get(&node).unwrap();
assert_eq!(eff.background_color, crate::rgb!(255, 0, 0));
}
}