mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 10:50:00 +00:00
parent
2643a327e8
commit
a9f526491a
25 changed files with 599 additions and 14 deletions
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
68
internal/compiler/passes/lower_timers.rs
Normal file
68
internal/compiler/passes/lower_timers.rs
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
25
internal/compiler/tests/syntax/elements/timer.slint
Normal file
25
internal/compiler/tests/syntax/elements/timer.slint
Normal 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}
|
||||
|
||||
}
|
||||
|
||||
}
|
14
internal/compiler/tests/syntax/elements/timer2.slint
Normal file
14
internal/compiler/tests/syntax/elements/timer2.slint
Normal 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 {}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
153
tests/cases/elements/timer.slint
Normal file
153
tests/cases/elements/timer.slint
Normal 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");
|
||||
```
|
||||
*/
|
|
@ -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,";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue