gui-update is a plain function, so it cannot wait. It returns the work to do as values. A command is an (Event %m), synced in a fiber of its own, whose answer is the next message. A subscription is a source of messages that the model asks for: after each update the app starts the new ones and cancels the ones that are gone, by key. every sends a message on a timer, and subscription takes a source of one's own. The fibers belong to the app's scope and post their messages to the UI thread, so gui-update and view only run there. Closing the window cancels them. The prelude's path macros already use the name update, hence gui-update. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8) Co-Authored-By: eca-agent <git@eca.dev>
134 lines
5.2 KiB
Org Mode
134 lines
5.2 KiB
Org Mode
#+TITLE: bjogui
|
|
|
|
Desktop programs in Bjolang, written as a model, a ~gui-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)))
|
|
|
|
(: gui-update (-> Msg int (Tuple int (List (Event Msg)))))
|
|
(defun (gui-update msg n)
|
|
(next (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 gui-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.
|
|
~gui-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.
|
|
|
|
* 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.
|
|
|
|
* Running: (bjogui app)
|
|
|
|
- ~(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
|
|
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 tests/effects.bjo # prints "bjogui effects ok"
|
|
bjo run examples/counter.bjo
|
|
bjo run examples/stopwatch.bjo
|
|
#+END_SRC
|
|
|
|
* Not yet
|
|
|
|
- Keyed children, for lists whose items move.
|
|
- More controls: lists, grids, images, scroll views, menus.
|