Timer Element

Closes #5724
This commit is contained in:
Olivier Goffart 2024-08-15 13:02:05 +02:00
parent 2643a327e8
commit a9f526491a
25 changed files with 599 additions and 14 deletions

View file

@ -7,6 +7,7 @@
#include <chrono>
#include <optional>
#include <slint_timer_internal.h>
namespace slint {
@ -59,6 +60,16 @@ struct Timer
void restart() { cbindgen_private::slint_timer_restart(id); }
/// Returns true if the timer is running; false otherwise.
bool running() const { return cbindgen_private::slint_timer_running(id); }
/// Returns the interval of the timer.
/// Returns `nullopt` if the timer is not running.
std::optional<std::chrono::milliseconds> interval() const
{
int64_t val = cbindgen_private::slint_timer_interval(id);
if (val < 0) {
return std::nullopt;
}
return std::chrono::milliseconds(val);
}
/// Call the callback after the given duration.
template<std::invocable F>

View file

@ -207,6 +207,7 @@ pub mod re_exports {
set_state_binding, ChangeTracker, Property, PropertyTracker, StateInfo,
};
pub use i_slint_core::slice::Slice;
pub use i_slint_core::timers::{Timer, TimerMode};
pub use i_slint_core::window::{
InputMethodRequest, WindowAdapter, WindowAdapterRc, WindowInner,
};

View file

@ -778,6 +778,57 @@ export component Example inherits Window {
}
```
## `Timer`
<!-- FIXME: Timer is not really an element so it doesn't really belong in the `Builtin Elements` section. -->
Timer is not an actual element visible in the tree, therefore it doesn't have the common properties such as `x`, `y`, `width`, `height`, etc.
It doesn't take room in a layout and cannot have any children or be inherited from.
The Timer pseudo-element is used to schedule a callback at a given interval.
The timer is only running when the `running` property is set to `true`. To stop or start the timer, simply set that property to `true` or `false`.
It can be also set to a binding.
When already running, the timer will be restarted if the `interval` property is changed.
Note that the default value for `running` is `true`, so if you don't specify it, it will be running.
### Properties
- **`interval`** (_in_ _duration_): The interval between timer ticks. (default value: `0ms`)
- **`running`** (_in_ _bool_): `true` if the timer is running. (default value: `true`)
### Callbacks
- **`triggered()`**: Invoked every time the timer ticks (every `interval`)
### Example
This example shows a timer that counts down from 10 to 0 every second:
```slint
import { Button } from "std-widgets.slint";
export component Example inherits Window {
property <int> value: 10;
timer := Timer {
interval: 1s;
running: true;
triggered() => {
value -= 1;
if (value == 0) {
self.running = false;
}
}
}
HorizontalLayout {
Text { text: value; }
Button {
text: "Reset";
clicked() => { value = 10; timer.running = true; }
}
}
}
```
## `TouchArea`
Use `TouchArea` to control what happens when the region it covers is touched or interacted with

View file

@ -325,7 +325,6 @@ component Close {
//-rust_type_constructor:slint::re_exports::PathElement::Close
//-cpp_type:slint::private_api::PathClose
//-is_non_item_type
//-is_non_item_type
}
export component Path {
@ -379,6 +378,15 @@ export component PopupWindow {
//show() is hardcoded in typeregister.rs
}
// Also not a real Item. Actually not an element at all
export component Timer {
in property <duration> interval;
callback triggered;
in property <bool> running: true;
//-is_non_item_type
//-disallow_global_types_as_child_elements
}
export component Dialog inherits WindowItem {}
component PropertyAnimation {

View file

@ -75,6 +75,7 @@ pub enum BuiltinFunction {
RegisterCustomFontByMemory,
RegisterBitmapFont,
Translate,
UpdateTimers,
}
#[derive(Debug, Clone)]
@ -307,6 +308,9 @@ impl BuiltinFunction {
BuiltinFunction::Use24HourFormat => {
Type::Function { return_type: Box::new(Type::Bool), args: vec![] }
}
BuiltinFunction::UpdateTimers => {
Type::Function { return_type: Box::new(Type::Void), args: vec![] }
}
}
}
@ -371,6 +375,7 @@ impl BuiltinFunction {
| BuiltinFunction::RegisterBitmapFont => false,
BuiltinFunction::Translate => false,
BuiltinFunction::Use24HourFormat => false,
BuiltinFunction::UpdateTimers => false,
}
}
@ -428,6 +433,7 @@ impl BuiltinFunction {
| BuiltinFunction::RegisterBitmapFont => false,
BuiltinFunction::Translate => true,
BuiltinFunction::Use24HourFormat => true,
BuiltinFunction::UpdateTimers => false,
}
}
}

View file

@ -1918,6 +1918,38 @@ fn generate_sub_component(
format!("self->change_tracker{idx}.init(self, [](auto self) {{ return {prop}; }}, []([[maybe_unused]] auto self, auto) {{ {code}; }});")
}));
if !component.timers.is_empty() {
let mut update_timers = vec!["auto self = this;".into()];
for (i, tmr) in component.timers.iter().enumerate() {
user_init.push(format!("self->update_timers();"));
let name = format!("timer{}", i);
let running = compile_expression(&tmr.running.borrow(), &ctx);
let interval = compile_expression(&tmr.interval.borrow(), &ctx);
let callback = compile_expression(&tmr.triggered.borrow(), &ctx);
update_timers.push(format!("if ({running}) {{"));
update_timers
.push(format!(" auto interval = std::chrono::milliseconds({interval});"));
update_timers.push(format!(
" if (!self->{name}.running() || *self->{name}.interval() != interval)"
));
update_timers.push(format!(" self->{name}.start(slint::TimerMode::Repeated, interval, [self] {{ {callback}; }});"));
update_timers.push(format!("}} else {{ self->{name}.stop(); }}").into());
target_struct.members.push((
field_access,
Declaration::Var(Var { ty: "slint::Timer".into(), name, ..Default::default() }),
));
}
target_struct.members.push((
field_access,
Declaration::Function(Function {
name: "update_timers".to_owned(),
signature: "() -> void".into(),
statements: Some(update_timers),
..Default::default()
}),
));
}
target_struct
.members
.extend(generate_functions(&component.functions, &ctx).map(|x| (Access::Public, x)));
@ -3445,6 +3477,9 @@ fn compile_builtin_function_call(
BuiltinFunction::Translate => {
format!("slint::private_api::translate({})", a.join(","))
}
BuiltinFunction::UpdateTimers => {
"self->update_timers()".into()
}
}
}

View file

@ -1064,6 +1064,41 @@ fn generate_sub_component(
quote!(usize::MAX)
};
let timer_names =
component.timers.iter().enumerate().map(|(idx, _)| format_ident!("timer{idx}"));
let update_timers = (!component.timers.is_empty()).then(|| {
let updt = component.timers.iter().enumerate().map(|(idx, tmr)| {
let ident = format_ident!("timer{idx}");
let interval = compile_expression(&tmr.interval.borrow(), &ctx);
let running = compile_expression(&tmr.running.borrow(), &ctx);
let callback = compile_expression(&tmr.triggered.borrow(), &ctx);
quote!(
if #running {
let interval = core::time::Duration::from_millis(#interval as u64);
let old_interval = self.#ident.interval();
if old_interval != Some(interval) || !self.#ident.running() {
let self_weak = self.self_weak.get().unwrap().clone();
self.#ident.start(sp::TimerMode::Repeated, interval, move || {
if let Some(self_rc) = self_weak.upgrade() {
let _self = self_rc.as_pin_ref();
#callback
}
});
}
} else {
self.#ident.stop();
}
)
});
user_init_code.push(quote!(_self.update_timers();));
quote!(
fn update_timers(self: ::core::pin::Pin<&Self>) {
let _self = self;
#(#updt)*
}
)
});
let pin_macro = if pinned_drop { quote!(#[pin_drop]) } else { quote!(#[pin]) };
quote!(
@ -1079,6 +1114,7 @@ fn generate_sub_component(
#(#declared_callbacks : sp::Callback<(#(#declared_callbacks_types,)*), #declared_callbacks_ret>,)*
#(#repeated_element_names : sp::Repeater<#repeated_element_components>,)*
#(#change_tracker_names : sp::ChangeTracker,)*
#(#timer_names : sp::Timer,)*
self_weak : sp::OnceCell<sp::VWeakMapped<sp::ItemTreeVTable, #inner_component_id>>,
#(parent : #parent_component_type,)*
globals: sp::OnceCell<sp::Rc<SharedGlobals>>,
@ -1214,6 +1250,8 @@ fn generate_sub_component(
}
}
#update_timers
#(#declared_functions)*
}
@ -2816,6 +2854,9 @@ fn compile_builtin_function_call(
panic!("internal error: invalid args to MapPointToWindow {:?}", arguments)
}
}
BuiltinFunction::UpdateTimers => {
quote!(_self.update_timers())
}
}
}

View file

@ -507,12 +507,18 @@ impl ElementType {
builtin.additional_accepted_child_types.keys().cloned().collect();
valid_children.sort();
return Err(format!(
"{} is not allowed within {}. Only {} are valid children",
name,
builtin.native_class.class_name,
valid_children.join(" ")
));
let err = if valid_children.is_empty() {
format!("{} cannot have children elements", builtin.native_class.class_name,)
} else {
format!(
"{} is not allowed within {}. Only {} are valid children",
name,
builtin.native_class.class_name,
valid_children.join(" ")
)
};
return Err(err);
}
}
_ => {}

View file

@ -241,6 +241,7 @@ pub struct SubComponent {
pub repeated: Vec<RepeatedElement>,
pub component_containers: Vec<ComponentContainerElement>,
pub popup_windows: Vec<PopupWindow>,
pub timers: Vec<Timer>,
pub sub_components: Vec<SubComponentInstance>,
/// The initial value or binding for properties.
/// This is ordered in the order they must be set.
@ -274,6 +275,13 @@ pub struct PopupWindow {
pub position: MutExpression,
}
#[derive(Debug)]
pub struct Timer {
pub interval: MutExpression,
pub running: MutExpression,
pub triggered: MutExpression,
}
#[derive(Debug, Clone)]
pub struct PropAnalysis {
/// Index in SubComponent::property_init for this property

View file

@ -4,12 +4,11 @@
use by_address::ByAddress;
use super::lower_expression::ExpressionContext;
use super::PopupWindow as llr_PopupWindow;
use crate::expression_tree::Expression as tree_Expression;
use crate::langtype::{ElementType, Type};
use crate::llr::item_tree::*;
use crate::namedreference::NamedReference;
use crate::object_tree::{Component, ElementRc, PopupWindow, PropertyAnalysis, PropertyVisibility};
use crate::object_tree::{self, Component, ElementRc, PropertyAnalysis, PropertyVisibility};
use crate::CompilerConfiguration;
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
@ -206,6 +205,7 @@ fn lower_sub_component(
repeated: Default::default(),
component_containers: Default::default(),
popup_windows: Default::default(),
timers: Default::default(),
sub_components: Default::default(),
property_init: Default::default(),
change_callbacks: Default::default(),
@ -447,6 +447,9 @@ fn lower_sub_component(
.map(|popup| lower_popup_component(&popup, &ctx, &compiler_config))
.collect();
sub_component.timers =
component.timers.borrow().iter().map(|t| lower_timer(&t, &ctx)).collect();
crate::generator::for_each_const_properties(component, |elem, n| {
let x = ctx.map_property_reference(&NamedReference::new(elem, n));
// ensure that all const properties have analysis
@ -645,10 +648,10 @@ fn lower_component_container(
}
fn lower_popup_component(
popup: &PopupWindow,
popup: &object_tree::PopupWindow,
ctx: &ExpressionContext,
compiler_config: &CompilerConfiguration,
) -> llr_PopupWindow {
) -> PopupWindow {
let sc = lower_sub_component(&popup.component, ctx.state, Some(ctx), compiler_config);
let item_tree = ItemTree {
tree: make_tree(ctx.state, &popup.component.root_element, &sc, &[]),
@ -677,7 +680,22 @@ fn lower_popup_component(
],
);
llr_PopupWindow { item_tree, position: position.into() }
PopupWindow { item_tree, position: position.into() }
}
fn lower_timer(timer: &object_tree::Timer, ctx: &ExpressionContext) -> Timer {
Timer {
interval: super::Expression::PropertyReference(ctx.map_property_reference(&timer.interval))
.into(),
running: super::Expression::PropertyReference(ctx.map_property_reference(&timer.running))
.into(),
// TODO: this calls a callback instead of inlining the callback code directly
triggered: super::Expression::CallBackCall {
callback: ctx.map_property_reference(&timer.triggered),
arguments: vec![],
}
.into(),
}
}
fn lower_global(

View file

@ -129,6 +129,12 @@ pub fn count_property_use(root: &CompilationUnit) {
);
popup.position.borrow().visit_recursive(&mut |e| visit_expression(e, &popup_ctx))
}
// 11. timer
for timer in &sc.timers {
timer.interval.borrow().visit_recursive(&mut |e| visit_expression(e, &ctx));
timer.running.borrow().visit_recursive(&mut |e| visit_expression(e, &ctx));
timer.triggered.borrow().visit_recursive(&mut |e| visit_expression(e, &ctx));
}
});
// TODO: only visit used function

View file

@ -123,6 +123,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST,
BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST,
BuiltinFunction::UpdateTimers => 10,
}
}

View file

@ -277,6 +277,13 @@ pub struct PopupWindow {
pub parent_element: ElementRc,
}
#[derive(Debug, Clone)]
pub struct Timer {
pub interval: NamedReference,
pub triggered: NamedReference,
pub running: NamedReference,
}
type ChildrenInsertionPoint = (ElementRc, usize, syntax_nodes::ChildrenPlaceholder);
/// Used sub types for a root component
@ -347,6 +354,7 @@ pub struct Component {
pub init_code: RefCell<InitCode>,
pub popup_windows: RefCell<Vec<PopupWindow>>,
pub timers: RefCell<Vec<Timer>>,
/// This component actually inherits PopupWindow (although that has been changed to a Window by the lower_popups pass)
pub inherits_popup_window: Cell<bool>,
@ -2321,6 +2329,11 @@ pub fn visit_all_named_references(
vis(&mut p.x);
vis(&mut p.y);
});
compo.timers.borrow_mut().iter_mut().for_each(|t| {
vis(&mut t.interval);
vis(&mut t.triggered);
vis(&mut t.running);
});
}
compo
},

View file

@ -36,6 +36,7 @@ mod lower_shadows;
mod lower_states;
mod lower_tabwidget;
mod lower_text_input_interface;
mod lower_timers;
pub mod materialize_fake_properties;
pub mod move_declarations;
mod optimize_useless_rectangles;
@ -98,6 +99,7 @@ pub async fn run_passes(
repeater_component::process_repeater_components(component);
lower_popups::lower_popups(component, &doc.local_registry, diag);
collect_init_code::collect_init_code(component);
lower_timers::lower_timers(component, diag);
});
inlining::inline(doc, inlining::InlineSelection::InlineOnlyRequiredComponents, diag);

View file

@ -282,7 +282,11 @@ fn inline_element(
fixup_reference(&mut p.x, &mapping);
fixup_reference(&mut p.y, &mapping);
}
for t in root_component.timers.borrow_mut().iter_mut() {
fixup_reference(&mut t.interval, &mapping);
fixup_reference(&mut t.running, &mapping);
fixup_reference(&mut t.triggered, &mapping);
}
// If some element were moved into PopupWindow, we need to report error if they are used outside of the popup window.
if !moved_into_popup.is_empty() {
recurse_elem_no_borrow(&root_component.root_element.clone(), &(), &mut |e, _| {
@ -393,6 +397,7 @@ fn duplicate_sub_component(
child_insertion_point: component_to_duplicate.child_insertion_point.clone(),
init_code: component_to_duplicate.init_code.clone(),
popup_windows: Default::default(),
timers: component_to_duplicate.timers.clone(),
exported_global_names: component_to_duplicate.exported_global_names.clone(),
private_properties: Default::default(),
inherits_popup_window: core::cell::Cell::new(false),
@ -413,6 +418,11 @@ fn duplicate_sub_component(
fixup_reference(&mut p.x, mapping);
fixup_reference(&mut p.y, mapping);
}
for t in new_component.timers.borrow_mut().iter_mut() {
fixup_reference(&mut t.interval, &mapping);
fixup_reference(&mut t.running, &mapping);
fixup_reference(&mut t.triggered, &mapping);
}
new_component
.root_constraints
.borrow_mut()

View file

@ -0,0 +1,68 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//! Passe that transform the Timer element into a timer in the Component
use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::{BuiltinFunction, Expression, NamedReference};
use crate::langtype::ElementType;
use crate::object_tree::*;
use std::rc::Rc;
pub fn lower_timers(component: &Rc<Component>, diag: &mut BuildDiagnostics) {
recurse_elem_including_sub_components_no_borrow(
component,
&None,
&mut |elem, parent_element: &Option<ElementRc>| {
let is_timer = matches!(&elem.borrow().base_type, ElementType::Builtin(base_type) if base_type.name == "Timer");
if is_timer {
lower_timer(elem, parent_element.as_ref(), diag);
}
Some(elem.clone())
},
)
}
fn lower_timer(
timer_element: &ElementRc,
parent_element: Option<&ElementRc>,
diag: &mut BuildDiagnostics,
) {
let parent_component = timer_element.borrow().enclosing_component.upgrade().unwrap();
let Some(parent_element) = parent_element else {
diag.push_error("A component cannot inherit from Timer".into(), &*timer_element.borrow());
return;
};
if Rc::ptr_eq(&parent_component.root_element, timer_element) {
diag.push_error(
"Timer cannot be directly repeated or conditional".into(),
&*timer_element.borrow(),
);
return;
}
// Remove the timer_element from its parent
let old_size = parent_element.borrow().children.len();
parent_element.borrow_mut().children.retain(|child| !Rc::ptr_eq(child, timer_element));
debug_assert_eq!(
parent_element.borrow().children.len() + 1,
old_size,
"Exactly one child must be removed (the timer itself)"
);
parent_component.optimized_elements.borrow_mut().push(timer_element.clone());
parent_component.timers.borrow_mut().push(Timer {
interval: NamedReference::new(timer_element, "interval"),
running: NamedReference::new(timer_element, "running"),
triggered: NamedReference::new(timer_element, "triggered"),
});
let update_timers = Expression::FunctionCall {
function: Expression::BuiltinFunctionReference(BuiltinFunction::UpdateTimers, None).into(),
arguments: vec![],
source_location: None,
};
let change_callbacks = &mut timer_element.borrow_mut().change_callbacks;
change_callbacks.entry("running".into()).or_default().borrow_mut().push(update_timers.clone());
change_callbacks.entry("interval".into()).or_default().borrow_mut().push(update_timers);
}

View file

@ -96,6 +96,11 @@ fn do_move_declarations(component: &Rc<Component>) {
fixup_reference(&mut p.y);
visit_all_named_references(&p.component, &mut fixup_reference)
});
component.timers.borrow_mut().iter_mut().for_each(|t| {
fixup_reference(&mut t.interval);
fixup_reference(&mut t.running);
fixup_reference(&mut t.triggered);
});
component.init_code.borrow_mut().iter_mut().for_each(|expr| {
visit_named_references_in_expression(expr, &mut fixup_reference);
});

View file

@ -0,0 +1,25 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component Def {
Timer {
interval: 100ms;
Rectangle {}
// ^error{Timer cannot have children elements}
}
Timer {
interval: 200ms;
width: 500px;
// ^error{Unknown property width in Timer}
y: 12px;
// ^error{Unknown property y in Timer}
opacity: 0.5;
// ^error{Unknown property opacity in Timer}
visible: false;
// ^error{Unknown property visible in Timer}
}
}

View file

@ -0,0 +1,14 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component Abc inherits Timer {
// ^error{A component cannot inherit from Timer}
interval: 50ms;
}
export component Def {
if true: Timer { interval: 32ms; }
// ^error{Timer cannot be directly repeated or conditional}
if false: Abc {}
}

View file

@ -298,6 +298,9 @@ impl Snapshotter {
.map(|p| self.snapshot_popup_window(p))
.collect(),
);
let timers = RefCell::new(
component.timers.borrow().iter().map(|p| self.snapshot_timer(p)).collect(),
);
let root_constraints = RefCell::new(
self.snapshot_layout_constraints(&component.root_constraints.borrow()),
);
@ -314,6 +317,7 @@ impl Snapshotter {
optimized_elements,
parent_element,
popup_windows,
timers,
private_properties: RefCell::new(component.private_properties.borrow().clone()),
root_constraints,
root_element,
@ -588,6 +592,14 @@ impl Snapshotter {
}
}
fn snapshot_timer(&mut self, popup_window: &object_tree::Timer) -> object_tree::Timer {
object_tree::Timer {
interval: popup_window.interval.snapshot(self),
running: popup_window.running.snapshot(self),
triggered: popup_window.triggered.snapshot(self),
}
}
fn snapshot_layout_constraints(
&mut self,
layout_constraints: &layout::LayoutConstraints,

View file

@ -169,6 +169,13 @@ impl Timer {
}
}
/// Returns the interval of the timer.
/// Returns `None` if the timer is not running.
pub fn interval(&self) -> Option<core::time::Duration> {
self.id()
.map(|timer_id| CURRENT_TIMERS.with(|timers| timers.borrow().timers[timer_id].duration))
}
fn id(&self) -> Option<usize> {
self.id.get().map(|v| usize::from(v) - 1)
}
@ -479,7 +486,12 @@ pub(crate) mod ffi {
if id != 0 {
timer.id.set(NonZeroUsize::new(id));
}
timer.start(mode, core::time::Duration::from_millis(duration), move || wrap.call());
if duration > i64::MAX as u64 {
// negative duration? stop the timer
timer.stop();
} else {
timer.start(mode, core::time::Duration::from_millis(duration), move || wrap.call());
}
timer.id.take().map(|x| usize::from(x)).unwrap_or(0)
}
@ -538,6 +550,18 @@ pub(crate) mod ffi {
timer.id.take(); // Make sure that dropping the Timer doesn't unregister it. C++ will call destroy() in the destructor.
running
}
/// Returns the interval in milliseconds if it is running, or -1 otherwise
#[no_mangle]
pub extern "C" fn slint_timer_interval(id: usize) -> i64 {
if id == 0 {
return -1;
}
let timer = Timer { id: Cell::new(NonZeroUsize::new(id)), _phantom: Default::default() };
let val = timer.interval().map_or(-1, |d| d.as_millis() as i64);
timer.id.take(); // Make sure that dropping the Timer doesn't unregister it. C++ will call destroy() in the destructor.
val
}
}
/**

View file

@ -32,6 +32,7 @@ use i_slint_core::platform::PlatformError;
use i_slint_core::properties::{ChangeTracker, InterpolatedPropertyValue};
use i_slint_core::rtti::{self, AnimatedBindingKind, FieldOffset, PropertyInfo};
use i_slint_core::slice::Slice;
use i_slint_core::timers::Timer;
use i_slint_core::window::{WindowAdapterRc, WindowInner};
use i_slint_core::{Brush, Color, Property, SharedString, SharedVector};
#[cfg(feature = "internal")]
@ -407,6 +408,7 @@ pub struct ItemTreeDescription<'id> {
FieldOffset<Instance<'id>, OnceCell<Vec<ChangeTracker>>>,
Vec<(NamedReference, Expression)>,
)>,
timers: Vec<FieldOffset<Instance<'id>, Timer>>,
/// The collection of compiled globals
compiled_globals: Option<Rc<CompiledGlobalCollection>>,
@ -1241,6 +1243,12 @@ pub(crate) fn generate_item_tree<'id>(
builder.change_callbacks,
)
});
let timers = component
.timers
.borrow()
.iter()
.map(|_| builder.type_builder.add_field_type::<Timer>())
.collect();
let public_properties = component.root_element.borrow().property_declarations.clone();
@ -1283,6 +1291,7 @@ pub(crate) fn generate_item_tree<'id>(
public_properties,
compiled_globals,
change_trackers,
timers,
#[cfg(feature = "highlight")]
type_loader: std::cell::OnceCell::new(),
#[cfg(feature = "highlight")]
@ -1612,6 +1621,8 @@ pub fn instantiate(
});
}
update_timers(instance_ref);
self_rc
}
@ -2302,3 +2313,41 @@ pub fn show_popup(
parent_item,
);
}
pub fn update_timers(instance: InstanceRef) {
let ts = instance.description.original.timers.borrow();
for (desc, offset) in ts.iter().zip(&instance.description.timers) {
let timer = offset.apply(instance.as_ref());
let running =
eval::load_property(instance, &desc.running.element(), desc.running.name()).unwrap();
if matches!(running, Value::Bool(true)) {
let millis: i64 =
eval::load_property(instance, &desc.interval.element(), desc.interval.name())
.unwrap()
.try_into()
.expect("interval must be a duration");
if millis < 0 {
timer.stop();
continue;
}
let interval = core::time::Duration::from_millis(millis as _);
let old_interval = timer.interval();
if old_interval != Some(interval) || !timer.running() {
let callback = desc.triggered.clone();
let self_weak = instance.self_weak().get().unwrap().clone();
timer.start(i_slint_core::timers::TimerMode::Repeated, interval, move || {
if let Some(instance) = self_weak.upgrade() {
generativity::make_guard!(guard);
let c = instance.unerase(guard);
let c = c.borrow_instance();
let inst = eval::ComponentInstance::InstanceRef(c);
eval::invoke_callback(inst, &callback.element(), callback.name(), &[])
.unwrap();
}
});
}
} else {
timer.stop();
}
}
}

View file

@ -1118,6 +1118,15 @@ fn call_builtin_function(
))
}
BuiltinFunction::Use24HourFormat => Value::Bool(corelib::date_time::use_24_hour_format()),
BuiltinFunction::UpdateTimers => match local_context.component_instance {
ComponentInstance::InstanceRef(component) => {
crate::dynamic_item_tree::update_timers(component);
Value::Void
}
ComponentInstance::GlobalComponent(_) => {
panic!("timer in global?")
}
},
}
}

View file

@ -0,0 +1,153 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
/*component FizBuzz {
out property <string> result;
}*/
export component TestCase inherits Window {
out property <string> result;
in property <int> tm2duration;
in property <bool> tm2running <=> tm2.running;
vl := VerticalLayout {
if true: HorizontalLayout {
Timer {
interval: 1s;
triggered => { result += "1"; }
}
}
}
out property <bool> test: vl.max-width == 0;
Rectangle {
tm2 := Timer {
running: false;
interval: tm2duration * 1ms;
triggered => { result += "2"; }
}
}
Rectangle {
oops := Timer {
interval: -5ms;
triggered => { result += "oops"; }
}
}
}
/*
```rust
let instance = TestCase::new().unwrap();
assert!(instance.get_test());
assert_eq!(instance.get_result(), "");
slint_testing::mock_elapsed_time(991);
assert_eq!(instance.get_result(), "");
slint_testing::mock_elapsed_time(10);
assert_eq!(instance.get_result(), "1");
instance.set_tm2running(true);
assert_eq!(instance.get_result(), "1");
slint_testing::mock_elapsed_time(500);
// despite we say to ellapse 500ms, the changed callback is only called once
slint_testing::mock_elapsed_time(510);
// Same, the timer event are only called onced
assert_eq!(instance.get_result(), "121");
slint_testing::mock_elapsed_time(0);
assert_eq!(instance.get_result(), "1212");
slint_testing::mock_elapsed_time(0);
assert_eq!(instance.get_result(), "12122");
instance.set_tm2duration(50);
slint_testing::mock_elapsed_time(8);
// even though we changed the duration, the timer fires before the changed callback
assert_eq!(instance.get_result(), "121222");
slint_testing::mock_elapsed_time(49);
assert_eq!(instance.get_result(), "121222");
slint_testing::mock_elapsed_time(2);
assert_eq!(instance.get_result(), "1212222");
slint_testing::mock_elapsed_time(47);
assert_eq!(instance.get_result(), "1212222");
instance.set_tm2duration(18);
slint_testing::mock_elapsed_time(2);
assert_eq!(instance.get_result(), "1212222");
slint_testing::mock_elapsed_time(19);
assert_eq!(instance.get_result(), "12122222");
```
```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
assert(instance.get_test());
assert_eq(instance.get_result(), "");
slint_testing::mock_elapsed_time(991);
assert_eq(instance.get_result(), "");
slint_testing::mock_elapsed_time(10);
assert_eq(instance.get_result(), "1");
instance.set_tm2running(true);
assert_eq(instance.get_result(), "1");
slint_testing::mock_elapsed_time(500);
// despite we say to ellapse 500ms, the changed callback is only called once
slint_testing::mock_elapsed_time(510);
// Same, the timer event are only called onced
assert_eq(instance.get_result(), "121");
slint_testing::mock_elapsed_time(0);
assert_eq(instance.get_result(), "1212");
slint_testing::mock_elapsed_time(0);
assert_eq(instance.get_result(), "12122");
instance.set_tm2duration(50);
slint_testing::mock_elapsed_time(8);
// even though we changed the duration, the timer fires before the changed callback
assert_eq(instance.get_result(), "121222");
slint_testing::mock_elapsed_time(49);
assert_eq(instance.get_result(), "121222");
slint_testing::mock_elapsed_time(2);
assert_eq(instance.get_result(), "1212222");
slint_testing::mock_elapsed_time(47);
assert_eq(instance.get_result(), "1212222");
instance.set_tm2duration(18);
slint_testing::mock_elapsed_time(2);
assert_eq(instance.get_result(), "1212222");
slint_testing::mock_elapsed_time(19);
assert_eq(instance.get_result(), "12122222");
```
```js
var instance = new slint.TestCase({});
assert(instance.test);
assert.equal(instance.result, "");
slintlib.private_api.mock_elapsed_time(991);
assert.equal(instance.result, "");
slintlib.private_api.mock_elapsed_time(10);
assert.equal(instance.result, "1");
instance.tm2running = true;
assert.equal(instance.result, "1");
slintlib.private_api.mock_elapsed_time(500);
// despite we say to ellapse 500ms, the changed callback is only called once
slintlib.private_api.mock_elapsed_time(510);
// Same, the timer event are only called onced
assert.equal(instance.result, "121");
slintlib.private_api.mock_elapsed_time(0);
assert.equal(instance.result, "1212");
slintlib.private_api.mock_elapsed_time(0);
assert.equal(instance.result, "12122");
instance.tm2duration = 50;
slintlib.private_api.mock_elapsed_time(8);
// even though we changed the duration, the timer fires before the changed callback
assert.equal(instance.result, "121222");
slintlib.private_api.mock_elapsed_time(49);
assert.equal(instance.result, "121222");
slintlib.private_api.mock_elapsed_time(2);
assert.equal(instance.result, "1212222");
slintlib.private_api.mock_elapsed_time(47);
assert.equal(instance.result, "1212222");
instance.tm2duration = 18;
slintlib.private_api.mock_elapsed_time(2);
assert.equal(instance.result, "1212222");
slintlib.private_api.mock_elapsed_time(19);
assert.equal(instance.result, "12122222");
```
*/

View file

@ -89,6 +89,15 @@ export component TestCase inherits Window {
changed sub-result => {
result += "||" + sub-result;
}
Rectangle {
probably-optimized := Rectangle {
property <int> foo: other;
changed foo => {
result += "foo,";
}
}
}
}