slint icon indicating copy to clipboard operation
slint copied to clipboard

Have callback emitted when a property changes

Open ogoffart opened this issue 3 years ago • 12 comments

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)

ogoffart avatar Nov 23 '20 10:11 ogoffart

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.

ogoffart avatar Feb 02 '21 10:02 ogoffart

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

tronical avatar Dec 08 '21 14:12 tronical

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)

ogoffart avatar Mar 12 '22 11:03 ogoffart

Will this also work with global properties ?

aamer-shaikh avatar Aug 07 '23 12:08 aamer-shaikh

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 :-(

ogoffart avatar Aug 18 '23 16:08 ogoffart

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

ogoffart avatar Aug 18 '23 16:08 ogoffart

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?

tronical avatar Aug 18 '23 20:08 tronical

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.

FloVanGH avatar Sep 11 '23 13:09 FloVanGH

It would be also great to have a possibility to check if an animation is finished like an callback.

FloVanGH avatar Nov 14 '23 05:11 FloVanGH

Another workaround was suggested in https://github.com/slint-ui/slint/discussions/4324 Edit: and in https://github.com/slint-ui/slint/discussions/4717

ogoffart avatar Feb 01 '24 08:02 ogoffart

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 PopupWindows are only controllable through function calls.

Tmpod avatar Feb 08 '24 12:02 Tmpod