The whole Readme again
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 <git@eca.dev>
This commit is contained in:
parent
3b06fc0488
commit
3b286dea9a
1 changed files with 320 additions and 1 deletions
321
Readme.org
321
Readme.org
|
|
@ -241,4 +241,323 @@ value is.
|
||||||
corner, its colours are strings, and its angles are radians, clockwise from
|
corner, its colours are strings, and its angles are radians, clockwise from
|
||||||
the right, as ~sin~ and ~cos~ take them.
|
the right, as ~sin~ and ~cos~ take them.
|
||||||
|
|
||||||
- Shapes:
|
- 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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue