diff --git a/CHANGELOG.md b/CHANGELOG.md
index df60a09a7d9..e7f0dd8ddee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ All notable changes to this project are documented in this file.
- Added support for rotation and scaling of all elements and their children
- GridLayout: allow access to row/col/rowspan/colspan properties from other bindings
- Added `Math.sign()` (#9444)
+ - `animate`: Added `enabled` boolean to toggle animations on/off (defaults to `true`).
### Widgets
diff --git a/docs/astro/src/content/docs/guide/language/coding/animation.mdx b/docs/astro/src/content/docs/guide/language/coding/animation.mdx
index b43761160aa..1c76d3ac491 100644
--- a/docs/astro/src/content/docs/guide/language/coding/animation.mdx
+++ b/docs/astro/src/content/docs/guide/language/coding/animation.mdx
@@ -71,3 +71,7 @@ Can be any of the following. See [`easings.net`](https://easings.net/) for a vis
Use this to set or change the direction of the animation.
+### enabled
+
+Controls whether the animation runs. When set to `false`, the property value changes immediately to its target without delay, easing, or iterations.
+
diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint
index 1d58af7d3b9..b331a8404e3 100644
--- a/internal/compiler/builtins.slint
+++ b/internal/compiler/builtins.slint
@@ -548,6 +548,7 @@ component PropertyAnimation {
in property direction;
in property easing;
in property iteration-count: 1.0;
+ in property enabled: true;
//-is_non_item_type
}
diff --git a/internal/compiler/llr/lower_expression.rs b/internal/compiler/llr/lower_expression.rs
index 0c058ffe930..bbad712975d 100644
--- a/internal/compiler/llr/lower_expression.rs
+++ b/internal/compiler/llr/lower_expression.rs
@@ -499,7 +499,13 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &mut ExpressionLoweringCtx<'_
values: animation_fields()
.map(|(k, ty)| {
let e = a.borrow().bindings.get(&k).map_or_else(
- || llr_Expression::default_value_for_type(&ty).unwrap(),
+ || {
+ if k == "enabled" {
+ llr_Expression::BoolLiteral(true)
+ } else {
+ llr_Expression::default_value_for_type(&ty).unwrap()
+ }
+ },
|v| lower_expression(&v.borrow().expression, ctx),
);
(k, e)
@@ -519,6 +525,7 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &mut ExpressionLoweringCtx<'_
),
(SmolStr::new_static("easing"), Type::Easing),
(SmolStr::new_static("delay"), Type::Int32),
+ (SmolStr::new_static("enabled"), Type::Bool),
])
}
@@ -538,8 +545,21 @@ pub fn lower_animation(a: &PropertyAnimation, ctx: &mut ExpressionLoweringCtx<'_
name: "state".into(),
value: Box::new(lower_expression(state_ref, ctx)),
};
- let animation_ty = Type::Struct(animation_ty());
- let mut get_anim = llr_Expression::default_value_for_type(&animation_ty).unwrap();
+ let anim_struct_ty = animation_ty();
+ let animation_ty = Type::Struct(anim_struct_ty.clone());
+ let mut get_anim = llr_Expression::Struct {
+ ty: anim_struct_ty,
+ values: animation_fields()
+ .map(|(k, ty)| {
+ let e = if k == "enabled" {
+ llr_Expression::BoolLiteral(true)
+ } else {
+ llr_Expression::default_value_for_type(&ty).unwrap()
+ };
+ (k, e)
+ })
+ .collect(),
+ };
for tr in animations.iter().rev() {
let condition = lower_expression(
&tr.condition(tree_Expression::ReadLocalVariable {
diff --git a/internal/core/items.rs b/internal/core/items.rs
index 7d8ff8ff55e..160e593bc4b 100644
--- a/internal/core/items.rs
+++ b/internal/core/items.rs
@@ -1158,6 +1158,8 @@ pub struct PropertyAnimation {
pub direction: AnimationDirection,
#[rtti_field]
pub easing: crate::animations::EasingCurve,
+ #[rtti_field]
+ pub enabled: bool,
}
impl Default for PropertyAnimation {
@@ -1170,6 +1172,7 @@ impl Default for PropertyAnimation {
iteration_count: 1.,
direction: Default::default(),
easing: Default::default(),
+ enabled: true,
}
}
}
diff --git a/internal/core/properties/properties_animations.rs b/internal/core/properties/properties_animations.rs
index fb94324366f..1ec84888c7e 100644
--- a/internal/core/properties/properties_animations.rs
+++ b/internal/core/properties/properties_animations.rs
@@ -31,6 +31,11 @@ impl PropertyValueAnimationData {
}
pub fn compute_interpolated_value(&mut self) -> (T, bool) {
+ // If animation is disabled, immediately return the target value
+ if !self.details.enabled {
+ return (self.to_value.clone(), true);
+ }
+
let new_tick = crate::animations::current_tick();
let mut time_progress = new_tick.duration_since(self.start_time).as_millis() as u64;
let reversed = |iteration: u64| -> bool {
diff --git a/tests/cases/properties/animation_enabled.slint b/tests/cases/properties/animation_enabled.slint
new file mode 100644
index 00000000000..f6edad89b38
--- /dev/null
+++ b/tests/cases/properties/animation_enabled.slint
@@ -0,0 +1,209 @@
+// Copyright © SixtyFPS GmbH
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+TestCase := Rectangle {
+ // Test static enabled values
+ property value-with-default: 100;
+ animate value-with-default {
+ duration: 1000ms;
+ // enabled defaults to true
+ }
+
+ property value-disabled: 100;
+ animate value-disabled {
+ duration: 1000ms;
+ enabled: false;
+ }
+
+ property value-enabled: 100;
+ animate value-enabled {
+ duration: 1000ms;
+ enabled: true;
+ }
+
+ // Test dynamic enabled value
+ in-out property animation-enabled: true;
+ in-out property value-dynamic: 100;
+ animate value-dynamic {
+ duration: 1000ms;
+ enabled: animation-enabled;
+ }
+
+ init => {
+ // Change enabled to false in init
+ animation-enabled = false;
+ }
+}
+
+/*
+
+```rust
+let instance = TestCase::new().unwrap();
+
+// Test static enabled values
+assert_eq!(instance.get_value_with_default(), 100);
+assert_eq!(instance.get_value_disabled(), 100);
+assert_eq!(instance.get_value_enabled(), 100);
+
+instance.set_value_with_default(200);
+instance.set_value_disabled(200);
+instance.set_value_enabled(200);
+
+// No time has elapsed yet
+assert_eq!(instance.get_value_with_default(), 100);
+assert_eq!(instance.get_value_disabled(), 200); // disabled: no animation
+assert_eq!(instance.get_value_enabled(), 100);
+
+// Half the animation
+slint_testing::mock_elapsed_time(500);
+assert_eq!(instance.get_value_with_default(), 150); // default enabled: animated
+assert_eq!(instance.get_value_disabled(), 200); // disabled: no animation
+assert_eq!(instance.get_value_enabled(), 150); // enabled: animated
+
+// Full animation
+slint_testing::mock_elapsed_time(500);
+assert_eq!(instance.get_value_with_default(), 200);
+assert_eq!(instance.get_value_disabled(), 200);
+assert_eq!(instance.get_value_enabled(), 200);
+
+// Test dynamic enabled value
+assert_eq!(instance.get_value_dynamic(), 100);
+assert_eq!(instance.get_animation_enabled(), false); // Changed in init
+
+// Set new value - should NOT animate because enabled was set to false in init
+instance.set_value_dynamic(200);
+assert_eq!(instance.get_value_dynamic(), 200); // Immediately changes, no animation
+
+// Enable animation
+instance.set_animation_enabled(true);
+instance.set_value_dynamic(300);
+assert_eq!(instance.get_value_dynamic(), 200); // Animation enabled, should animate
+
+// Half the animation
+slint_testing::mock_elapsed_time(500);
+assert_eq!(instance.get_value_dynamic(), 250); // Animated to halfway
+
+// Full animation
+slint_testing::mock_elapsed_time(500);
+assert_eq!(instance.get_value_dynamic(), 300);
+
+// Disable animation again
+instance.set_animation_enabled(false);
+instance.set_value_dynamic(100);
+assert_eq!(instance.get_value_dynamic(), 100); // Immediately changes, no animation
+```
+
+
+```cpp
+auto handle = TestCase::create();
+const TestCase &instance = *handle;
+
+// Test static enabled values
+assert_eq(instance.get_value_with_default(), 100);
+assert_eq(instance.get_value_disabled(), 100);
+assert_eq(instance.get_value_enabled(), 100);
+
+instance.set_value_with_default(200);
+instance.set_value_disabled(200);
+instance.set_value_enabled(200);
+
+// No time has elapsed yet
+assert_eq(instance.get_value_with_default(), 100);
+assert_eq(instance.get_value_disabled(), 200); // disabled: no animation
+assert_eq(instance.get_value_enabled(), 100);
+
+// Half the animation
+slint_testing::mock_elapsed_time(500);
+assert_eq(instance.get_value_with_default(), 150); // default enabled: animated
+assert_eq(instance.get_value_disabled(), 200); // disabled: no animation
+assert_eq(instance.get_value_enabled(), 150); // enabled: animated
+
+// Full animation
+slint_testing::mock_elapsed_time(500);
+assert_eq(instance.get_value_with_default(), 200);
+assert_eq(instance.get_value_disabled(), 200);
+assert_eq(instance.get_value_enabled(), 200);
+
+// Test dynamic enabled value
+assert_eq(instance.get_value_dynamic(), 100);
+assert_eq(instance.get_animation_enabled(), false); // Changed in init
+
+// Set new value - should NOT animate because enabled was set to false in init
+instance.set_value_dynamic(200);
+assert_eq(instance.get_value_dynamic(), 200); // Immediately changes, no animation
+
+// Enable animation
+instance.set_animation_enabled(true);
+instance.set_value_dynamic(300);
+assert_eq(instance.get_value_dynamic(), 200); // Animation enabled, should animate
+
+// Half the animation
+slint_testing::mock_elapsed_time(500);
+assert_eq(instance.get_value_dynamic(), 250); // Animated to halfway
+
+// Full animation
+slint_testing::mock_elapsed_time(500);
+assert_eq(instance.get_value_dynamic(), 300);
+
+// Disable animation again
+instance.set_animation_enabled(false);
+instance.set_value_dynamic(100);
+assert_eq(instance.get_value_dynamic(), 100); // Immediately changes, no animation
+```
+
+```js
+var instance = new slint.TestCase({});
+
+// Test static enabled values
+assert.equal(instance.value_with_default, 100);
+assert.equal(instance.value_disabled, 100);
+assert.equal(instance.value_enabled, 100);
+
+instance.value_with_default = 200;
+instance.value_disabled = 200;
+instance.value_enabled = 200;
+
+// No time has elapsed yet
+assert.equal(instance.value_with_default, 100);
+assert.equal(instance.value_disabled, 200); // disabled: no animation
+assert.equal(instance.value_enabled, 100);
+
+// Half the animation
+slintlib.private_api.mock_elapsed_time(500);
+assert.equal(instance.value_with_default, 150); // default enabled: animated
+assert.equal(instance.value_disabled, 200); // disabled: no animation
+assert.equal(instance.value_enabled, 150); // enabled: animated
+
+// Full animation
+slintlib.private_api.mock_elapsed_time(500);
+assert.equal(instance.value_with_default, 200);
+assert.equal(instance.value_disabled, 200);
+assert.equal(instance.value_enabled, 200);
+
+// Test dynamic enabled value
+assert.equal(instance.value_dynamic, 100);
+assert.equal(instance.animation_enabled, false); // Changed in init
+
+// Set new value - should NOT animate because enabled was set to false in init
+instance.value_dynamic = 200;
+assert.equal(instance.value_dynamic, 200); // Immediately changes, no animation
+
+// Enable animation
+instance.animation_enabled = true;
+instance.value_dynamic = 300;
+assert.equal(instance.value_dynamic, 200); // Animation enabled, should animate
+
+// Half the animation
+slintlib.private_api.mock_elapsed_time(500);
+assert.equal(instance.value_dynamic, 250); // Animated to halfway
+
+// Full animation
+slintlib.private_api.mock_elapsed_time(500);
+assert.equal(instance.value_dynamic, 300);
+
+// Disable animation again
+instance.animation_enabled = false;
+instance.value_dynamic = 100;
+assert.equal(instance.value_dynamic, 100); // Immediately changes, no animation
+```
+*/