Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create multiple components in generated Rust and C++ code #784

Closed
tronical opened this issue Jan 2, 2022 · 19 comments · Fixed by #5449
Closed

Create multiple components in generated Rust and C++ code #784

tronical opened this issue Jan 2, 2022 · 19 comments · Fixed by #5449
Assignees
Labels
a:compiler Slint compiler internal (not the codegen, not the parser) priority:high Important issue that needs to be fixed before the next release
Milestone

Comments

@tronical
Copy link
Member

tronical commented Jan 2, 2022

The export keyword in .60 files can be used to make a custom component usable from other .60 files, and it is also used to mark a component for becoming "visible" to generated Rust and C++ code.

Currently, only the last component in the main .60 file that is compiled is "exported". In Rust a struct and in C++ a class is generated for it, with the known show(), etc. functions and accessors for declared properties. It also provides API to access singletons.

We may want to extend support in the compiler to allow exporting and thus creating multiple structs/classes, using the export keyword.

There are open questions in this context, such as what should happen to data structures marked as global? How can they be shared? Where could state that is local to the created component/window go?

@tronical tronical added a:compiler Slint compiler internal (not the codegen, not the parser) a:language-c++ C++ API, codegen, CMake build system (mS,mO) a:language-rust Rust API and codegen (mO,mS) labels Jan 2, 2022
@Be-ing
Copy link
Contributor

Be-ing commented Jan 2, 2022

This would be a nice improvement. The current solution of using a bunch of property bindings on the top level Window element is quite clunky.

@ogoffart
Copy link
Member

ogoffart commented Jan 2, 2022

The goal here is to support multiple windows.

One would still need to make properties accessible in the top level, or in a global singleton component.

@Be-ing

This comment was marked as off-topic.

@Be-ing

This comment was marked as off-topic.

@ogoffart

This comment was marked as off-topic.

@Be-ing

This comment was marked as off-topic.

@tronical
Copy link
Member Author

We discussed this topic a bit this morning and decided to each think about separately how to solve this. This is what I think could be considered as a solution:

The proposal covers to questions:

  1. How can data structures declared as global singletons be shared across multiple instances of components created in Rust?
  2. How can we support "per-window" specific state?

Example Code

palette.slint:

global struct Palette {

}

main.slint:

import { Palette } from "palette.slint";

export component App {
    Window {
        background: Palette.my-window-background;
    }
}

dialog.slint:

import { Palette } from "palette.slint";

interface DialogSpecificState {
    property <string> dialog-button-text;
}

export component Dialog implements DialogSpecificState {
    Window {
        background: Palette.my-window-background;

        DialogSpecificButton {

        }
    }
}

component DialogSpecificButton requires DialogSpecificState {
    Button {
        text: DialogSpecificState.dialog-button-text;
    }
}

build.rs

fn main() {
    // creates a module for each input file, so `mod dialog {}` and `mod main {}`
    slint_build::compile(["dialog.slint", "main.slint"]).unwrap();
}

mod ui {
slint::include_modules!()
}

fn main() {
    let shared_globals = ui::Globals::new();
    let app = ui::main::App::new_with_shared_globals(&shared_globals);
    let dialog = ui::dialog::Dialog::new_with_shared_globals(&shared_globals);

    let black = slint::Color::from_rgb_u8(0, 0, 0);
    shared_globals::<ui::Palette>.set_my_window_background(black);
    assert_eq!(app.globals<ui::Palette>().get_my_window_background, black);

    // ...

    dialog.
}

Shared Globals

The proposed solution affects only the generated code:

  1. For each input file, the compiler generates a Rust module.
  2. In the top-level (mod ui in the example), the compiler generates a Globals struct that provides access to all global singletons, with the same API as the existing app.global<Foo>() accessor.
  3. The Rust API for creating components provides a variant of new that can take an explicitly shared reference to the Globals. This way it's evident to the developer how the global data sharing declared in Slint is reflected in Rust.

Internal implications to Rust generated code:

At the moment, each generated component that is not inlined results in a struct that has a reference back to the "app"/"root" component, in order to access globals and the window. This needs to be changed to support access to the shared globals.

"Per-Window" Specific State

The question of how to create state that is per-window is a special case of the general question: How can (possibly unrelated) components share state?

The proposed solution uses interfaces:

interface SharedState {
    property <string> shared-property;
}

A component can implement the interface:

export component Dialog implements SharedState { 
}

That means it provides the declared properties and callbacks.

A component can require that instantiating it requires access to a single instance of an interface:

component DialogSpecificButton requires SharedState {
    Text {
        text: SharedState.shared-property;
    }
}

This requirement propagates to anyone using this component:

component DialogButtonBox {
    VerticalLayout {
        DialogSpecificButton {}
    }
}
export component Dialog implements SharedState {
    Window {
        DialogButtonBox { } // OK! At this point we can reference all of SharedState's properties and
                            // pass them to DialogButtonBox, which can pass them to
                            // DialogSpecificButton
    }
}

Optional Exposure to Rust

We could also consider exposing SharedState to the Rust API if a component becomes exposed to Rust:

main.slint:

export component App {
    Window {
        DialogButtonBox { } // ERROR? App does not implement SharedState.
    }
}
// generated
mod ui {
    trait SharedState {
        fn get_shared_property(&self) -> SharedString;
        fn set_shared_property(&self, SharedString);
    }

    struct App {
        // ...
    }

    impl App {
        pub fn create(required_shared_state: &Rc<dyn SharedState>)
    }
}

struct AppState {
    // ...
}

impl ui::SharedState for AppState {
    ...
}

fn main() {
    let shared_state = Rc::new(AppState{});
    let app = ui::main::App::create(&shared_state);
}

@ogoffart ogoffart added this to the 1.0.0 milestone Nov 29, 2022
@tronical tronical removed this from the 1.0.0 milestone Jan 26, 2023
@preland
Copy link

preland commented Apr 10, 2023

Has there been any progress towards this issue as of late?

@ogoffart
Copy link
Member

Not much progress.
We are still wondering what to do with the globals: When you declare a global in slint, is the global global for the one component (this is the case right now) or is it global for the whole application?
And if it is global for the whole application where do we keep the global state?
One way would be to use some kind of thread local storage for them. Another way would be to have new_with_shared_globals and somehow have a slint::GlobalContext or some generated container to contain the globals.
We are still undecded.

@preland : since you are asking, could you elaborate on your exact use case, this would maybe help to design this feature

@ogoffart ogoffart added this to the 1.1 milestone Apr 11, 2023
@preland
Copy link

preland commented Apr 11, 2023

The use case is for an open source Rust DAW which is going through an overhaul of the entire codebase, so we are looking into alternative UI frameworks.

One req for the project’s UI is support for multiple windows

@tronical tronical modified the milestones: 1.1, 1.2 Jun 5, 2023
@andrew-otiv
Copy link
Contributor

RE use cases, I'm evaluating UI toolkits for an industrial use case where we would most likey drive 6 full-screened displays full of video feeds, indicators and buttons, ideally from one app (though we might have to split it up). multi-window support is also not in egui yet, so we're probably going to have to go with bindings to one of the huge mature UI frameworks.

@ogoffart
Copy link
Member

Note that multi-window do work in Slint if you either

  • Use the same root component multiple times
  • Use a different slint! macro or compilation unit for each windows

This commit show how to do it with one of our demo: #2094

The limitations are:

  • No concept of parenting or modality yet
  • The global are not shared accross windows (which you may or may not want, but this is the crux of this issue)
  • Can't use several exported components from the same file, they have to be in different file or slint macro, which makes the use of it a bit conmbersome as nothing is re-used between (everything gets duplicated)

We need to improve on that. But I think for the use case of @andrew-otiv, the current situation might actually be good enough.

@ChronosWS
Copy link

ChronosWS commented Nov 2, 2023

I think there is another, possibly more serious issue with this workaround: If you want two components that refer to the same struct, you will get duplicate definitions. For example, with a main window defined here:

import { GlobalConfiguration } from "models/global_configuration.slint";

export component AppWindow inherits Window {
    in-out property<GlobalConfiguration> global_config;
...

and a second window defined here:

import { GlobalConfiguration } from "../models/global_configuration.slint";

export component GlobalSettings inherits Window {
    in-out property <GlobalConfiguration> config;
...

with the workaround above instantiating is as follows:

slint::slint! {
        import { GlobalSettings } from "ui/windows/global_settings.slint";

        export component GlobalSettingsWindow inherits GlobalSettings {

        }
    }

    let global_settings_window = GlobalSettings::new();

You get the following, because GlobalConfiguration gets a definition from two separate places.

mismatched types
`GlobalConfiguration` and `GlobalConfiguration` have similar names, but are actually distinct types
arguments to this method are incorrect
`GlobalConfiguration` is defined in module `crate::main::slint_generatedGlobalSettingsWindow` of the current crate
`GlobalConfiguration` is defined in module `crate::slint_generatedAppWindow` of the current crate

This seems to mean you can't even have multiple windows that use any of the same structs (and I wonder about other components)?

Am I missing some detail of the workaround, or is this just straight up impossible?

EDIT: It looks like ultimately I'm running into the "globals aren't shared", which makes using this for any but trivial multi-window applications (that is, things like confirmatory dialogs) seemingly impossible. 😭

@melMass
Copy link

melMass commented Nov 3, 2023

From my little experience with slint I see this as one of the biggest drawbacks.
I pretty much just went the same path as @ChronosWS

In my case using the main window as a kind of "controller" is not a big deal (I use dialogs that just go over the content of the current window) but the main use case I see for multiple components definition is to have some shared logic in a widget.

For instance I'm making a pretty dumb PathEdit (the classic, line edit, browse button).
I would like to make the callback logic reusable (using rfd to spawn a file dialog and return the path to the widget), what would be the idiomatic way to go about this currently?

import { Button, VerticalBox } from "std-widgets.slint";
import { Button, ScrollView, ListView, HorizontalBox} from "std-widgets.slint";


export component PathEdit inherits HorizontalBox {
    in property <string> label: "Path:";
    in property <string> default_path: "/some/path";

    max-height: 96px;
    Text {
        text: root.label;
        vertical-alignment: center;
        color:gray;
    }

    TextInput { 
        text: root.default-path;
        horizontal-alignment: left;
        vertical-alignment: center;
        horizontal-stretch: 1;
        font-size: 1.5rem;
        padding: 1.5rem;
        
        
     }
     Button {
        text:"...";
        vertical-stretch: 1;

     }
}

I tried what is suggested in #2094, but I couldn't make it work, I'll try the exact example now.

What I tried that didn't work:

Reimport Error (TextStyle is defined multiple times):

  • Defining one compile macro in build.rs and slint()! macro in src
  • Using multiple slint()! macros that import other *.slint files

Only the last is exposed

  • Defining multiple compile macros in build.rs
  • Importing multiple component from a single macro

@chenyanchen
Copy link

Is Slint have a Dialog component or others, I just want to popup a box to show some message.

@chenyanchen
Copy link

Is Slint have a Dialog component or others, I just want to popup a box to show some message.

I found that: https://slint.dev/releases/1.3.2/docs/slint/src/language/builtins/elements#dialog

@ogoffart ogoffart removed this from the 1.4 milestone Jan 16, 2024
@ogoffart ogoffart added priority:high Important issue that needs to be fixed before the next release and removed a:language-c++ C++ API, codegen, CMake build system (mS,mO) a:language-rust Rust API and codegen (mO,mS) labels Jan 16, 2024
@jamesstidard
Copy link

jamesstidard commented Apr 24, 2024

Just to add in my strong interest in this feature and my use case.

The application we'd build will need the ability to have components within the main window be able to be popped out, so the user can multitask and make use of multiple monitors/OS-level window management. So in this case the component being popped out would want to have a shared state with the parent window/component.

Screen Recording 2024-04-24 at 14 31 17 mov

Also generally interested in the use cases described above as well.

Another thing I would mention related to the global state, and this may be wrong because I'm still learning the framework, but because the main window and global states need to both be exported in the same .slint file, it means that you cannot import (as far as I'm currently aware) the global state into child components defined in other .slint file - as it causes a circular dependancy. So you are instead force to pass through all state via component properties, which is ok and preferred in a lot of cases, but if you have things like Pallet you dont want to have to hand all these properties all the way through your component hierarchy. If this doesn't make sense, let me know and I can make an example.

@melMass
Copy link

melMass commented Jun 24, 2024

Glad to see this closed! Is it documented yet?

@ogoffart
Copy link
Member

No, it is not yet documented.

Cf also #5467

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a:compiler Slint compiler internal (not the codegen, not the parser) priority:high Important issue that needs to be fixed before the next release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants