Transition: Introduce in-out to allow writing symmetry animation (#8509)

This commit is contained in:
Tasuku Suzuki 2025-05-26 16:17:22 +09:00 committed by GitHub
parent c18b3cf02f
commit 83db461f63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 168 additions and 37 deletions

View file

@ -543,12 +543,19 @@ impl From<Type> for PropertyDeclaration {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TransitionDirection {
In,
Out,
InOut,
}
#[derive(Debug, Clone)]
pub struct TransitionPropertyAnimation {
/// The state id as computed in lower_state
pub state_id: i32,
/// false for 'to', true for 'out'
pub is_out: bool,
/// The direction of the transition
pub direction: TransitionDirection,
/// The content of the `animation` object
pub animation: ElementRc,
}
@ -557,13 +564,42 @@ impl TransitionPropertyAnimation {
/// Return an expression which returns a boolean which is true if the transition is active.
/// The state argument is an expression referencing the state property of type StateInfo
pub fn condition(&self, state: Expression) -> Expression {
Expression::BinaryExpression {
lhs: Box::new(Expression::StructFieldAccess {
base: Box::new(state),
name: (if self.is_out { "previous-state" } else { "current-state" }).into(),
}),
rhs: Box::new(Expression::NumberLiteral(self.state_id as _, Unit::None)),
op: '=',
match self.direction {
TransitionDirection::In => Expression::BinaryExpression {
lhs: Box::new(Expression::StructFieldAccess {
base: Box::new(state),
name: "current-state".into(),
}),
rhs: Box::new(Expression::NumberLiteral(self.state_id as _, Unit::None)),
op: '=',
},
TransitionDirection::Out => Expression::BinaryExpression {
lhs: Box::new(Expression::StructFieldAccess {
base: Box::new(state),
name: "previous-state".into(),
}),
rhs: Box::new(Expression::NumberLiteral(self.state_id as _, Unit::None)),
op: '=',
},
TransitionDirection::InOut => Expression::BinaryExpression {
lhs: Box::new(Expression::BinaryExpression {
lhs: Box::new(Expression::StructFieldAccess {
base: Box::new(state.clone()),
name: "current-state".into(),
}),
rhs: Box::new(Expression::NumberLiteral(self.state_id as _, Unit::None)),
op: '=',
}),
rhs: Box::new(Expression::BinaryExpression {
lhs: Box::new(Expression::StructFieldAccess {
base: Box::new(state),
name: "previous-state".into(),
}),
rhs: Box::new(Expression::NumberLiteral(self.state_id as _, Unit::None)),
op: '=',
}),
op: '|',
},
}
}
}
@ -601,7 +637,7 @@ impl Clone for PropertyAnimation {
.iter()
.map(|t| TransitionPropertyAnimation {
state_id: t.state_id,
is_out: t.is_out,
direction: t.direction,
animation: deep_clone(&t.animation),
})
.collect(),
@ -2447,8 +2483,7 @@ pub struct State {
#[derive(Debug, Clone)]
pub struct Transition {
/// false for 'to', true for 'out'
pub is_out: bool,
pub direction: TransitionDirection,
pub state_id: SmolStr,
pub property_animations: Vec<(NamedReference, SourceLocation, ElementRc)>,
pub node: syntax_nodes::Transition,
@ -2464,8 +2499,21 @@ impl Transition {
if let Some(star) = trs.child_token(SyntaxKind::Star) {
diag.push_error("catch-all not yet implemented".into(), &star);
};
let direction_text = trs
.first_child_or_token()
.and_then(|t| t.as_token().map(|tok| tok.text().to_string()))
.unwrap_or_default();
Transition {
is_out: parser::identifier_text(&trs).unwrap_or_default() == "out",
direction: match direction_text.as_str() {
"in" => TransitionDirection::In,
"out" => TransitionDirection::Out,
"in-out" => TransitionDirection::InOut,
"in_out" => TransitionDirection::InOut,
_ => {
unreachable!("Unknown transition direction: '{}'", direction_text);
}
},
state_id: trs
.DeclaredIdentifier()
.and_then(|x| parser::identifier_text(&x))

View file

@ -414,7 +414,7 @@ declare_syntax! {
StatePropertyChange -> [ QualifiedName, BindingExpression ],
/// `transitions: [...]`
Transitions -> [*Transition],
/// There is an identifier "in" or "out", the DeclaredIdentifier is the state name
/// There is an identifier "in", "out", "in-out", the DeclaredIdentifier is the state name
Transition -> [?DeclaredIdentifier, *PropertyAnimation],
/// Export a set of declared components by name
ExportsList -> [ *ExportSpecifier, ?Component, *StructDeclaration, ?ExportModule, *EnumDeclaration ],

View file

@ -535,10 +535,10 @@ fn parse_state(p: &mut impl Parser) -> bool {
SyntaxKind::Eof => return false,
_ => {
if p.nth(1).kind() == SyntaxKind::LBrace
&& matches!(p.peek().as_str(), "in" | "out")
&& matches!(p.peek().as_str(), "in" | "out" | "in-out" | "in_out")
{
let mut p = p.start_node(SyntaxKind::Transition);
p.consume(); // "in" or "out"
p.consume(); // "in", "out" or "in-out"
p.expect(SyntaxKind::LBrace);
if !parse_transition_inner(&mut *p) {
return false;
@ -562,7 +562,7 @@ fn parse_state(p: &mut impl Parser) -> bool {
#[cfg_attr(test, parser_test)]
/// ```test,Transitions
/// transitions []
/// transitions [in checked: {animate x { duration: 88ms; }} out checked: {animate x { duration: 88ms; }}]
/// transitions [in checked: {animate x { duration: 88ms; }} out checked: {animate x { duration: 88ms; }} in-out checked: {animate x { duration: 88ms; }}]
/// ```
fn parse_transitions(p: &mut impl Parser) {
debug_assert_eq!(p.peek().as_str(), "transitions");
@ -578,14 +578,15 @@ fn parse_transitions(p: &mut impl Parser) {
/// in pressed : {}
/// in pressed: { animate x { duration: 88ms; } }
/// out pressed: { animate x { duration: 88ms; } }
/// in-out pressed: { animate x { duration: 88ms; } }
/// ```
fn parse_transition(p: &mut impl Parser) -> bool {
if !matches!(p.peek().as_str(), "in" | "out") {
p.error("Expected 'in' or 'out' to declare a transition");
if !matches!(p.peek().as_str(), "in" | "out" | "in-out" | "in_out") {
p.error("Expected 'in', 'out', or 'in-out' to declare a transition");
return false;
}
let mut p = p.start_node(SyntaxKind::Transition);
p.consume(); // "in" or "out"
p.consume(); // "in", "out" or "in-out"
{
let mut p = p.start_node(SyntaxKind::DeclaredIdentifier);
p.expect(SyntaxKind::Identifier);

View file

@ -511,7 +511,7 @@ fn duplicate_property_animation(
.iter()
.map(|a| TransitionPropertyAnimation {
state_id: a.state_id,
is_out: a.is_out,
direction: a.direction,
animation: duplicate_element_with_mapping(
&a.animation,
mapping,
@ -568,7 +568,7 @@ fn duplicate_transition(
priority_delta: i32,
) -> Transition {
Transition {
is_out: t.is_out,
direction: t.direction,
state_id: t.state_id.clone(),
property_animations: t
.property_animations

View file

@ -161,7 +161,7 @@ fn lower_transitions_in_element(
let t = TransitionPropertyAnimation {
state_id: *state,
is_out: transition.is_out,
direction: transition.direction,
animation,
};
props.entry(p).or_insert_with(|| (span.clone(), vec![])).1.push(t);

View file

@ -38,7 +38,10 @@ export TestCase := Rectangle {
animate border { duration: 120ms; }
animate color, text.text { duration: 300ms; }
/// ^error{'text.text' is not a property that can be animated}
}
in-out checked: {
animate color { duration: 100ms; }
}
]
@ -57,6 +60,10 @@ export component NewSyntax {
color: blue; // same as root.color
text.color: red;
border: 42;
in-out {
animate color { duration: 100ms; }
}
}
pressed when touch.pressed: {
color: green;

View file

@ -453,7 +453,7 @@ impl Snapshotter {
.transitions
.iter()
.map(|t| object_tree::Transition {
is_out: t.is_out,
direction: t.direction,
state_id: t.state_id.clone(),
property_animations: t
.property_animations
@ -570,7 +570,7 @@ impl Snapshotter {
.iter()
.map(|tpa| object_tree::TransitionPropertyAnimation {
state_id: tpa.state_id,
is_out: tpa.is_out,
direction: tpa.direction,
animation: self.create_and_snapshot_element(&tpa.animation),
})
.collect(),