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>
2026-10-25 16:18:09 +01:00
|
|
|
#+TITLE: bjogui
|
|
|
|
|
|
2026-10-25 17:02:02 +01:00
|
|
|
Desktop programs in Bjolang, written as a model, a ~gui-update~ and a ~view~,
|
|
|
|
|
and shown with [[https://avaloniaui.net][Avalonia]].
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
|
|
|
|
|
#+BEGIN_SRC scheme
|
|
|
|
|
(import (bjogui view))
|
|
|
|
|
(import (bjogui app))
|
|
|
|
|
|
|
|
|
|
(type (: Msg (Union Inc Dec)))
|
|
|
|
|
|
2026-10-25 17:02:02 +01:00
|
|
|
(: gui-update (-> Msg int (Tuple int (List (Event Msg)))))
|
|
|
|
|
(defun (gui-update msg n)
|
|
|
|
|
(next (match msg (Inc (+ n 1)) (Dec (- n 1)))))
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
|
|
|
|
|
(: view (-> int (View Msg)))
|
|
|
|
|
(defun (view n)
|
|
|
|
|
(row #:spacing 8.0 #:margin 16.0
|
|
|
|
|
(button "-" Dec)
|
|
|
|
|
(text #"${n}")
|
|
|
|
|
(button "+" Inc)))
|
|
|
|
|
|
|
|
|
|
(defun (main)
|
2026-10-25 17:02:02 +01:00
|
|
|
(run-app 0 gui-update view #:title "Counter"))
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
#+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.
|
2026-10-25 17:02:02 +01:00
|
|
|
~gui-update~ makes the next model from the message, and ~view~ makes the next
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
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.
|
|
|
|
|
|
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>
2026-10-25 17:13:35 +01:00
|
|
|
Children are matched by key, and those with no key by their place among
|
|
|
|
|
the others with none. Give each item of a list that can change order a key
|
|
|
|
|
with ~(keyed id view)~: a child that moves then keeps its control, and the
|
|
|
|
|
control is moved rather than made again. A child whose kind changes gets a
|
|
|
|
|
new control.
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
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>
2026-10-25 17:13:35 +01:00
|
|
|
| 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 |
|
|
|
|
|
| ~(text-area value on-input)~ | ~TextBox~, several lines | ~(on-input text)~ |
|
|
|
|
|
| ~(checkbox label checked? on-toggle)~ | ~CheckBox~ | ~(on-toggle checked)~ |
|
|
|
|
|
| ~(slider value minimum maximum on-change)~ | ~Slider~ | ~(on-change value)~ |
|
|
|
|
|
| ~(progress-bar value)~ | ~ProgressBar~ | nothing |
|
|
|
|
|
| ~(dropdown options selected on-select)~ | ~ComboBox~ | ~(on-select index)~ |
|
|
|
|
|
| ~(image path)~ | ~Image~ | nothing |
|
|
|
|
|
| ~(column child ...)~, ~(row child ...)~ | ~StackPanel~ | nothing |
|
|
|
|
|
| ~(grid #:columns #:rows child ...)~ | ~Grid~ | nothing |
|
|
|
|
|
| ~(scroll child)~ | ~ScrollViewer~ | nothing |
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
|
|
|
|
|
They take keywords where they apply: ~#:size~ (the font size of a text),
|
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>
2026-10-25 17:13:35 +01:00
|
|
|
~#:wrap~ (a text breaks its lines at its width), ~#:width~, ~#:height~,
|
|
|
|
|
~#:margin~, ~#:padding~, ~#:spacing~ (between the children of a column, a row
|
|
|
|
|
or a grid), ~#:enabled~, ~#:placeholder~, ~#:step~ (a slider's arrow key) and
|
|
|
|
|
~#:maximum~ (a progress bar's full, 1 unless told otherwise). A size of zero,
|
|
|
|
|
the default, leaves the size to the control.
|
|
|
|
|
|
|
|
|
|
A grid's ~#:columns~ and ~#:rows~ are sizes as Avalonia writes them: ~"Auto"~
|
|
|
|
|
fits the content, ~"*"~ shares what is left, ~"2*"~ takes two shares, and a
|
|
|
|
|
number is pixels. ~(at row column view)~ puts a view in a cell, counting from
|
|
|
|
|
0, with ~#:row-span~ and ~#:column-span~.
|
|
|
|
|
|
|
|
|
|
#+BEGIN_SRC scheme
|
|
|
|
|
(grid #:columns "Auto,*" #:rows "Auto,Auto" #:spacing 6.0
|
|
|
|
|
(at 0 0 (text "Volume")) (at 0 1 (slider (.volume m) 0.0 10.0 Volume))
|
|
|
|
|
(at 1 0 (text "Colour")) (at 1 1 (dropdown ["red" "green" "blue"] (.colour m) Colour)))
|
|
|
|
|
|
|
|
|
|
(scroll (apply column (vec->array (vec-map (fun (t) (keyed (.id t) (text (.title t))))
|
|
|
|
|
(.todos m))))
|
|
|
|
|
#:height 200.0)
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
An image that cannot be read shows nothing.
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
|
2026-10-25 17:02:02 +01:00
|
|
|
* Work: commands and subscriptions
|
|
|
|
|
|
|
|
|
|
~gui-update~ is a plain function, so it cannot wait, and the compiler says so
|
|
|
|
|
if it tries. Work is a value it returns, and the app runs it.
|
|
|
|
|
|
|
|
|
|
A *command* is an ~(Event %m)~: an event whose answer is a message. ~(next m
|
|
|
|
|
command ...)~ is the next model and the commands to start; ~(next m)~ is the
|
|
|
|
|
model alone. The app syncs each command in a fiber of its own and gives its
|
|
|
|
|
message to ~gui-update~. Any event will do: a ~timeout~, a ~blocking~ call, a
|
|
|
|
|
channel, a ~choose~ of several.
|
|
|
|
|
|
|
|
|
|
#+BEGIN_SRC scheme
|
|
|
|
|
(Load (next (record-set m (loading #t))
|
|
|
|
|
(wrap (blocking (fun () (file-read-text path))) Loaded)))
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
A *subscription* is a source of messages that runs while the model asks for
|
|
|
|
|
it. ~subscriptions~ gives the list for a model. After each update the app
|
|
|
|
|
starts those that are new and cancels those that are gone; the others keep
|
|
|
|
|
running. A subscription is known by its key.
|
|
|
|
|
|
|
|
|
|
- ~(every duration msg)~ sends ~msg~ each time ~duration~ has passed.
|
|
|
|
|
- ~(subscription key next-event)~ is a source of your own: ~next-event~ gives
|
|
|
|
|
the event of the next message, and is called again after each one.
|
|
|
|
|
~(subscription "jobs" (fun () (wrap (chan-recv jobs) Got)))~ sends what
|
|
|
|
|
comes on a channel.
|
|
|
|
|
|
|
|
|
|
#+BEGIN_SRC scheme
|
|
|
|
|
(: subscriptions (-> Model (List (Sub Msg))))
|
|
|
|
|
(defun (subscriptions m)
|
|
|
|
|
(if (.running m) (list (every (milliseconds 100) Tick)) '()))
|
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
The fibers belong to the app. They post their messages to the UI thread, so
|
|
|
|
|
~gui-update~ and ~view~ always run there. When the window is closed they are
|
|
|
|
|
cancelled, and the program ends.
|
|
|
|
|
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
* Running: (bjogui app)
|
|
|
|
|
|
2026-10-25 17:02:02 +01:00
|
|
|
- ~(run-app init gui-update view #:subscriptions #:title #:width #:height)~
|
|
|
|
|
opens a window and runs until it is closed.
|
|
|
|
|
- ~(start-app init gui-update view on-root #:subscriptions)~ makes the controls
|
|
|
|
|
with no window, and gives ~on-root~ the control to show. Its fibers belong
|
|
|
|
|
to the scope it is started in.
|
|
|
|
|
- ~app-dispatch!~ gives a message on the UI thread, and ~app-send!~ from any
|
|
|
|
|
thread.
|
|
|
|
|
- ~app-run-pending!~ runs what fibers have posted, where no loop of
|
|
|
|
|
Avalonia's does.
|
|
|
|
|
- ~app-stop!~ cancels the subscriptions and drops later messages.
|
|
|
|
|
- ~app-model~ reads the model, and ~app-control-at~ finds a control by its
|
|
|
|
|
path of child indices.
|
|
|
|
|
|
|
|
|
|
~tests/app.bjo~ and ~tests/effects.bjo~ use these to drive an app through
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
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
|
2026-10-25 17:02:02 +01:00
|
|
|
bjo run tests/app.bjo # prints "bjogui ok"
|
|
|
|
|
bjo run tests/effects.bjo # prints "bjogui effects ok"
|
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>
2026-10-25 17:13:35 +01:00
|
|
|
bjo run tests/controls.bjo # prints "bjogui controls ok"
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
bjo run examples/counter.bjo
|
2026-10-25 17:02:02 +01:00
|
|
|
bjo run examples/stopwatch.bjo
|
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>
2026-10-25 16:18:09 +01:00
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
|
|
* Not yet
|
|
|
|
|
|
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>
2026-10-25 17:13:35 +01:00
|
|
|
- A list box with a selection, tabs, menus and dialogs.
|
|
|
|
|
- Colours, borders and styles of one's own.
|
|
|
|
|
- A moved control is taken out of the panel and put back, so it may lose the
|
|
|
|
|
keyboard focus.
|