Open Enyium opened 2 months ago
I tried to patch LineEdit
with the following code:
component PatchedLineEdit {
in property <InputType> input-type <=> line-edit.input-type;
in-out property <string> text;
callback edited(string);
init => {
line-edit.text = root.text;
}
changed text => {
line-edit.text = root.text;
}
line-edit := LineEdit {
width: root.width;
edited(text) => {
root.text = text;
root.edited(text);
}
}
}
But, unfortunately, this doesn't change anything, and I don't understand why!
The only way I can intermediately patch this is by having this changed-callback for the backing property in my component
inheriting Window
:
in-out property <duration> dur;
changed dur => {
line-edit.text = root.dur-to-mins(root.dur);
}
Among other things, this has the disadvantage that, when the user deletes the 2
in 0.2
, the decimal separator is also deleted.
Slider
's and SpinBox
's value
have the same problem. When used in the manner initially described, their inner changing of their value
property makes it so that setting a dependency of value
doesn't update value
anymore, leaving the widget "dead".
Only two-way bindings from own component property to widget's property don't give me these problems. But if I need to convert or transform the data, this isn't an option.
@hunger (@ogoffart?): If I'm not overlooking something fundamental here, this bug should have high priority in my opinion! Changing a property that a Slider
's value
, e.g., is bound to and directly setting value
(including by internal widget code through UI use) should just all be points for Slint where value
changes. Directly setting value
should never kill the binding! value
should just be a sink for data from various sources. I can't say that I'm familiar with all potential use cases, so in other cases breaking the binding may be desired, but your own widgets seem to be written with the assumption that directly setting a property doesn't break the user's one-way binding to the same property. Should this require a fundamental change in behavior of one-way bindings and this is undesired, then maybe you could establish a distinction between breakable and non-breakable one-way binding declarations with new syntax.
This is definitely for @ogoffart to answer:-)
I think here there is a documentation issue.
Binding are broken when we assign to them, and the TextInput's text property does that when it is edited by the user.
With the exception of two way bindings (<=>
) which stays the same.
But how is one supposed to implement transformations of widget values? I have a Slider
whose value
must be mapped to a pow()
-distorted scale, and LineEdit
s whose text is mapped to properties of type duration
(in both directions, of course, because Rust code may fill the settings pages).
Window
, not abstracted away in a dedicated component)?changed
-callbacks for that?I will take this as part of the doc updates. @Enyium This is working as intended and bindings should not be placed on the 'value' property of items such as the 'text' property of LineEdit or the 'value' of a slider. However what you are trying to achieve, such as being able to set a value with a slider AND a text input, is quite tricky right now. It will become easy to manage once the 'property changed' system is in place via https://slint.dev/blog/property-changed-callback
The logic is on a slider you move the slider and value is where the thumb is. Each move updates the value and destroys any binding someone else adds. Same with LineEdit. As you type the 'text' value is updated and any bindings placed there will be destroyed. Does it make sense now? So what is needed is that LineEdit and Slider don't bind to the root 'value'. Instead slider has changed text => { root.value = self.value }
and LineEdit has changed text => { root.value = self.text }
and then if you have a reset button you would set slider.value = 0, then LineEdit.text = "", root.value = "".
@NigelBreslaw:
Coincidentally, I have one case where a Window
component property is connected to a Slider
and a LineEdit
. But still, you misunderstood what I was referring to with this statement:
I have a
Slider
whose value must be mapped to apow()
-distorted scale, andLineEdit
s whose text is mapped to properties of typeduration
.
I meant these things individually. Different kinds of two-way data transformation is needed:
Slider
has a scale that's distorted via pow()
, so one end is more zoomed in scale-wise. Depending on the direction of the transformation of the Slider
's value
, I have to apply the pow()
exponent directly or its inverse 1 / e
.LineEdit
s, each of which is meant to edit a duration
.I'm not convinced non-breaking bindings wouldn't be worthwhile. As I see it, direct assignment could just clear the binding's dirty flag instead of destroying the binding, so that, when the property value is finally needed, the engine doesn't see a need to evaluate the binding, but directly takes the cached value which was directly assigned. If a binding source changes - leading to the dirty flag being set - and the property value is then needed, the binding would be evaluated.
As a workaround, I tried this DurationEdit
implementation:
component DurationEdit {
in-out property <duration> value;
callback edited;
changed value => {
line-edit.text = Std.dur-to-mins(root.value);
}
line-edit := LineEdit {
width: root.width;
input-type: decimal;
edited(text) => {
root.value = Std.mins-to-dur(text.to-float());
self.text = text; // Undesirable. Also doesn't work, because changed-callback is delayed.
root.edited();
}
}
}
Then I used the widget with a two-way binding to its value
property. Two-way bindings seem to be unproblematic regarding binding destruction.
It works in that the widget isn't left dead after manual editing, but value
can be set from Rust
after editing with the widget updating accordingly. However, when I, e.g., type 0.04
, the text is immediately replaced with 0.039983332
; and when I have the text 0.05
and delete the 5
, the text is immediately replaced with 0
. The problem is that assigning root.value
in edited(text) => { ... }
triggers the changed-callback, which is very much undesired. The workaround of following up with self.text = text;
doesn't work (I know that Olivier's article you linked contains the reason).
Olivier's article also talks about loops in the context of changed-callbacks. The use case of the code above underlines the importance of solving this. My idea would be to have a covert
keyword that you can use before assignments to tell Slint that this assignment shouldn't trigger changed-callbacks for the respective property. In the code above, you'd then have:
edited(text) => {
covert root.value = Std.mins-to-dur(text.to-float());
root.edited();
}
However, this assignment also being hidden from the changed-callbacks of the component users wouldn't be right. Maybe, covert
would need to be defined as only applying in the context of the component it's used in? Perhaps, changed-callbacks could additionally get special syntax for either going with the covert
filter or ignoring it.
Another relevant point: The DurationEdit
code above converts on each edited
event of the inner LineEdit
- many unnecessary times, like when typing regularly or holding a repeated key. What if you wanted to only perform the conversion when the value
property is actually requested (lazily)? You'd have to specify a binding on the value
property (deriving it from line-edit.text
) that's for reading it, and when value
is set from the outside (never from the inside), the binding must persist and the changed-callback runs, which updates the LineEdit
. This is another reason for non-breaking bindings.
The reference documentation of properties should explain that binding can be broken when the property is set by some external means.
It should also explain more about two ways binding and what exactly happens when two bindings have an original value in the same component (the right-hand-side wins) (see https://github.com/slint-ui/slint/issues/6105#issuecomment-2346314732)
Was: LineEdit's text property doesn't follow its bound value anymore after editing
This should be fairly self-explanatory (corresponding SlintPad link):
Reproduce the bug with these steps:
LineEdit
.