List boxes, tabs, colours and borders

A list box's items and a tab control's tabs are views, patched by key as a
column's children are: Panel.Children and ItemsControl.Items are both an
IList, so one patch serves both. A list can choose an item only once its
items are there, so the choice is set after the children.

A routed event bubbles, so each handler now takes only its own control's
events. Before, a dropdown in a list item would have been heard as the
list's choice too.

paint gives any view a text and a background colour, and border a frame. A
colour that cannot be read leaves the theme'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 17:21:40 +01:00
parent 95b6707cc1
commit e773bc424e
4 changed files with 432 additions and 36 deletions

View file

@ -19,9 +19,10 @@
;; Constructors.
text button text-input text-area checkbox slider progress-bar dropdown image
column row grid scroll
list-box tabs tab
column row grid scroll border
;; Wrappers.
keyed at
keyed at paint
;; Reading one.
view-kind view-props view-events view-children view-key)
@ -35,7 +36,8 @@
;; What control a view is.
(type (: Kind (Union TextKind ButtonKind TextInputKind TextAreaKind CheckBoxKind
SliderKind ProgressKind DropdownKind ImageKind
ColumnKind RowKind GridKind ScrollKind)))
ListBoxKind TabsKind TabKind
ColumnKind RowKind GridKind ScrollKind BorderKind)))
;; A property of a control. A kind takes only some of them; the app ignores
;; the others.
@ -56,14 +58,19 @@
(: 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 of them is chosen; -1 for none
(: 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))))
(: 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
;; An event, and the message it sends.
(type (: (On %m) (Union (: Click %m)
@ -109,6 +116,14 @@
(defun (keyed key v)
(record-set v (key (Some key))))
;; The view, in colours. A colour is a name, such as "red", or "#rgb",
;; "#rrggbb" or "#aarrggbb". The empty string, the default, leaves the colour
;; to the theme, and so does a colour that cannot be read.
(: 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))))
;; 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)
@ -219,6 +234,33 @@
(defun (image path #:width 0.0 #:height 0.0 #:margin 0.0)
(leaf ImageKind (with-layout [(Source 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
;; the list can change order.
(: (-> (Vec (View %m)) int (-> int %m) (#:width double) (#:height double) (#:margin double)
(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)])
(children items)
(key None)))
;; Tabs, with the one at `selected` shown. A click on another tab sends
;; `(on-select index)`. Each tab is made by `tab`.
(: (-> 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)])
(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)))
;; 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)))
@ -258,6 +300,23 @@
0.0 0.0 margin 0.0)
(array->vec children)))
;; A view in a frame: a line of `#:colour` and `#:thickness` around it,
;; corners as round as `#:radius`, `#:background` behind it, and `#:padding`
;; between the line and the view.
(: (-> (View %m) (#:colour string) (#:thickness double) (#:radius double)
(#:background string) (#:padding double) (#:margin double)
(View %m)))
(defun (border child #:colour "" #:thickness 1.0 #:radius 0.0 #:background ""
#:padding 0.0 #:margin 0.0)
(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)
[child]))
;; 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)