Skip to content

ComboBox and RadioGroup: writes to current-value are silently overridden #11970

Description

@tilladam

ComboBox and RadioGroup: writes to current-value are silently overridden

Bug Description

ComboBox.current-value and RadioGroup.current-value are declared in-out, suggesting host code can write them. In practice, writes from host code are silently overridden by a declarative binding inside the widget — the host's write is dropped without warning and the property re-derives from model[current-index].

In internal/compiler/widgets/common/combobox-base.slint:

in-out property <int> current-index: 0;
in-out property <string> current-value: root.model[root.current-index];  // <-- the binding

changed current-index => {
    root.update-current-value();  // re-writes current-value = model[current-index]
}

RadioGroup (internal/compiler/widgets/common/radiogroup-base.slint) has the identical pattern (current-value: root.model[root.current-index].text).

When the host calls set_current_value("X") from Rust/C++/JS, the property briefly holds "X", then the next read re-derives via the binding back to model[current-index]. The host's intent is lost, and there is no diagnostic.

Reproducer

Headless test, runs against current master (c0a00d2):
https://github.com/tilladam/slint-popup-authority-probe/blob/main/tests/binding_tests.rs#L182-L233 (test claim_set_current_value_finds_index_in_model, currently marked #[ignore] pending a fix).

Distilled:

slint::slint! {
    import { ComboBox } from "std-widgets.slint";
    export component App inherits Window {
        in-out property <int> idx <=> cb.current-index;
        in-out property <string> val <=> cb.current-value;
        cb := ComboBox {
            model: ["A", "B", "C", "D"];
        }
    }
}

let app = App::new()?;
app.set_val(SharedString::from("C"));
// expected: idx == 2, val == "C"
// observed: idx == 0, val == "A"

What the fix should look like (Option A)

A host writing set_current_value("C") is expressing intent "select the row whose value is C". The widget should reverse-look-up the written value in the model and update current-index accordingly:

changed current-value => {
    if root.current-value != root.model[root.current-index] {
        // Find the value in the model; set current-index to the first match,
        // or to -1 if not found. The subsequent changed current-index handler
        // will write current-value back to model[current-index] — which is
        // what the host wrote when it was found, and "" when it wasn't.
        root.current-index = find-index-for-value(root.current-value);
    }
}

This aligns with the philosophy of the prior two fixes in this area:

Reverse lookup applies the same principle to value-based writes.

Why this can't ship today

Slint has no iteration in function bodies and forbids recursive functions (the compiler reports a binding loop on a recursive helper). There is no [T].index-of(value: T) -> int builtin paralleling [T].length. So find-index-for-value(...) cannot be written in pure Slint.

The blockers for a pure-language fix:

Once those land, the combobox-base.slint fix is the ~10 lines sketched above (in both ComboBox and RadioGroup).

Interim mitigation proposal

Until the language work lands, surface the silent override as a runtime warning in the affected widgets. Both combobox-base.slint and radiogroup-base.slint would gain:

changed current-value => {
    if root.current-value != root.model[root.current-index] {
        debug("ComboBox: writes to `current-value` from host code are not supported "
              + "and will be silently overridden. Use `current-index` to change the "
              + "selection. Tracking: <this issue URL>.");
        root.update-current-value();  // explicit revert
    }
}

debug() is the existing Slint builtin (expression_tree.rs:354) which routes through log::debug! or stderr. It only fires when the host's write actually diverges from the model-derived value, so hosts following the documented contract (writing current-index instead) never see it. The block is small (~6 lines per widget) and trivially removed once Option A becomes implementable.

Documentation on the affected current-value properties should be updated in parallel to recommend current-index and warn that direct writes are unsupported.

Related

Suggested next steps

  1. Acknowledge the diagnosis or correct it.
  2. Decide on the interim mitigation: runtime warning, docs-only, or no-op until the language work lands.
  3. Track the proper fix against Add support for local variables #2752 + for - in statement loop in slint language #8561.

Happy to send a PR for the interim runtime warning (in both widgets) once a direction is chosen.


Environment

  • Slint: master (c0a00d2)
  • Platform: macOS
  • Language: Rust

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions