Add support for importing Rust types from another crate Slint compilation (#9329)

To implement an external Slint library, the types and components implemented
in the .slint files needs to be exposed through the Rust crate.
A simple example example/app-library is added to demonstrate how to use this feature.

CC #7060
This commit is contained in:
Benny Sjöstrand 2025-09-16 09:01:44 +02:00 committed by GitHub
parent b23a657c44
commit 0bda0a64eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 766 additions and 42 deletions

View file

@ -61,6 +61,9 @@ members = [
'tests/driver/rust', 'tests/driver/rust',
'tests/screenshots', 'tests/screenshots',
'tests/manual/windowattributes', 'tests/manual/windowattributes',
'tests/manual/module-builds/blogica',
'tests/manual/module-builds/blogicb',
'tests/manual/module-builds/app',
'tools/compiler', 'tools/compiler',
'tools/docsnapper', 'tools/docsnapper',
'tools/figma_import', 'tools/figma_import',

View file

@ -19,6 +19,7 @@ path = "lib.rs"
[features] [features]
default = [] default = []
experimental-module-builds = ["i-slint-compiler/experimental-library-module"]
sdf-fonts = ["i-slint-compiler/sdf-fonts"] sdf-fonts = ["i-slint-compiler/sdf-fonts"]
[dependencies] [dependencies]

View file

@ -205,6 +205,26 @@ impl CompilerConfiguration {
Self { config } Self { config }
} }
/// Configures the compiler to treat the Slint as part of a library.
///
/// Use this when the components and types of the Slint code need
/// to be accessible from other modules.
#[cfg(feature = "experimental-module-builds")]
#[must_use]
pub fn as_library(self, library_name: &str) -> Self {
let mut config = self.config;
config.library_name = Some(library_name.to_string());
Self { config }
}
/// Specify the Rust module to place the generated code in.
#[cfg(feature = "experimental-module-builds")]
#[must_use]
pub fn rust_module(self, rust_module: &str) -> Self {
let mut config = self.config;
config.rust_module = Some(rust_module.to_string());
Self { config }
}
/// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts. /// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts.
/// ///
/// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`], /// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`],
@ -429,6 +449,18 @@ pub fn compile_with_config(
.with_extension("rs"), .with_extension("rs"),
); );
#[cfg(feature = "experimental-module-builds")]
if let Some(library_name) = config.config.library_name.clone() {
println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
println!(
"cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
);
println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
if let Some(rust_module) = &config.config.rust_module {
println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
}
}
let paths_dependencies = let paths_dependencies =
compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?; compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;

View file

@ -35,6 +35,9 @@ sdf-fonts = ["dep:fdsm", "dep:fdsm-ttf-parser", "dep:nalgebra", "dep:rayon"]
# Translation bundler # Translation bundler
bundle-translations = ["dep:polib"] bundle-translations = ["dep:polib"]
# Enable expermental library module support
experimental-library-module = []
default = [] default = []
[dependencies] [dependencies]

View file

@ -20,6 +20,7 @@ use crate::llr::{
TypeResolutionContext as _, TypeResolutionContext as _,
}; };
use crate::object_tree::Document; use crate::object_tree::Document;
use crate::typeloader::LibraryInfo;
use crate::CompilerConfiguration; use crate::CompilerConfiguration;
use itertools::Either; use itertools::Either;
use lyon_path::geom::euclid::approxeq::ApproxEq; use lyon_path::geom::euclid::approxeq::ApproxEq;
@ -160,6 +161,36 @@ pub fn generate(
return super::rust_live_preview::generate(doc, compiler_config); return super::rust_live_preview::generate(doc, compiler_config);
} }
let module_header = generate_module_header();
let qualified_name_ident = |symbol: &SmolStr, library_info: &LibraryInfo| {
let symbol = ident(symbol);
let package = ident(&library_info.package);
if let Some(module) = &library_info.module {
let module = ident(module);
quote!(#package :: #module :: #symbol)
} else {
quote!(#package :: #symbol)
}
};
let library_imports = {
let doc_used_types = doc.used_types.borrow();
doc_used_types
.library_types_imports
.iter()
.map(|(symbol, library_info)| {
let ident = qualified_name_ident(symbol, library_info);
quote!(pub use #ident;)
})
.chain(doc_used_types.library_global_imports.iter().map(|(symbol, library_info)| {
let ident = qualified_name_ident(symbol, library_info);
let inner_symbol_name = smol_str::format_smolstr!("Inner{}", symbol);
let inner_ident = qualified_name_ident(&inner_symbol_name, library_info);
quote!(pub use #ident, #inner_ident;)
}))
.collect::<Vec<_>>()
};
let (structs_and_enums_ids, inner_module) = let (structs_and_enums_ids, inner_module) =
generate_types(&doc.used_types.borrow().structs_and_enums); generate_types(&doc.used_types.borrow().structs_and_enums);
@ -180,12 +211,22 @@ pub fn generate(
let popup_menu = let popup_menu =
llr.popup_menu.as_ref().map(|p| generate_item_tree(&p.item_tree, &llr, None, None, true)); llr.popup_menu.as_ref().map(|p| generate_item_tree(&p.item_tree, &llr, None, None, true));
let globals = llr let mut global_exports = Vec::<TokenStream>::new();
if let Some(library_name) = &compiler_config.library_name {
// Building as a library, SharedGlobals needs to be exported
let ident = format_ident!("{}SharedGlobals", library_name);
global_exports.push(quote!(SharedGlobals as #ident));
}
let globals =
llr.globals.iter_enumerated().filter(|(_, glob)| glob.must_generate()).map(
|(idx, glob)| generate_global(idx, glob, &llr, compiler_config, &mut global_exports),
);
let library_globals_getters = llr
.globals .globals
.iter_enumerated() .iter_enumerated()
.filter(|(_, glob)| glob.must_generate()) .filter(|(_, glob)| glob.from_library)
.map(|(idx, glob)| generate_global(idx, glob, &llr)); .map(|(_idx, glob)| generate_global_getters(glob, &llr));
let shared_globals = generate_shared_globals(&llr, compiler_config); let shared_globals = generate_shared_globals(&doc, &llr, compiler_config);
let globals_ids = llr.globals.iter().filter(|glob| glob.exported).flat_map(|glob| { let globals_ids = llr.globals.iter().filter(|glob| glob.exported).flat_map(|glob| {
std::iter::once(ident(&glob.name)).chain(glob.aliases.iter().map(|x| ident(x))) std::iter::once(ident(&glob.name)).chain(glob.aliases.iter().map(|x| ident(x)))
}); });
@ -207,8 +248,11 @@ pub fn generate(
Ok(quote! { Ok(quote! {
mod #generated_mod { mod #generated_mod {
#module_header
#(#library_imports)*
#inner_module #inner_module
#(#globals)* #(#globals)*
#(#library_globals_getters)*
#(#sub_compos)* #(#sub_compos)*
#popup_menu #popup_menu
#(#public_components)* #(#public_components)*
@ -217,12 +261,25 @@ pub fn generate(
#translations #translations
} }
#[allow(unused_imports)] #[allow(unused_imports)]
pub use #generated_mod::{#(#compo_ids,)* #(#structs_and_enums_ids,)* #(#globals_ids,)* #(#named_exports,)*}; pub use #generated_mod::{#(#compo_ids,)* #(#structs_and_enums_ids,)* #(#globals_ids,)* #(#named_exports,)* #(#global_exports,)*};
#[allow(unused_imports)] #[allow(unused_imports)]
pub use slint::{ComponentHandle as _, Global as _, ModelExt as _}; pub use slint::{ComponentHandle as _, Global as _, ModelExt as _};
}) })
} }
pub(super) fn generate_module_header() -> TokenStream {
quote! {
#![allow(non_snake_case, non_camel_case_types)]
#![allow(unused_braces, unused_parens)]
#![allow(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(unknown_lints, if_let_rescope, tail_expr_drop_order)] // We don't have fancy Drop
use slint::private_unstable_api::re_exports as sp;
#[allow(unused_imports)]
use sp::{RepeatedItemTree as _, ModelExt as _, Model as _, Float as _};
}
}
/// Generate the struct and enums. Return a vector of names to import and a token stream with the inner module /// Generate the struct and enums. Return a vector of names to import and a token stream with the inner module
pub fn generate_types(used_types: &[Type]) -> (Vec<Ident>, TokenStream) { pub fn generate_types(used_types: &[Type]) -> (Vec<Ident>, TokenStream) {
let (structs_and_enums_ids, structs_and_enum_def): (Vec<_>, Vec<_>) = used_types let (structs_and_enums_ids, structs_and_enum_def): (Vec<_>, Vec<_>) = used_types
@ -247,14 +304,6 @@ pub fn generate_types(used_types: &[Type]) -> (Vec<Ident>, TokenStream) {
); );
let inner_module = quote! { let inner_module = quote! {
#![allow(non_snake_case, non_camel_case_types)]
#![allow(unused_braces, unused_parens)]
#![allow(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(unknown_lints, if_let_rescope, tail_expr_drop_order)] // We don't have fancy Drop
use slint::private_unstable_api::re_exports as sp;
#[allow(unused_imports)]
use sp::{RepeatedItemTree as _, ModelExt as _, Model as _, Float as _};
#(#structs_and_enum_def)* #(#structs_and_enum_def)*
const _THE_SAME_VERSION_MUST_BE_USED_FOR_THE_COMPILER_AND_THE_RUNTIME : slint::#version_check = slint::#version_check; const _THE_SAME_VERSION_MUST_BE_USED_FOR_THE_COMPILER_AND_THE_RUNTIME : slint::#version_check = slint::#version_check;
}; };
@ -361,6 +410,7 @@ fn generate_public_component(
} }
fn generate_shared_globals( fn generate_shared_globals(
doc: &Document,
llr: &llr::CompilationUnit, llr: &llr::CompilationUnit,
compiler_config: &CompilerConfiguration, compiler_config: &CompilerConfiguration,
) -> TokenStream { ) -> TokenStream {
@ -377,6 +427,15 @@ fn generate_shared_globals(
.map(global_inner_name) .map(global_inner_name)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let from_library_global_names = llr
.globals
.iter()
.filter(|g| g.from_library)
.map(|g| format_ident!("global_{}", ident(&g.name)))
.collect::<Vec<_>>();
let from_library_global_types =
llr.globals.iter().filter(|g| g.from_library).map(global_inner_name).collect::<Vec<_>>();
let apply_constant_scale_factor = if !compiler_config.const_scale_factor.approx_eq(&1.0) { let apply_constant_scale_factor = if !compiler_config.const_scale_factor.approx_eq(&1.0) {
let factor = compiler_config.const_scale_factor as f32; let factor = compiler_config.const_scale_factor as f32;
Some( Some(
@ -386,18 +445,59 @@ fn generate_shared_globals(
None None
}; };
let library_global_vars = llr
.globals
.iter()
.filter(|g| g.from_library)
.map(|g| {
let library_info = doc.library_exports.get(g.name.as_str()).unwrap();
let shared_gloabls_var_name =
format_ident!("library_{}_shared_globals", library_info.name);
let global_name = format_ident!("global_{}", ident(&g.name));
quote!( #shared_gloabls_var_name.#global_name )
})
.collect::<Vec<_>>();
let pub_token = if compiler_config.library_name.is_some() { quote!(pub) } else { quote!() };
let (library_shared_globals_names, library_shared_globals_types): (Vec<_>, Vec<_>) = doc
.imports
.iter()
.filter_map(|import| import.library_info.clone())
.map(|library_info| {
let struct_name = format_ident!("{}SharedGlobals", library_info.name);
let shared_gloabls_var_name =
format_ident!("library_{}_shared_globals", library_info.name);
let shared_globals_type_name = if let Some(module) = library_info.module {
let package = ident(&library_info.package);
let module = ident(&module);
//(quote!(#shared_gloabls_var_name),quote!(let #shared_gloabls_var_name = #package::#module::#shared_globals_type_name::new(root_item_tree_weak.clone());))
quote!(#package::#module::#struct_name)
} else {
let package = ident(&library_info.package);
quote!(#package::#struct_name)
};
(quote!(#shared_gloabls_var_name), shared_globals_type_name)
})
.unzip();
quote! { quote! {
struct SharedGlobals { #pub_token struct SharedGlobals {
#(#global_names : ::core::pin::Pin<sp::Rc<#global_types>>,)* #(#pub_token #global_names : ::core::pin::Pin<sp::Rc<#global_types>>,)*
#(#pub_token #from_library_global_names : ::core::pin::Pin<sp::Rc<#from_library_global_types>>,)*
window_adapter : sp::OnceCell<sp::WindowAdapterRc>, window_adapter : sp::OnceCell<sp::WindowAdapterRc>,
root_item_tree_weak : sp::VWeak<sp::ItemTreeVTable>, root_item_tree_weak : sp::VWeak<sp::ItemTreeVTable>,
#(#[allow(dead_code)]
#library_shared_globals_names : sp::Rc<#library_shared_globals_types>,)*
} }
impl SharedGlobals { impl SharedGlobals {
fn new(root_item_tree_weak : sp::VWeak<sp::ItemTreeVTable>) -> sp::Rc<Self> { #pub_token fn new(root_item_tree_weak : sp::VWeak<sp::ItemTreeVTable>) -> sp::Rc<Self> {
#(let #library_shared_globals_names = #library_shared_globals_types::new(root_item_tree_weak.clone());)*
let _self = sp::Rc::new(Self { let _self = sp::Rc::new(Self {
#(#global_names : #global_types::new(),)* #(#global_names : #global_types::new(),)*
#(#from_library_global_names : #library_global_vars.clone(),)*
window_adapter : ::core::default::Default::default(), window_adapter : ::core::default::Default::default(),
root_item_tree_weak, root_item_tree_weak,
#(#library_shared_globals_names,)*
}); });
#(_self.#global_names.clone().init(&_self);)* #(_self.#global_names.clone().init(&_self);)*
_self _self
@ -1340,6 +1440,8 @@ fn generate_global(
global_idx: llr::GlobalIdx, global_idx: llr::GlobalIdx,
global: &llr::GlobalComponent, global: &llr::GlobalComponent,
root: &llr::CompilationUnit, root: &llr::CompilationUnit,
compiler_config: &CompilerConfiguration,
global_exports: &mut Vec<TokenStream>,
) -> TokenStream { ) -> TokenStream {
let mut declared_property_vars = vec![]; let mut declared_property_vars = vec![];
let mut declared_property_types = vec![]; let mut declared_property_types = vec![];
@ -1442,6 +1544,13 @@ fn generate_global(
} }
})); }));
let pub_token = if compiler_config.library_name.is_some() {
global_exports.push(quote! (#inner_component_id));
quote!(pub)
} else {
quote!()
};
let public_interface = global.exported.then(|| { let public_interface = global.exported.then(|| {
let property_and_callback_accessors = public_api( let property_and_callback_accessors = public_api(
&global.public_properties, &global.public_properties,
@ -1450,26 +1559,17 @@ fn generate_global(
&ctx, &ctx,
); );
let aliases = global.aliases.iter().map(|name| ident(name)); let aliases = global.aliases.iter().map(|name| ident(name));
let getters = root.public_components.iter().map(|c| { let getters = generate_global_getters(global, root);
let root_component_id = ident(&c.name);
quote! {
impl<'a> slint::Global<'a, #root_component_id> for #public_component_id<'a> {
fn get(component: &'a #root_component_id) -> Self {
Self(&component.0.globals.get().unwrap().#global_id)
}
}
}
});
quote!( quote!(
#[allow(unused)] #[allow(unused)]
pub struct #public_component_id<'a>(&'a ::core::pin::Pin<sp::Rc<#inner_component_id>>); pub struct #public_component_id<'a>(#pub_token &'a ::core::pin::Pin<sp::Rc<#inner_component_id>>);
impl<'a> #public_component_id<'a> { impl<'a> #public_component_id<'a> {
#property_and_callback_accessors #property_and_callback_accessors
} }
#(pub type #aliases<'a> = #public_component_id<'a>;)* #(pub type #aliases<'a> = #public_component_id<'a>;)*
#(#getters)* #getters
) )
}); });
@ -1478,10 +1578,10 @@ fn generate_global(
#[const_field_offset(sp::const_field_offset)] #[const_field_offset(sp::const_field_offset)]
#[repr(C)] #[repr(C)]
#[pin] #[pin]
struct #inner_component_id { #pub_token struct #inner_component_id {
#(#declared_property_vars: sp::Property<#declared_property_types>,)* #(#pub_token #declared_property_vars: sp::Property<#declared_property_types>,)*
#(#declared_callbacks: sp::Callback<(#(#declared_callbacks_types,)*), #declared_callbacks_ret>,)* #(#pub_token #declared_callbacks: sp::Callback<(#(#declared_callbacks_types,)*), #declared_callbacks_ret>,)*
#(#change_tracker_names : sp::ChangeTracker,)* #(#pub_token #change_tracker_names : sp::ChangeTracker,)*
globals : sp::OnceCell<sp::Weak<SharedGlobals>>, globals : sp::OnceCell<sp::Weak<SharedGlobals>>,
} }
@ -1504,6 +1604,29 @@ fn generate_global(
) )
} }
fn generate_global_getters(
global: &llr::GlobalComponent,
root: &llr::CompilationUnit,
) -> TokenStream {
let public_component_id = ident(&global.name);
let global_id = format_ident!("global_{}", public_component_id);
let getters = root.public_components.iter().map(|c| {
let root_component_id = ident(&c.name);
quote! {
impl<'a> slint::Global<'a, #root_component_id> for #public_component_id<'a> {
fn get(component: &'a #root_component_id) -> Self {
Self(&component.0.globals.get().unwrap().#global_id)
}
}
}
});
quote! (
#(#getters)*
)
}
fn generate_item_tree( fn generate_item_tree(
sub_tree: &llr::ItemTree, sub_tree: &llr::ItemTree,
root: &llr::CompilationUnit, root: &llr::CompilationUnit,

View file

@ -14,6 +14,8 @@ pub fn generate(
doc: &Document, doc: &Document,
compiler_config: &CompilerConfiguration, compiler_config: &CompilerConfiguration,
) -> std::io::Result<TokenStream> { ) -> std::io::Result<TokenStream> {
let module_header = super::rust::generate_module_header();
let (structs_and_enums_ids, inner_module) = let (structs_and_enums_ids, inner_module) =
super::rust::generate_types(&doc.used_types.borrow().structs_and_enums); super::rust::generate_types(&doc.used_types.borrow().structs_and_enums);
@ -60,6 +62,7 @@ pub fn generate(
Ok(quote! { Ok(quote! {
mod #generated_mod { mod #generated_mod {
#module_header
#inner_module #inner_module
#(#globals)* #(#globals)*
#(#public_components)* #(#public_components)*

View file

@ -163,6 +163,12 @@ pub struct CompilerConfiguration {
#[cfg(feature = "software-renderer")] #[cfg(feature = "software-renderer")]
pub font_cache: FontCache, pub font_cache: FontCache,
/// The name of the library when compiling as a library.
pub library_name: Option<String>,
/// Specify the Rust module to place the generated code in.
pub rust_module: Option<String>,
} }
impl CompilerConfiguration { impl CompilerConfiguration {
@ -249,6 +255,8 @@ impl CompilerConfiguration {
translation_path_bundle: std::env::var("SLINT_BUNDLE_TRANSLATIONS") translation_path_bundle: std::env::var("SLINT_BUNDLE_TRANSLATIONS")
.ok() .ok()
.map(|x| x.into()), .map(|x| x.into()),
library_name: None,
rust_module: None,
} }
} }

View file

@ -83,7 +83,8 @@ pub struct GlobalComponent {
pub aliases: Vec<SmolStr>, pub aliases: Vec<SmolStr>,
/// True when this is a built-in global that does not need to be generated /// True when this is a built-in global that does not need to be generated
pub is_builtin: bool, pub is_builtin: bool,
/// True if this component is imported from an external library
pub from_library: bool,
/// Analysis for each properties /// Analysis for each properties
pub prop_analysis: TiVec<PropertyIdx, crate::object_tree::PropertyAnalysis>, pub prop_analysis: TiVec<PropertyIdx, crate::object_tree::PropertyAnalysis>,
} }
@ -91,6 +92,7 @@ pub struct GlobalComponent {
impl GlobalComponent { impl GlobalComponent {
pub fn must_generate(&self) -> bool { pub fn must_generate(&self) -> bool {
!self.is_builtin !self.is_builtin
&& !self.from_library
&& (self.exported && (self.exported
|| !self.functions.is_empty() || !self.functions.is_empty()
|| self.properties.iter().any(|p| p.use_count.get() > 0)) || self.properties.iter().any(|p| p.use_count.get() > 0))

View file

@ -227,6 +227,8 @@ fn property_reference_within_sub_component(
fn component_id(component: &Rc<Component>) -> SmolStr { fn component_id(component: &Rc<Component>) -> SmolStr {
if component.is_global() { if component.is_global() {
component.root_element.borrow().id.clone() component.root_element.borrow().id.clone()
} else if component.from_library.get() {
component.id.clone()
} else if component.id.is_empty() { } else if component.id.is_empty() {
format_smolstr!("Component_{}", component.root_element.borrow().id) format_smolstr!("Component_{}", component.root_element.borrow().id)
} else { } else {
@ -833,6 +835,7 @@ fn lower_global(
exported: !global.exported_global_names.borrow().is_empty(), exported: !global.exported_global_names.borrow().is_empty(),
aliases: global.global_aliases(), aliases: global.global_aliases(),
is_builtin, is_builtin,
from_library: global.from_library.get(),
prop_analysis, prop_analysis,
} }
} }

View file

@ -18,7 +18,7 @@ use crate::layout::{LayoutConstraints, Orientation};
use crate::namedreference::NamedReference; use crate::namedreference::NamedReference;
use crate::parser; use crate::parser;
use crate::parser::{syntax_nodes, SyntaxKind, SyntaxNode}; use crate::parser::{syntax_nodes, SyntaxKind, SyntaxNode};
use crate::typeloader::{ImportKind, ImportedTypes}; use crate::typeloader::{ImportKind, ImportedTypes, LibraryInfo};
use crate::typeregister::TypeRegister; use crate::typeregister::TypeRegister;
use itertools::Either; use itertools::Either;
use smol_str::{format_smolstr, SmolStr, ToSmolStr}; use smol_str::{format_smolstr, SmolStr, ToSmolStr};
@ -53,6 +53,7 @@ pub struct Document {
pub custom_fonts: Vec<(SmolStr, crate::parser::SyntaxToken)>, pub custom_fonts: Vec<(SmolStr, crate::parser::SyntaxToken)>,
pub exports: Exports, pub exports: Exports,
pub imports: Vec<ImportedTypes>, pub imports: Vec<ImportedTypes>,
pub library_exports: HashMap<String, LibraryInfo>,
/// Map of resources that should be embedded in the generated code, indexed by their absolute path on /// Map of resources that should be embedded in the generated code, indexed by their absolute path on
/// disk on the build system /// disk on the build system
@ -265,6 +266,7 @@ impl Document {
custom_fonts, custom_fonts,
imports, imports,
exports, exports,
library_exports: Default::default(),
embedded_file_resources: Default::default(), embedded_file_resources: Default::default(),
#[cfg(feature = "bundle-translations")] #[cfg(feature = "bundle-translations")]
translation_builder: None, translation_builder: None,
@ -339,6 +341,12 @@ pub struct UsedSubTypes {
/// All the sub components use by this components and its children, /// All the sub components use by this components and its children,
/// and the amount of time it is used /// and the amount of time it is used
pub sub_components: Vec<Rc<Component>>, pub sub_components: Vec<Rc<Component>>,
/// All types, structs, enums, that orignates from an
/// external library
pub library_types_imports: Vec<(SmolStr, LibraryInfo)>,
/// All global components that originates from an
/// external library
pub library_global_imports: Vec<(SmolStr, LibraryInfo)>,
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -413,6 +421,9 @@ pub struct Component {
/// The list of properties (name and type) declared as private in the component. /// The list of properties (name and type) declared as private in the component.
/// This is used to issue better error in the generated code if the property is used. /// This is used to issue better error in the generated code if the property is used.
pub private_properties: RefCell<Vec<(SmolStr, Type)>>, pub private_properties: RefCell<Vec<(SmolStr, Type)>>,
/// True if this component is imported from an external library.
pub from_library: Cell<bool>,
} }
impl Component { impl Component {

View file

@ -10,6 +10,7 @@ mod clip;
mod collect_custom_fonts; mod collect_custom_fonts;
mod collect_globals; mod collect_globals;
mod collect_init_code; mod collect_init_code;
mod collect_libraries;
mod collect_structs_and_enums; mod collect_structs_and_enums;
mod collect_subcomponents; mod collect_subcomponents;
mod compile_paths; mod compile_paths;
@ -99,6 +100,7 @@ pub async fn run_passes(
let raw_type_loader = let raw_type_loader =
keep_raw.then(|| crate::typeloader::snapshot_with_extra_doc(type_loader, doc).unwrap()); keep_raw.then(|| crate::typeloader::snapshot_with_extra_doc(type_loader, doc).unwrap());
collect_libraries::collect_libraries(doc);
collect_subcomponents::collect_subcomponents(doc); collect_subcomponents::collect_subcomponents(doc);
lower_tabwidget::lower_tabwidget(doc, type_loader, diag).await; lower_tabwidget::lower_tabwidget(doc, type_loader, diag).await;
lower_menus::lower_menus(doc, type_loader, diag).await; lower_menus::lower_menus(doc, type_loader, diag).await;
@ -208,6 +210,7 @@ pub async fn run_passes(
} }
binding_analysis::binding_analysis(doc, &type_loader.compiler_config, diag); binding_analysis::binding_analysis(doc, &type_loader.compiler_config, diag);
collect_globals::mark_library_globals(doc);
unique_id::assign_unique_id(doc); unique_id::assign_unique_id(doc);
doc.visit_all_used_components(|component| { doc.visit_all_used_components(|component| {

View file

@ -42,7 +42,7 @@ pub fn check_public_api(
if is_last { if is_last {
diag.push_warning(format!("Exported component '{}' doesn't inherit Window. No code will be generated for it", export.0.name), &export.0.name_ident); diag.push_warning(format!("Exported component '{}' doesn't inherit Window. No code will be generated for it", export.0.name), &export.0.name_ident);
return false; return false;
} else { } else if config.library_name.is_none () {
diag.push_warning(format!("Exported component '{}' doesn't inherit Window. This is deprecated", export.0.name), &export.0.name_ident); diag.push_warning(format!("Exported component '{}' doesn't inherit Window. This is deprecated", export.0.name), &export.0.name_ident);
} }
} }

View file

@ -4,6 +4,7 @@
//! This pass fills the root component used_types.globals //! This pass fills the root component used_types.globals
use by_address::ByAddress; use by_address::ByAddress;
use smol_str::format_smolstr;
use crate::diagnostics::BuildDiagnostics; use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::NamedReference; use crate::expression_tree::NamedReference;
@ -31,6 +32,19 @@ pub fn collect_globals(doc: &Document, _diag: &mut BuildDiagnostics) {
doc.used_types.borrow_mut().globals = sorted_globals; doc.used_types.borrow_mut().globals = sorted_globals;
} }
pub fn mark_library_globals(doc: &Document) {
let mut used_types = doc.used_types.borrow_mut();
used_types.globals.clone().iter().for_each(|component| {
if let Some(library_info) = doc.library_exports.get(component.id.as_str()) {
component.from_library.set(true);
used_types.library_types_imports.push((component.id.clone(), library_info.clone()));
used_types
.library_types_imports
.push((format_smolstr!("Inner{}", component.id.clone()), library_info.clone()));
}
});
}
fn collect_in_component( fn collect_in_component(
component: &Rc<Component>, component: &Rc<Component>,
global_set: &mut HashSet<ByAddress<Rc<Component>>>, global_set: &mut HashSet<ByAddress<Rc<Component>>>,

View file

@ -0,0 +1,15 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
//! This pass fills the root component library_imports
use crate::object_tree::Document;
pub fn collect_libraries(doc: &mut Document) {
doc.imports.iter().for_each(|import| {
if let Some(library_info) = &import.library_info {
library_info.exports.iter().for_each(|export_name| {
doc.library_exports.insert(export_name.to_string(), library_info.clone());
});
}
});
}

View file

@ -23,11 +23,18 @@ pub fn collect_structs_and_enums(doc: &Document) {
doc.visit_all_used_components(|component| collect_types_in_component(component, &mut hash)); doc.visit_all_used_components(|component| collect_types_in_component(component, &mut hash));
let mut used_types = doc.used_types.borrow_mut(); let mut used_types = doc.used_types.borrow_mut();
let used_struct_and_enums = &mut used_types.structs_and_enums; used_types.structs_and_enums = Vec::with_capacity(hash.len());
*used_struct_and_enums = Vec::with_capacity(hash.len());
while let Some(next) = hash.iter().next() { while let Some(next) = hash.iter().next() {
// Here, using BTreeMap::pop_first would be great when it is stable
let key = next.0.clone(); let key = next.0.clone();
if let Some(library_info) = doc.library_exports.get(key.as_str()) {
// This is a type imported from an external library, just skip it for code generation
hash.remove(&key);
used_types.library_types_imports.push((key, library_info.clone()));
continue;
}
// Here, using BTreeMap::pop_first would be great when it is stable
let used_struct_and_enums = &mut used_types.structs_and_enums;
sort_types(&mut hash, used_struct_and_enums, &key); sort_types(&mut hash, used_struct_and_enums, &key);
} }
} }

View file

@ -447,6 +447,7 @@ fn duplicate_sub_component(
used: component_to_duplicate.used.clone(), used: component_to_duplicate.used.clone(),
private_properties: Default::default(), private_properties: Default::default(),
inherits_popup_window: core::cell::Cell::new(false), inherits_popup_window: core::cell::Cell::new(false),
from_library: core::cell::Cell::new(false),
}; };
let new_component = Rc::new(new_component); let new_component = Rc::new(new_component);

View file

@ -53,6 +53,8 @@ fn rename_globals(doc: &Document, mut count: u32) {
root.id.clone_from(&g.id); root.id.clone_from(&g.id);
} else if let Some(s) = g.exported_global_names.borrow().first() { } else if let Some(s) = g.exported_global_names.borrow().first() {
root.id = s.to_smolstr(); root.id = s.to_smolstr();
} else if g.from_library.get() {
root.id = format_smolstr!("{}", g.id);
} else { } else {
root.id = format_smolstr!("{}-{}", g.id, count); root.id = format_smolstr!("{}-{}", g.id, count);
} }

View file

@ -49,11 +49,23 @@ pub enum ImportKind {
ModuleReexport(syntax_nodes::ExportsList), ModuleReexport(syntax_nodes::ExportsList),
} }
#[derive(Debug, Clone)]
pub struct LibraryInfo {
pub name: String,
pub package: String,
pub module: Option<String>,
pub exports: Vec<ExportedName>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ImportedTypes { pub struct ImportedTypes {
pub import_uri_token: SyntaxToken, pub import_uri_token: SyntaxToken,
pub import_kind: ImportKind, pub import_kind: ImportKind,
pub file: String, pub file: String,
/// `import {Foo, Bar} from "@Foo"` where Foo is an external
/// library located in another crate
pub library_info: Option<LibraryInfo>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -290,6 +302,7 @@ impl Snapshotter {
custom_fonts: document.custom_fonts.clone(), custom_fonts: document.custom_fonts.clone(),
imports: document.imports.clone(), imports: document.imports.clone(),
exports, exports,
library_exports: document.library_exports.clone(),
embedded_file_resources: document.embedded_file_resources.clone(), embedded_file_resources: document.embedded_file_resources.clone(),
#[cfg(feature = "bundle-translations")] #[cfg(feature = "bundle-translations")]
translation_builder: document.translation_builder.clone(), translation_builder: document.translation_builder.clone(),
@ -369,6 +382,7 @@ impl Snapshotter {
private_properties: RefCell::new(component.private_properties.borrow().clone()), private_properties: RefCell::new(component.private_properties.borrow().clone()),
root_constraints, root_constraints,
root_element, root_element,
from_library: core::cell::Cell::new(false),
} }
}); });
self.keep_alive.push((component.clone(), result.clone())); self.keep_alive.push((component.clone(), result.clone()));
@ -623,7 +637,15 @@ impl Snapshotter {
Weak::upgrade(&self.use_component(component)).expect("Looking at a known component") Weak::upgrade(&self.use_component(component)).expect("Looking at a known component")
}) })
.collect(); .collect();
object_tree::UsedSubTypes { globals, structs_and_enums, sub_components } let library_types_imports = used_types.library_types_imports.clone();
let library_global_imports = used_types.library_global_imports.clone();
object_tree::UsedSubTypes {
globals,
structs_and_enums,
sub_components,
library_types_imports,
library_global_imports,
}
} }
fn snapshot_popup_window( fn snapshot_popup_window(
@ -988,11 +1010,56 @@ impl TypeLoader {
imports.push(import); imports.push(import);
continue; continue;
} }
dependencies_futures.push(Box::pin(async move { dependencies_futures.push(Box::pin(async move {
let file = import.file.as_str(); #[cfg(feature = "experimental-library-module")]
let import_file = import.file.clone();
#[cfg(feature = "experimental-library-module")]
if let Some(maybe_library_import) = import_file.strip_prefix('@') {
if let Some(library_name) = std::env::var(format!(
"DEP_{}_SLINT_LIBRARY_NAME",
maybe_library_import.to_uppercase()
))
.ok()
{
if library_name == maybe_library_import {
let library_slint_source = std::env::var(format!(
"DEP_{}_SLINT_LIBRARY_SOURCE",
maybe_library_import.to_uppercase()
))
.ok()
.unwrap_or_default();
import.file = library_slint_source;
if let Some(library_package) = std::env::var(format!(
"DEP_{}_SLINT_LIBRARY_PACKAGE",
maybe_library_import.to_uppercase()
))
.ok()
{
import.library_info = Some(LibraryInfo {
name: library_name,
package: library_package,
module: std::env::var(format!("DEP_{}_SLINT_LIBRARY_MODULE",
maybe_library_import.to_uppercase()
)).ok(),
exports: Vec::new(),
});
} else {
// This should never happen
let mut state = state.borrow_mut();
let state: &mut BorrowedTypeLoader<'a> = &mut *state;
state.diag.push_error(format!("DEP_{}_SLINT_LIBRARY_PACKAGE is missing for external library import", maybe_library_import.to_uppercase()).into(), &import.import_uri_token.parent());
}
}
}
}
let doc_path = Self::ensure_document_loaded( let doc_path = Self::ensure_document_loaded(
state, state,
file, import.file.as_str(),
Some(import.import_uri_token.clone().into()), Some(import.import_uri_token.clone().into()),
import_stack.clone(), import_stack.clone(),
) )
@ -1008,7 +1075,7 @@ impl TypeLoader {
let core::task::Poll::Ready((mut import, doc_path)) = fut.as_mut().poll(cx) else { return true; }; let core::task::Poll::Ready((mut import, doc_path)) = fut.as_mut().poll(cx) else { return true; };
let Some(doc_path) = doc_path else { return false }; let Some(doc_path) = doc_path else { return false };
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
let state = &mut *state; let state: &mut BorrowedTypeLoader<'a> = &mut *state;
let Some(doc) = state.tl.get_document(&doc_path) else { let Some(doc) = state.tl.get_document(&doc_path) else {
panic!("Just loaded document not available") panic!("Just loaded document not available")
}; };
@ -1017,6 +1084,13 @@ impl TypeLoader {
let mut imported_types = ImportedName::extract_imported_names(imported_types).peekable(); let mut imported_types = ImportedName::extract_imported_names(imported_types).peekable();
if imported_types.peek().is_some() { if imported_types.peek().is_some() {
Self::register_imported_types(doc, &import, imported_types, registry_to_populate, state.diag); Self::register_imported_types(doc, &import, imported_types, registry_to_populate, state.diag);
#[cfg(feature = "experimental-library-module")]
if import.library_info.is_some() {
import.library_info.as_mut().unwrap().exports = doc.exports.iter().map(|(exported_name, _compo_or_type)| {
exported_name.clone()
}).collect();
}
} else { } else {
state.diag.push_error("Import names are missing. Please specify which types you would like to import".into(), &import.import_uri_token.parent()); state.diag.push_error("Import names are missing. Please specify which types you would like to import".into(), &import.import_uri_token.parent());
} }
@ -1549,6 +1623,7 @@ impl TypeLoader {
doc.ImportSpecifier() doc.ImportSpecifier()
.map(|import| { .map(|import| {
let maybe_import_uri = import.child_token(SyntaxKind::StringLiteral); let maybe_import_uri = import.child_token(SyntaxKind::StringLiteral);
let kind = import let kind = import
.ImportIdentifierList() .ImportIdentifierList()
.map(ImportKind::ImportList) .map(ImportKind::ImportList)
@ -1587,6 +1662,7 @@ impl TypeLoader {
import_uri_token: import_uri, import_uri_token: import_uri,
import_kind: type_specifier, import_kind: type_specifier,
file: path_to_import, file: path_to_import,
library_info: None,
}) })
}) })
} }

View file

@ -0,0 +1,20 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: MIT
[package]
name = "bapp"
version = "0.1.0"
edition = "2021"
authors = ["Slint Developers <info@slint.dev>"]
publish = false
license = "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0"
[dependencies]
slint = { path = "../../../../api/rs/slint" }
blogica = { path = "../blogica" }
blogicb = { path = "../blogicb" }
rand = "0.8.5"
random_word = { version = "0.5.2", features = ["en"] }
[build-dependencies]
slint-build = { path = "../../../../api/rs/build", features = ["experimental-module-builds"] }

View file

@ -0,0 +1,6 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
fn main() {
slint_build::compile("ui/app-window.slint").expect("Slint build failed");
}

View file

@ -0,0 +1,53 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
use blogica;
use blogicb;
use random_word;
use std::error::Error;
slint::include_modules!();
fn main() -> Result<(), Box<dyn Error>> {
let ui = AppWindow::new()?;
let blociga_api = ui.global::<blogica::backend::BLogicAAPI>();
blogica::backend::init(&blociga_api);
let blogicb_api = ui.global::<blogicb::BLogicBAPI>();
blogicb::init(&blogicb_api);
ui.on_update_blogic_data({
let ui_handle = ui.as_weak();
move || {
let ui = ui_handle.upgrade().unwrap();
let blogica_api = ui.global::<blogica::backend::BLogicAAPI>();
let mut bdata = blogica::backend::BData::default();
bdata.colors = slint::ModelRc::new(slint::VecModel::from(
(1..6)
.into_iter()
.map(|_| {
let red = rand::random::<u8>();
let green = rand::random::<u8>();
let blue = rand::random::<u8>();
slint::Color::from_rgb_u8(red, green, blue)
})
.collect::<Vec<_>>(),
));
bdata.codes = slint::ModelRc::new(slint::VecModel::from(
(1..6)
.into_iter()
.map(|_| slint::SharedString::from(random_word::get(random_word::Lang::En)))
.collect::<Vec<_>>(),
));
blogica_api.invoke_update(bdata);
}
});
ui.run()?;
Ok(())
}

View file

@ -0,0 +1,32 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, VerticalBox } from "std-widgets.slint";
import { BLogicA, BLogicAAPI } from "@BLogicA";
import { BLogicB, BLogicBAPI } from "@BLogicB";
export component AppWindow inherits Window {
callback update-blogic-data();
VerticalBox {
BLogicA {}
BLogicB {}
Button {
text: "Crank me up!";
clicked => {
BLogicBAPI.crank-it({ magic-number: 42, cranks: [ "delta", "alfta", "sorta", "coso", "tokyo", "denia" ]});
}
}
Text {
text:BLogicBAPI.status;
}
}
Timer {
interval: 1s;
running: true;
triggered => {
root.update-blogic-data();
}
}
}

View file

@ -0,0 +1,16 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: MIT
[package]
name = "blogica"
links = "blogica"
authors = ["Slint Developers <info@slint.dev>"]
edition = "2021"
publish = false
license = "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0"
[dependencies]
slint = { path = "../../../../api/rs/slint" }
[build-dependencies]
slint-build = { path = "../../../../api/rs/build", features = ["experimental-module-builds"] }

View file

@ -0,0 +1,10 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
fn main() -> Result<(), slint_build::CompileError> {
let config =
slint_build::CompilerConfiguration::new().as_library("BLogicA").rust_module("backend");
slint_build::compile_with_config("ui/blogica.slint", config)?;
Ok(())
}

View file

@ -0,0 +1,17 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
pub mod backend {
use slint::SharedString;
slint::include_modules!();
pub fn init(blogica_api: &BLogicAAPI) {
blogica_api.set_code1(SharedString::from("Important thing"));
blogica_api.set_code2(SharedString::from("Another important thing"));
blogica_api.set_code3(SharedString::from("Yet another important thing"));
blogica_api.set_code4(SharedString::from("One more important thing"));
blogica_api.set_initialized(true);
}
}

View file

@ -0,0 +1,95 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct BData {
colors: [color],
codes: [string]
}
export global BLogicAAPI {
in property <bool> initialized: false;
out property <color> color1: #0e3151;
out property <color> color2: #107013;
out property <color> color3: #8a1624;
out property <color> color4: #e4d213;
in-out property <string> code1: "Important thing";
in-out property <string> code2: "Also important thing";
in-out property <string> code3: "May be an important thingy";
in-out property <string> code4: "Not a important thing";
public function update(bdata:BData) {
if (bdata.colors.length >= 4) {
self.color1 = bdata.colors[0];
self.color2 = bdata.colors[1];
self.color3 = bdata.colors[2];
self.color4 = bdata.colors[3];
}
if (bdata.codes.length >= 4) {
self.code1 = bdata.codes[0];
self.code2 = bdata.codes[1];
self.code3 = bdata.codes[2];
self.code4 = bdata.codes[3];
}
}
}
export component BLogicA {
private property <bool> api-initialized <=> BLogicAAPI.initialized;
width: 600px; height: 200px;
Rectangle {
x: 0px; y:0px;
width: 50%; height: 50%;
background: BLogicAAPI.color1;
Text {
text <=> BLogicAAPI.code1;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
x: root.width / 2; y:0px;
width: 50%; height: 50%;
background: BLogicAAPI.color2;
Text {
text <=> BLogicAAPI.code2;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
x: 0px; y:root.height / 2;
width: 50%; height: 50%;
background: BLogicAAPI.color3;
Text {
text <=> BLogicAAPI.code3;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
x: root.width / 2; y: root.height / 2;
width: 50%; height: 50%;
background: BLogicAAPI.color4;
Text {
text <=> BLogicAAPI.code4;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
changed api-initialized => {
if (self.api-initialized) {
debug("BLogicAAPI initialized");
}
}
}

View file

@ -0,0 +1,16 @@
# Copyright © SixtyFPS GmbH <info@slint.dev>
# SPDX-License-Identifier: MIT
[package]
name = "blogicb"
links = "blogicb"
authors = ["Slint Developers <info@slint.dev>"]
edition = "2021"
publish = false
license = "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0"
[dependencies]
slint = { path = "../../../../api/rs/slint" }
[build-dependencies]
slint-build = { path = "../../../../api/rs/build", features = ["experimental-module-builds"] }

View file

@ -0,0 +1,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
fn main() -> Result<(), slint_build::CompileError> {
let config = slint_build::CompilerConfiguration::new().as_library("BLogicB");
slint_build::compile_with_config("ui/blogicb.slint", config)?;
Ok(())
}

View file

@ -0,0 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
use slint::SharedString;
slint::include_modules!();
pub fn init(blogicb_api: &BLogicBAPI) {
blogicb_api.set_crank1(SharedString::from("1"));
blogicb_api.set_crank2(SharedString::from("2"));
blogicb_api.set_crank3(SharedString::from("3"));
blogicb_api.set_crank4(SharedString::from("5"));
blogicb_api.set_crank5(SharedString::from("7"));
blogicb_api.set_crank6(SharedString::from("11"));
// TODO: if BLogicBAPI can be a shared reference, so we can connect callbacks here
// and pass / move the reference to the closures
blogicb_api.set_initialized(true);
}

View file

@ -0,0 +1,123 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct CrankData {
magic-number: int,
cranks: [string]
}
export global BLogicBAPI {
in property <bool> initialized: false;
in property <color> color1: #6c839a;
in property <color> color2: #4b5c72;
in property <color> color3: #185151;
in property <color> color4: #464969;
in property <color> color5: #1b1143;
in property <color> color6: #203158;
in-out property <string> crank1: "1";
in-out property <string> crank2: "2";
in-out property <string> crank3: "3";
in-out property <string> crank4: "5";
in-out property <string> crank5: "7";
in-out property <string> crank6: "11";
out property <string> status;
public function crank-it(crank-data:CrankData) {
if (crank-data.magic-number == 42) {
self.status = "The answer to life, the universe and everything";
} else {
self.status = "Just a number";
}
if (crank-data.cranks.length >= 6) {
self.crank1 = crank-data.cranks[0];
self.crank2 = crank-data.cranks[1];
self.crank3 = crank-data.cranks[2];
self.crank4 = crank-data.cranks[3];
self.crank5 = crank-data.cranks[4];
self.crank6 = crank-data.cranks[5];
}
}
}
export component BLogicB {
private property <bool> api-initialized <=> BLogicBAPI.initialized;
width: 600px; height: 240px;
GridLayout {
Row {
Rectangle {
background: BLogicBAPI.color1;
Text {
text <=> BLogicBAPI.crank1;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
background: BLogicBAPI.color2;
Text {
text <=> BLogicBAPI.crank2;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
Row {
Rectangle {
background: BLogicBAPI.color3;
Text {
text <=> BLogicBAPI.crank3;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
background: BLogicBAPI.color4;
Text {
text <=> BLogicBAPI.crank4;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
Row {
Rectangle {
background: BLogicBAPI.color5;
Text {
text <=> BLogicBAPI.crank5;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Rectangle {
background: BLogicBAPI.color6;
Text {
text <=> BLogicBAPI.crank6;
color: white;
font-size: 24px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
changed api-initialized => {
if (self.api-initialized) {
debug("BLogicBAPI initialized");
}
}
}