slint-ui / slint

Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.
https://slint.dev
Other
17.58k stars 604 forks source link

Modifying ComboBox.model resets current-index/value #5921

Open JakubKoralewski opened 2 months ago

JakubKoralewski commented 2 months ago

Problem

When changing the model of a ComboBox the selected value is reset. This breaks UX in some cases.

Motivation

I've already hit 2 instances where I'd like to be able to modify the model of a ComboBox. Maybe it's bad practice, but it's what I'd like my user interface to happen. For example with a SpinBox counter I'd like to conjugate the verbs of hours/minutes depending on the count. So that an "hour" becomes "hours" or in my case "godzina" becomes "godziny". In another case I wanted to add an element to a ComboBox.model instead of replacing it, and succeeded by modifying current-index and current-value from a Rust callback and pushing onto the VecModel instead of replacing it. But in one case I have a component, and the Rust solution requires access to a Window, and awkward to call a property setter from Rust code if the ComboBox is inside a component (if I'm doing things right).

Code

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];
    property <bool> v: false;

    VerticalLayout {

        s := Switch {
            checked: false;
        }

        ComboBox {
            model: s.checked == true ? model-a : model-b;
        }
    }
}

If that's the expected behavior that's fine, but at least I'd like to be able to save the current-index to a property and be able to restore that index, but I can't seem to be able to do that -- this code behaves the same as the one above:

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];
    property <int> current-index: 0;

    VerticalLayout {

        s := Switch {
            checked: false;
            toggled() => {
                c.current-index = root.current-index;
            }
        }

        c := ComboBox {
            model: s.checked == true ? model-a : model-b;
            selected(str) => {
                root.current-index = self.current-index;
            }
            init() => {
                self.current-index = root.current-index;
            }
        }
        Text {
            text: root.current-index + " " + c.current-index + " " +c.current-value;
        }
    }
}

same with current-value instead of current-index

hunger commented 2 months ago

I had expected this to work:

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];

    VerticalLayout {

        s := Switch {
            checked: false;

            property <int> keeper;

            toggled => {
                self.keeper = c.current-index;
                debug("Before", self.keeper, c.current-index);
                c.model = self.checked ? root.model-a : root.model-b;
                c.current-index = self.keeper;
                debug("After", self.keeper, c.current-index);   
            }
        }

        c := ComboBox {
            model: root.model-b;
        }

        Text {
            text: c.current-index + " " + c.current-value;
        }
    }
}

That debug-prints the expected current-index, before and after setting the model, but the ComboBox still resets to index 0.

JakubKoralewski commented 2 months ago

Here is the workaround I mentioned, maybe it helps in some way https://github.com/JakubKoralewski/slint-workaround-5921

hunger commented 2 months ago

This is probably not a widgets problem, but a problem lazily evaluating the bindings...

            toggled => {
                self.keeper = c.current-index;
                debug("Before", self.keeper, c.current-index);
                c.model = self.checked ? root.model-a : root.model-b;
                c.current-index = self.keeper;
                debug("After", self.keeper, c.current-index);   
            }

This bit of code reports that the current-index is as expected in both places. My theory is that current-index is lazily evaluated to its value of 5 by the debug statements.

The model gets evaluated lazily later -- resetting the current-index again as a side effect.

ogoffart commented 2 months ago

I think it's on purpose, the ComboBox resets the current-index there when the model changes:

https://github.com/slint-ui/slint/blob/45a9c7235a1322d377eb567f8637ec37388a90cd/internal/compiler/widgets/common/combobox-base.slint#L49-L51

hunger commented 2 months ago

Of course changing the model resets the current-index. That is expected.

The code above does this (as I would read it naively):

  1. get the current-index (let's say 2)
  2. debug print 2
  3. Set the model, which I expect to set current-index to 0.
  4. set the current-index to 2.
  5. debug print 2
  6. show index 2 in the ComboBox.

What (I think) happens is this:

  1. get the current-index (2)
  2. debug print 2
  3. set the current-index to 2
  4. debug print 2
  5. set the model, resetting the current-index to 0.
  6. show index 0 in the ComboBox

I find that surprising.

hunger commented 2 months ago

Doing some more experiments:

I can debug print the c.current-index and c.model[c.current-index] right after setting the model. That prints the unchanged index and the value taken from the new model.

So the issue seems to be that the model reset delays resetting the current-index to 0 somehow.