Views, events as messages, and a patch, over plain Avalonia

A program is a model, an update and a view. A view is a Bjolang value; an
event finds its message in the view its control was last given, and the next
view is compared with the last so only what differs is changed. Controls are
made with plain constructors and changed with SetValue, so nothing is
subclassed, and a program never names an Avalonia type.

Text, button, text input, checkbox, column and row. tests/app.bjo drives an
app through its real controls with no window.

🤖 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 16:18:09 +01:00
commit 2f4c99ba62
9 changed files with 1415 additions and 0 deletions

144
src/view.bjo Normal file
View file

@ -0,0 +1,144 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; bjogui/src/view.bjo — the module (bjogui view).
;;
;; A view is a value: what the window shows for one model. `view` functions
;; build it with the constructors here, and (bjogui app) makes the controls
;; for it and changes them when the next view differs. Nothing here knows
;; about Avalonia, so a view can be built and compared in a test with no
;; window.
;;
;; An event is a message, or a function that makes one from what the control
;; holds. The app gives the message to `update`.
(export
;; Types.
View Kind Prop On
;; Constructors.
text button text-input checkbox column row
;; Reading one.
view-kind view-props view-events view-children)
(import (std prelude))
;; ---------------------------------------------------------------------------
;; The types
;; ---------------------------------------------------------------------------
;; What control a view is.
(type (: Kind (Union TextKind ButtonKind TextInputKind CheckBoxKind ColumnKind RowKind)))
;; 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))))
;; An event, and the message it sends.
(type (: (On %m) (Union (: Click %m)
(: Input (-> string %m))
(: Toggle (-> bool %m)))))
;; One control and what is in it.
(type (: (View %m) (Record (: kind Kind)
(: props (Vec Prop))
(: events (Vec (On %m)))
(: children (Vec (View %m))))))
(: view-kind (-> (View %m) Kind))
(defun (view-kind v) (.kind v))
(: view-props (-> (View %m) (Vec Prop)))
(defun (view-props v) (.props v))
(: view-events (-> (View %m) (Vec (On %m))))
(defun (view-events v) (.events v))
(: view-children (-> (View %m) (Vec (View %m))))
(defun (view-children v) (.children v))
;; ---------------------------------------------------------------------------
;; Constructors
;; ---------------------------------------------------------------------------
;; A size of zero or less means: the size the control asks for. The
;; constructors leave such a size out of the props, so the control keeps its
;; own.
(: (-> (Vec Prop) (-> double Prop) double (Vec Prop)))
(defun (with-size props make size)
(if (> size 0.0) (vec-add props (make size)) props))
(: (-> (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))
(: (-> Kind (Vec Prop) (Vec (On %m)) (View %m)))
(defun (leaf kind props events)
(View (kind kind) (props props) (events events) (children [])))
;; A text.
(: (-> string (#:size double) (#:width double) (#:height double) (#:margin double)
(View %m)))
(defun (text s #:size 0.0 #:width 0.0 #:height 0.0 #:margin 0.0)
(leaf TextKind
(with-layout (with-size [(Label s)] FontSize size) width height margin 0.0)
[]))
;; A button that sends `msg` when it is clicked.
(: (-> string %m (#:enabled bool) (#:width double) (#:height double)
(#:margin double) (#:padding double)
(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)]))
;; A one-line text input that holds `value`. Each change sends `(on-input
;; text)`, with the whole text.
(: (-> string (-> string %m) (#:placeholder string) (#:enabled bool)
(#:width double) (#:margin double)
(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)]
width 0.0 margin 0.0)
[(Input on-input)]))
;; 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)]))
(: (-> Kind double double (Array (View %m)) (View %m)))
(defun (stack kind spacing margin children)
(View (kind kind)
(props (with-layout (with-size [] Spacing spacing) 0.0 0.0 margin 0.0))
(events [])
(children (array->vec children))))
;; Views one above the other.
(: (-> (#:spacing double) (#:margin double) #:rest (View %m) (View %m)))
(defun (column #:spacing 0.0 #:margin 0.0 #:rest children)
(stack ColumnKind spacing margin children))
;; Views side by side.
(: (-> (#:spacing double) (#:margin double) #:rest (View %m) (View %m)))
(defun (row #:spacing 0.0 #:margin 0.0 #:rest children)
(stack RowKind spacing margin children))