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

88
Readme.org Normal file
View file

@ -0,0 +1,88 @@
#+TITLE: bjogui
Desktop programs in Bjolang, written as a model, an ~update~ and a ~view~, and
shown with [[https://avaloniaui.net][Avalonia]].
#+BEGIN_SRC scheme
(import (bjogui view))
(import (bjogui app))
(type (: Msg (Union Inc Dec)))
(: update (-> Msg int int))
(defun (update msg n)
(match msg (Inc (+ n 1)) (Dec (- n 1))))
(: view (-> int (View Msg)))
(defun (view n)
(row #:spacing 8.0 #:margin 16.0
(button "-" Dec)
(text #"${n}")
(button "+" Inc)))
(defun (main)
(run-app 0 update view #:title "Counter"))
#+END_SRC
A program never names an Avalonia type. A view is a Bjolang value, an event
is a message, and the controls are made and changed by the package.
* How it works
An event finds its message in the view its control was last given.
~update~ makes the next model from the message, and ~view~ makes the next
view. The next view is compared with the last one, control by control, and
only what differs is changed. So a control keeps its focus, its caret and its
scroll position when the model changes.
Children are matched by their place in the list. A child whose kind changes
gets a new control.
A control that is changed raises events of its own: a text input whose text
is set says that its text changed. Those events come while the controls are
being changed, and they are dropped, so only the user's events are messages.
* The views: (bjogui view)
| Constructor | Control | Sends |
|--------------------------------------------------+--------------+-----------------------------|
| ~(text s)~ | ~TextBlock~ | nothing |
| ~(button label msg)~ | ~Button~ | ~msg~ on a click |
| ~(text-input value on-input)~ | ~TextBox~ | ~(on-input text)~ on a change |
| ~(checkbox label checked? on-toggle)~ | ~CheckBox~ | ~(on-toggle checked)~ |
| ~(column child ...)~, ~(row child ...)~ | ~StackPanel~ | nothing |
They take keywords where they apply: ~#:size~ (the font size of a text),
~#:width~, ~#:height~, ~#:margin~, ~#:padding~, ~#:spacing~ (between the
children of a column or a row), ~#:enabled~ and ~#:placeholder~. A size of
zero, the default, leaves the size to the control.
* Running: (bjogui app)
- ~(run-app init update view #:title #:width #:height)~ opens a window and
runs until it is closed.
- ~(start-app init update view on-root)~ makes the controls with no window,
and gives ~on-root~ the control to show. ~app-dispatch!~ sends a message,
~app-model~ reads the model, and ~app-control-at~ finds a control by its
path of child indices. ~tests/app.bjo~ uses these to drive an app through
its real controls.
* Requirements
- Avalonia 11.3, which ~bjo~ restores from NuGet. On Linux it needs X11 or
XWayland.
- A Bjolang with ~(std interop)~, which makes the delegates that Avalonia's
events take.
* Tests
#+BEGIN_SRC sh
bjo run tests/app.bjo # prints "bjogui ok"
bjo run examples/counter.bjo
#+END_SRC
* Not yet
- Keyed children, for lists whose items move.
- Messages from fibers and timers, for work that is not a click.
- More controls: lists, grids, images, scroll views, menus.