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

@ -221,6 +221,10 @@ fn default_config() -> cbindgen::Config {
("PointArg".into(), "slint::LogicalPosition".into()),
("FloatArg".into(), "float".into()),
("IntArg".into(), "int".into()),
("MenuEntryArg".into(), "MenuEntry".into()),
// Note: these types are not the same, but they are only used in callback return types that are only used in C++ (set and called)
// therefore it is ok to reinterpret_cast
("MenuEntryModel".into(), "std::shared_ptr<slint::Model<MenuEntry>>".into()),
("Coord".into(), "float".into()),
]
.iter()
@ -293,6 +297,7 @@ fn gen_corelib(
"Rotate",
"Opacity",
"Layer",
"ContextMenu",
];
config.export.include = [
@ -370,6 +375,8 @@ fn gen_corelib(
"PointerScrollEventArg",
"PointArg",
"Point",
"MenuEntryModel",
"MenuEntryArg",
"slint_color_brighter",
"slint_color_darker",
"slint_color_transparentize",
@ -500,7 +507,7 @@ fn gen_corelib(
"slint_image_set_nine_slice_edges",
"slint_image_to_rgb8",
"slint_image_to_rgba8",
"slint_image_to_rgba8_premultiplied",
"slint_image_to_rgba8_premultiplied",
"SharedPixelBuffer",
"SharedImageBuffer",
"StaticTextures",
@ -590,7 +597,7 @@ fn gen_corelib(
"slint_image_set_nine_slice_edges",
"slint_image_to_rgb8",
"slint_image_to_rgba8",
"slint_image_to_rgba8_premultiplied",
"slint_image_to_rgba8_premultiplied",
"slint_image_from_embedded_textures",
"slint_image_compare_equal",
]
@ -644,7 +651,7 @@ fn gen_corelib(
public_config.export.item_types = vec![cbindgen::ItemType::Enums, cbindgen::ItemType::Structs];
// Previously included types are now excluded (to avoid duplicates)
public_config.export.exclude = private_exported_types.into_iter().collect();
public_config.export.exclude.push("Point".into());
public_config.export.exclude.push("LogicalPosition".into());
public_config.export.include = public_exported_types.into_iter().map(str::to_string).collect();
public_config.export.body.insert(
"Rgb8Pixel".to_owned(),
@ -760,6 +767,7 @@ namespace slint {
struct ItemVTable;
using types::IntRect;
}
template<typename ModelData> class Model;
}",
)
.with_trailer(gen_item_declarations(&items))

View file

@ -43,6 +43,7 @@ inline void assert_main_thread()
}
using ItemTreeRc = vtable::VRc<cbindgen_private::ItemTreeVTable>;
using slint::LogicalPosition;
class WindowAdapterRc
{
@ -111,7 +112,7 @@ public:
cbindgen_private::ItemRc parent_item) const
{
auto popup = Component::create(parent_component);
cbindgen_private::Point p = pos(popup);
auto p = pos(popup);
auto popup_dyn = popup.into_dyn();
return cbindgen_private::slint_windowrc_show_popup(&inner, &popup_dyn, p, close_policy,
&parent_item);
@ -124,6 +125,20 @@ public:
}
}
template<typename Component, typename SharedGlobals, typename InitFn>
uint32_t show_popup_menu(SharedGlobals *globals, LogicalPosition pos,
cbindgen_private::ItemRc context_menu_rc, InitFn init) const
{
// if (cbindgen_private::slint_windowrc_show_native_context_menu(....)) { return }
auto popup = Component::create(globals);
init(&*popup);
auto popup_dyn = popup.into_dyn();
return cbindgen_private::slint_windowrc_show_popup(
&inner, &popup_dyn, pos, cbindgen_private::PopupClosePolicy::CloseOnClickOutside,
&context_menu_rc);
}
template<std::invocable<RenderingState, GraphicsAPI> F>
std::optional<SetRenderingNotifierError> set_rendering_notifier(F callback) const
{

View file

@ -187,6 +187,7 @@ pub mod re_exports {
AccessibilityAction, AccessibleStringProperty, SupportedAccessibilityAction,
};
pub use i_slint_core::animations::{animation_tick, EasingCurve};
pub use i_slint_core::api::LogicalPosition;
pub use i_slint_core::callbacks::Callback;
pub use i_slint_core::date_time::*;
pub use i_slint_core::graphics::*;

View file

@ -182,6 +182,25 @@ macro_rules! for_each_builtin_structs {
private {
}
}
/// An item in the menu of a menu bar or context menu
struct MenuEntry {
@name = "slint::private_api::MenuEntry"
export {
/// The text of the menu entry
title: SharedString,
// /// the icon associated with the menu entry
// icon: Image,
/// an opaque id that can be used to identify the menu entry
id: SharedString,
// keyboard_shortcut: KeySequence,
// /// whether the menu entry is enabled
// enabled: bool,
/// Sub menu
has_sub_menu: bool,
}
private {}
}
];
};
}

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";

View file

@ -34,7 +34,7 @@ use crate::lengths::{
#[cfg(feature = "rtti")]
use crate::rtti::*;
use crate::window::{WindowAdapter, WindowAdapterRc};
use crate::{Coord, Property, SharedString};
use crate::{Callback, Coord, Property, SharedString};
use alloc::rc::Rc;
use const_field_offset::FieldOffsets;
use core::pin::Pin;
@ -66,6 +66,8 @@ pub type KeyEventArg = (KeyEvent,);
type PointerEventArg = (PointerEvent,);
type PointerScrollEventArg = (PointerScrollEvent,);
type PointArg = (Point,);
type MenuEntryArg = (MenuEntry,);
type MenuEntryModel = crate::model::ModelRc<MenuEntry>;
#[cfg(all(feature = "ffi", windows))]
#[macro_export]
@ -1055,6 +1057,85 @@ declare_item_vtable! {
fn slint_get_WindowItemVTable() -> WindowItemVTable for WindowItem
}
/// The implementation of the `Window` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]
#[pin]
pub struct ContextMenu {
//pub entries: Property<crate::model::ModelRc<MenuEntry>>,
pub sub_menu: Callback<MenuEntryArg, MenuEntryModel>,
pub activated: Callback<MenuEntryArg>,
pub cached_rendering_data: CachedRenderingData,
}
impl Item for ContextMenu {
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
fn layout_info(
self: Pin<&Self>,
_orientation: Orientation,
_window_adapter: &Rc<dyn WindowAdapter>,
) -> LayoutInfo {
LayoutInfo::default()
}
fn input_event_filter_before_children(
self: Pin<&Self>,
_: MouseEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventFilterResult {
InputEventFilterResult::ForwardAndIgnore
}
fn input_event(
self: Pin<&Self>,
_event: MouseEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> InputEventResult {
InputEventResult::EventIgnored
}
fn key_event(
self: Pin<&Self>,
_: &KeyEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> KeyEventResult {
KeyEventResult::EventIgnored
}
fn focus_event(
self: Pin<&Self>,
_: &FocusEvent,
_window_adapter: &Rc<dyn WindowAdapter>,
_self_rc: &ItemRc,
) -> FocusEventResult {
FocusEventResult::FocusIgnored
}
fn render(
self: Pin<&Self>,
_backend: &mut ItemRendererRef,
_self_rc: &ItemRc,
_size: LogicalSize,
) -> RenderingResult {
RenderingResult::ContinueRenderingChildren
}
}
impl ContextMenu {}
impl ItemConsts for ContextMenu {
const cached_rendering_data_offset: const_field_offset::FieldOffset<Self, CachedRenderingData> =
Self::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
}
declare_item_vtable! {
fn slint_get_ContextMenuVTable() -> ContextMenuVTable for ContextMenu
}
/// The implementation of the `BoxShadow` element
#[repr(C)]
#[derive(FieldOffsets, Default, SlintElement)]

View file

@ -48,6 +48,8 @@ macro_rules! declare_ValueType_2 {
crate::component_factory::ComponentFactory,
crate::api::LogicalPosition,
crate::items::FontMetrics,
crate::items::MenuEntry,
crate::model::ModelRc<crate::items::MenuEntry>,
$(crate::items::$Name,)*
];
};

View file

@ -10,7 +10,6 @@ use crate::api::{
CloseRequestResponse, LogicalPosition, PhysicalPosition, PhysicalSize, PlatformError, Window,
WindowPosition, WindowSize,
};
use crate::graphics::Point;
use crate::input::{
key_codes, ClickState, InternalKeyboardModifierState, KeyEvent, KeyEventType, MouseEvent,
MouseInputState, TextCursorBlinker,
@ -981,13 +980,12 @@ impl WindowInner {
pub fn show_popup(
&self,
popup_componentrc: &ItemTreeRc,
position: Point,
position: LogicalPosition,
close_policy: PopupClosePolicy,
parent_item: &ItemRc,
) -> NonZeroU32 {
let position = parent_item.map_to_window(
parent_item.geometry().origin + LogicalPoint::from_untyped(position).to_vector(),
);
let position = parent_item
.map_to_window(parent_item.geometry().origin + position.to_euclid().to_vector());
let root_of = |mut item_tree: ItemTreeRc| loop {
if ItemRc::new(item_tree.clone(), 0).downcast::<crate::items::WindowItem>().is_some() {
return item_tree;
@ -1087,6 +1085,20 @@ impl WindowInner {
popup_id
}
/// Attempt to show a native popup menu
///
/// context_menu_item is an instance of a ContextMenu
///
/// Returns false if the native platform doesn't support it
pub fn show_native_popup_menu(
&self,
_context_menu_item: &ItemRc,
_position: LogicalPosition,
) -> bool {
// TODO
false
}
// Close the popup associated with the given popup window.
fn close_popup_impl(&self, current_popup: &PopupWindow) {
match &current_popup.location {
@ -1401,7 +1413,7 @@ pub mod ffi {
pub unsafe extern "C" fn slint_windowrc_show_popup(
handle: *const WindowAdapterRcOpaque,
popup: &ItemTreeRc,
position: crate::graphics::Point,
position: LogicalPosition,
close_policy: PopupClosePolicy,
parent_item: &ItemRc,
) -> NonZeroU32 {

View file

@ -14,6 +14,7 @@ use i_slint_compiler::{generator, object_tree, parser, CompilerConfiguration};
use i_slint_core::accessibility::{
AccessibilityAction, AccessibleStringProperty, SupportedAccessibilityAction,
};
use i_slint_core::api::LogicalPosition;
use i_slint_core::component_factory::ComponentFactory;
use i_slint_core::item_tree::{
IndexRange, ItemTree, ItemTreeRef, ItemTreeRefPin, ItemTreeVTable, ItemTreeWeak,
@ -43,6 +44,7 @@ use smol_str::{SmolStr, ToSmolStr};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::rc::Weak;
use std::{pin::Pin, rc::Rc};
pub const SPECIAL_PROPERTY_INDEX: &str = "$index";
@ -415,6 +417,8 @@ pub struct ItemTreeDescription<'id> {
/// Map of element IDs to their active popup's ID
popup_ids: std::cell::RefCell<HashMap<SmolStr, NonZeroU32>>,
pub(crate) popup_menu_description: PopupMenuDescription,
/// The collection of compiled globals
compiled_globals: Option<Rc<CompiledGlobalCollection>>,
@ -430,6 +434,20 @@ pub struct ItemTreeDescription<'id> {
std::cell::OnceCell<Option<std::rc::Rc<i_slint_compiler::typeloader::TypeLoader>>>,
}
#[derive(Clone, derive_more::From)]
pub(crate) enum PopupMenuDescription {
Rc(Rc<ErasedItemTreeDescription>),
Weak(Weak<ErasedItemTreeDescription>),
}
impl PopupMenuDescription {
pub fn unerase<'id>(&self, guard: generativity::Guard<'id>) -> Rc<ItemTreeDescription<'id>> {
match self {
PopupMenuDescription::Rc(rc) => rc.unerase(guard).clone(),
PopupMenuDescription::Weak(weak) => weak.upgrade().unwrap().unerase(guard).clone(),
}
}
}
fn internal_properties_to_public<'a>(
prop_iter: impl Iterator<Item = (&'a SmolStr, &'a PropertyDeclaration)> + 'a,
) -> impl Iterator<Item = (SmolStr, i_slint_compiler::langtype::Type)> + 'a {
@ -890,10 +908,31 @@ pub async fn load(
let compiled_globals = Rc::new(CompiledGlobalCollection::compile(doc));
let mut components = HashMap::new();
let popup_menu_description = if let Some(popup_menu_impl) = &doc.popup_menu_impl {
PopupMenuDescription::Rc(Rc::new_cyclic(|weak| {
generativity::make_guard!(guard);
ErasedItemTreeDescription::from(generate_item_tree(
popup_menu_impl,
Some(compiled_globals.clone()),
PopupMenuDescription::Weak(weak.clone()),
true,
guard,
))
}))
} else {
PopupMenuDescription::Weak(Default::default())
};
for c in doc.exported_roots() {
generativity::make_guard!(guard);
#[allow(unused_mut)]
let mut it = generate_item_tree(&c, Some(compiled_globals.clone()), guard);
let mut it = generate_item_tree(
&c,
Some(compiled_globals.clone()),
popup_menu_description.clone(),
false,
guard,
);
#[cfg(feature = "highlight")]
{
let _ = it.type_loader.set(loader.clone());
@ -966,6 +1005,7 @@ fn generate_rtti() -> HashMap<&'static str, Rc<ItemRTTI>> {
rtti_for::<Rotate>(),
rtti_for::<Opacity>(),
rtti_for::<Layer>(),
rtti_for::<ContextMenu>(),
]
.iter()
.cloned(),
@ -996,6 +1036,8 @@ fn generate_rtti() -> HashMap<&'static str, Rc<ItemRTTI>> {
pub(crate) fn generate_item_tree<'id>(
component: &Rc<object_tree::Component>,
compiled_globals: Option<Rc<CompiledGlobalCollection>>,
popup_menu_description: PopupMenuDescription,
is_popup_menu_impl: bool,
guard: generativity::Guard<'id>,
) -> Rc<ItemTreeDescription<'id>> {
//dbg!(&*component.root_element.borrow());
@ -1014,6 +1056,7 @@ pub(crate) fn generate_item_tree<'id>(
repeater: Vec<ErasedRepeaterWithinComponent<'id>>,
repeater_names: HashMap<SmolStr, usize>,
change_callbacks: Vec<(NamedReference, Expression)>,
popup_menu_description: PopupMenuDescription,
}
impl<'id> generator::ItemTreeBuilder for TreeBuilder<'id> {
type SubComponentState = ();
@ -1033,7 +1076,13 @@ pub(crate) fn generate_item_tree<'id>(
generativity::make_guard!(guard);
self.repeater.push(
RepeaterWithinItemTree {
item_tree_to_repeat: generate_item_tree(base_component, None, guard),
item_tree_to_repeat: generate_item_tree(
base_component,
None,
self.popup_menu_description.clone(),
false,
guard,
),
offset: self.type_builder.add_field_type::<Repeater<ErasedItemTreeBox>>(),
model: item.repeated.as_ref().unwrap().model.clone(),
}
@ -1126,6 +1175,7 @@ pub(crate) fn generate_item_tree<'id>(
repeater: vec![],
repeater_names: HashMap::new(),
change_callbacks: vec![],
popup_menu_description,
};
if !component.is_global() {
@ -1272,11 +1322,12 @@ pub(crate) fn generate_item_tree<'id>(
}
}
let parent_item_tree_offset = if component.parent_element.upgrade().is_some() {
Some(builder.type_builder.add_field_type::<OnceCell<ErasedItemTreeBoxWeak>>())
} else {
None
};
let parent_item_tree_offset =
if component.parent_element.upgrade().is_some() || is_popup_menu_impl {
Some(builder.type_builder.add_field_type::<OnceCell<ErasedItemTreeBoxWeak>>())
} else {
None
};
let root_offset = builder.type_builder.add_field_type::<OnceCell<ErasedItemTreeBoxWeak>>();
@ -1345,6 +1396,7 @@ pub(crate) fn generate_item_tree<'id>(
change_trackers,
timers,
popup_ids: std::cell::RefCell::new(HashMap::new()),
popup_menu_description: builder.popup_menu_description,
#[cfg(feature = "highlight")]
type_loader: std::cell::OnceCell::new(),
#[cfg(feature = "highlight")]
@ -2340,7 +2392,7 @@ pub fn show_popup(
element: ElementRc,
instance: InstanceRef,
popup: &object_tree::PopupWindow,
pos_getter: impl FnOnce(InstanceRef<'_, '_>) -> i_slint_core::graphics::Point,
pos_getter: impl FnOnce(InstanceRef<'_, '_>) -> LogicalPosition,
close_policy: PopupClosePolicy,
parent_comp: ErasedItemTreeBoxWeak,
parent_window_adapter: WindowAdapterRc,
@ -2348,7 +2400,13 @@ pub fn show_popup(
) {
generativity::make_guard!(guard);
// FIXME: we should compile once and keep the cached compiled component
let compiled = generate_item_tree(&popup.component, None, guard);
let compiled = generate_item_tree(
&popup.component,
None,
parent_comp.upgrade().unwrap().0.description().popup_menu_description.clone(),
false,
guard,
);
let inst = instantiate(
compiled,
Some(parent_comp),

View file

@ -623,7 +623,7 @@ fn call_builtin_function(
load_property_helper(comp, &popup.x.element(), popup.x.name()).unwrap();
let y =
load_property_helper(comp, &popup.y.element(), popup.y.name()).unwrap();
i_slint_core::graphics::Point::new(
corelib::api::LogicalPosition::new(
x.try_into().unwrap(),
y.try_into().unwrap(),
)
@ -659,6 +659,93 @@ fn call_builtin_function(
panic!("internal error: argument to ClosePopupWindow must be an element")
}
}
BuiltinFunction::ShowPopupMenu => {
let [Expression::ElementReference(element), entries, position] = arguments else {
panic!("internal error: incorrect argument count to ShowPopupMenu")
};
let position = eval_expression(position, local_context)
.try_into()
.expect("internal error: popup menu position argument should be a point");
let component = match local_context.component_instance {
ComponentInstance::InstanceRef(c) => c,
ComponentInstance::GlobalComponent(_) => {
panic!("Cannot show popup from a global component")
}
};
let elem = element.upgrade().unwrap();
generativity::make_guard!(guard);
let enclosing_component = enclosing_component_for_element(&elem, component, guard);
let description = enclosing_component.description;
let item_info = &description.items[elem.borrow().id.as_str()];
let item_comp = enclosing_component.self_weak().get().unwrap().upgrade().unwrap();
let item_rc = corelib::items::ItemRc::new(
vtable::VRc::into_dyn(item_comp),
item_info.item_index(),
);
if component.access_window(|window| window.show_native_popup_menu(&item_rc, position)) {
return Value::Void;
}
generativity::make_guard!(guard);
let compiled = enclosing_component.description.popup_menu_description.unerase(guard);
let inst = crate::dynamic_item_tree::instantiate(
compiled.clone(),
Some(enclosing_component.self_weak().get().unwrap().clone()),
None,
Some(&crate::dynamic_item_tree::WindowOptions::UseExistingWindow(
component.window_adapter(),
)),
Default::default(),
);
inst.run_setup_code();
generativity::make_guard!(guard);
let inst_ref = inst.unerase(guard);
let entries = eval_expression(entries, local_context);
compiled.set_property(inst_ref.borrow(), "entries", entries).unwrap();
let item_rc_ = item_rc.clone();
compiled
.set_callback_handler(
inst_ref.borrow(),
"sub-menu",
Box::new(move |args: &[Value]| -> Value {
item_rc_
.downcast::<corelib::items::ContextMenu>()
.unwrap()
.sub_menu
.call(&(args[0].clone().try_into().unwrap(),))
.into()
}),
)
.unwrap();
let item_rc_ = item_rc.clone();
compiled
.set_callback_handler(
inst_ref.borrow(),
"activated",
Box::new(move |args: &[Value]| -> Value {
item_rc_
.downcast::<corelib::items::ContextMenu>()
.unwrap()
.activated
.call(&(args[0].clone().try_into().unwrap(),))
.into()
}),
)
.unwrap();
component.access_window(|window| {
window.show_popup(
&vtable::VRc::into_dyn(inst),
position,
corelib::items::PopupClosePolicy::CloseOnClickOutside,
&item_rc,
)
});
Value::Void
}
BuiltinFunction::SetSelectionOffsets => {
if arguments.len() != 3 {
panic!("internal error: incorrect argument count to select range function call")

View file

@ -7,7 +7,9 @@ use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
use crate::api::Value;
use crate::dynamic_item_tree::{ErasedItemTreeBox, ErasedItemTreeDescription};
use crate::dynamic_item_tree::{
ErasedItemTreeBox, ErasedItemTreeDescription, PopupMenuDescription,
};
use crate::SetPropertyError;
use i_slint_compiler::langtype::ElementType;
use i_slint_compiler::namedreference::NamedReference;
@ -317,8 +319,14 @@ fn generate(component: &Rc<Component>) -> CompiledGlobal {
ElementType::Global => {
generativity::make_guard!(guard);
CompiledGlobal::Component {
component: crate::dynamic_item_tree::generate_item_tree(component, None, guard)
.into(),
component: crate::dynamic_item_tree::generate_item_tree(
component,
None,
PopupMenuDescription::Weak(Default::default()),
false,
guard,
)
.into(),
public_properties: Default::default(),
}
}

View file

@ -0,0 +1,68 @@
// 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
import { AboutSlint, Button } from "std-widgets.slint";
export component TestCase inherits Window {
MenuBar {
entries: [
{ title: "File" },
{ title: "Edit" },
];
sub-menu(entry) => {
if entry.title == "File" {
return [
{ title: "New" },
{ title: "Open" },
{ title: "Save" },
{ title: "Open Recent", has-sub-menu: true },
];
} else if entry.title == "Edit" {
return [
{ title: "Copy" },
{ title: "Paste" },
{ title: "Xxx", has-sub-menu: true },
];
} else if entry.title == "Open Recent" {
return [
{ title: "Recent 1" },
{ title: "Recent 2" },
{ title: "Recent 3" },
{ title: "Recent 4" },
];
} else if entry.title == "Xxx" {
return [
{ title: "Aaa" },
{ title: "Xxx", has-sub-menu: true },
{ title: "Yyy" },
];
} else {
return [];
}
}
activated(entry) => {
debug("Activated", entry);
}
}
vl := VerticalLayout {
AboutSlint {}
Button { text: "Hello"; }
}
out property <bool> check-geometry: vl.x == 0 && vl.y == 0 && vl.width == root.width && vl.height == root.height;
out property <bool> test: check-geometry;
}
/*
```rust
let instance = TestCase::new().unwrap();
assert!(instance.get_test());
```
```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
assert(instance.get_test());
```
*/

View file

@ -165,7 +165,10 @@ pub fn extract_builtin_structs() -> std::collections::BTreeMap<String, StructDoc
stringify!(string)
};
(Coord) => {
stringify!(length)
"length"
};
(Image) => {
"image"
};
($pub_type:ident) => {
stringify!($pub_type)
@ -211,6 +214,8 @@ pub fn extract_builtin_structs() -> std::collections::BTreeMap<String, StructDoc
// `StateInfo` should not be in the documentation, so remove it again
structs.remove("StateInfo");
// Experimental type
structs.remove("MenuEntry");
structs
}