The Readme on keyed commands, failed commands and many messages

🤖 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 23:00:47 +01:00
parent b13ff4369a
commit 3b06fc0488

View file

@ -241,314 +241,4 @@ value is.
corner, its colours are strings, and its angles are radians, clockwise from
the right, as ~sin~ and ~cos~ take them.
- Shapes: ~fill-rect~, ~stroke-rect~ (~#:radius~), ~fill-circle~,
~stroke-circle~, ~fill-ellipse~, ~stroke-ellipse~, ~stroke-line~,
~stroke-polyline~, ~fill-polygon~, ~stroke-arc~, and ~fill-path~ and
~stroke-path~ with SVG path data. Lines take ~#:width~, ~#:cap~ and
~#:dash~.
- Text and pictures: ~draw-text~ (~#:size~, ~#:bold~, ~#:font~, ~#:align~,
~#:max-width~), ~text-size~ to lay text out, and ~draw-image~.
- ~translated~, ~scaled~, ~rotated~, ~clipped~ and ~faded~ change what a
function given to them draws.
The drawing is vectors, so it is sharp at any scale.
[[file:examples/mixer.bjo][examples/mixer.bjo]] has knobs and a timeline with
a playhead, marks and a hover line. ~start-headless!~ sets Avalonia up with no
screen, so that a test can draw components and read their pixels; see
~tests/components.bjo~.
* Controls bjogui does not wrap: (bjogui native)
Any control of Avalonia's, or of another package's, can be put in a view.
The program makes the control and hooks its events itself, so this is the
one place where it names Avalonia's types.
#+BEGIN_SRC scheme
(import (std interop))
(import (bjogui native))
(import/class (Control (: Avalonia.Controls.Control))
(Calendar (: Avalonia.Controls.Calendar (-> Calendar)))
(DateTime (: System.DateTime))
((Nullable %a) (: System.Nullable)))
(import/extern
(selected-date (: Avalonia.Controls.Calendar.SelectedDate (-> Calendar (Nullable DateTime)) #:get))
(selected-date! (: Avalonia.Controls.Calendar.SelectedDate (-> Calendar (Nullable DateTime) void) #:set))
(parse-date (: System.DateTime.Parse (-> string DateTime))))
;; A calendar, for a date as "2026-10-25".
(: calendar (Native string string))
(def calendar
(native (fun (send) ; makes one control
(def c (Calendar.))
(ignore (add-event-handler! c "SelectedDatesChanged"
(: (fun (s e)
(def d (selected-date c))
(when (.-HasValue d) (send (.ToString (.-Value d) "yyyy-MM-dd"))))
(-> Object Object void))))
(cast Control c))
(fun (control date) ; puts a value on it
(selected-date! (cast Calendar control) (parse-date date)))))
;; In a view:
(hosted calendar (.date model) Picked)
#+END_SRC
- ~(make send)~ makes the control once for its place in the view. ~send~
gives an output to the app, as a message that ~hosted~ makes with
~on-output~. It may be called from any thread.
- ~(put-value control value)~ is called after ~make~, and again each time the
view gives another value. What the control sends while it is given a value
is dropped, because it is not the user's.
- ~#:width~, ~#:height~ and ~#:margin~ are set on the control, as on any
view. A control measures itself, as Avalonia's do.
- Make a ~native~ once, at the top of a module. A view in the place of
another one is a new control.
* Styles and themes
[[file:docs/styles.png]]
~run-app~'s ~#:theme~ gives ~"light"~, ~"dark"~, or anything else for the
system's, for a model, so a setting can switch it. ~#:styles~ is a list of
rules made by ~style~: a kind, by the name of its constructor, a class
given with ~classed~, and the look.
#+BEGIN_SRC scheme
(run-app init gui-update view
#:theme (fun (m) (if (.dark m) "dark" "light"))
#:styles (list (style "text" #:class "title" #:font-size 22.0 #:colour "#f1c40f")
(style "button" #:class "primary" #:background "#27ae60" #:colour "white"
#:hover-background "#2ecc71" #:pressed-background "#1e8449")))
#+END_SRC
A rule takes ~#:colour~, ~#:background~, ~#:font-size~, ~#:padding~ and
~#:radius~. A button's rule also takes ~#:hover-colour~, ~#:hover-background~,
~#:pressed-colour~ and ~#:pressed-background~, which are set inside its
template, as the Fluent theme sets its own; no other kind has them.
[[file:examples/styles.bjo][examples/styles.bjo]] is the picture above.
* Menus and keys
#+BEGIN_SRC scheme
(menu-bar
(submenu "File"
(menu-item "New" New #:shortcut "Ctrl+N")
(menu-separator)
(menu-item "Quit" Quit #:shortcut "Ctrl+Q")))
#+END_SRC
A menu bar is a view, so it goes where the program puts it, usually at the
top of a ~dock~. A menu item's ~#:shortcut~ is shown beside it, and the keys
send its message while it is in the view and enabled. ~run-app~'s ~#:keys~
gives more, for a model: ~(fun (m) (list (key "F5" Refresh)))~. Keys are
the window's after the control that has the keyboard had its chance, so a
text input keeps ~Ctrl+C~.
* 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.
** Commands that act on the controls and the clipboard
Some commands do work of their own and send no message. Each finds its
control by the key of its view, given with ~keyed~, and runs once the
controls of the update are laid out, so a view the same update made is
there.
| Command | Does |
|-------------------------------+---------------------------------------------------------|
| ~(focus key)~ | gives the keyboard to the control |
| ~(scroll-to-end key)~ | scrolls a ~scroll~ view to its end, as a log wants |
| ~(scroll-to-start key)~ | scrolls a ~scroll~ view to its start |
| ~(scroll-into-view key)~ | scrolls the views around the control until it is seen |
| ~(copy-text text)~ | puts ~text~ on the clipboard |
| ~(paste-text on-text)~ | sends ~(on-text (Some text))~, or ~(on-text None)~ |
#+BEGIN_SRC scheme
(Add (next (record-set m (items (vec-add (.items m) (.draft m))) (draft ""))
(focus "draft")
(scroll-to-end "items")))
(keyed "draft" (text-input (.draft m) Draft))
#+END_SRC
* Windows and dialogs of your own
[[file:docs/contacts.png]] [[file:docs/contacts-edit.png]]
A program can have windows besides the main one. ~run-app~'s ~#:windows~
gives the windows a model has, as ~view~ gives the main window's view, and
the app opens, changes and closes them to match.
#+BEGIN_SRC scheme
(: windows (-> Model (List (WindowView Msg))))
(defun (windows m)
(match (.draft m)
((Some d) (list (window "edit" "Edit contact" Cancel (draft-view d) #:modal #t)))
(None '())))
(run-app init gui-update view #:windows windows)
#+END_SRC
- ~(window key title on-close content #:width #:height #:modal)~ is a window,
known by its key. It is opened when it is first in the list, its view and
title are changed as the main window's view is while it stays, and it is
closed when it is gone.
- A click on its close button does not close it: it sends ~on-close~, and the
window closes when the next model has it no more.
- With no ~#:width~ or ~#:height~ it is as big as its content.
- A ~#:modal~ window is a dialog: the main window takes no input until it is
closed. So a dialog of one's own is a window whose state, such as a draft
being edited, is in the model, and whose view is a view like any other.
- ~window-map~ maps a window's messages, as ~view-map~ does a view's.
- The program ends when the main window is closed, and the others with it.
[[file:examples/contacts.bjo][examples/contacts.bjo]] edits a contact in a
modal dialog, and shows the chosen one in a second window.
* Dialogs: (bjogui dialogs)
A dialog is a command, and its answer comes back as a message.
| Command | Sends |
|-------------------------------------------+-----------------------------------------------|
| ~(alert text msg)~ | ~msg~ when it is closed |
| ~(confirm text on-answer)~ | ~(on-answer #t)~ for OK, ~#f~ otherwise |
| ~(prompt text on-answer #:value)~ | ~(on-answer (Some text))~, or ~None~ |
| ~(open-file on-answer #:filters)~ | ~(on-answer (Some path))~, or ~None~ |
| ~(open-files on-answer)~ | ~(on-answer paths)~ |
| ~(save-file on-answer #:name)~ | ~(on-answer (Some path))~, or ~None~ |
| ~(open-folder on-answer)~ | ~(on-answer (Some path))~, or ~None~ |
| ~(quit)~ | nothing: it closes the windows |
#+BEGIN_SRC scheme
(Delete (next m (confirm "Delete it?" Deleted?)))
((Deleted? #t) (next (remove m)))
((Deleted? #f) (next m))
#+END_SRC
~#:filters~ are kinds of file, each a name and patterns: ~(list (Tuple "Text"
'("*.txt" "*.md")))~. ~run-app~'s ~#:on-close msg~ makes the close button
send ~msg~ instead of closing, and the program closes with ~(quit)~ when it
is ready, say after asking about unsaved work.
A dialog belongs to the main window. With none, as under ~start-app~, each
answers as if it was cancelled. Avalonia's dialogs cannot be cancelled, so a
command that is cancelled stops waiting, and the dialog stays until it is
answered.
* Running: (bjogui app)
- ~(run-app init gui-update view #:subscriptions #:keys #:theme #:styles
#:windows #:on-close #:title #:width #:height)~ opens a window and runs
until it is closed.
- ~#:screenshot path~ makes ~run-app~ save a PNG of the window once it is
shown, at twice its size, and close it. Each other window that is open is
saved beside it, with its key after the name: ~shot-edit.png~. It is the
environment variable ~BJOGUI_SCREENSHOT~ unless the program says
otherwise, so any program can be pictured with no change:
~BJOGUI_SCREENSHOT=docs/todo.png bjo run examples/todo.bjo~ made the
picture above.
- ~(start-app init gui-update view on-root #:subscriptions #:windows)~ 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-press!~ presses keys, such as ~"Ctrl+S"~, as the window does.
- ~app-input!~ gives an ~Input~ to the component at a path, as its control
does.
- ~app-model~ reads the model, and ~app-control-at~ finds a control by its
path of child indices.
- ~app-windows~ gives the keys of the other windows that are open, and
~app-window-control~ finds a control in one of them.
~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 tests/controls.bjo # prints "bjogui controls ok"
bjo run tests/lists.bjo # prints "bjogui lists ok"
bjo run tests/inputs.bjo # prints "bjogui inputs ok"
bjo run tests/menus.bjo # prints "bjogui menus ok"
bjo run tests/dialogs.bjo # prints "bjogui dialogs ok"
bjo run tests/tree.bjo # prints "bjogui tree ok"
bjo run tests/canvas.bjo # prints "bjogui canvas ok"
bjo run tests/components.bjo # prints "bjogui components ok"
bjo run tests/native.bjo # prints "bjogui native ok"
bjo run tests/commands.bjo # prints "bjogui commands ok"
bjo run tests/windows.bjo # prints "bjogui windows ok"
bjo run examples/counter.bjo
bjo run examples/stopwatch.bjo
bjo run examples/todo.bjo
bjo run examples/styles.bjo
bjo run examples/sketch.bjo
bjo run examples/mixer.bjo
bjo run examples/contacts.bjo
#+END_SRC
* Not yet
- A data grid, and lists that make controls only for the items on screen.
- The macOS menu bar.
- Pointer-over and pressed colours for kinds other than buttons.
- A component asks for no size of its own: it is given one, or fills the
space it gets. Measuring itself, an accessibility tree, and text input
inside a component would need a subclass.
- A moved control is taken out of the panel and put back, so it may lose the
keyboard focus.
- A window's size and place are not kept from one run to the next.
- The keys of a menu item are looked for in the views of all the windows,
and not only in the window that has the keyboard.
- A component is drawn again whole when its value, its state or its size
changes. A few thousand shapes are fast; a very large drawing that changes
at 60 frames a second is not.
- Drag and drop, a tray icon, notifications and printing.
- The tests click a window's close button through the callback that the
platform calls, and not with a real pointer.
- Shapes: