slint
slint copied to clipboard
Have callback emitted when a property changes
Do we want a signal when a property changes.
If we do, what would be the syntax. Some suggestions:
Rectangle {
changed color => { debug("the color was changed"); }
color changed => { debug("the color was changed"); }
color => { debug("the color was changed"); }
color ~> { debug("the color was changed"); }
}
Can there be multiple event handler? (signal can only have one event handler currently) When is the signal emitted? (properties are lazy right now, and they can be dirty without changing)
We have concern that this will not help keeping the language pure. This kind of signal would violate the lazyness and make implementation of design tools harder. If this is just to be able to debug changes, we should have a debugger that allow to watch property.
We discussed this further a bit today and one way to address the laziness would be to treat each handler as if it were tracking a "gui" related property. So a "change handler" would result in the implementation using
- a
PropertyChangeHandler
which queues the associated callback for invocation and schedules the rendering of a new frame (if window is visible, otherwise post an event to wake up the event loop) - When the event loop wakes up to render a new frame, call all queued change handlers, after verifying that the property value has actually changed
- If no other gui property changed, then nothing needs to be re-drawn
In the mean time, as a workaround/hack, it is possible to put callback using the fact that property are re-evaluated when their dependency changes
export Demo := Window {
callback my_callback(bool) -> color;
my_callback(x) => { debug("HELLO", x); white; }
background: my_callback(touch.has-hover);
touch := TouchArea { height: 50%; }
}
In that example, background is going to be re-evaluated when dirty by the rendering code. and it is going to be dirty if the has-hover property is changed. So the my_callback
callback is called every time the has-hover
property changes.
(This is just a workaround/hack until such mechanism are implemented)
Will this also work with global properties ?
We decided to go with the following syntax:
Rectangle {
changed background => { debug("the background was changed"); }
}
The concern is still that this feature is opening a can of worm because some user would tend to use change handler instead of using binding. This is bad because it makes the design tool and tooling more complex.
We decided that we will execute change handler in the next event loop iteration. When the property is marked as dirty, it is added to a queue and is evaluated in the next change handler. In particular, this will force the evaluation of the property at component creation no matter if the propery is queried by other mean (even if the component is hidden). This kind of disable the lazyness.
Another point is the loop detection. Should the compiler detect loops? This can get quite difficult as it involves multiple components:
component Inner {
in property <int> c;
out property <int> b: c;
}
component Foo {
out property <int> a: inner.b;
property <int> xxx;
changed a => {
func();
}
function func() {
xxx = inner.b; // Ok even if a depends on b
xxx = z; // ok even if z depends on a
inner.c = 42; // NOT OK: Loop: because a depends on c, and c is modified (so changes a)
}
property <int> z: a;
inner := Inner {}
}
Another example:
component Base {
callback do_something;
}
component Foo inherits Base {
in property <int> xyz;
changed xyz => { root.do_something() }
public function change() {
xyz = 88;
}
}
component XXX {
Foo {
do_something => {
self.xyz = 42; // Should be an error (loop)
self.change(); // another error as change also change xyz
}
}
}
And finally:
component Foo {
b0 := LineEdit {
// Ok, no loop so far
changed text => { b1.text = text; }
}
b1 := LineEdit {
// but this would be a loop
// changed text => { b0.text = text; }
}
}
This last example is something that may actually be wanted, to synchronize two propery, possibly with some code in the middle. It is kind of alright if, like in this case, the property converge, but if the binding was b0.text = text + 'a'
we would get an infinite loop and that's bad.
So since it is really hard to detect loops in the compiler, we might actually not do it and resort to runtime detection: if the evaluation of a debug handler mark the property dirty again, we would stop. but the problem is that if it causes another change handler to mark another property dirty which then mark the same property again we don't know that easily so that's also not trivial to detect and in the end we would still have a runtime infinite loop :-(
One of the reason we want change handler is to keep some property in sync when it is not possible to have a proper two way binding.
component SpinBox {
property <int> value;
// ...
LineEdit {
// this doesn't work because that's not the same type
// text <=> root.value;
// this breaks the binding as soon as text is edited or set.
text: root.value;
// works one way
edited => { value = text.to-float(); }
// this binding would actually solve the problem (but written at the root, not here)
changed value => { text = value; }
// but what about other possibilities such as
text <=> root.value {
=> text.to-float();
<= root.value;
}
}
}
This is similar to https://github.com/slint-ui/slint/issues/814
In particular, this will force the evaluation of the property at component creation no matter if the propery is queried by other mean (even if the component is hidden).
Hmm, maybe there was a misunderstanding. IMO we should not call change handlers at component creation time, only at the next event loop iteration. Can you elaborate what you mean?
I have the next use case for changed handlers. The native macOS
scroll bars are only displayed if the user start scrolling in the corresponding direction. After a short time the scrollbar will be hide again. At the moment I have no way to detect if viewport-x
or viewport-y
is changed.
It would be also great to have a possibility to check if an animation is finished like an callback.
Another workaround was suggested in https://github.com/slint-ui/slint/discussions/4324 Edit: and in https://github.com/slint-ui/slint/discussions/4717
Would be quite nice to have this natively supported by Slint.
One specific use case for this I ran into the other day would be implementing something akin to QtQuick's ToolTip
(which could probably be a built-in thing as well). At the moment, it's very impractical to show/hide a popup on hover, seeing as that information comes solely from a has-hover
property, and PopupWindow
s are only controllable through function calls.