Experimental support for MenuBar

Introduces `MenuBar{ ... }` that can be put in a Window
This commit is contained in:
Olivier Goffart 2024-10-08 18:24:52 +02:00
parent 20443ec0df
commit 5bd20def0e
38 changed files with 1023 additions and 69 deletions

View file

@ -183,6 +183,31 @@ export component SwipeGestureHandler {
//-default_size_binding:expands_to_parent_geometry
}
// Lowered to MenuBarImpl from the style
component MenuBar {
//-is_non_item_type
//-disallow_global_types_as_child_elements
in property <[MenuEntry]> entries;
callback activated(MenuEntry);
callback sub-menu(MenuEntry) -> [MenuEntry];
// Currently experimental
//-is_internal
}
// Lowered to a call that generate a ContextMenuImpl
export component ContextMenu {
function show() {}
//in property <[MenuEntry]> entries;
callback activated(MenuEntry);
callback sub-menu(MenuEntry) -> [MenuEntry];
//-default_size_binding:expands_to_parent_geometry
// Currently experimental
//-is_internal
}
component WindowItem {
in-out property <length> width;
in-out property <length> height;
@ -198,7 +223,9 @@ component WindowItem {
in property <image> icon;
}
export component Window inherits WindowItem { }
export component Window inherits WindowItem {
MenuBar {}
}
export component BoxShadow inherits Empty {
in property <length> border_radius;
@ -418,7 +445,7 @@ export component PopupWindow {
in property <length> anchor_y;
in property <length> anchor_height;
in property <length> anchor_width;*/
in property <bool> close-on-click;
in property <bool> close-on-click;
in property <PopupClosePolicy> close-policy; // constexpr hardcoded in typeregister.rs
//show() is hardcoded in typeregister.rs
}

View file

@ -43,6 +43,7 @@ pub enum BuiltinFunction {
ClearFocusItem,
ShowPopupWindow,
ClosePopupWindow,
ShowPopupMenu,
SetSelectionOffsets,
/// A function that belongs to an item (such as TextInput's select-all function).
ItemMemberFunction(SmolStr),
@ -156,6 +157,7 @@ declare_builtin_function_types!(
ClearFocusItem: (Type::ElementReference) -> Type::Void,
ShowPopupWindow: (Type::ElementReference) -> Type::Void,
ClosePopupWindow: (Type::ElementReference) -> Type::Void,
ShowPopupMenu: (Type::ElementReference, Type::Model, crate::typeregister::logical_point_type()) -> Type::Void,
ItemMemberFunction(..): (Type::ElementReference) -> Type::Void,
SetSelectionOffsets: (Type::ElementReference, Type::Int32, Type::Int32) -> Type::Void,
ItemFontMetrics: (Type::ElementReference) -> crate::typeregister::font_metrics_type(),
@ -264,7 +266,9 @@ impl BuiltinFunction {
| BuiltinFunction::ATan
| BuiltinFunction::ATan2 => true,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::ShowPopupWindow
| BuiltinFunction::ClosePopupWindow
| BuiltinFunction::ShowPopupMenu => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::ItemFontMetrics => false, // depends also on Window's font properties
@ -331,7 +335,9 @@ impl BuiltinFunction {
| BuiltinFunction::ATan
| BuiltinFunction::ATan2 => true,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::ShowPopupWindow
| BuiltinFunction::ClosePopupWindow
| BuiltinFunction::ShowPopupMenu => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::ItemFontMetrics => true,

View file

@ -819,6 +819,24 @@ pub fn generate(
}
file.declarations.push(Declaration::Struct(globals_struct));
if let Some(popup_menu) = &llr.popup_menu {
let component_id = ident(&popup_menu.item_tree.root.name);
let mut popup_struct = Struct { name: component_id.clone(), ..Default::default() };
generate_item_tree(
&mut popup_struct,
&popup_menu.item_tree,
&llr,
None,
true,
component_id,
Access::Public,
&mut file,
&conditional_includes,
);
file.definitions.extend(popup_struct.extract_definitions().collect::<Vec<_>>());
file.declarations.push(Declaration::Struct(popup_struct));
};
for p in &llr.public_components {
generate_public_component(&mut file, &conditional_includes, p, &llr);
}
@ -1199,6 +1217,7 @@ fn generate_public_component(
&component.item_tree,
unit,
None,
false,
component_id,
Access::Private, // Hide properties and other fields from the C++ API
file,
@ -1289,6 +1308,7 @@ fn generate_item_tree(
sub_tree: &llr::ItemTree,
root: &llr::CompilationUnit,
parent_ctx: Option<ParentCtx>,
is_popup_menu: bool,
item_tree_class_name: SmolStr,
field_access: Access,
file: &mut File,
@ -1708,21 +1728,24 @@ fn generate_item_tree(
"self->self_weak = vtable::VWeak(self_rc).into_dyn();".into(),
];
#[cfg(feature = "bundle-translations")]
if let Some(translations) = &root.translations {
let lang_len = translations.languages.len();
create_code.push(format!(
"std::array<slint::cbindgen_private::Slice<uint8_t>, {lang_len}> languages {{ {} }};",
translations
.languages
.iter()
.map(|l| format!("slint::private_api::string_to_slice({l:?})"))
.join(", ")
));
create_code.push(format!("slint::cbindgen_private::slint_translate_set_bundled_languages({{ languages.data(), {lang_len} }});"));
}
if is_popup_menu {
create_code.push("self->globals = globals;".into());
create_parameters.push("const SharedGlobals *globals".into());
} else if parent_ctx.is_none() {
#[cfg(feature = "bundle-translations")]
if let Some(translations) = &root.translations {
let lang_len = translations.languages.len();
create_code.push(format!(
"std::array<slint::cbindgen_private::Slice<uint8_t>, {lang_len}> languages {{ {} }};",
translations
.languages
.iter()
.map(|l| format!("slint::private_api::string_to_slice({l:?})"))
.join(", ")
));
create_code.push(format!("slint::cbindgen_private::slint_translate_set_bundled_languages({{ languages.data(), {lang_len} }});"));
}
if parent_ctx.is_none() {
create_code.push("self->globals = &self->m_globals;".into());
create_code.push("self->m_globals.root_weak = self->self_weak;".into());
create_code.push("slint::cbindgen_private::slint_ensure_backend();".into());
@ -1865,6 +1888,7 @@ fn generate_sub_component(
&popup.item_tree,
root,
Some(ParentCtx::new(&ctx, None)),
false,
component_id,
Access::Public,
file,
@ -2404,6 +2428,7 @@ fn generate_repeated_component(
&repeated.sub_tree,
root,
Some(parent_ctx),
false,
repeater_id.clone(),
Access::Public,
file,
@ -3661,6 +3686,53 @@ fn compile_builtin_function_call(
panic!("internal error: invalid args to ClosePopupWindow {:?}", arguments)
}
}
BuiltinFunction::ShowPopupMenu => {
let [llr::Expression::PropertyReference(context_menu_ref), entries, position] = arguments
else {
panic!("internal error: invalid args to ShowPopupMenu {arguments:?}")
};
let context_menu = access_member(context_menu_ref, ctx);
let context_menu_rc = access_item_rc(context_menu_ref, ctx);
let position = compile_expression(position, ctx);
let entries = compile_expression(entries, ctx);
let popup = ctx
.compilation_unit
.popup_menu
.as_ref()
.expect("there should be a popup menu if we want to show it");
let popup_id = ident(&popup.item_tree.root.name);
let window = access_window_field(ctx);
let popup_ctx = EvaluationContext::new_sub_component(
ctx.compilation_unit,
&popup.item_tree.root,
CppGeneratorContext { global_access: "self->globals".into(), conditional_includes: ctx.generator_state.conditional_includes },
None,
);
let access_entries = access_member(&popup.entries, &popup_ctx);
let forward_callback = |pr, cb| {
let access = access_member(pr, &popup_ctx);
format!("{access}.set_handler(
[context_menu](const auto &entry) {{
return context_menu->{cb}.call(entry);
}});")
};
let fw_sub_menu = forward_callback(&popup.sub_menu, "sub_menu");
let fw_activated = forward_callback(&popup.activated, "activated");
let init = format!(r"
auto entries = {entries};
const slint::cbindgen_private::ContextMenu *context_menu = &({context_menu});
{{
auto self = popup_menu;
{access_entries}.set(std::move(entries));
{fw_sub_menu}
{fw_activated}
}}");
format!("{window}.show_popup_menu<{popup_id}>({globals}, {position}, {{ {context_menu_rc} }}, [self](auto popup_menu) {{ {init} }})", globals = ctx.generator_state.global_access)
}
BuiltinFunction::SetSelectionOffsets => {
if let [llr::Expression::PropertyReference(pr), from, to] = arguments {
let item = access_member(pr, ctx);

View file

@ -186,6 +186,9 @@ pub fn generate(
let public_components =
llr.public_components.iter().map(|p| generate_public_component(p, &llr));
let popup_menu =
llr.popup_menu.as_ref().map(|p| generate_item_tree(&p.item_tree, &llr, None, None, true));
let version_check = format_ident!(
"VersionCheck_{}_{}_{}",
env!("CARGO_PKG_VERSION_MAJOR"),
@ -229,6 +232,7 @@ pub fn generate(
#(#structs_and_enum_def)*
#(#globals)*
#(#sub_compos)*
#popup_menu
#(#public_components)*
#shared_globals
#(#resource_symbols)*
@ -249,7 +253,7 @@ fn generate_public_component(
let public_component_id = ident(&llr.name);
let inner_component_id = inner_component_id(&llr.item_tree.root);
let component = generate_item_tree(&llr.item_tree, unit, None, None);
let component = generate_item_tree(&llr.item_tree, unit, None, None, false);
let ctx = EvaluationContext {
compilation_unit: unit,
@ -680,7 +684,13 @@ fn generate_sub_component(
.popup_windows
.iter()
.map(|popup| {
generate_item_tree(&popup.item_tree, root, Some(ParentCtx::new(&ctx, None)), None)
generate_item_tree(
&popup.item_tree,
root,
Some(ParentCtx::new(&ctx, None)),
None,
false,
)
})
.collect::<Vec<_>>();
@ -1479,6 +1489,7 @@ fn generate_item_tree(
root: &llr::CompilationUnit,
parent_ctx: Option<ParentCtx>,
index_property: Option<llr::PropertyIndex>,
is_popup_menu: bool,
) -> TokenStream {
let sub_comp = generate_sub_component(&sub_tree.root, root, parent_ctx, index_property, true);
let inner_component_id = self::inner_component_id(&sub_tree.root);
@ -1491,11 +1502,14 @@ fn generate_item_tree(
})
.collect::<Vec<_>>();
let globals = if parent_ctx.is_some() {
let globals = if is_popup_menu {
quote!(globals)
} else if parent_ctx.is_some() {
quote!(parent.upgrade().unwrap().globals.get().unwrap().clone())
} else {
quote!(sp::Rc::new(SharedGlobals::new(sp::VRc::downgrade(&self_dyn_rc))))
};
let globals_arg = is_popup_menu.then(|| quote!(globals: sp::Rc<SharedGlobals>));
let embedding_function = if parent_ctx.is_some() {
quote!(todo!("Components written in Rust can not get embedded yet."))
@ -1577,7 +1591,7 @@ fn generate_item_tree(
#sub_comp
impl #inner_component_id {
pub fn new(#(parent: #parent_component_type)*) -> core::result::Result<sp::VRc<sp::ItemTreeVTable, Self>, slint::PlatformError> {
fn new(#(parent: #parent_component_type,)* #globals_arg) -> core::result::Result<sp::VRc<sp::ItemTreeVTable, Self>, slint::PlatformError> {
#![allow(unused)]
slint::private_unstable_api::ensure_backend()?;
let mut _self = Self::default();
@ -1736,7 +1750,7 @@ fn generate_repeated_component(
parent_ctx: ParentCtx,
) -> TokenStream {
let component =
generate_item_tree(&repeated.sub_tree, unit, Some(parent_ctx), repeated.index_prop);
generate_item_tree(&repeated.sub_tree, unit, Some(parent_ctx), repeated.index_prop, false);
let ctx = EvaluationContext {
compilation_unit: unit,
@ -2016,6 +2030,7 @@ fn access_member(reference: &llr::PropertyReference, ctx: &EvaluationContext) ->
/// Helper to access a member property/callback of a component.
///
/// Because the parent can be deleted (issue #3464), this might be an option when accessing the parent
#[derive(Clone)]
enum MemberAccess {
/// The token stream is just an expression to the member
Direct(TokenStream),
@ -2724,6 +2739,68 @@ fn compile_builtin_function_call(
panic!("internal error: invalid args to ClosePopupWindow {:?}", arguments)
}
}
BuiltinFunction::ShowPopupMenu => {
let [Expression::PropertyReference(context_menu_ref), entries, position] = arguments
else {
panic!("internal error: invalid args to ShowPopupMenu {arguments:?}")
};
let context_menu = access_member(context_menu_ref, ctx);
let context_menu_rc = access_item_rc(context_menu_ref, ctx);
let position = compile_expression(position, ctx);
let entries = compile_expression(entries, ctx);
let popup = ctx
.compilation_unit
.popup_menu
.as_ref()
.expect("there should be a popup menu if we want to show it");
let popup_id = inner_component_id(&popup.item_tree.root);
let window_adapter_tokens = access_window_adapter_field(ctx);
let popup_ctx = EvaluationContext::new_sub_component(
ctx.compilation_unit,
&popup.item_tree.root,
RustGeneratorContext { global_access: quote!(_self.globals.get().unwrap()) },
None,
);
let access_entries = access_member(&popup.entries, &popup_ctx).unwrap();
let forward_callback = |pr, cb| {
let access = access_member(pr, &popup_ctx).unwrap();
let call = context_menu
.clone()
.map_or_default(|context_menu| quote!(#context_menu.#cb.call(entry)));
quote!(
let self_weak = parent_weak.clone();
#access.set_handler(move |entry| {
let self_rc = self_weak.upgrade().unwrap();
let _self = self_rc.as_pin_ref();
#call
}
))
};
let fw_sub_menu = forward_callback(&popup.sub_menu, quote!(sub_menu));
let fw_activated = forward_callback(&popup.activated, quote!(activated));
quote!({
let entries = #entries;
let position = #position;
let popup_instance = #popup_id::new(_self.globals.get().unwrap().clone()).unwrap();
let popup_instance_vrc = sp::VRc::map(popup_instance.clone(), |x| x);
let parent_weak = _self.self_weak.get().unwrap().clone();
{
let _self = popup_instance_vrc.as_pin_ref();
#access_entries.set(entries);
#fw_sub_menu;
#fw_activated;
};
#popup_id::user_init(popup_instance_vrc.clone());
sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup(
&sp::VRc::into_dyn(popup_instance.into()),
position,
sp::PopupClosePolicy::CloseOnClickOutside,
#context_menu_rc,
);
})
}
BuiltinFunction::SetSelectionOffsets => {
if let [llr::Expression::PropertyReference(pr), from, to] = arguments {
let item = access_member(pr, ctx);

View file

@ -380,7 +380,7 @@ impl BuiltinPropertyInfo {
}
/// The base of an element
#[derive(Clone, Debug)]
#[derive(Clone, Debug, derive_more::From)]
pub enum ElementType {
/// The element is based of a component
Component(Rc<Component>),

View file

@ -275,6 +275,14 @@ pub struct PopupWindow {
pub position: MutExpression,
}
#[derive(Debug)]
pub struct PopupMenu {
pub item_tree: ItemTree,
pub sub_menu: PropertyReference,
pub activated: PropertyReference,
pub entries: PropertyReference,
}
#[derive(Debug)]
pub struct Timer {
pub interval: MutExpression,
@ -348,7 +356,7 @@ impl std::fmt::Debug for SubComponentInstance {
pub struct ItemTree {
pub root: SubComponent,
pub tree: TreeNode,
/// This tree has a parent. e.g: it is a Repeater or a PopupMenu whose property can access
/// This tree has a parent. e.g: it is a Repeater or a PopupWindow whose property can access
/// the parent ItemTree.
/// The String is the type of the parent ItemTree
pub parent_context: Option<SmolStr>,
@ -367,6 +375,7 @@ pub struct CompilationUnit {
pub public_components: Vec<PublicComponent>,
pub sub_components: Vec<Rc<SubComponent>>,
pub globals: Vec<GlobalComponent>,
pub popup_menu: Option<PopupMenu>,
pub has_debug_info: bool,
#[cfg(feature = "bundle-translations")]
pub translations: Option<super::translations::Translations>,
@ -408,6 +417,9 @@ impl CompilationUnit {
for p in &self.public_components {
visit_component(self, &p.item_tree.root, visitor, None);
}
if let Some(p) = &self.popup_menu {
visit_component(self, &p.item_tree.root, visitor, None);
}
}
pub fn for_each_expression<'a>(

View file

@ -62,6 +62,30 @@ pub fn lower_to_item_tree(
})
.collect();
let popup_menu = document.popup_menu_impl.as_ref().map(|c| {
let sc = lower_sub_component(&c, &state, None, compiler_config);
let item_tree = ItemTree {
tree: make_tree(&state, &c.root_element, &sc, &[]),
root: Rc::try_unwrap(sc.sub_component).unwrap(),
parent_context: None,
};
PopupMenu {
item_tree,
sub_menu: sc.mapping.map_property_reference(
&NamedReference::new(&c.root_element, SmolStr::new_static("sub-menu")),
&state,
),
activated: sc.mapping.map_property_reference(
&NamedReference::new(&c.root_element, SmolStr::new_static("activated")),
&state,
),
entries: sc.mapping.map_property_reference(
&NamedReference::new(&c.root_element, SmolStr::new_static("entries")),
&state,
),
}
});
let root = CompilationUnit {
public_components,
globals,
@ -75,6 +99,7 @@ pub fn lower_to_item_tree(
})
.collect(),
has_debug_info: compiler_config.debug_info,
popup_menu,
#[cfg(feature = "bundle-translations")]
translations: state.translation_builder.take().map(|x| x.into_inner().result()),
};
@ -691,7 +716,7 @@ fn lower_popup_component(
use super::Expression::PropertyReference as PR;
let position = super::lower_expression::make_struct(
"Point",
"LogicalPosition",
[
("x", Type::LogicalLength, PR(sc.mapping.map_property_reference(&popup.x, ctx.state))),
("y", Type::LogicalLength, PR(sc.mapping.map_property_reference(&popup.y, ctx.state))),

View file

@ -97,7 +97,9 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::Log => 10,
BuiltinFunction::Pow => 10,
BuiltinFunction::SetFocusItem | BuiltinFunction::ClearFocusItem => isize::MAX,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX,
BuiltinFunction::ShowPopupWindow
| BuiltinFunction::ClosePopupWindow
| BuiltinFunction::ShowPopupMenu => isize::MAX,
BuiltinFunction::SetSelectionOffsets => isize::MAX,
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,
BuiltinFunction::ItemFontMetrics => PROPERTY_ACCESS_COST,

View file

@ -390,6 +390,7 @@ mod plural_rule_parser {
globals: Vec::new(),
has_debug_info: false,
translations: None,
popup_menu: None,
},
current_sub_component: None,
current_global: None,

View file

@ -62,6 +62,9 @@ pub struct Document {
/// The list of used extra types used recursively.
pub used_types: RefCell<UsedSubTypes>,
/// The popup_menu_impl
pub popup_menu_impl: Option<Rc<Component>>,
}
impl Document {
@ -243,6 +246,7 @@ impl Document {
exports,
embedded_file_resources: Default::default(),
used_types: Default::default(),
popup_menu_impl: None,
}
}
@ -272,6 +276,9 @@ impl Document {
for c in &used_types.globals {
v(c);
}
if let Some(c) = &self.popup_menu_impl {
v(c);
}
}
}

View file

@ -30,6 +30,7 @@ mod lower_absolute_coordinates;
mod lower_accessibility;
mod lower_component_container;
mod lower_layout;
mod lower_menus;
mod lower_popups;
mod lower_property_to_element;
mod lower_shadows;
@ -96,6 +97,7 @@ pub async fn run_passes(
);
});
lower_tabwidget::lower_tabwidget(doc, type_loader, diag).await;
lower_menus::lower_menus(doc, type_loader, diag).await;
collect_subcomponents::collect_subcomponents(doc);
doc.visit_all_used_components(|component| {

View file

@ -24,12 +24,10 @@ pub fn collect_globals(doc: &Document, _diag: &mut BuildDiagnostics) {
}
}
}
for component in doc.exported_roots() {
collect_in_component(&component, &mut set, &mut sorted_globals);
}
for component in &doc.used_types.borrow().sub_components {
collect_in_component(component, &mut set, &mut sorted_globals);
}
doc.visit_all_used_components(|component| {
collect_in_component(component, &mut set, &mut sorted_globals)
});
doc.used_types.borrow_mut().globals = sorted_globals;
}

View file

@ -14,7 +14,7 @@ use std::rc::Rc;
pub fn collect_subcomponents(doc: &Document) {
let mut result = vec![];
let mut hash = HashSet::new();
for component in doc.exported_roots() {
for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
collect_subcomponents_recursive(&component, &mut result, &mut hash);
}
doc.used_types.borrow_mut().sub_components = result;

View file

@ -59,11 +59,11 @@ pub fn inline(doc: &Document, inline_selection: InlineSelection, diag: &mut Buil
}
let mut roots = HashSet::new();
if inline_selection == InlineSelection::InlineOnlyRequiredComponents {
for component in doc.exported_roots() {
for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
roots.insert(ByAddress(component.clone()));
}
}
for component in doc.exported_roots() {
for component in doc.exported_roots().chain(doc.popup_menu_impl.iter().cloned()) {
inline_components_recursively(&component, &roots, inline_selection, diag);
let mut init_code = component.init_code.borrow_mut();
let inlined_init_code = core::mem::take(&mut init_code.inlined_init_code);

View file

@ -0,0 +1,164 @@
// 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
//! Passe lower the `MenuBar` and `ContextMenu` as well as all their contents
//!
//! Must be done before inlining and many other passes because the lowered code must
//! be further inlined as it may expends to native widget that needs inlining
use crate::diagnostics::BuildDiagnostics;
use crate::expression_tree::NamedReference;
use crate::langtype::ElementType;
use crate::object_tree::*;
use smol_str::{format_smolstr, SmolStr};
struct UsefulMenuComponents {
menubar_impl: ElementType,
vertical_layout: ElementType,
empty: ElementType,
}
pub async fn lower_menus(
doc: &mut Document,
type_loader: &mut crate::typeloader::TypeLoader,
diag: &mut BuildDiagnostics,
) {
// Ignore import errors
let mut build_diags_to_ignore = BuildDiagnostics::default();
let menubar_impl = type_loader
.import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
.await
.expect("MenuBarImpl should be in std-widgets.slint");
let useful_menu_component = UsefulMenuComponents {
menubar_impl: menubar_impl.clone().into(),
vertical_layout: type_loader
.global_type_registry
.borrow()
.lookup_builtin_element("VerticalLayout")
.expect("VerticalLayout is a builtin type"),
empty: type_loader.global_type_registry.borrow().empty_type(),
};
let mut has_menu = false;
doc.visit_all_used_components(|component| {
recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
has_menu |= process_window(elem, &useful_menu_component, diag);
}
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "ContextMenu") {
has_menu |= process_context_menu(elem, &useful_menu_component, diag);
}
})
});
if has_menu {
let popup_menu_impl = type_loader
.import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
.await
.expect("PopupMenuImpl should be in std-widgets.slint");
{
let mut root = popup_menu_impl.root_element.borrow_mut();
for prop in ["entries", "sub-menu", "activated"] {
match root.property_declarations.get_mut(prop) {
Some(d) => d.expose_in_public_api = true,
None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
}
}
root.property_analysis.borrow_mut().entry("entries".into()).or_default().is_set = true;
}
recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "ContextMenu") {
process_context_menu(elem, &useful_menu_component, diag);
}
});
recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "ContextMenu") {
process_context_menu(elem, &useful_menu_component, diag);
}
});
doc.popup_menu_impl = popup_menu_impl.into();
}
}
fn process_context_menu(
_context_menu_elem: &ElementRc,
_useful_menu_components: &UsefulMenuComponents,
_diag: &mut BuildDiagnostics,
) -> bool {
// TODO:
true
}
fn process_window(
win: &ElementRc,
components: &UsefulMenuComponents,
diag: &mut BuildDiagnostics,
) -> bool {
/* if matches!(&elem.borrow_mut().base_type, ElementType::Builtin(_)) {
// That's the TabWidget re-exported from the style, it doesn't need to be processed
return;
}*/
let mut window = win.borrow_mut();
let mut menu_bar = None;
window.children.retain(|x| {
if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
if menu_bar.is_some() {
diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
} else {
menu_bar = Some(x.clone());
}
false
} else {
true
}
});
let Some(menu_bar) = menu_bar else {
return false;
};
menu_bar.borrow_mut().base_type = components.menubar_impl.clone();
// Create a child that contains all the child but the menubar
let child = Element {
id: format_smolstr!("{}-child", window.id),
base_type: components.empty.clone(),
enclosing_component: window.enclosing_component.clone(),
children: std::mem::take(&mut window.children),
..Default::default()
}
.make_rc();
const HEIGHT: &str = "height";
let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
// Create a layout
let layout = Element {
id: format_smolstr!("{}-menulayout", window.id),
base_type: components.vertical_layout.clone(),
enclosing_component: window.enclosing_component.clone(),
children: vec![menu_bar, child],
..Default::default()
}
.make_rc();
window.children.push(layout);
let component = window.enclosing_component.upgrade().unwrap();
drop(window);
// Rename every access to `root.height` into `child.height`
let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
crate::object_tree::visit_all_named_references(&component, &mut |nr| {
if nr == &win_height {
*nr = child_height.clone()
}
});
// except for the actual geometry
win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
true
}

View file

@ -13,13 +13,11 @@ use std::rc::Rc;
/// It currently does so by adding a number to the existing id
pub fn assign_unique_id(doc: &Document) {
let mut count = 0;
for component in doc.exported_roots() {
assign_unique_id_in_component(&component, &mut count);
}
for c in &doc.used_types.borrow().sub_components {
assign_unique_id_in_component(c, &mut count);
}
doc.visit_all_used_components(|component| {
if !component.is_global() {
assign_unique_id_in_component(component, &mut count)
}
});
rename_globals(doc, count);
}

View file

@ -0,0 +1,32 @@
// 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
export component A inherits Window {
mb := MenuBar {
}
MenuBar {
// ^error{Only one MenuBar is allowed in a Window}
}
Rectangle {
x: 45px;
}
Rectangle {
x: mb.absolute-position.x;
// ^error{Element 'MenuBar' does not have a property 'absolute-position'}
y: mb.height;
// ^error{Element 'MenuBar' does not have a property 'height'}
init => {
mb.focus();
// ^error{Element 'MenuBar' does not have a property 'focus'}
}
}
}
// TESTS TODO
// - test that setting the window height sets it to the window height plus that of the menubar

View file

@ -0,0 +1,30 @@
// 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
export component MyMenu inherits MenuBar {
// ^error{MenuBar can only be within a Window element}
}
export component A inherits Window {
MenuBar {
Rectangle {}
// ^error{MenuBar cannot have children elements}
x: 45px;
// ^error{Unknown property x in MenuBar}
width: 45px;
// ^error{Unknown property width in MenuBar}
entries: [];
init => {}
// ^error{'init' is not a callback in MenuBar}
}
Rectangle {
x: 45px;
MenuBar {
// ^error{MenuBar can only be within a Window element}
}
}
}

View file

@ -261,6 +261,9 @@ impl Snapshotter {
document.inner_components.iter().for_each(|ic| {
let _ = self.create_component(ic);
});
if let Some(popup_menu_impl) = &document.popup_menu_impl {
let _ = self.create_component(popup_menu_impl);
}
}
fn snapshot_document(&mut self, document: &object_tree::Document) -> object_tree::Document {
@ -284,6 +287,10 @@ impl Snapshotter {
exports,
embedded_file_resources: document.embedded_file_resources.clone(),
used_types: RefCell::new(self.snapshot_used_sub_types(&document.used_types.borrow())),
popup_menu_impl: document.popup_menu_impl.as_ref().map(|p| {
Weak::upgrade(&self.use_component(p))
.expect("Components can get upgraded at this point")
}),
}
}

View file

@ -399,6 +399,7 @@ impl TypeRegister {
($pub_type:ident, i32) => { Type::Int32 };
($pub_type:ident, f32) => { Type::Float32 };
($pub_type:ident, SharedString) => { Type::String };
($pub_type:ident, Image) => { Type::Image };
($pub_type:ident, Coord) => { Type::LogicalLength };
($pub_type:ident, KeyboardModifiers) => { $pub_type.clone() };
($pub_type:ident, $_:ident) => {
@ -472,6 +473,19 @@ impl TypeRegister {
_ => unreachable!(),
};
match &mut register.elements.get_mut("ContextMenu").unwrap() {
ElementType::Builtin(ref mut b) => {
let b = Rc::get_mut(b).unwrap();
b.properties.insert(
"show".into(),
BuiltinPropertyInfo::new(Type::Function(BuiltinFunction::ShowPopupMenu.ty())),
);
b.member_functions.insert("show".into(), BuiltinFunction::ShowPopupMenu);
}
_ => unreachable!(),
};
let font_metrics_prop = crate::langtype::BuiltinPropertyInfo {
ty: font_metrics_type(),
property_visibility: PropertyVisibility::Output,

View file

@ -0,0 +1,112 @@
// 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 file contains a generic implementation of the MenuBar and ContextMenu
import { Palette } from "std-widgets-impl.slint";
export component PopupMenuImpl inherits Window {
property <length> px: 1rem / 14;
in property <[MenuEntry]> entries: [];
callback sub-menu(MenuEntry) -> [MenuEntry];
callback activated(MenuEntry);
Rectangle {
border-radius: 7*px;
border-color: Palette.border;
background: Palette.background;
drop-shadow-blur: 2px;
drop-shadow-color: Palette.foreground.transparentize(0.5);
min-width: 10rem;
VerticalLayout {
padding: 5px;
for entry in entries: Rectangle {
background: ita.has-hover || ita.pressed ? Palette.alternate-background : transparent;
border-radius: 3*px;
border-width: 1px;
HorizontalLayout {
spacing: 7*px;
padding: 11*px;
padding-top: 4*px;
padding-bottom: 6*px;
Text {
text: entry.title;
horizontal-stretch: 1;
}
if entry.has-sub-menu : Text {
text: "▶";
horizontal-stretch: 0;
}
}
ita := TouchArea {
clicked => {
if entry.has-sub-menu {
subMenu.show(sub-menu(entry), {
x: root.width,
y: self.absolute-position.y - subMenu.absolute-position.y,
});
} else {
activated(entry);
}
}
}
}
}
}
subMenu := ContextMenu {
x: 0; y: 0; width: 0; height: 0;
sub-menu(entry) => { root.sub-menu(entry); }
activated(entry) => { root.activated(entry); }
}
}
export component MenuBarImpl {
callback activated(MenuEntry);
callback sub-menu(MenuEntry) -> [MenuEntry];
property <[MenuEntry]> entries;
property <length> px: 1rem / 14;
preferred-width: 100%;
height: l.preferred-height;
l := HorizontalLayout {
padding: 5*px;
alignment: start;
spacing: 1*px;
for entry in entries: e := Rectangle {
background: ta.has-hover || ta.pressed ? Palette.alternate-background : transparent;
border-radius: 3*px;
HorizontalLayout {
padding: 11*px;
padding-top: 4*px;
padding-bottom: 6*px;
Text {
text: entry.title;
}
}
ta := TouchArea {
clicked => {
cm.show(sub-menu(entry), { x: e.x, y: root.height });
}
}
}
// For the default size when there is no entries
Rectangle {
HorizontalLayout {
padding-top: 0.4rem;
padding-bottom: 0.6rem;
Text { text: ""; }
}
}
}
cm := ContextMenu {
activated(entry) => { root.activated(entry); }
sub-menu(entry) => { root.sub-menu(entry); }
}
}

View file

@ -22,5 +22,5 @@ export { Switch } from "switch.slint";
export { TextEdit } from "textedit.slint";
export { TimePickerPopup, Time } from "time-picker.slint";
export { DatePickerPopup, Date } from "datepicker.slint";
export { MenuBarImpl, PopupMenuImpl } from "../common/menus.slint";
export * from "tableview.slint";

View file

@ -22,4 +22,5 @@ export { Switch } from "switch.slint";
export { TextEdit } from "textedit.slint";
export { TimePickerPopup, Time } from "time-picker.slint";
export { DatePickerPopup, Date } from "./datepicker.slint";
export { MenuBarImpl, PopupMenuImpl } from "../common/menus.slint";
export * from "tableview.slint";

View file

@ -22,4 +22,5 @@ export { Switch } from "switch.slint";
export { TextEdit } from "textedit.slint";
export { TimePickerPopup, Time } from "time-picker.slint";
export { DatePickerPopup, Date } from "datepicker.slint";
export { MenuBarImpl, PopupMenuImpl } from "../common/menus.slint";
export * from "tableview.slint";

View file

@ -21,3 +21,4 @@ export { Spinner } from "spinner.slint";
export { TextEdit } from "textedit.slint";
export { TimePickerPopup, Time } from "time-picker.slint";
export { DatePickerPopup, Date } from "./datepicker.slint";
export { MenuBarImpl, PopupMenuImpl } from "../common/menus.slint";

View file

@ -20,3 +20,4 @@ export { TimePickerPopup, Time } from "time-picker.slint";
export { StandardListView, ListView } from "../common/listview.slint";
export { TextEdit } from "textedit.slint";
export { DatePickerPopup, Date } from "./datepicker.slint";
export { MenuBarImpl, PopupMenuImpl } from "../common/menus.slint";