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 +``` +*/