Switches, radios, numbers, dates, completion, expanders, tooltips, flows, docks

A number input's numbers are decimals in Avalonia, so they are converted
where they are set. A date is "yyyy-MM-dd" both ways. An expander and a
completion box raise their own events from the parts of their templates, so
they listen to the property instead.

The cases of Prop and On are now PropLabel, OnClick and so on. They are
exported with their types, and as Label, Value, Selected and Click they
took names that a program's own messages want.

The panel is flow and not wrap, which is CML's.

🤖 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 18:05:12 +01:00
parent cfcb1851b7
commit 56a01efe76
3 changed files with 532 additions and 170 deletions

View file

@ -21,9 +21,10 @@
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
toggle-switch radio number-input date-picker auto-complete expander
column row grid scroll border flow dock
;; Wrappers.
keyed at paint with-context-menu
keyed at paint with-context-menu tooltip at-top at-bottom at-left at-right
;; Keyboard shortcuts.
Key key key-gesture key-message
@ -42,48 +43,54 @@
SliderKind ProgressKind DropdownKind ImageKind
ListBoxKind TabsKind TabKind
MenuBarKind MenuItemKind SeparatorKind ContextKind ContextMenuKind
ToggleSwitchKind RadioKind NumberKind DateKind AutoCompleteKind
ExpanderKind WrapKind DockKind
ColumnKind RowKind GridKind ScrollKind BorderKind)))
;; A property of a control. A kind takes only some of them; the app ignores
;; the others.
(type (: Prop (Union (: Label string) ; the text of a text, a button or a checkbox
(: Value string) ; the text in a text input
(: Placeholder string) ; shown in an empty text input
(: Checked bool)
(: Enabled bool)
(: FontSize double)
(: Width double)
(: Height double)
(: Margin double)
(: Padding double)
(: Spacing double)
(: Wrap bool) ; a text breaks its lines at the width
(: Minimum double) ; of a slider or a progress bar
(: Maximum double)
(: Amount double) ; where a slider or a progress bar is
(: Step double) ; what an arrow key moves a slider by
(: Items (Vec string)) ; what a dropdown offers
(: Selected int) ; which item is chosen; -1 for none
(: Source string) ; the file an image shows
(: Columns string) ; a grid's columns, as "Auto,*,2*"
(: Rows string)
(: Row int) ; where a view is in its grid
(: Column int)
(: RowSpan int)
(: ColumnSpan int)
(: Background string) ; a colour: "red", "#08f", "#80ff8800"
(: Foreground string) ; the colour of the text
(: BorderColour string)
(: BorderWidth double)
(: Radius double) ; how round a border's corners are
(: Shortcut string)))) ; a menu item's keys, as "Ctrl+S"
(type (: Prop (Union (: PropLabel string) ; the text of a text, a button or a checkbox
(: PropValue string) ; the text in a text input
(: PropPlaceholder string) ; shown in an empty text input
(: PropChecked bool)
(: PropEnabled bool)
(: PropFontSize double)
(: PropWidth double)
(: PropHeight double)
(: PropMargin double)
(: PropPadding double)
(: PropSpacing double)
(: PropWrap bool) ; a text breaks its lines at the width
(: PropMinimum double) ; of a slider or a progress bar
(: PropMaximum double)
(: PropAmount double) ; where a slider or a progress bar is
(: PropStep double) ; what an arrow key moves a slider by
(: PropItems (Vec string)) ; what a dropdown offers
(: PropSelected int) ; which item is chosen; -1 for none
(: PropSource string) ; the file an image shows
(: PropColumns string) ; a grid's columns, as "Auto,*,2*"
(: PropRows string)
(: PropRow int) ; where a view is in its grid
(: PropColumn int)
(: PropRowSpan int)
(: PropColumnSpan int)
(: PropBackground string) ; a colour: "red", "#08f", "#80ff8800"
(: PropForeground string) ; the colour of the text
(: PropBorderColour string)
(: PropBorderWidth double)
(: PropRadius double) ; how round a border's corners are
(: PropShortcut string) ; a menu item's keys, as "Ctrl+S"
(: PropTip string) ; shown when the pointer rests on a view
(: PropExpanded bool) ; whether an expander shows its content
(: PropDate string) ; a date picker's date, as "2026-10-25"
(: PropDockSide int)))) ; where a view is in its dock: 0 to 3
;; An event, and the message it sends.
(type (: (On %m) (Union (: Click %m)
(: Input (-> string %m))
(: Toggle (-> bool %m))
(: Slide (-> double %m))
(: Select (-> int %m)))))
(type (: (On %m) (Union (: OnClick %m)
(: OnInput (-> string %m))
(: OnToggle (-> bool %m))
(: OnSlide (-> double %m))
(: OnSelect (-> int %m)))))
;; One control and what is in it.
;;
@ -128,7 +135,7 @@
(: paint (-> (View %m) (#:colour string) (#:background string) (View %m)))
(defun (paint v #:colour "" #:background "")
(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))))
(record-set v (props (with (with (.props v) PropForeground colour) PropBackground 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`.
@ -136,12 +143,35 @@
(defun (with-context-menu v #:rest items)
(container ContextKind [] [v (container ContextMenuKind [] (array->vec items))]))
;; The view, with `text` shown when the pointer rests on it.
(: tooltip (-> (View %m) string (View %m)))
(defun (tooltip v text)
(record-set v (props (vec-add (.props v) (PropTip text)))))
(: (-> int (View %m) (View %m)))
(defun (docked side v)
(record-set v (props (vec-add (.props v) (PropDockSide side)))))
;; The view, at an edge of its dock. The last child of a dock that is put at
;; no edge fills what is left.
(: at-left (-> (View %m) (View %m)))
(defun (at-left v) (docked 0 v))
(: at-top (-> (View %m) (View %m)))
(defun (at-top v) (docked 1 v))
(: at-right (-> (View %m) (View %m)))
(defun (at-right v) (docked 2 v))
(: at-bottom (-> (View %m) (View %m)))
(defun (at-bottom v) (docked 3 v))
;; 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)
(record-set v (props (vec-add (vec-add (vec-add (vec-add (.props v) (Row row)) (Column column))
(RowSpan row-span))
(ColumnSpan column-span)))))
(record-set v (props (vec-add (vec-add (vec-add (vec-add (.props v) (PropRow row)) (PropColumn column))
(PropRowSpan row-span))
(PropColumnSpan column-span)))))
;; ---------------------------------------------------------------------------
;; Constructors
@ -156,9 +186,9 @@
(: (-> (Vec Prop) double double double double (Vec Prop)))
(defun (with-layout props width height margin padding)
(with-size (with-size (with-size (with-size props Width width) Height height)
Margin margin)
Padding padding))
(with-size (with-size (with-size (with-size props PropWidth width) PropHeight height)
PropMargin margin)
PropPadding padding))
(: (-> Kind (Vec Prop) (Vec (On %m)) (View %m)))
(defun (leaf kind props events)
@ -170,7 +200,7 @@
(View %m)))
(defun (text s #:size 0.0 #:wrap #f #:width 0.0 #:height 0.0 #:margin 0.0)
(leaf TextKind
(with-layout (with-size [(Label s) (Wrap wrap)] FontSize size) width height margin 0.0)
(with-layout (with-size [(PropLabel s) (PropWrap wrap)] PropFontSize size) width height margin 0.0)
[]))
;; A button that sends `msg` when it is clicked.
@ -179,8 +209,8 @@
(View %m)))
(defun (button label msg #:enabled #t #:width 0.0 #:height 0.0 #:margin 0.0 #:padding 0.0)
(leaf ButtonKind
(with-layout [(Label label) (Enabled enabled)] width height margin padding)
[(Click msg)]))
(with-layout [(PropLabel label) (PropEnabled enabled)] width height margin padding)
[(OnClick msg)]))
;; A one-line text input that holds `value`. Each change sends `(on-input
;; text)`, with the whole text.
@ -189,9 +219,9 @@
(View %m)))
(defun (text-input value on-input #:placeholder "" #:enabled #t #:width 0.0 #:margin 0.0)
(leaf TextInputKind
(with-layout [(Value value) (Placeholder placeholder) (Enabled enabled)]
(with-layout [(PropValue value) (PropPlaceholder placeholder) (PropEnabled enabled)]
width 0.0 margin 0.0)
[(Input on-input)]))
[(OnInput on-input)]))
;; A text input of several lines, that breaks its lines at its width. Each
;; change sends `(on-input text)`, with the whole text.
@ -201,9 +231,9 @@
(defun (text-area value on-input #:placeholder "" #:enabled #t #:width 0.0 #:height 0.0
#:margin 0.0)
(leaf TextAreaKind
(with-layout [(Value value) (Placeholder placeholder) (Enabled enabled)]
(with-layout [(PropValue value) (PropPlaceholder placeholder) (PropEnabled enabled)]
width height margin 0.0)
[(Input on-input)]))
[(OnInput on-input)]))
;; A slider at `value`, from `minimum` to `maximum`. A move sends `(on-change
;; value)`.
@ -215,17 +245,17 @@
;; The bounds come before the value, because a value outside the bounds
;; that are there is moved inside them.
(leaf SliderKind
(with-layout [(Minimum minimum) (Maximum maximum) (Amount value) (Step step)
(Enabled enabled)]
(with-layout [(PropMinimum minimum) (PropMaximum maximum) (PropAmount value) (PropStep step)
(PropEnabled enabled)]
width 0.0 margin 0.0)
[(Slide on-change)]))
[(OnSlide on-change)]))
;; A bar that is `value` of the way from 0 to `#:maximum`, 1 unless told
;; otherwise.
(: (-> double (#:maximum double) (#:width double) (#:margin double) (View %m)))
(defun (progress-bar value #:maximum 1.0 #:width 0.0 #:margin 0.0)
(leaf ProgressKind
(with-layout [(Minimum 0.0) (Maximum maximum) (Amount value)] width 0.0 margin 0.0)
(with-layout [(PropMinimum 0.0) (PropMaximum maximum) (PropAmount value)] width 0.0 margin 0.0)
[]))
;; A list to choose one of `options` from, with the one at `selected` chosen,
@ -236,15 +266,15 @@
;; The items come before the choice, because the choice is an index into
;; them.
(leaf DropdownKind
(with-layout [(Items options) (Selected selected) (Enabled enabled)]
(with-layout [(PropItems options) (PropSelected selected) (PropEnabled enabled)]
width 0.0 margin 0.0)
[(Select on-select)]))
[(OnSelect on-select)]))
;; The picture in the file at `path`. A file that cannot be read shows
;; nothing.
(: (-> string (#:width double) (#:height double) (#:margin double) (View %m)))
(defun (image path #:width 0.0 #:height 0.0 #:margin 0.0)
(leaf ImageKind (with-layout [(Source path)] width height margin 0.0) []))
(leaf ImageKind (with-layout [(PropSource path)] width height margin 0.0) []))
;; A list of views to choose one of, with the one at `selected` chosen, or
;; none for -1. A choice sends `(on-select index)`. Give the items keys if
@ -253,8 +283,8 @@
(View %m)))
(defun (list-box items selected on-select #:width 0.0 #:height 0.0 #:margin 0.0)
(View (kind ListBoxKind)
(props (with-layout [(Selected selected)] width height margin 0.0))
(events [(Select on-select)])
(props (with-layout [(PropSelected selected)] width height margin 0.0))
(events [(OnSelect on-select)])
(children items)
(key None)))
@ -263,15 +293,15 @@
(: (-> int (-> int %m) (#:margin double) #:rest (View %m) (View %m)))
(defun (tabs selected on-select #:margin 0.0 #:rest tabs)
(View (kind TabsKind)
(props (with-layout [(Selected selected)] 0.0 0.0 margin 0.0))
(events [(Select on-select)])
(props (with-layout [(PropSelected selected)] 0.0 0.0 margin 0.0))
(events [(OnSelect on-select)])
(children (array->vec tabs))
(key None)))
;; A tab of `tabs`: its header, and what it shows when it is chosen.
(: (-> string (View %m) (View %m)))
(defun (tab header content)
(View (kind TabKind) (props [(Label header)]) (events []) (children [content]) (key None)))
(View (kind TabKind) (props [(PropLabel header)]) (events []) (children [content]) (key None)))
;; A bar of menus, made by `submenu`, for the top of the window.
(: (-> #:rest (View %m) (View %m)))
@ -281,7 +311,7 @@
;; 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))
(View (kind MenuItemKind) (props [(PropLabel label)]) (events []) (children (array->vec items))
(key None)))
;; An item that sends `msg` when it is chosen. `#:shortcut`, such as
@ -291,22 +321,78 @@
(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)]))
[(PropLabel label) (PropEnabled enabled)]
[(PropLabel label) (PropEnabled enabled) (PropShortcut shortcut)])
[(OnClick msg)]))
;; A line between items of a menu.
(: (-> (View %m)))
(defun (menu-separator)
(leaf SeparatorKind [] []))
;; A switch that is on when `on?`. A flip sends `(on-toggle on)`.
(: (-> string bool (-> bool %m) (#:enabled bool) (#:margin double) (View %m)))
(defun (toggle-switch label on? on-toggle #:enabled #t #:margin 0.0)
(leaf ToggleSwitchKind
(with-layout [(PropLabel label) (PropChecked on?) (PropEnabled enabled)] 0.0 0.0 margin 0.0)
[(OnToggle on-toggle)]))
;; One of a set of choices, chosen when `chosen?`. Choosing it sends `msg`.
;; The model says which is chosen, so give one of a set `#t` and the others
;; `#f`.
(: (-> string bool %m (#:enabled bool) (#:margin double) (View %m)))
(defun (radio label chosen? msg #:enabled #t #:margin 0.0)
(leaf RadioKind
(with-layout [(PropLabel label) (PropChecked chosen?) (PropEnabled enabled)] 0.0 0.0 margin 0.0)
[(OnClick msg)]))
;; A number to type, or to step with arrows by `#:step`. A change sends
;; `(on-change value)`.
(: (-> double (-> double %m) (#:minimum double) (#:maximum double) (#:step double)
(#:enabled bool) (#:width double) (#:margin double)
(View %m)))
(defun (number-input value on-change #:minimum -1e9 #:maximum 1e9 #:step 1.0 #:enabled #t
#:width 0.0 #:margin 0.0)
(leaf NumberKind
(with-layout [(PropMinimum minimum) (PropMaximum maximum) (PropStep step) (PropAmount value)
(PropEnabled enabled)]
width 0.0 margin 0.0)
[(OnSlide on-change)]))
;; A date to pick, as "2026-10-25", or "" for none. A pick sends
;; `(on-change date)`.
(: (-> string (-> string %m) (#:enabled bool) (#:margin double) (View %m)))
(defun (date-picker date on-change #:enabled #t #:margin 0.0)
(leaf DateKind (with-layout [(PropDate date) (PropEnabled enabled)] 0.0 0.0 margin 0.0)
[(OnInput on-change)]))
;; A text input that offers the `options` that match what is typed. Each
;; change sends `(on-input text)`.
(: (-> string (Vec string) (-> string %m) (#:placeholder string) (#:width double)
(#:margin double)
(View %m)))
(defun (auto-complete value options on-input #:placeholder "" #:width 0.0 #:margin 0.0)
(leaf AutoCompleteKind
(with-layout [(PropItems options) (PropValue value) (PropPlaceholder placeholder)] width 0.0 margin 0.0)
[(OnInput on-input)]))
;; A header that shows `child` below it when `expanded?`. A click on the
;; header sends `(on-toggle expanded)`.
(: (-> string bool (-> bool %m) (View %m) (#:margin double) (View %m)))
(defun (expander header expanded? on-toggle child #:margin 0.0)
(View (kind ExpanderKind)
(props (with-layout [(PropLabel header) (PropExpanded expanded?)] 0.0 0.0 margin 0.0))
(events [(OnToggle on-toggle)])
(children [child])
(key None)))
;; 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)))
(defun (checkbox label checked? on-toggle #:enabled #t #:margin 0.0)
(leaf CheckBoxKind
(with-layout [(Label label) (Checked checked?) (Enabled enabled)] 0.0 0.0 margin 0.0)
[(Toggle on-toggle)]))
(with-layout [(PropLabel label) (PropChecked checked?) (PropEnabled enabled)] 0.0 0.0 margin 0.0)
[(OnToggle on-toggle)]))
(: (-> Kind (Vec Prop) (Vec (View %m)) (View %m)))
(defun (container kind props children)
@ -314,7 +400,7 @@
(: (-> Kind double double (Array (View %m)) (View %m)))
(defun (stack kind spacing margin children)
(container kind (with-layout (with-size [] Spacing spacing) 0.0 0.0 margin 0.0)
(container kind (with-layout (with-size [] PropSpacing spacing) 0.0 0.0 margin 0.0)
(array->vec children)))
;; Views one above the other.
@ -335,7 +421,7 @@
#:rest (View %m) (View %m)))
(defun (grid #:columns "*" #:rows "*" #:spacing 0.0 #:margin 0.0 #:rest children)
(container GridKind
(with-layout (with-size [(Columns columns) (Rows rows)] Spacing spacing)
(with-layout (with-size [(PropColumns columns) (PropRows rows)] PropSpacing spacing)
0.0 0.0 margin 0.0)
(array->vec children)))
@ -350,12 +436,24 @@
(defun (with props make c) (if (string-empty? c) props (vec-add props (make c))))
(container BorderKind
(with (with (with-size (with-size (with-layout [] 0.0 0.0 margin padding)
BorderWidth thickness)
Radius radius)
BorderColour colour)
Background background)
PropBorderWidth thickness)
PropRadius radius)
PropBorderColour colour)
PropBackground background)
[child]))
;; Views side by side, on as many lines as they need.
(: (-> (#:spacing double) (#:margin double) #:rest (View %m) (View %m)))
(defun (flow #:spacing 0.0 #:margin 0.0 #:rest children)
(stack WrapKind spacing margin children))
;; Views at the edges, put there with `at-top`, `at-bottom`, `at-left` and
;; `at-right`, in order from the outside in. The last child, at no edge,
;; fills the middle.
(: (-> (#:margin double) #:rest (View %m) (View %m)))
(defun (dock #:margin 0.0 #:rest children)
(stack DockKind 0.0 margin children))
;; A view that scrolls when it is taller than the space it has.
(: (-> (View %m) (#:width double) (#:height double) (#:margin double) (View %m)))
(defun (scroll child #:width 0.0 #:height 0.0 #:margin 0.0)