Introduce MenuSeparator

Fixes #7790
This commit is contained in:
Olivier Goffart 2025-04-01 18:31:58 +02:00
parent 3797ed79bc
commit e5289af154
22 changed files with 135 additions and 21 deletions

View file

@ -189,9 +189,16 @@ component MenuItem {
//-disallow_global_types_as_child_elements
//-is_non_item_type
}
component MenuSeparator {
//-disallow_global_types_as_child_elements
//-is_non_item_type
}
component Menu {
in property <string> title;
MenuItem {}
MenuSeparator {}
Menu {}
//-disallow_global_types_as_child_elements
//-is_non_item_type

View file

@ -85,6 +85,7 @@ use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReferen
use crate::langtype::{ElementType, Type};
use crate::object_tree::*;
use core::cell::RefCell;
use i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
use smol_str::{format_smolstr, SmolStr};
use std::collections::HashMap;
use std::rc::{Rc, Weak};
@ -531,7 +532,18 @@ fn lower_menu_items(
));
element.borrow_mut().enclosing_component = component_weak.clone();
element.borrow_mut().geometry_props = None;
// Menu -> MenuItem
if element.borrow().base_type.type_name() == Some("MenuSeparator") {
element.borrow_mut().bindings.insert(
"title".into(),
RefCell::new(
Expression::StringLiteral(SmolStr::new_static(
MENU_SEPARATOR_PLACEHOLDER_TITLE,
))
.into(),
),
);
}
// Menu/MenuSeparator -> MenuItem
element.borrow_mut().base_type = components.menu_item_element.clone();
}
false
@ -593,15 +605,22 @@ fn generate_menu_entries(
state: &mut GenMenuState,
) -> Vec<Expression> {
let mut entries = Vec::new();
let mut last_is_separator = false;
for item in menu_items {
let mut borrow_mut = item.borrow_mut();
let base_name = borrow_mut.base_type.type_name().unwrap();
let is_sub_menu = base_name == "Menu";
if !is_sub_menu {
let is_separator = base_name == "MenuSeparator";
if !is_sub_menu && !is_separator {
assert_eq!(base_name, "MenuItem");
}
if is_separator && (last_is_separator || entries.is_empty()) {
continue;
}
last_is_separator = is_separator;
borrow_mut
.enclosing_component
.upgrade()
@ -613,10 +632,14 @@ fn generate_menu_entries(
assert!(borrow_mut.repeated.is_none());
let mut values = HashMap::<SmolStr, Expression>::new();
for prop in ["title"] {
if let Some(binding) = borrow_mut.bindings.remove(prop) {
values.insert(SmolStr::new_static(prop), binding.into_inner().expression);
if !is_separator {
for prop in ["title"] {
if let Some(binding) = borrow_mut.bindings.remove(prop) {
values.insert(SmolStr::new_static(prop), binding.into_inner().expression);
}
}
} else {
values.insert(SmolStr::new_static("is_separator"), Expression::BoolLiteral(true));
}
state.id += 1;
@ -641,6 +664,9 @@ fn generate_menu_entries(
entries.push(mk_struct(state.menu_entry.clone(), values));
}
if last_is_separator {
entries.pop();
}
entries
}

View file

@ -27,9 +27,25 @@ export component A {
col: 45;
// ^error{Unknown property col in MenuItem}
}
MenuSeparator {
entries: [];
// ^error{Unknown property entries in MenuSeparator}
title: "ok";
// ^error{Unknown property title in MenuSeparator}
width: 45px;
// ^error{Unknown property width in MenuSeparator}
MenuItem {}
// ^error{MenuSeparator cannot have children elements}
Rectangle {}
// ^error{MenuSeparator cannot have children elements}
}
}
MenuItem {}
// ^error{Unknown element 'MenuItem'}
MenuSeparator {}
// ^error{Unknown element 'MenuSeparator'}
}
TouchArea {

View file

@ -28,7 +28,7 @@ export component A inherits Window {
opacity: 0.4;
// ^error{Unknown property opacity in Menu}
Rectangle {
// ^error{Rectangle is not allowed within Menu. Only Menu MenuItem are valid children}
// ^error{Rectangle is not allowed within Menu. Only Menu MenuItem MenuSeparator are valid children}
}
MenuItem {
@ -45,6 +45,9 @@ export component A inherits Window {
MenuItem {}
// ^error{MenuItem is not allowed within MenuBar. Only Menu are valid children}
MenuSeparator {}
// ^error{MenuSeparator is not allowed within MenuBar. Only Menu are valid children}
}
Rectangle {

View file

@ -96,6 +96,7 @@ export component MenuItemBase {
in property <brush> default-background;
in property <brush> current-foreground;
in property <brush> current-background;
in property <brush> separator-color;
in property <length> font-size <=> label.font-size;
in property <int> font-weight <=> label.font-weight;
in property <length> border-radius <=> background-layer.border-radius;
@ -113,6 +114,8 @@ export component MenuItemBase {
background: root.default-background;
touch-area := TouchArea {
visible: !entry.is-separator;
layout := HorizontalLayout {
padding-top: root.padding-top;
padding-bottom: root.padding-bottom;
@ -153,6 +156,11 @@ export component MenuItemBase {
}
}
}
if entry.is-separator: Rectangle {
height: 1px;
background: root.separator-color;
}
}
states [

View file

@ -67,17 +67,21 @@ export component PopupMenuImpl inherits Window {
open-sub-menu-after-timeout.running = false;
if event.text == Key.UpArrow {
if current-highlight >= 1 {
current-highlight -= 1;
} else {
if current-highlight < 1 {
current-highlight = entries.length - 1;
} else if entries[current-highlight - 1].is_separator {
current-highlight -= 2;
} else {
current-highlight -= 1;
}
return accept;
} else if event.text == Key.DownArrow {
if current-highlight < entries.length - 1 {
current-highlight += 1;
} else {
if current-highlight >= entries.length - 1 {
current-highlight = 0;
} else if entries[current-highlight + 1].is_separator {
current-highlight += 2;
} else {
current-highlight += 1;
}
return accept;
} else if event.text == Key.Return {

View file

@ -61,6 +61,7 @@ export component MenuItem {
default-foreground: CosmicPalette.foreground;
current-foreground: CosmicPalette.foreground;
current-background: CosmicPalette.state-hover;
separator-color: CosmicPalette.border;
font-size: CosmicFontSettings.body.font-size;
font-weight: CosmicFontSettings.body.font-weight;
padding-left: 16px;

View file

@ -62,6 +62,7 @@ export component MenuItem {
default-foreground: CupertinoPalette.foreground;
current-foreground: CupertinoPalette.accent-foreground;
current-background: CupertinoPalette.accent-background;
separator-color: CupertinoPalette.border;
font-size: CupertinoFontSettings.body.font-size;
font-weight: CupertinoFontSettings.body.font-weight;
border-radius: 5px;

View file

@ -64,6 +64,7 @@ export component MenuItem {
default-foreground: FluentPalette.foreground;
current-foreground: FluentPalette.foreground;
current-background: FluentPalette.subtle-secondary;
separator-color: FluentPalette.border;
font-size: FluentFontSettings.body.font-size;
font-weight: FluentFontSettings.body.font-weight;
border-radius: 4px;

View file

@ -63,6 +63,7 @@ export component MenuItem {
default-foreground: MaterialPalette.foreground;
current-foreground: MaterialPalette.foreground;
current-background: MaterialPalette.surface-container-highest;
separator-color: MaterialPalette.border;
font-size: MaterialFontSettings.body-large.font-size;
font-weight: MaterialFontSettings.body-large.font-weight;
padding-left: 12px;

View file

@ -61,6 +61,7 @@ export component MenuItem {
default-foreground: NativePalette.foreground;
current-foreground: NativePalette.accent-foreground;
current-background: NativePalette.accent-background;
separator-color: NativePalette.border;
font-size: NativeStyleMetrics.default-font-size;
font-weight: 300;
border-radius: 4px;