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.56k stars 604 forks source link

Find a solution to expose entire element in the public API #251

Open ogoffart opened 3 years ago

ogoffart commented 3 years ago

The problem we're trying to solve here is to forward many properties from an inner component, to the root, so they can be set by the business logic. Currently the work around is to do something like that:

App := Window {
    inner_page := InnerPage { 
       processed(val) => { inner_processed(val); }
       /* ... */ 
   }

    property <string> inner_foobar <=> inner_page.foobar;
    property <int> inner_something <=> inner_page.something;
    callback inner_processed(int);
    // many more properties
}

This is a lot of boiler plate, especially for callbacks. (See issue #111 for a callback forarding issue).

This can be a bit simplified by using use a global singleton to set all the properties in the singleton, and the page can read that singleton. Unfortunately, it is not yet possible to set property from singleton from native code (issue #96) so for now one still need boiler plate code to forward everything. And this only work if there is a single instance of that page.

Also we can pack all the property in a single sutructure. This remove some of the forwarding boilerplate, but is less efficient because we always need to read and write the whole struct. Also that does not work with callbacks

struct InnerData := { foobar: string, something: int, /*...*/ }
//...
App := Window {
    inner_page := InnerPage {  /* ... */  }
    property <InnerData> inner_data <=> inner_page.data;
}

So is the solution to expose inner elements in the public api, for example using a public keyword

App := Window {
   public inner := InnerPage { /* ... */ }
   // ...
}

But we also need to somehow re-export from deeper hierarchy:

//...
InnerData := Page { 
   Rectangle { 
       public inner_widget := ActualInnerWidget { /* ... */ }
   }
}

App := Window {
   property <ActualInnerWidget> inner <=> inner_page.inner_widget;
   Rectangle { 
        inner_page := InnerPage { /* ... */ }
   }
   // ...
}

This raises several question:

What types are then generated in C++ and Rust for these element properties. The use of setter and getter like other property don't really make sense as one can't really set a reference to an object. What would the lifetime of the reference be? What properties are exposed, does that imply we can also export builtin elements (Image/ListView), and these needs to be in the public Rust / C++ API? (Maybe we can make it play nice with something like #191 which allow to change access.) Do we care about items in repeater?

Is that really the best solution to the problem or is there a better way? (For example, extend the Model API for smarter property based model)

sztomi commented 2 years ago

The use of setter and getter like other property don't really make sense as one can't really set a reference to an object.

For my use-case, a getter (without a setter) would be sufficient. My main window has this code:

MainWindow := Window {
  login := LoginScreen {
    visible: root.selected-screen == 0;
  }

  main := MainScreen {
    visible: root.selected-screen == 1;
  }
}

and I would like to access properties and further inner containers in main from Rust, e.g.

let menu_items = main.menubar.items;

As for the types, I think for this use-case it would be sufficient to export them with same base type that the root element has.

Is that really the best solution to the problem or is there a better way?

I could see CSS-style selectors filling this niche, although that still implies the export types being decided upon.

let menu_items = slint::select(&mainwindow, "#menuitems");
// or
let menu_items = slint::select(&mainwindow, "#main > #menubar > #items");
tronical commented 2 years ago

Now that we don't inline everything anymore, I wonder if components could be supported as property types:

export Button := TouchArea { ... }

export App := Window {
    property <Button> button: b;
    VerticalLayout {
        b := Button {}
    }
}

and then (in the example of Rust) we would generate a public wrapper for Button that - similar to App wraps around VRc (except that it's VRcMapped in that case). We would also need to find a way to be able to use slint::Weak with this (or use a new type), but in principle the property would be a weak reference and exposed as such.

The application code could obtain it and selectively upgrade it.

At least we do have the building blocks for that now.

This is not a complete picture though, and the questions asked earlier ("is this really the best solution to the problem?") remain valid.

This keeps coming up and is closely related to the question how to attach native code more easily to sub-components of a bigger UI, without routing every single callback and property through the root.

bombela commented 2 years ago

Do I understand correctly that until this issue is solved, I can very well declare multiple windows, but I cannot access them from Rust? In any case, how can I declare and open two different windows?

and-elf commented 2 years ago

What about generating new objects for all exported types? Objects reimplemented by the user would provide a clear API separation Internally, the impl could contain a pointer to a possibly user implemented version. Like this: if(user_impl && user_impl->cb) [return] user_impl->cb()

export MyType := Rectangle {
callback cb;
}

On the user side:

auto myType = ..
auto ui = ...
ui->set_mytype(myType)

MyType could, possibly, be generated in the exact same way that Window does

Not sure how this would work for types used as delegates in repeaters... I guess the slint-defined version should have priority... somehow

ogoffart commented 2 years ago

I think this is basically what Global objects are: https://slint-ui.com/releases/0.3.1/docs/cpp/markdown/recipes/recipes.html#global-callbacks

and-elf commented 2 years ago

Except that it would be "global" per object type, and not application wide. I chose main window as an example, but it would be the same for all exported types.

Tastaturtaste commented 1 year ago

Is there progress on this issue? I am just starting out with slint, but even with a simple UI I immediately noticed how cumbersome it is to bind every subsubproperty in the root component for interactions with native code.

I am sure you already considered it, but what I intuitively think makes sense would be to access elements as nested structs from rust:

let ui = App::new();
let button = ui.get_tile().get_button();
let button_state = button.get_button_state();

In this example I would envision button_state to also be of reference type like ui itself.