Menus, context menus and keyboard shortcuts

A menu bar, submenus and menu items are views. A menu item's items are an
ItemsControl's, so they are patched as a list's. A context menu wraps a
view: the view and its menu are two places of the wrapper.

Keys are matched in the window's KeyDown, after the focused control had its
chance: an enabled menu item in the view whose shortcut it is, or else the
app's #:keys. Avalonia's own key bindings take an ICommand, which a Bjolang
program cannot make. app-press! presses keys for a test.

🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-25 17:55:19 +01:00
parent 1e46202eef
commit be288ac03e
3 changed files with 300 additions and 28 deletions

View file

@ -20,9 +20,13 @@
;; Constructors.
text button text-input text-area checkbox slider progress-bar dropdown image
list-box tabs tab
menu-bar submenu menu-item menu-separator
column row grid scroll border
;; Wrappers.
keyed at paint
keyed at paint with-context-menu
;; Keyboard shortcuts.
Key key key-gesture key-message
;; Reading one.
view-kind view-props view-events view-children view-key)
@ -37,6 +41,7 @@
(type (: Kind (Union TextKind ButtonKind TextInputKind TextAreaKind CheckBoxKind
SliderKind ProgressKind DropdownKind ImageKind
ListBoxKind TabsKind TabKind
MenuBarKind MenuItemKind SeparatorKind ContextKind ContextMenuKind
ColumnKind RowKind GridKind ScrollKind BorderKind)))
;; A property of a control. A kind takes only some of them; the app ignores
@ -70,7 +75,8 @@
(: Foreground string) ; the colour of the text
(: BorderColour string)
(: BorderWidth double)
(: Radius double)))) ; how round a border's corners are
(: Radius double) ; how round a border's corners are
(: Shortcut string)))) ; a menu item's keys, as "Ctrl+S"
;; An event, and the message it sends.
(type (: (On %m) (Union (: Click %m)
@ -124,6 +130,12 @@
(defun (with props make c) (if (string-empty? c) props (vec-add props (make c))))
(record-set v (props (with (with (.props v) Foreground colour) Background background))))
;; The view, with a menu that opens when it is clicked with the right button.
;; The items are made by `menu-item`, `submenu` and `menu-separator`.
(: with-context-menu (-> (View %m) #:rest (View %m) (View %m)))
(defun (with-context-menu v #:rest items)
(container ContextKind [] [v (container ContextMenuKind [] (array->vec items))]))
;; The view, in a cell of its grid. Rows and columns count from 0.
(: at (-> int int (View %m) (#:row-span int) (#:column-span int) (View %m)))
(defun (at row column v #:row-span 1 #:column-span 1)
@ -261,6 +273,33 @@
(defun (tab header content)
(View (kind TabKind) (props [(Label header)]) (events []) (children [content]) (key None)))
;; A bar of menus, made by `submenu`, for the top of the window.
(: (-> #:rest (View %m) (View %m)))
(defun (menu-bar #:rest menus)
(container MenuBarKind [] (array->vec menus)))
;; A menu of items, in a menu bar or in another menu.
(: (-> string #:rest (View %m) (View %m)))
(defun (submenu label #:rest items)
(View (kind MenuItemKind) (props [(Label label)]) (events []) (children (array->vec items))
(key None)))
;; An item that sends `msg` when it is chosen. `#:shortcut`, such as
;; "Ctrl+S", is shown beside it, and the keys send `msg` too while the item
;; is in the view and enabled.
(: (-> string %m (#:shortcut string) (#:enabled bool) (View %m)))
(defun (menu-item label msg #:shortcut "" #:enabled #t)
(leaf MenuItemKind
(if (string-empty? shortcut)
[(Label label) (Enabled enabled)]
[(Label label) (Enabled enabled) (Shortcut shortcut)])
[(Click msg)]))
;; A line between items of a menu.
(: (-> (View %m)))
(defun (menu-separator)
(leaf SeparatorKind [] []))
;; A checkbox that is `checked?`. A click sends `(on-toggle checked)`, with
;; the new state.
(: (-> string bool (-> bool %m) (#:enabled bool) (#:margin double) (View %m)))
@ -321,3 +360,21 @@
(: (-> (View %m) (#:width double) (#:height double) (#:margin double) (View %m)))
(defun (scroll child #:width 0.0 #:height 0.0 #:margin 0.0)
(container ScrollKind (with-layout [] width height margin 0.0) [child]))
;; ---------------------------------------------------------------------------
;; Keyboard shortcuts
;; ---------------------------------------------------------------------------
;; Keys, such as "Ctrl+N" or "F5", and the message they send.
(type (: (Key %m) (Record (: gesture string) (: msg %m))))
;; `msg` is sent when `gesture` is pressed in the window, and no control that
;; has the keyboard takes the keys itself.
(: key (-> string %m (Key %m)))
(defun (key gesture msg) (Key (gesture gesture) (msg msg)))
(: key-gesture (-> (Key %m) string))
(defun (key-gesture k) (.gesture k))
(: key-message (-> (Key %m) %m))
(defun (key-message k) (.msg k))