A todo example, and its picture in the Readme

run-app takes #:screenshot, a path: the window saves a PNG of itself there
once it is shown and laid out, at twice its size, and closes. It defaults to
the environment variable BJOGUI_SCREENSHOT, so any program can be pictured
with no change. docs/todo.png is examples/todo.bjo, taken that way.

🤖 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 17:24:32 +01:00
parent e773bc424e
commit 5a80685397
3 changed files with 150 additions and 2 deletions

BIN
docs/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

103
examples/todo.bjo Normal file
View file

@ -0,0 +1,103 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; bjogui/examples/todo.bjo — a todo list.
;;
;; bjo run examples/todo.bjo
;;
;; The picture in the Readme is this program, taken with
;;
;; BJOGUI_SCREENSHOT=docs/todo.png bjo run examples/todo.bjo
(import (std prelude))
(import (bjogui view))
(import (bjogui app))
(type (: Todo (Record (: id int) (: title string) (: done bool))))
(type (: Msg (Union (: Typed string) Add (: Toggle int bool) (: Chose int)
ClearDone (: Tab int) (: Size double))))
(type (: Model (Record (: todos (List Todo)) (: draft string) (: next-id int)
(: chosen int) (: tab int) (: size double))))
(: gui-update (-> Msg Model (Tuple Model (List (Event Msg)))))
(defun (gui-update msg m)
(next
(match msg
((Typed s) (record-set m (draft s)))
(Add
(if (string-empty? (.draft m))
m
(record-set m (todos (list-append (.todos m)
(list (Todo (id (.next-id m)) (title (.draft m))
(done #f)))))
(draft "")
(next-id (+ (.next-id m) 1)))))
((Toggle id done)
(record-set m (todos (list-map (fun (t) (if (= (.id t) id) (record-set t (done done)) t))
(.todos m)))))
((Chose i) (record-set m (chosen i)))
(ClearDone (record-set m (todos (list-filter (fun (t) (not (.done t))) (.todos m)))
(chosen -1)))
((Tab i) (record-set m (tab i)))
((Size d) (record-set m (size d))))))
(: (-> Model int))
(defun (done-count m)
(length (list-filter (fun (t) (.done t)) (.todos m))))
(: (-> Model double Todo (View Msg)))
(defun (todo-row m size t)
(keyed (int->string (.id t))
(row #:spacing 8.0
(checkbox "" (.done t) (fun (b) (Toggle (.id t) b)))
(if (.done t)
(paint (text (.title t) #:size size) #:colour "#7f8c8d")
(text (.title t) #:size size)))))
(: (-> Model (View Msg)))
(defun (todo-tab m)
(def total (length (.todos m)))
(column #:spacing 10.0 #:margin 12.0
(grid #:columns "*,Auto" #:spacing 8.0
(at 0 0 (text-input (.draft m) Typed #:placeholder "What needs doing?"))
(at 0 1 (paint (button "Add" Add #:enabled (not (string-empty? (.draft m))))
#:background "#27ae60" #:colour "white")))
(list-box (vec-map (fun (t) (todo-row m (.size m) t)) (list->vec (.todos m)))
(.chosen m) Chose #:height 218.0)
(grid #:columns "*,Auto" #:spacing 8.0
(at 0 0 (column #:spacing 4.0
(text #"${(done-count m)} of ${total} done")
(progress-bar (if (= total 0) 0.0
(/ (cast System.Double (done-count m)) (cast System.Double total))))))
(at 0 1 (button "Clear done" ClearDone #:enabled (> (done-count m) 0))))))
(: (-> Model (View Msg)))
(defun (settings-tab m)
(column #:spacing 10.0 #:margin 12.0
(text "Text size")
(slider (.size m) 10.0 24.0 Size)
(border (text "Written in Bjolang, drawn by Avalonia." #:wrap #t)
#:colour "#2980b9" #:radius 6.0 #:background "#ecf0f1" #:padding 10.0)))
(: (-> Model (View Msg)))
(defun (view m)
(column #:margin 8.0
(paint (text "Things to do" #:size 22.0 #:margin 6.0) #:colour "#2c3e50")
(tabs (.tab m) Tab
(tab "List" (todo-tab m))
(tab "Settings" (settings-tab m)))))
(: (-> string bool int Todo))
(defun (todo title done id) (Todo (id id) (title title) (done done)))
(defun (main)
(run-app (Model (todos (list (todo "Buy milk" #t 1)
(todo "Write the bjogui Readme" #t 2)
(todo "Add menus and dialogs" #f 3)
(todo "Walk the dog" #f 4)))
(draft "Water the plants") (next-id 5) (chosen 2) (tab 0) (size 14.0))
gui-update view
#:title "bjogui todo" #:width 420.0 #:height 450.0))

View file

@ -81,6 +81,13 @@
(ItemCollection (: Avalonia.Controls.ItemCollection))
(IList (: System.Collections.IList))
(ICollection (: System.Collections.ICollection))
(Timer (: Avalonia.Threading.DispatcherTimer (-> Timer)))
(TimeSpan (: System.TimeSpan))
(EventArgs (: System.EventArgs))
(PixelSize (: Avalonia.PixelSize (-> int int PixelSize)))
(Vector (: Avalonia.Vector (-> double double Vector)))
(Visual (: Avalonia.Visual))
(RenderBitmap (: Avalonia.Media.Imaging.RenderTargetBitmap (-> PixelSize Vector RenderBitmap)))
(Controls (: Avalonia.Controls.Controls))
(Panel (: Avalonia.Controls.Panel))
(StackPanel (: Avalonia.Controls.StackPanel (-> StackPanel)))
@ -221,6 +228,16 @@
(start-lifetime (: Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start
(-> Lifetime (Array ClrString) int)))
(window-title! (: Avalonia.Controls.Window.Title (-> Window string void) #:set))
(shutdown! (: Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Shutdown
(-> Lifetime int void)))
;; A picture of the window, for `#:screenshot`.
(seconds->span (: System.TimeSpan.FromSeconds (-> double TimeSpan)))
(interval! (: Avalonia.Threading.DispatcherTimer.Interval (-> Timer TimeSpan void) #:set))
(timer-start! (: Avalonia.Threading.DispatcherTimer.Start (-> Timer void)))
(timer-stop! (: Avalonia.Threading.DispatcherTimer.Stop (-> Timer void)))
(render-into! (: Avalonia.Media.Imaging.RenderTargetBitmap.Render (-> RenderBitmap Visual void)))
(save-png! (: Avalonia.Media.Imaging.Bitmap.Save (-> Bitmap string (Nullable int) void)))
(window-content! (: Avalonia.Controls.ContentControl.Content (-> ContentControl Object void) #:set)))
;; ---------------------------------------------------------------------------
@ -931,14 +948,40 @@
(step! app '())
app)
;; Saves a picture of the window to `path` once it has been shown and laid
;; out, and closes it. Twice the pixels of the window, so that it is sharp on
;; a screen with a scale of 2.
(: (-> Lifetime Window double double string void))
(defun (screenshot-later! lifetime w width height path)
(def timer (Timer.))
(interval! timer (seconds->span 1.0))
(ignore (add-event-handler! timer "Tick"
(: (fun (sender e)
(timer-stop! timer)
(def bitmap (RenderBitmap. (PixelSize. (cast System.Int32 (* 2.0 width))
(cast System.Int32 (* 2.0 height)))
(Vector. 192.0 192.0)))
(render-into! bitmap (cast Visual w))
(save-png! (cast Bitmap bitmap) path (clr-default))
(shutdown! lifetime 0))
(-> Object EventArgs void))))
(timer-start! timer))
;; Runs the program in a window, until the window is closed. Then the
;; subscriptions and the commands still running are cancelled.
;;
;; With `#:screenshot`, a path, the window saves a PNG of itself there once it
;; is shown, and closes. It is the environment variable BJOGUI_SCREENSHOT
;; unless told otherwise, so any program can be pictured with no change:
;;
;; BJOGUI_SCREENSHOT=shot.png bjo run examples/todo.bjo
(: (-> %model (-> %m %model (Tuple %model (List (Event %m)))) (-> %model (View %m))
(#:subscriptions (-> %model (List (Sub %m))))
(#:title string) (#:width double) (#:height double)
(#:title string) (#:width double) (#:height double) (#:screenshot string)
void))
(defun (run-app init gui-update view #:subscriptions (fun (m) '())
#:title "bjogui" #:width 400.0 #:height 300.0)
#:title "bjogui" #:width 400.0 #:height 300.0
#:screenshot (option-ref-or (get-environment-variable "BJOGUI_SCREENSHOT") ""))
;; The Application is made inside the setup, because making one uses the
;; dispatcher, and the dispatcher is the platform's only after the setup.
(def lifetime (Lifetime.))
@ -956,6 +999,8 @@
(fun (c) (window-content! (cast ContentControl w) (cast Object c)))
#:subscriptions subscriptions))
(main-window! lifetime w)
(unless (string-empty? screenshot)
(screenshot-later! lifetime w width height screenshot))
(ignore (start-lifetime lifetime (: (make-array 0) (Array ClrString))))
(app-stop! app)
(cancel (Requested "the window is closed"))))