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>
145 lines
7.1 KiB
Text
145 lines
7.1 KiB
Text
;; 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/tests/windows.bjo — windows besides the main one.
|
|
;;
|
|
;; bjo run tests/windows.bjo
|
|
;;
|
|
;; Avalonia runs with no screen, so the windows are real but not seen. A
|
|
;; click on a window's close button is made as the platform makes it.
|
|
|
|
(import (std prelude))
|
|
(import (std simpletest))
|
|
(import (std interop))
|
|
(import (bjogui view))
|
|
(import (bjogui app))
|
|
|
|
(import/class
|
|
(Object (: System.Object))
|
|
(Control (: Avalonia.Controls.Control))
|
|
(Window (: Avalonia.Controls.Window))
|
|
(TopLevel (: Avalonia.Controls.TopLevel))
|
|
(Visual (: Avalonia.Visual))
|
|
(TextBox (: Avalonia.Controls.TextBox))
|
|
(TextBlock (: Avalonia.Controls.TextBlock))
|
|
(InputElement (: Avalonia.Input.InputElement))
|
|
(Interactive (: Avalonia.Interactivity.Interactive))
|
|
(RoutedEvent (: Avalonia.Interactivity.RoutedEvent))
|
|
(RoutedEventArgs (: Avalonia.Interactivity.RoutedEventArgs (-> RoutedEvent RoutedEventArgs)))
|
|
(CloseReason (: Avalonia.Controls.WindowCloseReason))
|
|
(ClrType (: System.Type))
|
|
(PropertyInfo (: System.Reflection.PropertyInfo)))
|
|
|
|
(import/extern
|
|
(top-level-of (: Avalonia.Controls.TopLevel.GetTopLevel (-> Visual TopLevel)))
|
|
(title-of (: Avalonia.Controls.Window.Title (-> Window string) #:get))
|
|
(visible? (: Avalonia.Visual.IsVisible (-> Visual bool) #:get))
|
|
(box-text! (: Avalonia.Controls.TextBox.Text (-> TextBox string void) #:set))
|
|
(block-text (: Avalonia.Controls.TextBlock.Text (-> TextBlock string) #:get))
|
|
(focused? (: Avalonia.Input.InputElement.IsFocused (-> InputElement bool) #:get))
|
|
(click-event (: Avalonia.Controls.Button.ClickEvent #:get))
|
|
(raise-event (: Avalonia.Interactivity.Interactive.RaiseEvent (-> Interactive RoutedEventArgs void)))
|
|
;; What a click on the close button calls: the window's platform's Closing.
|
|
;; Avalonia hides it from C#, so it is found by reflection.
|
|
(type-named (: System.Type.GetType (-> string ClrType)))
|
|
(property-of (: System.Type.GetProperty (-> ClrType string PropertyInfo)))
|
|
(value-of (: System.Reflection.PropertyInfo.GetValue (-> PropertyInfo Object Object)))
|
|
(invoke (: System.Delegate.DynamicInvoke (-> Delegate (Array Object) Object)))
|
|
(sleep-ms (: System.Threading.Thread.Sleep (-> int void) #:blocking)))
|
|
|
|
(type (: Msg (Union OpenSettings CloseSettings OpenNotes CloseNotes (: Name string) Save FocusName)))
|
|
(type (: Model (Record (: settings bool) (: notes bool) (: name string) (: saved (List string)))))
|
|
|
|
(: (-> Msg Model (Tuple Model (List (Event Msg)))))
|
|
(defun (gui-update msg m)
|
|
(match msg
|
|
(OpenSettings (next (record-set m (settings #t))))
|
|
(CloseSettings (next (record-set m (settings #f))))
|
|
(OpenNotes (next (record-set m (notes #t))))
|
|
(CloseNotes (next (record-set m (notes #f))))
|
|
((Name s) (next (record-set m (name s))))
|
|
(Save (next (record-set m (saved (cons (.name m) (.saved m))))))
|
|
(FocusName (next m (focus "name")))))
|
|
|
|
(: (-> Model (View Msg)))
|
|
(defun (view m)
|
|
(text #"name: ${(.name m)}"))
|
|
|
|
(: (-> Model (List (WindowView Msg))))
|
|
(defun (windows m)
|
|
(def settings
|
|
(window "settings" "Settings" CloseSettings
|
|
(column (keyed "name" (text-input (.name m) Name)) ; 0
|
|
(button "Save" Save)) ; 1
|
|
#:modal #t))
|
|
(def notes
|
|
(window "notes" #"Notes: ${(.name m)}" CloseNotes (text (.name m)) #:width 200.0 #:height 100.0))
|
|
(list-append (if (.settings m) (list settings) '())
|
|
(if (.notes m) (list notes) '())))
|
|
|
|
(defun (main)
|
|
(start-headless!)
|
|
(def app (start-app (Model (settings #f) (notes #f) (name "") (saved '()))
|
|
gui-update view (fun (c) unit) #:windows windows))
|
|
(defun (in-window key path) (app-window-control app key path))
|
|
(defun (frame key) (cast Window (top-level-of (cast Visual (in-window key '())))))
|
|
(defun (click c) (raise-event (cast Interactive c) (RoutedEventArgs. (cast RoutedEvent click-event))))
|
|
(: (-> (-> bool) bool))
|
|
(defun (wait-for done?)
|
|
(let go ((tries 0))
|
|
(app-run-pending! app)
|
|
(cond ((done?) #t)
|
|
((> tries 200) #f)
|
|
(else (sleep-ms 10) (go (+ tries 1))))))
|
|
|
|
(expect "no windows at first" (app-windows app) '())
|
|
|
|
;; --- opening, and the window's own view ---------------------------------------
|
|
(app-dispatch! app OpenSettings)
|
|
(expect "a window is opened" (app-windows app) '("settings"))
|
|
(def settings (frame "settings"))
|
|
(expect "with its title" (title-of settings) "Settings")
|
|
(expect "and shown" (visible? (cast Visual settings)) #t)
|
|
(box-text! (cast TextBox (in-window "settings" '(0))) "Ada")
|
|
(expect "its controls send messages" (.name (app-model app)) "Ada")
|
|
(expect "which the main view shows" (block-text (cast TextBlock (app-control app))) "name: Ada")
|
|
(click (in-window "settings" '(1)))
|
|
(expect "a button in it" (.saved (app-model app)) '("Ada"))
|
|
|
|
;; --- two windows, and changes ---------------------------------------------------
|
|
(app-dispatch! app OpenNotes)
|
|
(expect "a second window" (app-windows app) '("settings" "notes"))
|
|
(expect "the first is the same window" (eq? (cast Object (frame "settings")) (cast Object settings)) #t)
|
|
(expect "a title from the model" (title-of (frame "notes")) "Notes: Ada")
|
|
(app-dispatch! app (Name "Bob"))
|
|
(expect "changes with it" (title-of (frame "notes")) "Notes: Bob")
|
|
(expect "and so does its view" (block-text (cast TextBlock (in-window "notes" '()))) "Bob")
|
|
|
|
;; --- a command finds a control in a window ---------------------------------------
|
|
(app-dispatch! app FocusName)
|
|
(expect "focus in a window" (wait-for (fun () (focused? (cast InputElement (in-window "settings" '(0)))))) #t)
|
|
|
|
;; --- closing ----------------------------------------------------------------------
|
|
(app-dispatch! app CloseSettings)
|
|
(expect "a window the model has no more is closed" (app-windows app) '("notes"))
|
|
(expect "and not shown" (visible? (cast Visual settings)) #f)
|
|
|
|
(def notes (frame "notes"))
|
|
(defun (impl-of (: o Object) type-name property)
|
|
(value-of (property-of (type-named type-name) property) o))
|
|
(def platform (impl-of (cast Object notes) "Avalonia.Controls.TopLevel, Avalonia.Controls" "PlatformImpl"))
|
|
(def closing (impl-of platform "Avalonia.Platform.IWindowImpl, Avalonia.Controls" "Closing"))
|
|
(ignore (invoke (cast Delegate closing) (: (make-array 1 #:fill (cast Object CloseReason.WindowClosing)) (Array Object))))
|
|
(expect "a click on the close button does not close it" (visible? (cast Visual notes)) #t)
|
|
(expect "it sends on-close" (wait-for (fun () (not (.notes (app-model app))))) #t)
|
|
(expect "and the model closes it" (app-windows app) '())
|
|
(expect "so it is gone" (visible? (cast Visual notes)) #f)
|
|
|
|
;; --- stopping closes them ---------------------------------------------------------
|
|
(app-dispatch! app OpenNotes)
|
|
(def again (frame "notes"))
|
|
(app-stop! app)
|
|
(expect "a stopped app closes its windows" (visible? (cast Visual again)) #f)
|
|
|
|
(println "bjogui windows ok"))
|