slint/internal/compiler/widgets/common/menus.slint
Olivier Goffart c0b72cad2f
ContextMenu: show submenu on hover after a timeout
This means that the parent menu still get the mouse events

Also add the ability to close the menu programmatically
2025-02-07 13:27:32 +01:00

192 lines
6.4 KiB
Text

// 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);
private property <int> current: -1;
forward-focus: fs;
Rectangle {
border-radius: 7*px;
border-color: Palette.border;
border-width: 1px;
background: Palette.background;
drop-shadow-blur: 2px;
drop-shadow-color: Palette.foreground.transparentize(0.5);
min-width: 10rem;
}
fs := FocusScope {
layout := VerticalLayout {
padding: 5px;
for entry[idx] in entries: Rectangle {
// FIXME: note that the fluent style uses the secondary color for highlighted item
background: current == idx ? Palette.accent-background : Palette.background;
border-radius: 3*px;
border-width: 1px;
HorizontalLayout {
spacing: 7*px;
padding-left: 11*px;
padding-top: 4*px;
padding-bottom: 6*px;
menu-text := Text {
text: entry.title;
horizontal-stretch: 1;
color: current == idx ? Palette.accent-foreground : Palette.foreground;
}
if entry.has-sub-menu : Image {
source: @image-url("../fluent/_arrow_forward.svg");
colorize: menu-text.color;
}
}
TouchArea {
pointer-event(event) => {
fs.focus();
if event.kind == PointerEventKind.move {
current = idx;
open-sub-menu-after-timeout.running = true;
}
}
changed has-hover => {
if !self.has-hover && current == idx {
current = -1
}
}
clicked => {
activate(entry, self.absolute-position.y);
}
}
}
}
function y-pos(idx: int) -> length {
layout.padding + idx * (layout.height - 2 * layout.padding) / entries.length
}
key-pressed(event) => {
open-sub-menu-after-timeout.running = false;
if event.text == Key.UpArrow {
if current >= 1 {
current -= 1;
} else {
current = entries.length - 1;
}
return accept;
} else if event.text == Key.DownArrow {
if current < entries.length - 1 {
current += 1;
} else {
current = 0;
}
return accept;
} else if event.text == Key.Return {
if current >= 0 && current < entries.length {
activate(entries[current], y-pos(current));
}
return accept;
} else if event.text == Key.RightArrow {
if current >= 0 && current < entries.length && entries[current].has-sub-menu {
activate(entries[current], y-pos(current));
}
return accept;
} else if event.text == Key.LeftArrow {
// TODO: should close only if this menu is a sub menu
}
return reject;
}
open-sub-menu-after-timeout := Timer {
interval: 500ms;
running: false;
triggered => {
self.running = false;
if current >= 0 {
if entries[current].has-sub-menu {
activate(entries[current], y-pos(current));
} else {
subMenu.close();
}
}
}
}
}
subMenu := ContextMenuInternal {
x: 0; y: 0; width: 0; height: 0;
sub-menu(entry) => { root.sub-menu(entry); }
activated(entry) => { root.activated(entry); }
}
function activate(entry : MenuEntry, y: length) {
open-sub-menu-after-timeout.running = false;
if entry.has-sub-menu {
subMenu.entries = root.sub-menu(entry);
subMenu.show({
x: root.width,
y: y - subMenu.absolute-position.y,
});
} else {
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 {
// FIXME: note that the fluent style uses the secondary color for highlighted item
background: ta.has-hover || ta.pressed ? Palette.accent-background : transparent;
border-radius: 3*px;
HorizontalLayout {
padding: 11*px;
padding-top: 4*px;
padding-bottom: 6*px;
Text {
text: entry.title;
color: ta.has-hover || ta.pressed ? Palette.accent-foreground : Palette.foreground;
}
}
ta := TouchArea {
clicked => {
cm.entries = root.sub-menu(entry);
cm.show({ 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 := ContextMenuInternal {
activated(entry) => { root.activated(entry); }
sub-menu(entry) => { root.sub-menu(entry); }
}
}