slint icon indicating copy to clipboard operation
slint copied to clipboard

Component Library

Open John0x opened this issue 4 years ago • 15 comments
trafficstars

Hey Is it possible to write a component library for sixtyfps as of the current state? Meaning a Rust lib, that exports component to be used in other packages. Primarily for Rust (both lib and consumer).

Didn't find any documentation/issues/examples talking about that

John0x avatar Feb 02 '21 09:02 John0x

This is currently not supported but it is on our radar.

Could you explain your use-case a little bit? What kind of components are you thinking about?

tronical avatar Feb 02 '21 10:02 tronical

It's pretty important for separation of concerns and distribution. Let's take web development as an example, where this is pretty common or even standard. You create a lib for your UI components so that you can reuse them for different applications and to have a better structure.

Once that's possible we could also create a library for standard UI components for different systems like material design for example.

John0x avatar Feb 02 '21 14:02 John0x

Yes, we do have a module system which allow to put components in different files to be re-used. https://github.com/sixtyfpsui/sixtyfps/blob/master/docs/langref.md#modules and we use that for the standard widget library.

Now, we haven't anything in place regarding how to distribute theses, especially if they need to mix with native code.

ogoffart avatar Feb 03 '21 07:02 ogoffart

Okay, thank you :) I think it would be beneficial if distributing these modules would be as easy as possible, cause that's one of the reasons why modern web frameworks are so damn popular, even for desktop apps. They make it as easy as possible so that the ecosystem can grow. And therefore more people use it because they can reuse component frameworks that are already pretty slick

John0x avatar Feb 03 '21 10:02 John0x

Hi, is there any update on that point. I think this is also necessary if you want to create a library with resources / themes or a custom widget library that you want to use in different projects / crates.

FloVanGH avatar Oct 29 '21 18:10 FloVanGH

There is still no progress. right now you'd just have to put your files in a .zip and share that? (or, better, in a git repository)

It would be also possible to put these files in a crate.io and somehow add ~/.cargo/registry/src/github.com-1ecc6299db9ec823/foobar-1.2.3 to the .60 include path. Or what better option could there be?

ogoffart avatar Nov 01 '21 09:11 ogoffart

There is still no progress. right now you'd just have to put your files in a .zip and share that? (or, better, in a git repository)

It would be also possible to put these files in a crate.io and somehow add ~/.cargo/registry/src/github.com-1ecc6299db9ec823/foobar-1.2.3 to the .60 include path. Or what better option could there be?

I'd recommend using the big web frameworks as an inspiration, as close as possible. Would definitely help adoption if it's easily reusable. In JavaScript you usually just include the package as a dependency and then import the components you intend to use.

John0x avatar Nov 02 '21 18:11 John0x

I managed to easily include .slint files from a Cargo crate without any changes in Slint itself. The only issue here is that internal Cargo paths change and I have to manually update the LSP configuration all the time, so IDE integration works properly. Currently experimenting with using symlinks that are maintained by the build script to create a stable path for the LSP to reference. Official support from Slint's side probably would be better compared to such hacky solutions.

levrik avatar May 21 '22 08:05 levrik

I found a solution to reference Slint files from my custom Slint widget library crate with a little build magic.

First in the build.rs of my crate all new or changed .slint files are copied to the output directory of the crate. On the lib.rs file there is a create_include method that you can call from your build.rs file. This creates a slint file in your project that exports all stuff of the .slint output files. You can add this include file to .gitignore.

I have to cleanup the code a little but after that I want to provide a cargo generate template.

It's a solution until there is a build in solution in slint :-)

Result

image
  • include file
import { co } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/co/co.slint";
import { mi } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/assets/icons/material_icons.slint"; 
import { MaterialIcon } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/assets/icons/material_icon.slint"; 
import { CoWindow } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/components/components.slint";
import { Button, OutlineButton, IconButton, TextLine } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/widgets/widgets.slint";
import { KeyboardPage, Keyboard } from "$PROJECT_PATH/target/release/build/co_widgets-580ab606e89d424c/out/co_widgets/keyboard/keyboard.slint";

export { co }
export { mi, MaterialIcon }
export { CoWindow } 
export { Button, OutlineButton, IconButton, TextLine }   
export { KeyboardPage, Keyboard }

FloVanGH avatar Nov 02 '22 09:11 FloVanGH

I haven't tested this, but i propose the following approach: In the crates that provide slint files, the build.rs would contains something like (named the mylib crate in this example)

// (in mylib/build.rs 's main)
println!("cargo:SLINT_INCLUDE_PATH={}/ui", env!("CARGO_MANIFEST_DIR"))

Then, the application will need to add the include path in the build script such as

// (in app/build.rs)
slint_build::compile_with_config("ui/main.slint", 
    slint_build::CompilerConfiguration::new().with_include_path(vec![
        env!("DEP_MYLIB_SLINT_INCLUDE_PATH").into()
    ]));

And that's it if there is only slint file.

If there is also native code, maybe the library provides a global or some defined callback, and then the application cde would need to do:

//...
app.global::<MyLibLogic>().on_render(mylib::render);
app.global::<MyLibLogic>().on_foo_bar(mylib::foo_bar);
//...

This should all works now with the current release and has worked for a long time.

But it would be nice if there was some automated way to do this so the use would be easier. and in the app's build.rs we could just do something like slint_build::CompilerConfiguration::new().add_libary("mylib") and somehow this would take care also of initializing the global or so with code form ::mylib if mylib added some metadata on how to do it.

ogoffart avatar Nov 02 '22 09:11 ogoffart

@ogoffart I tried this

lint_build::compile_with_config("ui/main.slint", 
    slint_build::CompilerConfiguration::new().with_include_path(vec![
        env!("DEP_MYLIB_SLINT_INCLUDE_PATH").into()
    ]));

I get

environment variable `SLINT_INCLUDE_GENERATED` not defined
 --> examples/widgets/src/main.rs:10:1
  |
10 | slint::include_modules!();
  | ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: this error originates in the macro `env` which comes from the expansion of the macro `slint::include_modules` (in Nightly builds, run with -Z macro-backtrace for more info)

error: could not compile `widgets` due to previous error

The path that is read by usign env! in the build.rs is correct

Error from the compiler

Err(CompileError(["$PROJECT_PATH/examples/widgets/ui/widgets.slint:1: Cannot find requested import \"co_widgets/co_widgets.slint\" in the include search path", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:3: Unknown type CoWindow", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:25: Unknown type KeyboardPage", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:35: Unknown type OutlineButton", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:38: 'clicked' is not a callback in <error>", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:44: Unknown type OutlineButton", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:47: 'clicked' is not a callback in <error>", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:53: Unknown type Button", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:55: 'clicked' is not a callback in <error>", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:58: Unknown type Button", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:65: Unknown type Button", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:69: 'clicked' is not a callback in <error>", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:72: Unknown type IconButton", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:79: Unknown type TextLine", "$PROJECT_PATH/examples/widgets/ui/widgets.slint:86: 'clicked' is not a callback in <error>"]))

Looks like there is a problem with imports import { co } from "co/co.slint";

FloVanGH avatar Nov 02 '22 10:11 FloVanGH

Here you can find a template that makes it possible to ship your slint files as crate with lsp and viewer support: https://codeberg.org/flovansl/slint_lib_template and I've crated also an example https://codeberg.org/flovansl/slint_lib_template_example.

FloVanGH avatar Nov 09 '22 12:11 FloVanGH

Any ETA for this feature?

bog-dan-ro avatar Jun 27 '23 10:06 bog-dan-ro

In the crates that provide slint files, the build.rs would contains something like (named the mylib crate in this example)

// (in mylib/build.rs 's main)
println!("cargo:SLINT_INCLUDE_PATH={}/ui", env!("CARGO_MANIFEST_DIR"))

DEP_FOO_KEY=value metadata is only passed when using package.links. With this added to mylib/Cargo.toml, DEP_MYLIB_SLINT_INCLUDE_PATH is visible in the app's build script:

[package]
# ...
links = "mylib" # <==

Not sure if it would be a problem in practice but with this approach one could only have .slint files provided by an immediate dependency because the metadata is not passed to transitive dependents.

What if slint-build would use cargo_metadata to find any dependencies that depend on slint and automatically add them to include paths? It could default to <dep-manifest-dir>/ui by convention, and allow specifying the value in mylib/Cargo.toml metadata if desired:

[package.metadata.slint]
include_path = "foo" # or ["ui", "foo", "bar"]

jpnurmi avatar Sep 20 '23 09:09 jpnurmi

Could slint_build::compile() do something like this out of the box?

// app/build.rs
use cargo_metadata::{MetadataCommand, Package};
use serde_json::Value;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
    let metadata = MetadataCommand::new()
        .manifest_path(std::path::PathBuf::from(manifest_dir).join("Cargo.toml"))
        .exec()?;

    let dependencies = metadata
        .packages
        .iter()
        .filter(|p| p.dependencies.iter().any(|d| d.name == "slint"))
        .collect::<Vec<_>>();

    let include_paths = dependencies
        .into_iter()
        .map(resolve_slint_include_path)
        .collect::<Vec<_>>()
        .concat();

    slint_build::compile_with_config(
        "ui/main.slint",
        slint_build::CompilerConfiguration::new().with_include_paths(include_paths),
    )
    .unwrap();

    Ok(())
}

fn resolve_slint_include_path(package: &Package) -> Vec<std::path::PathBuf> {
    let manifest_dir = package.manifest_path.parent().unwrap();

    let include_path = package
        .metadata
        .get("slint")
        .and_then(|s| s.get("include_path"));

    match include_path {
        Some(Value::String(s)) => vec![manifest_dir.join(s).into()],
        Some(Value::Array(a)) => a
            .iter()
            .map(|s| manifest_dir.join(s.as_str().unwrap()).into())
            .collect(),
        _ => vec![manifest_dir.join("ui").into()],
    }
}

jpnurmi avatar Sep 20 '23 10:09 jpnurmi