diff --git a/docs/todo.png b/docs/todo.png new file mode 100644 index 0000000..e841f82 Binary files /dev/null and b/docs/todo.png differ diff --git a/examples/todo.bjo b/examples/todo.bjo new file mode 100644 index 0000000..ecdf4ae --- /dev/null +++ b/examples/todo.bjo @@ -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)) diff --git a/src/app.bjo b/src/app.bjo index fdb7652..d7eb4a6 100644 --- a/src/app.bjo +++ b/src/app.bjo @@ -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"))))