Keyed children, and eight more controls

A child is now known by its key, or by its place among the children with no
key. One whose identity was there before keeps its control, and the
controls are put in the new order by moving only those out of place. So a
list that is sorted, or has an item put in front, keeps its controls.

Text areas, sliders, progress bars, dropdowns, images, grids with cells and
spans, and scroll views. A grid's sizes are .NET properties and not
Avalonia's, so they are set by a setter of their own. A text is centred in
its height, so a label lines up with the control beside it.

🤖 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:13:35 +01:00
parent 4f48282314
commit 95b6707cc1
4 changed files with 680 additions and 91 deletions

View file

@ -36,6 +36,7 @@
(import (std prelude))
(import (std interop))
(import (std mutable vec))
(import (std mutable map))
(import (bjogui view))
;; ---------------------------------------------------------------------------
@ -56,7 +57,20 @@
(Thickness (: Avalonia.Thickness (-> double Thickness)))
(Orientation (: Avalonia.Layout.Orientation))
(HAlignment (: Avalonia.Layout.HorizontalAlignment))
(VAlignment (: Avalonia.Layout.VerticalAlignment))
(Control (: Avalonia.Controls.Control))
(Grid (: Avalonia.Controls.Grid (-> Grid)))
(ColumnDefs (: Avalonia.Controls.ColumnDefinitions))
(RowDefs (: Avalonia.Controls.RowDefinitions))
(ScrollViewer (: Avalonia.Controls.ScrollViewer (-> ScrollViewer)))
(Slider (: Avalonia.Controls.Slider (-> Slider)))
(ProgressBar (: Avalonia.Controls.ProgressBar (-> ProgressBar)))
(RangeBase (: Avalonia.Controls.Primitives.RangeBase))
(ComboBox (: Avalonia.Controls.ComboBox (-> ComboBox)))
(SelectingItemsControl (: Avalonia.Controls.Primitives.SelectingItemsControl))
(Image (: Avalonia.Controls.Image (-> Image)))
(Bitmap (: Avalonia.Media.Imaging.Bitmap (-> string Bitmap)))
(TextWrapping (: Avalonia.Media.TextWrapping))
(Controls (: Avalonia.Controls.Controls))
(Panel (: Avalonia.Controls.Panel))
(StackPanel (: Avalonia.Controls.StackPanel (-> StackPanel)))
@ -100,6 +114,32 @@
(orientation-property (: Avalonia.Controls.StackPanel.OrientationProperty #:get))
(content-alignment-property
(: Avalonia.Controls.ContentControl.HorizontalContentAlignmentProperty #:get))
(horizontal-alignment-property (: Avalonia.Layout.Layoutable.HorizontalAlignmentProperty #:get))
(vertical-alignment-property (: Avalonia.Layout.Layoutable.VerticalAlignmentProperty #:get))
(text-block-wrapping (: Avalonia.Controls.TextBlock.TextWrappingProperty #:get))
(text-box-wrapping (: Avalonia.Controls.TextBox.TextWrappingProperty #:get))
(accepts-return-property (: Avalonia.Controls.TextBox.AcceptsReturnProperty #:get))
(minimum-property (: Avalonia.Controls.Primitives.RangeBase.MinimumProperty #:get))
(maximum-property (: Avalonia.Controls.Primitives.RangeBase.MaximumProperty #:get))
(value-property (: Avalonia.Controls.Primitives.RangeBase.ValueProperty #:get))
(small-change-property (: Avalonia.Controls.Primitives.RangeBase.SmallChangeProperty #:get))
(items-source-property (: Avalonia.Controls.ItemsControl.ItemsSourceProperty #:get))
(selected-index-property
(: Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndexProperty #:get))
(image-source-property (: Avalonia.Controls.Image.SourceProperty #:get))
(grid-row-property (: Avalonia.Controls.Grid.RowProperty #:get))
(grid-column-property (: Avalonia.Controls.Grid.ColumnProperty #:get))
(grid-row-span-property (: Avalonia.Controls.Grid.RowSpanProperty #:get))
(grid-column-span-property (: Avalonia.Controls.Grid.ColumnSpanProperty #:get))
(grid-row-spacing-property (: Avalonia.Controls.Grid.RowSpacingProperty #:get))
(grid-column-spacing-property (: Avalonia.Controls.Grid.ColumnSpacingProperty #:get))
;; A grid's sizes are not Avalonia properties, so they are set as .NET
;; properties are.
(grid-columns! (: Avalonia.Controls.Grid.ColumnDefinitions (-> Grid ColumnDefs void) #:set))
(grid-rows! (: Avalonia.Controls.Grid.RowDefinitions (-> Grid RowDefs void) #:set))
(parse-columns (: Avalonia.Controls.ColumnDefinitions.Parse (-> string ColumnDefs)))
(parse-rows (: Avalonia.Controls.RowDefinitions.Parse (-> string RowDefs)))
(set-value (: Avalonia.AvaloniaObject.SetValue
(-> AvaloniaObject AvaloniaProperty Object Priority Disposable)))
@ -111,6 +151,9 @@
;; dispatcher and comes later, after the patch that set the text is over.
(text-changing-event (: Avalonia.Controls.TextBox.TextChangingEvent #:get))
(checked-event (: Avalonia.Controls.Primitives.ToggleButton.IsCheckedChangedEvent #:get))
(value-changed-event (: Avalonia.Controls.Primitives.RangeBase.ValueChangedEvent #:get))
(selection-changed-event
(: Avalonia.Controls.Primitives.SelectingItemsControl.SelectionChangedEvent #:get))
(add-handler (: Avalonia.Interactivity.Interactive.AddHandler
(-> Interactive RoutedEvent Delegate Routes bool void)))
@ -118,7 +161,11 @@
(text-box-value (: Avalonia.Controls.TextBox.Text (-> TextBox string) #:get))
(is-checked (: Avalonia.Controls.Primitives.ToggleButton.IsChecked
(-> ToggleButton (Nullable bool)) #:get))
(range-value (: Avalonia.Controls.Primitives.RangeBase.Value (-> RangeBase double) #:get))
(selected-index (: Avalonia.Controls.Primitives.SelectingItemsControl.SelectedIndex
(-> SelectingItemsControl int) #:get))
(panel-children (: Avalonia.Controls.Panel.Children (-> Panel Controls) #:get))
(content! (: Avalonia.Controls.ContentControl.Content (-> ContentControl Object void) #:set))
(clr-string (: System.Convert.ToString (-> string ClrString)))
;; The UI thread. Controls may be touched only there, so a message from a
@ -146,6 +193,15 @@
;; Properties
;; ---------------------------------------------------------------------------
;; A kind whose children are the children of an Avalonia Panel.
(: (-> Kind bool))
(defun (panel? kind)
(match kind
(ColumnKind #t)
(RowKind #t)
(GridKind #t)
(_ #f)))
(: (-> Kind bool))
(defun (stack? kind)
(match kind
@ -158,15 +214,33 @@
(: (-> Kind bool))
(defun (templated? kind)
(match kind
(ButtonKind #t)
(TextKind #f)
(ImageKind #f)
(ColumnKind #f)
(RowKind #f)
(GridKind #f)
(_ #t)))
(: (-> Kind bool))
(defun (text-box? kind)
(match kind
(TextInputKind #t)
(CheckBoxKind #t)
(TextAreaKind #t)
(_ #f)))
(: (-> Kind bool))
(defun (range? kind)
(match kind
(SliderKind #t)
(ProgressKind #t)
(_ #f)))
;; The Avalonia property that a prop sets on a control of this kind. None
;; for a prop that the kind does not take.
;; for a prop that the kind does not take, and for the props that `set-prop!`
;; sets in a way of their own.
(: (-> Kind Prop (Option AvaloniaProperty)))
(defun (slot kind p)
(defun (when-kind ok? prop) (if ok? (Some prop) None))
(match p
((Label _)
(match kind
@ -174,9 +248,9 @@
(ButtonKind (Some (cast AvaloniaProperty content-property)))
(CheckBoxKind (Some (cast AvaloniaProperty content-property)))
(_ None)))
((Value _) (if (= kind TextInputKind) (Some (cast AvaloniaProperty text-box-text)) None))
((Placeholder _) (if (= kind TextInputKind) (Some (cast AvaloniaProperty watermark-property)) None))
((Checked _) (if (= kind CheckBoxKind) (Some (cast AvaloniaProperty is-checked-property)) None))
((Value _) (when-kind (text-box? kind) (cast AvaloniaProperty text-box-text)))
((Placeholder _) (when-kind (text-box? kind) (cast AvaloniaProperty watermark-property)))
((Checked _) (when-kind (= kind CheckBoxKind) (cast AvaloniaProperty is-checked-property)))
((Enabled _) (Some (cast AvaloniaProperty is-enabled-property)))
((FontSize _)
(cond ((= kind TextKind) (Some (cast AvaloniaProperty text-block-font-size)))
@ -185,16 +259,39 @@
((Width _) (Some (cast AvaloniaProperty width-property)))
((Height _) (Some (cast AvaloniaProperty height-property)))
((Margin _) (Some (cast AvaloniaProperty margin-property)))
((Padding _) (if (templated? kind) (Some (cast AvaloniaProperty padding-property)) None))
((Spacing _) (if (stack? kind) (Some (cast AvaloniaProperty spacing-property)) None))))
((Padding _) (when-kind (templated? kind) (cast AvaloniaProperty padding-property)))
((Spacing _) (when-kind (stack? kind) (cast AvaloniaProperty spacing-property)))
((Wrap _) (when-kind (= kind TextKind) (cast AvaloniaProperty text-block-wrapping)))
((Minimum _) (when-kind (range? kind) (cast AvaloniaProperty minimum-property)))
((Maximum _) (when-kind (range? kind) (cast AvaloniaProperty maximum-property)))
((Amount _) (when-kind (range? kind) (cast AvaloniaProperty value-property)))
((Step _) (when-kind (= kind SliderKind) (cast AvaloniaProperty small-change-property)))
((Items _) (when-kind (= kind DropdownKind) (cast AvaloniaProperty items-source-property)))
((Selected _) (when-kind (= kind DropdownKind) (cast AvaloniaProperty selected-index-property)))
((Row _) (Some (cast AvaloniaProperty grid-row-property)))
((Column _) (Some (cast AvaloniaProperty grid-column-property)))
((RowSpan _) (Some (cast AvaloniaProperty grid-row-span-property)))
((ColumnSpan _) (Some (cast AvaloniaProperty grid-column-span-property)))
;; Set by `set-prop!` itself.
((Source _) None)
((Columns _) None)
((Rows _) None)))
(: (-> bool TextWrapping))
(defun (wrapping wrap?)
(if wrap? TextWrapping.Wrap TextWrapping.NoWrap))
(: (-> string ClrString))
(defun (->clr-string s) (clr-string s))
;; The value of a prop, as the object `SetValue` takes.
(: (-> Prop Object))
(defun (boxed p)
(defun (text s) (cast Object (->clr-string s)))
(match p
((Label s) (cast Object (clr-string s)))
((Value s) (cast Object (clr-string s)))
((Placeholder s) (cast Object (clr-string s)))
((Label s) (text s))
((Value s) (text s))
((Placeholder s) (text s))
((Checked b) (cast Object b))
((Enabled b) (cast Object b))
((FontSize d) (cast Object d))
@ -202,7 +299,22 @@
((Height d) (cast Object d))
((Margin d) (cast Object (Thickness. d)))
((Padding d) (cast Object (Thickness. d)))
((Spacing d) (cast Object d))))
((Spacing d) (cast Object d))
((Wrap b) (cast Object (wrapping b)))
((Minimum d) (cast Object d))
((Maximum d) (cast Object d))
((Amount d) (cast Object d))
((Step d) (cast Object d))
;; A .NET array of .NET strings, which a ComboBox shows as they are.
((Items xs) (cast Object (: (vec->array (vec-map ->clr-string xs)) (Array ClrString))))
((Selected i) (cast Object i))
((Row i) (cast Object i))
((Column i) (cast Object i))
((RowSpan i) (cast Object i))
((ColumnSpan i) (cast Object i))
((Source s) (text s))
((Columns s) (text s))
((Rows s) (text s))))
;; Which prop it is, without its value. Two props with one tag set the same
;; property.
@ -211,20 +323,53 @@
(match p
((Label _) 0) ((Value _) 1) ((Placeholder _) 2) ((Checked _) 3)
((Enabled _) 4) ((FontSize _) 5) ((Width _) 6) ((Height _) 7)
((Margin _) 8) ((Padding _) 9) ((Spacing _) 10)))
((Margin _) 8) ((Padding _) 9) ((Spacing _) 10) ((Wrap _) 11)
((Minimum _) 12) ((Maximum _) 13) ((Amount _) 14) ((Step _) 15)
((Items _) 16) ((Selected _) 17) ((Source _) 18) ((Columns _) 19)
((Rows _) 20) ((Row _) 21) ((Column _) 22) ((RowSpan _) 23) ((ColumnSpan _) 24)))
(: (-> Control AvaloniaProperty Object void))
(defun (put! control property value)
(ignore (set-value (cast AvaloniaObject control) property value Priority.LocalValue)))
;; The picture in a file, or None when it cannot be read.
(: (-> string (Option Bitmap)))
(defun (load-bitmap path)
(match (try (Bitmap. path) #:catch (System.Exception))
((Ok b) (Some b))
((Err _) None)))
(: (-> Control Kind Prop void))
(defun (set-prop! control kind p)
(match (slot kind p)
((Some property)
(ignore (set-value (cast AvaloniaObject control) property (boxed p) Priority.LocalValue)))
(None unit)))
(match (Tuple kind p)
((Tuple ImageKind (Source path))
(match (load-bitmap path)
((Some b) (put! control (cast AvaloniaProperty image-source-property) (cast Object b)))
(None (clear-value (cast AvaloniaObject control) (cast AvaloniaProperty image-source-property)))))
((Tuple GridKind (Columns s)) (grid-columns! (cast Grid control) (parse-columns s)))
((Tuple GridKind (Rows s)) (grid-rows! (cast Grid control) (parse-rows s)))
((Tuple GridKind (Spacing d))
(put! control (cast AvaloniaProperty grid-row-spacing-property) (cast Object d))
(put! control (cast AvaloniaProperty grid-column-spacing-property) (cast Object d)))
(_
(match (slot kind p)
((Some property) (put! control property (boxed p)))
(None unit)))))
(: (-> Control Kind Prop void))
(defun (clear-prop! control kind p)
(match (slot kind p)
((Some property) (clear-value (cast AvaloniaObject control) property))
(None unit)))
(defun (clear property) (clear-value (cast AvaloniaObject control) property))
(match (Tuple kind p)
((Tuple ImageKind (Source _)) (clear (cast AvaloniaProperty image-source-property)))
((Tuple GridKind (Columns _)) (grid-columns! (cast Grid control) (parse-columns "*")))
((Tuple GridKind (Rows _)) (grid-rows! (cast Grid control) (parse-rows "*")))
((Tuple GridKind (Spacing _))
(clear (cast AvaloniaProperty grid-row-spacing-property))
(clear (cast AvaloniaProperty grid-column-spacing-property)))
(_
(match (slot kind p)
((Some property) (clear property))
(None unit)))))
;; Clears the props that are gone, and sets the ones that are new or have
;; another value. A prop with the same value is not set again.
@ -403,18 +548,21 @@
(spawn/daemon (run-sub app (.next-event sub))))
scope))
(: (-> (Sub %m) string))
(defun (sub-key sub) (.key sub))
(: (-> (App %model %m) void))
(defun (sync-subs! app)
(def wanted ((.subscriptions app) (.model app)))
(defun (wanted? key) (any? (fun (sub) (= (.key sub) key)) wanted))
(defun (wanted? key) (any? (fun (sub) (= (sub-key sub) key)) wanted))
(def/mutable kept '())
(loop (:for (Tuple key scope) (.subs app))
(:do (if (wanted? key)
(set! kept (cons (Tuple key scope) kept))
(stop-scope! scope))))
(loop (:for sub wanted)
(:when (not (any? (fun (t) (= (fst t) (.key sub))) kept)))
(:do (set! kept (cons (Tuple (.key sub) (start-sub! app sub)) kept))))
(:when (not (any? (fun (t) (= (fst t) (sub-key sub))) kept)))
(:do (set! kept (cons (Tuple (sub-key sub) (start-sub! app sub)) kept))))
(record-set! app (subs kept)))
(: (-> (App %model %m) void))
@ -455,41 +603,76 @@
(: (-> (App %model %m) (Live %m) Kind void))
(defun (listen! app live kind)
(def control (.control live))
(defun (on-event (: event RoutedEvent) pick)
(on! control event (fun (sender e) (fire! app live (pick)))))
(match kind
(ButtonKind
(on! control (cast RoutedEvent click-event)
(fun (sender e)
(fire! app live (fun (on) (match on ((Click msg) (Some msg)) (_ None)))))))
(TextInputKind
(on! control (cast RoutedEvent text-changing-event)
(fun (sender e)
(def s (text-box-value (cast TextBox control)))
(fire! app live (fun (on) (match on ((Input f) (Some (f s))) (_ None)))))))
(on-event (cast RoutedEvent click-event)
(fun () (fun (on) (match on ((Click msg) (Some msg)) (_ None))))))
((or TextInputKind TextAreaKind)
(on-event (cast RoutedEvent text-changing-event)
(fun ()
(def s (text-box-value (cast TextBox control)))
(fun (on) (match on ((Input f) (Some (f s))) (_ None))))))
(CheckBoxKind
(on! control (cast RoutedEvent checked-event)
(fun (sender e)
(def b (.GetValueOrDefault (is-checked (cast ToggleButton control))))
(fire! app live (fun (on) (match on ((Toggle f) (Some (f b))) (_ None)))))))
(on-event (cast RoutedEvent checked-event)
(fun ()
(def b (.GetValueOrDefault (is-checked (cast ToggleButton control))))
(fun (on) (match on ((Toggle f) (Some (f b))) (_ None))))))
(SliderKind
(on-event (cast RoutedEvent value-changed-event)
(fun ()
(def d (range-value (cast RangeBase control)))
(fun (on) (match on ((Slide f) (Some (f d))) (_ None))))))
(DropdownKind
(on-event (cast RoutedEvent selection-changed-event)
(fun ()
(def i (selected-index (cast SelectingItemsControl control)))
(fun (on) (match on ((Select f) (Some (f i))) (_ None))))))
(_ unit)))
(: (-> Kind Control))
(defun (make-control kind)
(match kind
(TextKind (cast Control (TextBlock.)))
(TextKind
;; Centred in the height it is given, so a label lines up with the
;; control beside it in a row or a grid.
(def t (cast Control (TextBlock.)))
(put! t (cast AvaloniaProperty vertical-alignment-property) (cast Object VAlignment.Center))
t)
(ButtonKind
;; Centred, because a button given a width shows its label at the left.
(def b (Button.))
(ignore (set-value (cast AvaloniaObject b) (cast AvaloniaProperty content-alignment-property)
(cast Object HAlignment.Center) Priority.LocalValue))
(cast Control b))
(def b (cast Control (Button.)))
(put! b (cast AvaloniaProperty content-alignment-property) (cast Object HAlignment.Center))
b)
(TextInputKind (cast Control (TextBox.)))
(TextAreaKind
(def t (cast Control (TextBox.)))
(put! t (cast AvaloniaProperty accepts-return-property) (cast Object #t))
(put! t (cast AvaloniaProperty text-box-wrapping) (cast Object TextWrapping.Wrap))
t)
(CheckBoxKind (cast Control (CheckBox.)))
(SliderKind (cast Control (Slider.)))
(ProgressKind (cast Control (ProgressBar.)))
(DropdownKind
(def d (cast Control (ComboBox.)))
(put! d (cast AvaloniaProperty horizontal-alignment-property) (cast Object HAlignment.Stretch))
d)
(ImageKind (cast Control (Image.)))
(ColumnKind (cast Control (StackPanel.)))
(RowKind
(def panel (StackPanel.))
(ignore (set-value (cast AvaloniaObject panel) (cast AvaloniaProperty orientation-property)
(cast Object Orientation.Horizontal) Priority.LocalValue))
(cast Control panel))))
(def panel (cast Control (StackPanel.)))
(put! panel (cast AvaloniaProperty orientation-property) (cast Object Orientation.Horizontal))
panel)
(GridKind (cast Control (Grid.)))
(ScrollKind (cast Control (ScrollViewer.)))))
;; Puts a child's control in its parent's, as the last one.
(: (-> Kind Control Control void))
(defun (adopt! kind parent child)
(if (= kind ScrollKind)
(content! (cast ContentControl parent) (cast Object child))
(.Add (panel-children (cast Panel parent)) child)))
;; Makes the controls for a view.
(: (-> (App %model %m) (View %m) (Live %m)))
@ -502,7 +685,7 @@
(loop (:for child (view-children v))
(:do (def c (mount app child))
(mutablevec-add! (.children live) c)
(.Add (panel-children (cast Panel control)) (.control c))))
(adopt! kind control (.control c))))
live)
;; Changes the controls of `live` to show `next`. Gives `live` itself, or new
@ -516,30 +699,80 @@
(begin
(patch-props! (.control live) kind (view-props old) (view-props next))
(record-set! live (view next))
(when (stack? kind)
(patch-children! app live (view-children next)))
(cond ((panel? kind) (patch-children! app live (view-children next)))
((= kind ScrollKind) (patch-content! app live (view-children next)))
(else unit))
live)))
;; Children are matched by their place: the first with the first, and so on.
;; Those past the end of the shorter list are added or removed.
;; A scroll's one child is its content.
(: (-> (App %model %m) (Live %m) (Vec (View %m)) void))
(defun (patch-content! app live views)
(def kids (.children live))
(def was (mutablevec-ref kids 0))
(def now (patch! app was (vec-ref views 0)))
(unless (eq? now was)
(mutablevec-set! kids 0 now)
(content! (cast ContentControl (.control live)) (cast Object (.control now)))))
;; Which child each view is: its key, or its place among the children that
;; have none. The two kinds cannot be mistaken for each other.
(: (-> (Vec (View %m)) (Vec string)))
(defun (identities views)
(def/mutable unkeyed 0)
(loop (:for v views)
(:acc out (vecing (match (view-key v)
((Some k) (string-append "k" k))
(None (set! unkeyed (+ unkeyed 1))
#"i${(- unkeyed 1)}"))))
=> out))
;; Children are matched by identity: a child of the last view whose key, or
;; whose place among those with no key, is a child's of the next one keeps
;; its control, and is changed to the next view. The others are made new,
;; and those left over are removed. Then the controls are put in the order
;; of the next view, moving only those that are out of place.
(: (-> (App %model %m) (Live %m) (Vec (View %m)) void))
(defun (patch-children! app live views)
(def kids (.children live))
(def controls (panel-children (cast Panel (.control live))))
(def n (vec-length views))
(loop (:for i (range 0 (min n (mutablevec-length kids))))
(:do (def was (mutablevec-ref kids i))
(def now (patch! app was (vec-ref views i)))
(unless (eq? now was)
(mutablevec-set! kids i now)
(.set_Item controls i (.control now)))))
(loop (:while (> (mutablevec-length kids) n))
(:do (ignore (mutablevec-pop! kids))
(.RemoveAt controls (mutablevec-length kids))))
(loop (:for i (range (mutablevec-length kids) n))
(:do (def c (mount app (vec-ref views i)))
(mutablevec-add! kids c)
(.Add controls (.control c)))))
(def olds (mutablevec->vec kids))
(def old-ids (identities (vec-map (fun (l) (.view l)) olds)))
(def new-ids (identities views))
;; Where each identity was. A second child with one identity is new.
(def where (mutablemap))
(loop (:for (Tuple i id) (enumerate old-ids))
(:when (not (mutablemap-contains? where id)))
(:do (mutablemap-set! where id i)))
(def kept (: (make-array (vec-length olds) #:fill #f) (Array bool)))
(def nexts (mutablevec))
(loop (:for (Tuple j v) (enumerate views))
(:do (def now
(match (mutablemap-try-ref where (vec-ref new-ids j))
((Some i)
(ignore (mutablemap-remove! where (vec-ref new-ids j)))
(def was (vec-ref olds i))
(def now (patch! app was v))
(when (eq? now was) (array-set! kept i #t))
now)
(None (mount app v))))
(mutablevec-add! nexts now)))
(loop (:for (Tuple i was) (enumerate olds))
(:when (not (array-ref kept i)))
(:do (ignore (.Remove controls (.control was)))))
(loop (:for (Tuple j now) (enumerate (mutablevec->vec nexts)))
(:do (def c (.control now))
(unless (and (< j (.-Count controls)) (eq? (.get_Item controls j) c))
(def at (.IndexOf controls c))
(if (>= at 0)
(.Move controls at j)
(.Insert controls j c)))))
(mutablevec-clear! kids)
(loop (:for now (mutablevec->vec nexts)) (:do (mutablevec-add! kids now))))
;; An app with no window: its view is made now, its subscriptions started,
;; and `on-root` is given the control to show each time that control changes.