// Copyright © SixtyFPS GmbH // 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"; component MenuItemImpl { in property is-current; in property entry; callback set-current(); callback clear-current(); callback activate(entry : MenuEntry, y: length); background-layer := Rectangle { background: Palette.background; border-radius: 3px; border-width: 1px; HorizontalLayout { spacing: 7px; padding-left: 11px; padding-top: 4px; padding-bottom: 6px; menu-text := Text { text: entry.title; horizontal-stretch: 1; color: Palette.foreground; } if entry.has-sub-menu : Image { source: @image-url("../fluent/_arrow_forward.svg"); colorize: menu-text.color; accessible-role: none; } } TouchArea { pointer-event(event) => { if event.kind == PointerEventKind.move && !root.is-current { root.set-current() } else if event.kind == PointerEventKind.down && entry.has-sub-menu { activate(entry, self.absolute-position.y); } else if event.kind == PointerEventKind.up && self.mouse-y > 0 && self.mouse-y < self.height && self.mouse-x > 0 && self.mouse-x < self.width { // can't put this in `clicked` because then the menu would close causing a panic in the pointer-event activate(entry, self.absolute-position.y); } } changed has-hover => { if !self.has-hover && root.is-current { root.clear-current(); } } } } states [ is-current when root.is-current: { // FIXME: note that the fluent style uses the secondary color for highlighted item background-layer.background: Palette.accent-background; menu-text.color: Palette.accent-foreground; } ] } export component PopupMenuImpl inherits Window { in property <[MenuEntry]> entries: []; property current: -1; property px: 1rem / 14; callback sub-menu(menu-entry: MenuEntry) -> [MenuEntry]; callback activated(menu-entry: MenuEntry); callback close(); forward-focus: focus-scope; Rectangle { border-radius: 7px; 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; } focus-scope := FocusScope { layout := VerticalLayout { padding: 5px; for entry[index] in entries: MenuItemImpl { entry: entry; is-current: current == index; set-current => { focus-scope.focus(); root.current = index; open-sub-menu-after-timeout.running = true; } clear-current => { root.current = -1; } activate(entry, y) => { root.activate(entry, y); } } } 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 { sub-menu.close(); } } } } 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 root.close(); } reject } } sub-menu := ContextMenuInternal { x: 0; y: 0; width: 0; height: 0; sub-menu(entry) => { root.sub-menu(entry); } activated(entry) => { root.activated(entry); root.close(); } } function activate(entry : MenuEntry, y: length) { open-sub-menu-after-timeout.running = false; if entry.has-sub-menu { sub-menu.entries = root.sub-menu(entry); sub-menu.show({ x: root.width, y: y - sub-menu.absolute-position.y, }); } else { activated(entry); close(); } } } export component MenuBarImpl { callback activated(MenuEntry); callback sub-menu(MenuEntry) -> [MenuEntry]; property <[MenuEntry]> entries; property px: 1rem / 14; preferred-width: 100%; height: layout.preferred-height; layout := HorizontalLayout { padding: 5px; alignment: start; spacing: 1px; for entry in entries: e := Rectangle { // FIXME: note that the fluent style uses the secondary color for highlighted item background: touch-area.has-hover || touch-area.pressed ? Palette.accent-background : transparent; border-radius: 3px; HorizontalLayout { padding: 11px; padding-top: 4px; padding-bottom: 6px; Text { text: entry.title; color: touch-area.has-hover || touch-area.pressed ? Palette.accent-foreground : Palette.foreground; } } touch-area := TouchArea { clicked => { context-menu.entries = root.sub-menu(entry); context-menu.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: ""; } } } } context-menu := ContextMenuInternal { activated(entry) => { root.activated(entry); self.close(); } sub-menu(entry) => { root.sub-menu(entry); } } }