run-app's #:windows gives the windows a model has. The app opens a window when it is first in the list, patches its view and title while it stays, and closes it when it is gone. A click on its close button sends its on-close message, and the model closes it. A #:modal window is a dialog over the main window, so a dialog of the program's own is a window whose state is in the model. Windows wait for the main window to be shown, because a dialog cannot belong to a hidden one, and the program ends when the main window closes. Key shortcuts and commands such as focus reach the other windows too, and #:screenshot saves a picture of each one beside the main window's. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8) Co-Authored-By: eca-agent <git@eca.dev>
545 lines
26 KiB
Org Mode
545 lines
26 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]].
|
|
|
|
[[file:docs/todo.png]]
|
|
|
|
The todo list above is [[file:examples/todo.bjo][examples/todo.bjo]].
|
|
|
|
#+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 key, and those with no key by their place among
|
|
the others with none. Give each item of a list that can change order a key
|
|
with ~(keyed id view)~: a child that moves then keeps its control, and the
|
|
control is moved rather than made again. 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 |
|
|
| ~(text-area value on-input)~ | ~TextBox~, several lines | ~(on-input text)~ |
|
|
| ~(checkbox label checked? on-toggle)~ | ~CheckBox~ | ~(on-toggle checked)~ |
|
|
| ~(slider value minimum maximum on-change)~ | ~Slider~ | ~(on-change value)~ |
|
|
| ~(progress-bar value)~ | ~ProgressBar~ | nothing |
|
|
| ~(dropdown options selected on-select)~ | ~ComboBox~ | ~(on-select index)~ |
|
|
| ~(image path)~ | ~Image~ | nothing |
|
|
| ~(image-data bytes)~, the bytes of a picture file | ~Image~ | nothing |
|
|
| ~(list-box views selected on-select)~ | ~ListBox~ | ~(on-select index)~ |
|
|
| ~(tabs selected on-select (tab header view) ...)~ | ~TabControl~ | ~(on-select index)~ |
|
|
| ~(column child ...)~, ~(row child ...)~ | ~StackPanel~ | nothing |
|
|
| ~(grid #:columns #:rows child ...)~ | ~Grid~ | nothing |
|
|
| ~(scroll child)~ | ~ScrollViewer~ | nothing |
|
|
| ~(border child)~ | ~Border~ | nothing |
|
|
| ~(toggle-switch label on? on-toggle)~ | ~ToggleSwitch~ | ~(on-toggle on)~ |
|
|
| ~(radio label chosen? msg)~ | ~RadioButton~ | ~msg~ when it is chosen |
|
|
| ~(number-input value on-change)~ | ~NumericUpDown~ | ~(on-change value)~ |
|
|
| ~(date-picker "2026-10-25" on-change)~ | ~DatePicker~ | ~(on-change date)~ |
|
|
| ~(auto-complete value options on-input)~ | ~AutoCompleteBox~ | ~(on-input text)~ |
|
|
| ~(expander header expanded? on-toggle child)~ | ~Expander~ | ~(on-toggle expanded)~ |
|
|
| ~(tree-view chosen on-pick (tree-node key header ...) ...)~ | ~TreeView~ | ~(on-pick key)~ |
|
|
| ~(flow child ...)~ | ~WrapPanel~ | nothing |
|
|
| ~(dock child ...)~, with ~at-top~, ~at-left~ … | ~DockPanel~ | nothing |
|
|
| ~(menu-bar (submenu label (menu-item label msg) ...))~ | ~Menu~ | ~msg~ when an item is chosen |
|
|
|
|
They take keywords where they apply: ~#:size~ (the font size of a text),
|
|
~#:wrap~ (a text breaks its lines at its width), ~#:width~, ~#:height~,
|
|
~#:margin~, ~#:padding~, ~#:spacing~ (between the children of a column, a row
|
|
or a grid), ~#:enabled~, ~#:placeholder~, ~#:step~ (a slider's arrow key) and
|
|
~#:maximum~ (a progress bar's full, 1 unless told otherwise). A size of zero,
|
|
the default, leaves the size to the control.
|
|
|
|
A grid's ~#:columns~ and ~#:rows~ are sizes as Avalonia writes them: ~"Auto"~
|
|
fits the content, ~"*"~ shares what is left, ~"2*"~ takes two shares, and a
|
|
number is pixels. ~(at row column view)~ puts a view in a cell, counting from
|
|
0, with ~#:row-span~ and ~#:column-span~.
|
|
|
|
#+BEGIN_SRC scheme
|
|
(grid #:columns "Auto,*" #:rows "Auto,Auto" #:spacing 6.0
|
|
(at 0 0 (text "Volume")) (at 0 1 (slider (.volume m) 0.0 10.0 Volume))
|
|
(at 1 0 (text "Colour")) (at 1 1 (dropdown ["red" "green" "blue"] (.colour m) Colour)))
|
|
|
|
(scroll (apply column (vec->array (vec-map (fun (t) (keyed (.id t) (text (.title t))))
|
|
(.todos m))))
|
|
#:height 200.0)
|
|
#+END_SRC
|
|
|
|
An image that cannot be read shows nothing. ~image-data~ reads its bytes
|
|
again only when it is given another array, so keep the array in the model.
|
|
|
|
A list box's items are views, so an item can be anything: a text, a row with
|
|
a checkbox, a dropdown. Give them keys if the list can change order. Each
|
|
item is a control, so a list of thousands makes thousands of controls.
|
|
|
|
#+BEGIN_SRC scheme
|
|
(tabs (.tab m) Tab
|
|
(tab "Todo" (list-box (vec-map (fun (t) (keyed (.id t) (text (.title t)))) (.todos m))
|
|
(.chosen m) Chose))
|
|
(tab "Settings" (checkbox "Dark" (.dark m) Dark)))
|
|
#+END_SRC
|
|
|
|
* Colours and borders
|
|
|
|
~(paint view #:colour #:background)~ colours any view: ~#:colour~ is the
|
|
text, ~#:background~ what is behind it. A colour is a name such as ~"red"~,
|
|
or ~"#rgb"~, ~"#rrggbb"~ or ~"#aarrggbb"~. The empty string, the default,
|
|
leaves the colour to the theme, and so does a colour that cannot be read.
|
|
|
|
~(border child #:colour #:thickness #:radius #:background #:padding)~ draws
|
|
a line around a view, with round corners if ~#:radius~ is more than zero.
|
|
|
|
#+BEGIN_SRC scheme
|
|
(border (paint (text "Saved") #:colour "#c0392b")
|
|
#:colour "#2980b9" #:radius 8.0 #:background "#ecf0f1" #:padding 8.0)
|
|
#+END_SRC
|
|
|
|
The Fluent theme gives a button its own colours when the pointer is over it
|
|
or it is pressed, so a painted button shows the theme's colour then.
|
|
|
|
More wrappers: ~(tooltip view "text")~, ~(with-context-menu view item ...)~
|
|
for a menu on the right button, and ~(classed view "name" ...)~ for styles.
|
|
|
|
A tree node is known by its key, which must be its own in the tree. A node
|
|
is open at first when it says ~#:expanded~; after that the user opens and
|
|
closes it.
|
|
|
|
* Components: view-map
|
|
|
|
A part of a program can have a message type, an update and a view of its
|
|
own, and know nothing of the program it is in. ~(view-map f view)~ puts its
|
|
view in a larger one: each message it sends is given to ~f~ first, which is
|
|
usually the case of the larger message that carries it.
|
|
|
|
#+BEGIN_SRC scheme
|
|
(type (: Msg (Union (: Colour PickerMsg) (: Width PickerMsg) ...)))
|
|
|
|
(column (view-map Colour (picker-view (.colour m)))
|
|
(view-map Width (picker-view (.width m))))
|
|
|
|
((Colour pm) (next (record-set m (colour (picker-update pm (.colour m))))))
|
|
#+END_SRC
|
|
|
|
* Drawing: canvas
|
|
|
|
[[file:docs/sketch.png]]
|
|
|
|
~(canvas width height shape ...)~ is a surface to draw on. A shape is placed
|
|
by its own numbers, and any other view by ~(at-point x y view)~.
|
|
|
|
| Shape | Takes |
|
|
|------------------------------------------+-----------------------------------------------|
|
|
| ~(rect x y width height)~ | ~#:fill~, ~#:stroke~, ~#:stroke-width~, ~#:radius~ |
|
|
| ~(ellipse x y width height)~ | ~#:fill~, ~#:stroke~, ~#:stroke-width~ |
|
|
| ~(line x1 y1 x2 y2)~ | ~#:stroke~, ~#:stroke-width~ |
|
|
| ~(path "M 10 10 L 50 10 L 30 40 Z")~ | ~#:fill~, ~#:stroke~, ~#:stroke-width~ |
|
|
|
|
A path is written as SVG writes one. ~(on-press canvas f)~, ~(on-move canvas
|
|
f)~ and ~(on-release canvas f)~ send ~(f x y)~, the place on the canvas, when
|
|
a button is pressed, when the pointer moves, and when the button is let go.
|
|
A press takes the pointer, so a drag that leaves the canvas still reaches it.
|
|
Shapes are views like any other, so keyed ones are kept as their list
|
|
changes. [[file:examples/sketch.bjo][examples/sketch.bjo]] is the picture
|
|
above, and uses two components of its own.
|
|
|
|
* Controls of your own: component and (bjogui draw)
|
|
|
|
[[file:docs/mixer.png]]
|
|
|
|
~(component state draw #:input #:cursor #:focusable)~ makes a control that
|
|
draws itself and hears the pointer and the keys. It keeps a state of its own,
|
|
such as where a drag started or where the pointer hovers, which the model
|
|
never sees. ~(drawn c value on-output)~ puts it in a view: it shows ~value~,
|
|
and each output it sends is made a message with ~on-output~, as a slider's
|
|
value is.
|
|
|
|
#+BEGIN_SRC scheme
|
|
(import (bjogui draw))
|
|
|
|
;; Where a drag started, and the value then.
|
|
(type (: Drag (Union Idle (: Dragging double double))))
|
|
|
|
(: knob (Component double Drag double))
|
|
(def knob
|
|
(component Idle
|
|
(fun (pen w h s v)
|
|
(fill-circle pen (/ w 2.0) (/ h 2.0) (/ w 2.0) "#2c3e50")
|
|
(stroke-arc pen (/ w 2.0) (/ h 2.0) (- (/ w 2.0) 6.0) 2.4 (* v 4.7) "orange"
|
|
#:width 5.0 #:cap "round"))
|
|
#:input (fun (in w h s v)
|
|
(match (Tuple in s)
|
|
((Tuple (PointerDown p) _) (Tuple (Dragging (.y p) v) None))
|
|
((Tuple (PointerMove p) (Dragging y0 v0))
|
|
(Tuple s (Some (max 0.0 (min 1.0 (+ v0 (/ (- y0 (.y p)) 150.0)))))))
|
|
((Tuple (PointerUp _) _) (Tuple Idle None))
|
|
(_ (Tuple s None))))
|
|
#:cursor (fun (s v) "Hand")))
|
|
|
|
;; In a view:
|
|
(drawn knob (.volume model) Volume #:width 80.0 #:height 80.0)
|
|
#+END_SRC
|
|
|
|
- ~(draw pen width height state value)~ draws the component at its size. It
|
|
is called again when the value, the state or the size changes, and not
|
|
otherwise.
|
|
- ~(input in width height state value)~ gives the next state, and maybe an
|
|
output. ~in~ is an ~Input~: ~(PointerDown p)~, ~(PointerMove p)~,
|
|
~(PointerUp p)~, ~PointerEnter~, ~PointerLeave~, ~(Wheel p dx dy)~,
|
|
~(KeyDown "Ctrl+Z")~, ~FocusIn~ and ~FocusOut~. A ~Pointer~ has ~x~ and ~y~
|
|
from the component's corner, ~button~ (~"left"~, ~"right"~, ~"middle"~),
|
|
~clicks~ (2 for a double click) and ~mods~ (~"Ctrl+Shift"~).
|
|
- A press takes the pointer, so a drag that leaves the component still
|
|
reaches it. A ~#:focusable~ component takes the keys when it is pressed.
|
|
- An input that changes the state or sends an output is the component's, and
|
|
goes no further. Any other goes on to the parents, so a key the component
|
|
does not use still works as a shortcut.
|
|
- With no ~#:width~ or ~#:height~, a component fills the space it is given,
|
|
and is drawn again when that changes.
|
|
- Make a component once, at the top of a module. A view in the place of
|
|
another component is a new control, whose state starts again.
|
|
- A component that sends nothing needs its type written, because its output
|
|
type is not known: ~(: lamp (Component bool Unit Unit))~.
|
|
|
|
(bjogui draw) is the pen. Its coordinates start at the component's top left
|
|
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.
|