From 3b286dea9acfd1a0010d7b48f6ba91c45f09e3fd7bfed3b219c4960f2b39d3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Sun, 25 Oct 2026 23:01:08 +0100 Subject: [PATCH] The whole Readme again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The commit before took the Readme while it was being written, and lost its second half. This puts it back, with the new entries in "Not yet". 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8) Co-Authored-By: eca-agent --- Readme.org | 321 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 1 deletion(-) diff --git a/Readme.org b/Readme.org index 9653f26..aa68fb9 100644 --- a/Readme.org +++ b/Readme.org @@ -241,4 +241,323 @@ value is. corner, its colours are strings, and its angles are radians, clockwise from the right, as ~sin~ and ~cos~ take them. -- Shapes: \ No newline at end of file +- 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. +- Keyed commands: a command started with the key of one still running, such + as a search for what is typed now, cancels the one before it. Until then, + tag the answers and drop those that are stale, or make the work a + subscription, which is cancelled when its key is gone. +- ~#:on-error~: a message for a command that fails. Now its fiber ends and + nothing is sent. +- A subscription does not wait for its message to be handled before it + syncs again. A source of many messages a second should send them in + batches, which is better for the window in any case.