Tree views, a styles example, and the Readme for all of it

A tree node is a TreeViewItem: a header and items, patched by key as a
list's items are. The chosen node is chosen by its key, once the nodes are
there, and a click sends the key. SelectedItem is a direct property, which
a clear does not reset, so no choice is set as null.

The Readme now covers menus and keys, dialogs, styles and themes, and the
new controls. docs/styles.png is examples/styles.bjo.

🤖 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:10:53 +01:00
parent e3c54c4348
commit 8941425f05
6 changed files with 321 additions and 10 deletions

View file

@ -99,6 +99,8 @@
(WrapPanel (: Avalonia.Controls.WrapPanel (-> WrapPanel)))
(DockPanel (: Avalonia.Controls.DockPanel (-> DockPanel)))
(DockEdge (: Avalonia.Controls.Dock))
(TreeView (: Avalonia.Controls.TreeView (-> TreeView)))
(TreeViewItem (: Avalonia.Controls.TreeViewItem (-> TreeViewItem)))
(PropertyChange (: Avalonia.AvaloniaPropertyChangedEventArgs))
(Classes (: Avalonia.Controls.Classes))
(StyledElement (: Avalonia.StyledElement))
@ -220,6 +222,10 @@
(auto-watermark-property (: Avalonia.Controls.AutoCompleteBox.WatermarkProperty #:get))
(auto-text (: Avalonia.Controls.AutoCompleteBox.Text (-> AutoCompleteBox string) #:get))
(expanded? (: Avalonia.Controls.Expander.IsExpanded (-> Expander bool) #:get))
(tree-header-property (: Avalonia.Controls.Primitives.HeaderedItemsControl.HeaderProperty #:get))
(tree-expanded-property (: Avalonia.Controls.TreeViewItem.IsExpandedProperty #:get))
(tree-selected-property (: Avalonia.Controls.TreeView.SelectedItemProperty #:get))
(tree-selected (: Avalonia.Controls.TreeView.SelectedItem (-> TreeView Object) #:get))
(classes-of (: Avalonia.StyledElement.Classes (-> StyledElement Classes) #:get))
(classes-clear! (: Avalonia.Controls.Classes.Clear (-> Classes void)))
(classes-add! (: Avalonia.Controls.Classes.Add (-> Classes string void)))
@ -378,6 +384,8 @@
(MenuBarKind #t)
(MenuItemKind #t)
(ContextMenuKind #t)
(TreeKind #t)
(TreeNodeKind #t)
(_ #f)))
;; A kind whose children each have a place of their own: the content of a
@ -430,6 +438,7 @@
(ToggleSwitchKind (Some (cast AvaloniaProperty content-property)))
(RadioKind (Some (cast AvaloniaProperty content-property)))
(ExpanderKind (Some (cast AvaloniaProperty header-property)))
(TreeNodeKind (Some (cast AvaloniaProperty tree-header-property)))
(TabKind (Some (cast AvaloniaProperty header-property)))
(MenuItemKind (Some (cast AvaloniaProperty menu-header-property)))
(_ None)))
@ -484,7 +493,12 @@
((PropRadius _) (when-kind (= kind BorderKind) (cast AvaloniaProperty corner-radius-property)))
((PropShortcut _) (when-kind (= kind MenuItemKind) (cast AvaloniaProperty input-gesture-property)))
((PropTip _) (Some (cast AvaloniaProperty tip-property)))
((PropExpanded _) (when-kind (= kind ExpanderKind) (cast AvaloniaProperty expanded-property)))
((PropExpanded _)
(cond ((= kind ExpanderKind) (Some (cast AvaloniaProperty expanded-property)))
((= kind TreeNodeKind) (Some (cast AvaloniaProperty tree-expanded-property)))
(else None)))
;; Set after the children, by `sync-tree-choice!`.
((PropChosenKey _) None)
((PropDockSide _) (Some (cast AvaloniaProperty dock-property)))
((PropClasses _) None)
((PropDate _) (when-kind (= kind DateKind) (cast AvaloniaProperty date-property)))
@ -573,6 +587,7 @@
((PropExpanded b) (cast Object b))
((PropDate s) (text s))
((PropClasses _) (cast Object 0))
((PropChosenKey s) (text s))
((PropDockSide i)
(cast Object (match i (0 DockEdge.Left) (1 DockEdge.Top) (2 DockEdge.Right) (_ DockEdge.Bottom))))))
@ -589,7 +604,7 @@
((PropRows _) 20) ((PropRow _) 21) ((PropColumn _) 22) ((PropRowSpan _) 23) ((PropColumnSpan _) 24)
((PropBackground _) 25) ((PropForeground _) 26) ((PropBorderColour _) 27) ((PropBorderWidth _) 28)
((PropRadius _) 29) ((PropShortcut _) 30) ((PropTip _) 31) ((PropExpanded _) 32) ((PropDate _) 33)
((PropDockSide _) 34) ((PropClasses _) 35)))
((PropDockSide _) 34) ((PropClasses _) 35) ((PropChosenKey _) 36)))
(: (-> Control AvaloniaProperty Object void))
(defun (put! control property value)
@ -1009,6 +1024,13 @@
(fun ()
(def d (range-value (cast RangeBase control)))
(fun (on) (match on ((OnSlide f) (Some (f d))) (_ None))))))
(TreeKind
(on-event (cast RoutedEvent selection-changed-event)
(fun ()
(def key (match (tree-node-key live (tree-selected (cast TreeView control)))
((Some k) k)
(None "")))
(fun (on) (match on ((OnPick f) (Some (f key))) (_ None))))))
((or DropdownKind ListBoxKind TabsKind)
(on-event (cast RoutedEvent selection-changed-event)
(fun ()
@ -1060,6 +1082,8 @@
(SeparatorKind (cast Control (Separator.)))
(ContextKind (cast Control (Border.)))
(ContextMenuKind (cast Control (ContextMenu.)))
(TreeKind (cast Control (TreeView.)))
(TreeNodeKind (cast Control (TreeViewItem.)))
(ToggleSwitchKind (cast Control (ToggleSwitch.)))
(RadioKind (cast Control (RadioButton.)))
(NumberKind (cast Control (NumericUpDown.)))
@ -1119,6 +1143,7 @@
(mutablevec-add! (.children live) c)
(adopt! kind control (- (mutablevec-length (.children live)) 1) (.control c))))
(sync-selection! control kind (view-props v))
(sync-tree-choice! live)
live)
;; Changes the controls of `live` to show `next`. Gives `live` itself, or new
@ -1136,8 +1161,51 @@
((or (panel? kind) (items? kind)) (patch-children! app live kind (view-children next)))
(else unit))
(sync-selection! (.control live) kind (view-props next))
(sync-tree-choice! live)
live)))
;; The node of a tree whose control is `control`, among the live nodes below
;; `live`, and its key.
(: (-> (Live %m) Object (Option string)))
(defun (tree-node-key live control)
(: (-> (Live %m) (Option string)))
(defun (walk l)
(if (and (= (view-kind (.view l)) TreeNodeKind) (eq? (cast Object (.control l)) control))
(view-key (.view l))
(loop (:for c (mutablevec->vec (.children l)))
(:when-let (Some k) (walk c))
(:finish #t (Some k))
=> None)))
(walk live))
;; The control of the node with `key` below `live`.
(: (-> (Live %m) string (Option Control)))
(defun (tree-node-control live key)
(if (and (= (view-kind (.view live)) TreeNodeKind) (= (view-key (.view live)) (Some key)))
(Some (.control live))
(loop (:for c (mutablevec->vec (.children live)))
(:when-let (Some found) (tree-node-control c key))
(:finish #t (Some found))
=> None)))
;; Chooses the node of a tree whose key the view gives, once the nodes are
;; there, if it is not chosen already.
(: (-> (Live %m) void))
(defun (sync-tree-choice! live)
(when (= (view-kind (.view live)) TreeKind)
(def control (.control live))
(loop (:for p (view-props (.view live)))
(:do (match p
((PropChosenKey key)
(match (if (string-empty? key) None (tree-node-control live key))
((Some node)
(unless (eq? (tree-selected (cast TreeView control)) (cast Object node))
(put! control (cast AvaloniaProperty tree-selected-property) (cast Object node))))
;; Null and not a clear: SelectedItem is a direct property,
;; which a clear does not reset.
(None (put! control (cast AvaloniaProperty tree-selected-property) (clr-default)))))
(_ unit))))))
;; The children of a kind whose children each have a place of their own.
(: (-> (App %model %m) (Live %m) Kind (Vec (View %m)) void))
(defun (patch-single! app live kind views)

View file

@ -22,6 +22,7 @@
list-box tabs tab
menu-bar submenu menu-item menu-separator
toggle-switch radio number-input date-picker auto-complete expander
tree-view tree-node
column row grid scroll border flow dock
;; Wrappers.
keyed at paint with-context-menu tooltip at-top at-bottom at-left at-right classed
@ -44,7 +45,7 @@
ListBoxKind TabsKind TabKind
MenuBarKind MenuItemKind SeparatorKind ContextKind ContextMenuKind
ToggleSwitchKind RadioKind NumberKind DateKind AutoCompleteKind
ExpanderKind WrapKind DockKind
ExpanderKind WrapKind DockKind TreeKind TreeNodeKind
ColumnKind RowKind GridKind ScrollKind BorderKind)))
;; A property of a control. A kind takes only some of them; the app ignores
@ -84,14 +85,16 @@
(: 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
(: PropClasses (Vec string))))) ; the classes that styles match
(: PropClasses (Vec string)) ; the classes that styles match
(: PropChosenKey string)))) ; the key of a tree's chosen node
;; An event, and the message it sends.
(type (: (On %m) (Union (: OnClick %m)
(: OnInput (-> string %m))
(: OnToggle (-> bool %m))
(: OnSlide (-> double %m))
(: OnSelect (-> int %m)))))
(: OnSelect (-> int %m))
(: OnPick (-> string %m)))))
;; One control and what is in it.
;;
@ -393,6 +396,28 @@
(children [child])
(key None)))
;; A tree of nodes made by `tree-node`, with the node whose key is `chosen`
;; chosen, or none for "". A click on a node sends `(on-pick key)`.
(: (-> string (-> string %m) (#:width double) (#:height double) (#:margin double)
#:rest (View %m) (View %m)))
(defun (tree-view chosen on-pick #:width 0.0 #:height 0.0 #:margin 0.0 #:rest nodes)
(View (kind TreeKind)
(props (with-layout [(PropChosenKey chosen)] width height margin 0.0))
(events [(OnPick on-pick)])
(children (array->vec nodes))
(key None)))
;; A node of a tree, known by `key`, which must be one of its own in the
;; tree. It opens to show `children`, more nodes, and is open at first when
;; `#:expanded`; after that it is opened and closed by the user.
(: (-> string string (#:expanded bool) #:rest (View %m) (View %m)))
(defun (tree-node key header #:expanded #f #:rest children)
(View (kind TreeNodeKind)
(props [(PropLabel header) (PropExpanded expanded)])
(events [])
(children (array->vec children))
(key (Some key))))
;; 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)))