From 8941425f0569ab9811cb3765a974c69420ec86b62323d9d92cc65fa0ce55968e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Sun, 25 Oct 2026 18:10:53 +0100 Subject: [PATCH] Tree views, a styles example, and the Readme for all of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tree node is a TreeViewItem: a header and items, patched by key as a list's items are. The chosen node is chosen by its key, once the nodes are there, and a click sends the key. SelectedItem is a direct property, which a clear does not reset, so no choice is set as null. The Readme now covers menus and keys, dialogs, styles and themes, and the new controls. docs/styles.png is examples/styles.bjo. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8) Co-Authored-By: eca-agent --- Readme.org | 106 +++++++++++++++++++++++++++++++++++++++++--- docs/styles.png | Bin 0 -> 33712 bytes examples/styles.bjo | 38 ++++++++++++++++ src/app.bjo | 72 +++++++++++++++++++++++++++++- src/view.bjo | 31 +++++++++++-- tests/tree.bjo | 84 +++++++++++++++++++++++++++++++++++ 6 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 docs/styles.png create mode 100644 examples/styles.bjo create mode 100644 tests/tree.bjo diff --git a/Readme.org b/Readme.org index 4e468d0..0c2540c 100644 --- a/Readme.org +++ b/Readme.org @@ -68,6 +68,16 @@ being changed, and they are dropped, so only the user's events are messages. | ~(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~, @@ -122,6 +132,53 @@ a line around a view, with round corners if ~#:radius~ is more than zero. 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. + +* 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 @@ -159,10 +216,42 @@ 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. +* 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 #:title #:width #:height)~ - opens a window and runs until it is closed. +- ~(run-app init gui-update view #:subscriptions #:keys #:theme #:styles + #: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. It is the environment variable ~BJOGUI_SCREENSHOT~ unless the program says otherwise, so any program can @@ -176,6 +265,7 @@ cancelled, and the program ends. - ~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-model~ reads the model, and ~app-control-at~ finds a control by its path of child indices. @@ -196,15 +286,21 @@ 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 examples/counter.bjo bjo run examples/stopwatch.bjo bjo run examples/todo.bjo +bjo run examples/styles.bjo #+END_SRC * Not yet -- Menus and dialogs. -- Styles of one's own, with classes, and colours for the pointer-over and - pressed states. +- A data grid, and lists that make controls only for the items on screen. +- Dialogs of one's own, with a view and a state. +- The macOS menu bar. +- Pointer-over and pressed colours for kinds other than buttons. - A moved control is taken out of the panel and put back, so it may lose the keyboard focus. diff --git a/docs/styles.png b/docs/styles.png new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..b8127f86dbb5000cb005dd5346fd87ead54c7d3db4bdde07ec9bcb8f40be59ce GIT binary patch literal 33712 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 $eE{Li^Kp~o< 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 WrapPanel))) (DockPanel (: Avalonia.Controls.DockPanel (-> DockPanel))) (DockEdge (: Avalonia.Controls.Dock)) + (TreeView (: Avalonia.Controls.TreeView (-> TreeView))) + (TreeViewItem (: Avalonia.Controls.TreeViewItem (-> TreeViewItem))) (PropertyChange (: Avalonia.AvaloniaPropertyChangedEventArgs)) (Classes (: Avalonia.Controls.Classes)) (StyledElement (: Avalonia.StyledElement)) @@ -220,6 +222,10 @@ (auto-watermark-property (: Avalonia.Controls.AutoCompleteBox.WatermarkProperty #:get)) (auto-text (: Avalonia.Controls.AutoCompleteBox.Text (-> AutoCompleteBox string) #:get)) (expanded? (: Avalonia.Controls.Expander.IsExpanded (-> Expander bool) #:get)) + (tree-header-property (: Avalonia.Controls.Primitives.HeaderedItemsControl.HeaderProperty #:get)) + (tree-expanded-property (: Avalonia.Controls.TreeViewItem.IsExpandedProperty #:get)) + (tree-selected-property (: Avalonia.Controls.TreeView.SelectedItemProperty #:get)) + (tree-selected (: Avalonia.Controls.TreeView.SelectedItem (-> TreeView Object) #:get)) (classes-of (: Avalonia.StyledElement.Classes (-> StyledElement Classes) #:get)) (classes-clear! (: Avalonia.Controls.Classes.Clear (-> Classes void))) (classes-add! (: Avalonia.Controls.Classes.Add (-> Classes string void))) @@ -378,6 +384,8 @@ (MenuBarKind #t) (MenuItemKind #t) (ContextMenuKind #t) + (TreeKind #t) + (TreeNodeKind #t) (_ #f))) ;; A kind whose children each have a place of their own: the content of a @@ -430,6 +438,7 @@ (ToggleSwitchKind (Some (cast AvaloniaProperty content-property))) (RadioKind (Some (cast AvaloniaProperty content-property))) (ExpanderKind (Some (cast AvaloniaProperty header-property))) + (TreeNodeKind (Some (cast AvaloniaProperty tree-header-property))) (TabKind (Some (cast AvaloniaProperty header-property))) (MenuItemKind (Some (cast AvaloniaProperty menu-header-property))) (_ None))) @@ -484,7 +493,12 @@ ((PropRadius _) (when-kind (= kind BorderKind) (cast AvaloniaProperty corner-radius-property))) ((PropShortcut _) (when-kind (= kind MenuItemKind) (cast AvaloniaProperty input-gesture-property))) ((PropTip _) (Some (cast AvaloniaProperty tip-property))) - ((PropExpanded _) (when-kind (= kind ExpanderKind) (cast AvaloniaProperty expanded-property))) + ((PropExpanded _) + (cond ((= kind ExpanderKind) (Some (cast AvaloniaProperty expanded-property))) + ((= kind TreeNodeKind) (Some (cast AvaloniaProperty tree-expanded-property))) + (else None))) + ;; Set after the children, by `sync-tree-choice!`. + ((PropChosenKey _) None) ((PropDockSide _) (Some (cast AvaloniaProperty dock-property))) ((PropClasses _) None) ((PropDate _) (when-kind (= kind DateKind) (cast AvaloniaProperty date-property))) @@ -573,6 +587,7 @@ ((PropExpanded b) (cast Object b)) ((PropDate s) (text s)) ((PropClasses _) (cast Object 0)) + ((PropChosenKey s) (text s)) ((PropDockSide i) (cast Object (match i (0 DockEdge.Left) (1 DockEdge.Top) (2 DockEdge.Right) (_ DockEdge.Bottom)))))) @@ -589,7 +604,7 @@ ((PropRows _) 20) ((PropRow _) 21) ((PropColumn _) 22) ((PropRowSpan _) 23) ((PropColumnSpan _) 24) ((PropBackground _) 25) ((PropForeground _) 26) ((PropBorderColour _) 27) ((PropBorderWidth _) 28) ((PropRadius _) 29) ((PropShortcut _) 30) ((PropTip _) 31) ((PropExpanded _) 32) ((PropDate _) 33) - ((PropDockSide _) 34) ((PropClasses _) 35))) + ((PropDockSide _) 34) ((PropClasses _) 35) ((PropChosenKey _) 36))) (: (-> Control AvaloniaProperty Object void)) (defun (put! control property value) @@ -1009,6 +1024,13 @@ (fun () (def d (range-value (cast RangeBase control))) (fun (on) (match on ((OnSlide f) (Some (f d))) (_ None)))))) + (TreeKind + (on-event (cast RoutedEvent selection-changed-event) + (fun () + (def key (match (tree-node-key live (tree-selected (cast TreeView control))) + ((Some k) k) + (None ""))) + (fun (on) (match on ((OnPick f) (Some (f key))) (_ None)))))) ((or DropdownKind ListBoxKind TabsKind) (on-event (cast RoutedEvent selection-changed-event) (fun () @@ -1060,6 +1082,8 @@ (SeparatorKind (cast Control (Separator.))) (ContextKind (cast Control (Border.))) (ContextMenuKind (cast Control (ContextMenu.))) + (TreeKind (cast Control (TreeView.))) + (TreeNodeKind (cast Control (TreeViewItem.))) (ToggleSwitchKind (cast Control (ToggleSwitch.))) (RadioKind (cast Control (RadioButton.))) (NumberKind (cast Control (NumericUpDown.))) @@ -1119,6 +1143,7 @@ (mutablevec-add! (.children live) c) (adopt! kind control (- (mutablevec-length (.children live)) 1) (.control c)))) (sync-selection! control kind (view-props v)) + (sync-tree-choice! live) live) ;; Changes the controls of `live` to show `next`. Gives `live` itself, or new @@ -1136,8 +1161,51 @@ ((or (panel? kind) (items? kind)) (patch-children! app live kind (view-children next))) (else unit)) (sync-selection! (.control live) kind (view-props next)) + (sync-tree-choice! live) live))) +;; The node of a tree whose control is `control`, among the live nodes below +;; `live`, and its key. +(: (-> (Live %m) Object (Option string))) +(defun (tree-node-key live control) + (: (-> (Live %m) (Option string))) + (defun (walk l) + (if (and (= (view-kind (.view l)) TreeNodeKind) (eq? (cast Object (.control l)) control)) + (view-key (.view l)) + (loop (:for c (mutablevec->vec (.children l))) + (:when-let (Some k) (walk c)) + (:finish #t (Some k)) + => None))) + (walk live)) + +;; The control of the node with `key` below `live`. +(: (-> (Live %m) string (Option Control))) +(defun (tree-node-control live key) + (if (and (= (view-kind (.view live)) TreeNodeKind) (= (view-key (.view live)) (Some key))) + (Some (.control live)) + (loop (:for c (mutablevec->vec (.children live))) + (:when-let (Some found) (tree-node-control c key)) + (:finish #t (Some found)) + => None))) + +;; Chooses the node of a tree whose key the view gives, once the nodes are +;; there, if it is not chosen already. +(: (-> (Live %m) void)) +(defun (sync-tree-choice! live) + (when (= (view-kind (.view live)) TreeKind) + (def control (.control live)) + (loop (:for p (view-props (.view live))) + (:do (match p + ((PropChosenKey key) + (match (if (string-empty? key) None (tree-node-control live key)) + ((Some node) + (unless (eq? (tree-selected (cast TreeView control)) (cast Object node)) + (put! control (cast AvaloniaProperty tree-selected-property) (cast Object node)))) + ;; Null and not a clear: SelectedItem is a direct property, + ;; which a clear does not reset. + (None (put! control (cast AvaloniaProperty tree-selected-property) (clr-default))))) + (_ unit)))))) + ;; The children of a kind whose children each have a place of their own. (: (-> (App %model %m) (Live %m) Kind (Vec (View %m)) void)) (defun (patch-single! app live kind views) diff --git a/src/view.bjo b/src/view.bjo index 6046508..5b63129 100644 --- a/src/view.bjo +++ b/src/view.bjo @@ -22,6 +22,7 @@ list-box tabs tab menu-bar submenu menu-item menu-separator toggle-switch radio number-input date-picker auto-complete expander + tree-view tree-node column row grid scroll border flow dock ;; Wrappers. keyed at paint with-context-menu tooltip at-top at-bottom at-left at-right classed @@ -44,7 +45,7 @@ ListBoxKind TabsKind TabKind MenuBarKind MenuItemKind SeparatorKind ContextKind ContextMenuKind ToggleSwitchKind RadioKind NumberKind DateKind AutoCompleteKind - ExpanderKind WrapKind DockKind + ExpanderKind WrapKind DockKind TreeKind TreeNodeKind ColumnKind RowKind GridKind ScrollKind BorderKind))) ;; A property of a control. A kind takes only some of them; the app ignores @@ -84,14 +85,16 @@ (: PropExpanded bool) ; whether an expander shows its content (: PropDate string) ; a date picker's date, as "2026-10-25" (: PropDockSide int) ; where a view is in its dock: 0 to 3 - (: PropClasses (Vec string))))) ; the classes that styles match + (: PropClasses (Vec string)) ; the classes that styles match + (: PropChosenKey string)))) ; the key of a tree's chosen node ;; An event, and the message it sends. (type (: (On %m) (Union (: OnClick %m) (: OnInput (-> string %m)) (: OnToggle (-> bool %m)) (: OnSlide (-> double %m)) - (: OnSelect (-> int %m))))) + (: OnSelect (-> int %m)) + (: OnPick (-> string %m))))) ;; One control and what is in it. ;; @@ -393,6 +396,28 @@ (children [child]) (key None))) +;; A tree of nodes made by `tree-node`, with the node whose key is `chosen` +;; chosen, or none for "". A click on a node sends `(on-pick key)`. +(: (-> string (-> string %m) (#:width double) (#:height double) (#:margin double) + #:rest (View %m) (View %m))) +(defun (tree-view chosen on-pick #:width 0.0 #:height 0.0 #:margin 0.0 #:rest nodes) + (View (kind TreeKind) + (props (with-layout [(PropChosenKey chosen)] width height margin 0.0)) + (events [(OnPick on-pick)]) + (children (array->vec nodes)) + (key None))) + +;; A node of a tree, known by `key`, which must be one of its own in the +;; tree. It opens to show `children`, more nodes, and is open at first when +;; `#:expanded`; after that it is opened and closed by the user. +(: (-> string string (#:expanded bool) #:rest (View %m) (View %m))) +(defun (tree-node key header #:expanded #f #:rest children) + (View (kind TreeNodeKind) + (props [(PropLabel header) (PropExpanded expanded)]) + (events []) + (children (array->vec children)) + (key (Some key)))) + ;; A checkbox that is `checked?`. A click sends `(on-toggle checked)`, with ;; the new state. (: (-> string bool (-> bool %m) (#:enabled bool) (#:margin double) (View %m))) diff --git a/tests/tree.bjo b/tests/tree.bjo new file mode 100644 index 0000000..68f5af8 --- /dev/null +++ b/tests/tree.bjo @@ -0,0 +1,84 @@ +;; 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/tree.bjo — tree views. +;; +;; bjo run tests/tree.bjo +;; +;; No window is opened. + +(import (std prelude)) +(import (std simpletest)) +(import (std interop)) +(import (bjogui view)) +(import (bjogui app)) + +(import/class + (Object (: System.Object)) + (Disposable (: System.IDisposable)) + (AvaloniaObject (: Avalonia.AvaloniaObject)) + (AvaloniaProperty (: Avalonia.AvaloniaProperty)) + (Priority (: Avalonia.Data.BindingPriority)) + (ItemsControl (: Avalonia.Controls.ItemsControl)) + (TreeView (: Avalonia.Controls.TreeView)) + (TreeViewItem (: Avalonia.Controls.TreeViewItem)) + (Headered (: Avalonia.Controls.Primitives.HeaderedItemsControl))) + +(import/extern + (set-value (: Avalonia.AvaloniaObject.SetValue + (-> AvaloniaObject AvaloniaProperty Object Priority Disposable))) + (selected-property (: Avalonia.Controls.TreeView.SelectedItemProperty #:get)) + (selected (: Avalonia.Controls.TreeView.SelectedItem (-> TreeView Object) #:get)) + (item-count (: Avalonia.Controls.ItemsControl.ItemCount (-> ItemsControl int) #:get)) + (header (: Avalonia.Controls.Primitives.HeaderedItemsControl.Header (-> Headered Object) #:get)) + (expanded? (: Avalonia.Controls.TreeViewItem.IsExpanded (-> TreeViewItem bool) #:get)) + (same? (: System.Object.ReferenceEquals (-> Object Object bool)))) + +(type (: Msg (Union (: Chose string) Reverse))) +(type (: Model (Record (: chosen string) (: fruit (List string))))) + +(: (-> Msg Model (Tuple Model (List (Event Msg))))) +(defun (gui-update msg m) + (next (match msg + ((Chose k) (record-set m (chosen k))) + (Reverse (record-set m (fruit (list-reverse (.fruit m)))))))) + +(: (-> Model (View Msg))) +(defun (view m) + (tree-view (.chosen m) Chose + (apply tree-node "fruit" "Fruit" #:expanded #t + (vec->array (vec-map (fun (f) (tree-node f (string-upcase f))) (list->vec (.fruit m))))) + (tree-node "veg" "Vegetables" + (tree-node "kale" "KALE")))) + +(defun (main) + (def app (start-app (Model (chosen "pear") (fruit '("apple" "pear"))) + gui-update view (fun (c) unit))) + (defun (ctl path) (app-control-at app path)) + (def tree (cast TreeView (ctl '()))) + (defun (chosen-is? path) (same? (selected tree) (cast Object (ctl path)))) + + (expect "the top nodes" (item-count (cast ItemsControl tree)) 2) + (expect "a node's nodes" (item-count (cast ItemsControl (ctl '(0)))) 2) + (expect "a header" (.ToString (header (cast Headered (ctl '(0 1))))) "PEAR") + (expect "open at first" (expanded? (cast TreeViewItem (ctl '(0)))) #t) + (expect "closed at first" (expanded? (cast TreeViewItem (ctl '(1)))) #f) + (expect "the chosen node is chosen once the nodes are there" (chosen-is? '(0 1)) #t) + + (ignore (set-value (cast AvaloniaObject tree) (cast AvaloniaProperty selected-property) + (cast Object (ctl '(1 0))) Priority.LocalValue)) + (expect "a node chosen sends its key" (.chosen (app-model app)) "kale") + + (app-dispatch! app (Chose "apple")) + (expect "the model chooses the node" (chosen-is? '(0 0)) #t) + + (def apple (ctl '(0 0))) + (app-dispatch! app Reverse) + (expect "nodes moved by key keep their controls" (same? (cast Object (ctl '(0 1))) (cast Object apple)) #t) + (expect "and the choice" (chosen-is? '(0 1)) #t) + + (app-dispatch! app (Chose "")) + (expect "no choice" (same? (selected tree) (clr-default)) #t) + + (println "bjogui tree ok"))