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> |
||
|---|---|---|
| examples | ||
| src | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| manifest.bjodat | ||
| packages.lock.json | ||
| Readme.org | ||
bjogui
- How it works
- The views: (bjogui view)
- Work: commands and subscriptions
- Running: (bjogui app)
- Requirements
- Tests
- Not yet
Desktop programs in Bjolang, written as a model, a gui-update and a view,
and shown with Avalonia.
(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"))
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.
(Load (next (record-set m (loading #t))
(wrap (blocking (fun () (file-read-text path))) Loaded)))
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)sendsmsgeach timedurationhas passed.(subscription key next-event)is a source of your own:next-eventgives 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.
(: subscriptions (-> Model (List (Sub Msg))))
(defun (subscriptions m)
(if (.running m) (list (every (milliseconds 100) Tick)) '()))
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 giveson-rootthe control to show. Its fibers belong to the scope it is started in.app-dispatch!gives a message on the UI thread, andapp-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-modelreads the model, andapp-control-atfinds a control by its path of child indices.
tests/app.bjoandtests/effects.bjouse these to drive an app through its real controls.
Requirements
- Avalonia 11.3, which
bjorestores from NuGet. On Linux it needs X11 or XWayland. - A Bjolang with
(std interop), which makes the delegates that Avalonia's events take.
Tests
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
Not yet
- Keyed children, for lists whose items move.
- More controls: lists, grids, images, scroll views, menus.