Open tronical opened 4 years ago
We probably need a few things here. Which we still need to design API for.
Action
type.maybe a struct { text: string, icon: image, tool-tip: string, enabled: bool, checkable : bool, sub-menu: [Action], shortcut: string }
maybe we even need an icon
type instead of re-using image?
For convenience, we might want to have implicit conversion from string to Menu
It is also quite similar to the StandardListViewItem
we already have.
What about sub_menus? currently we can't have a recursive struct. but maybe we can make an exception for the action type.
Maybe something like this
MainWindow := Window {
MenuBar {
model: [
{
text: "File",
sub-menu: [ { text: "Open", icon: @image-url(...) }, { text: "Close", ... }, ... ]
},
{
text: "Edit",
sub-menu: [ { text: "Copy", icon: @image-url(...) }, { text: "Paste", ... }, ... ]
},
];
trigerred(item) => {
if (item == "File") { // re-using the text is a very bad idea because of translation, we need something better than data
...
} else ...
}
}
...
}
An alternative would be NOT using an Action type, but using something more looking like our syntax like this:
MainWindow := Window {
property <[Action]> recent_model;
MenuBar {
Menu {
name: "File";
MenuItem { name: "Open"; icon: "..."; triggered => { ... } }
MenuItem { name: "Close"; icon: "..."; triggered => { ... } }
Menu {
name: "Open Recent";
model: recent_model; // provided by the logic
}
}
}
...
}
Another way would be to have a collection of action with another special syntax
global MyActions := ActionCollections {
file-open := Action {
text: "Open";
icon: @image-url(...);
triggered => {...}
shortcut: "Ctrl+O";
}
file-close := Action {
text: "Close";
icon: @image-url(...);
triggered => {...}
}
}
MainWindow := Window {
MenuBar {
Menu {
text: "File";
model: [MyActions.file-open, MyActions.file-close, ...];
}
Menu { ... }
}
The problem with that last approach is that ActionCollections
it is kind of a very special case which somehow re-use the same syntax, but with a completely different semantic. Although we also abuse that for the Path
sub-elements and the Row
within GridLayout.
What i mean here is that Elements are no longer really graphical items. But also that one can access the sub elements of an ActionCollection by their id from a different component.
Similar to Menu bar, we want to be able to set context menu to element.
Do we want each element to get a context-menu
property:
Foo := Rectangle {
...
Rectangle {
context-menu: [ MyActions.file-open, {text: "Another Action" } ];
...
}
}
Or should we get another element
Foo := Rectangle {
...
Rectangle {
ContextMenu {
// similar to the MenuBar above
model: [ ... ];
}
}
}
Implementation wise, I see that tauri has created a separate crate for menubar / popup menus that works on macOS, Windows, and Linux - the latter using gtk: https://github.com/tauri-apps/muda
Sumary of our discussion in the office:
The ContextMenu
pseudo-element contains MenuItem { ... }
MenuSeparator { ... }
and `MenuEntries { ... }
There exist a struct
struct MenuEntry {
title: text;
icon: image; // or icon?
id: string; // opaque
keyboard-shortcut: key-sequence;
enabled: bool;
}
The ContextMenu can be used like so:
ContextMenu {
MenuItem {
text: "Blah";
// icon, toolip, etc.
triggered => {}
}
MenuSeparator {}
SubMenu { // is a MenuItem
text: "Sub Menu Item Text";
MenuItem { ... }
...
}
MenuEntries {
// this is a [MenuEntry] model
entries: Globals.recent-files;
triggered(entry, index) => {
root.open-recent-file(index);
}
}
MenuItem { ... }
// or just a [MenuEntry]
// this is mutually exclusive with MenuItem, etc. inline
actions: ...;
}
When placed somewhere in the tree, it will automatically be shown on right click or with the menu key. We were also considering a ContexMenuArea
export component AppWindow {
...
// intercepts context menu key press and mouse right click
// this is an element that has a geometry
// can have any children, acts like a Empty,
// must have exactly one ContextMenu
ContexMenuArea {
ContextMenu {
// no geometry properties
}
}
}
Not sure if ContexMenuArea is needed or not.
Introduce a MenuBar
widget that can only be used in Window.
Its presence changes the layout of the content area. (So that y:0
means just after the MenuBar)
On platforms that have native menu bar, we would use the muda
crate to do so.
On other platforms we would render the menu as a slint widget and using PopupWindow for the menus.
export component AppWindow inherits Window {
// there can only be one, must be child of `Window` import from std-widgets.slint
MenuBar {
// has no geometry properties, etc.
MenuItem {
title: @tr("Menu" => "File")
MenuItem {
title: "delete currently selected";
enabled: SomeGlobal.is-design-mode;
keyboard-shortcut: @keys(ctrl+d) // lowers to string with our fancy encoded control codes
}
MenuEntries {}
MenuSeparator{}
MenuItem {
blah := MenuItem {
// all the same property as MenuEntry + triggered
}
}
//Action { action: SomeGloba.file-open; }
}
MenuItem { ... }
}
// Fill the rest: y is after the menu bar.
Rectangle { y:0; height: 100%; }
}
MenuBarImpl
widget that the style providesContextMenuImpl
in the style and also entries and separatorPopupWindow
s. PopupWindow
-PopupWindow
placement strategies (anchoring, etc. - see wayland strategy) @keys
and keysequence type in slint.
PopupWindow
placement strategies (anchoring, etc. - see wayland strategy)
Note that Windows provides system settings like which side a context menu should open towards.
I'm very interested in menu bars & context menus. So I'd like to bring in my thoughts on this.
keyboard-shortcut: key-sequence;
It would be great to allow having multiple keyboard shortcuts for the same action. QAction
allows this by assigning a list of QKeySequence
's. It is very handy for example to support both "Ctrl+Y" and "Ctrl+Shift+Z" for the redo action as you never know which application supports which of those shortcuts, so just support both of them. In addition, some day the shortcuts can be extended to support sequence-based shortcuts in addition to combination-shortcuts (e.g. press "e" for edit, then "r" for redo). Then an application can support both shortcut styles at the same time (like Qt does).
The ContextMenu can be used like so: [...]
For simple (static) UIs it's great to declare all the menu items (for both menu bar & context menus) right in the .slint file as you suggest. But for more dynamic UIs (e.g. configurable keyboard shortcuts, menu items enabled/disabled or even visible/invisible depending on app state etc) it's crucial to allow defining (or modifying) menu items from native code. Maybe through a model would be easiest, though other ways might be fine as well. From reading the code examples above I'm not 100% sure if this case is considered.
Introduce a MenuBar widget that can only be used in Window.
Would there be a technical reason for this restriction? If the menu bar would also work in nested UI elements (e.g. tabs) I'd say the user should not be prevented from doing this. Also in PopupWindow it might be nice to have a menu bar. Of course the system integration would only work with the main window's menu bar.
Its presence changes the layout of the content area. (So that y:0 means just after the MenuBar)
Would this prevent the user from placing any other UI elements above this pseudo y:0? Usually menu bars contain a lot of unused space, where it would be nice to allow using it for other things. For example a search box:
I'm aware this is not a standard UI paradigm. But should the UI toolkit prevent the user from doing nonstandard things? It is the great flexibility of Slint I really like compared to the very restrictive and unflexible QtWidgets so IMHO it would be sad to implement similar restrictions in Slint too ;)
Actually now I wonder if it would even be feasible to use arbitrary UI elements in menus? For example switches (though checked menu items should probably be a built-in feature):
On platforms that have native menu bar, we would use the muda crate to do so.
I agree, but it would be nice to allow disabling it, e.g. for cases as described above where the menu bar is used for other purpose too or contains arbitrary widgets.
So far just my thoughts, up to you to decide what makes sense and what doesn't :slightly_smiling_face:
Its presence changes the layout of the content area. (So that y:0 means just after the MenuBar)
Would this prevent the user from placing any other UI elements above this pseudo y:0? Usually menu bars contain a lot of unused space, where it would be nice to allow using it for other things. For example a search box:
Under Windows, when making use of the OS-provided menu bar, the window's client area naturally doesn't include the menu bar. Use cases like you described would be a good reason in my opinion to provide custom menu bars and menus. Firefox, e.g., draws its menu bar and menus itself. According to GPT-4o, trying to custom-draw into the non-client area of the menu bar quickly gets complex and inflexible.
I think there could also be theme inconsistencies with OS-provided menu bars and menus (like light-themed menu bars and menus in a dark-themed app).
Actually now I wonder if it would even be feasible to use arbitrary UI elements in menus? For example switches (though checked menu items should probably be a built-in feature):
Some custom menu behavior could IMO indeed be desirable. I think everybody knows how annoying it can be having to repeatedly show a series of menus and submenus, paying attention to find the end spot again, just to quickly test a number of features reachable via menu (e.g., enabling and quickly disabling it again, or toggling multiple check marks). A custom menu implementation could provide a means to keep the menu shown after clicking on an item (like when holding a keyboard modifier when clicking).
are there any workaround examples available for this using muda in conjunction with slint?
@dougcooper
are there any workaround examples available for this using muda in conjunction with slint?
This is a bit out of topic for this issue, but we do use muda to change the menu of the live preview on macOs. (although this uses private API) https://github.com/slint-ui/slint/blob/master/tools/lsp/preview/native.rs#L273
This ticket tracks the ability to declare popup menus in
.60
markup as well as the ability to integrate them into a menu bar.This needs further refinement/triaging regarding a more actionable acceptance criteria.