From ff9db1cc5bfac2c5bc7a0d97e992fddabc97c136b9eda155f7904e33f951ec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Sun, 25 Oct 2026 19:26:37 +0100 Subject: [PATCH] Components with view-map, and canvases with shapes and the pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit view-map gives a view's every message to a function first, so a part with a message type, an update and a view of its own goes in a larger view with the case of the larger message that carries its own. canvas is a surface for rect, ellipse, line and path, placed by their own numbers, and other views by at-point. on-press, on-move and on-release send the place on the canvas. They come from whatever is drawn on it too, so they are taken from any source, and a press takes the pointer for the drag. examples/sketch.bjo draws with it, and docs/sketch.png is its picture. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-8) Co-Authored-By: eca-agent --- docs/sketch.png | Bin 0 -> 43330 bytes examples/sketch.bjo | 132 +++++++++++++++++++++++++++++++++ src/app.bjo | 103 +++++++++++++++++++++++++- src/view.bjo | 125 ++++++++++++++++++++++++++++++- tests/canvas.bjo | 176 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 533 insertions(+), 3 deletions(-) create mode 100644 docs/sketch.png create mode 100644 examples/sketch.bjo create mode 100644 tests/canvas.bjo diff --git a/docs/sketch.png b/docs/sketch.png new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..046dce38928792881d7a8a9a87719645815171097c01ffb5dcb53e8c860f5cee GIT binary patch literal 43330 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 PickerMsg Picker Picker)) +(defun (picker-update msg p) + (match msg ((Picked i) (record-set p (chosen i))))) + +(: (-> Picker (View PickerMsg))) +(defun (picker-view p) + (apply row #:spacing 4.0 + (vec->array + (vec-add-first + (vec-map (fun (i) + (def label (vec-ref (.choices p) i)) + (if (= i (.chosen p)) + (paint (button label (Picked i)) #:background "#2c3e50" #:colour "white") + (button label (Picked i)))) + (->vec (range 0 (vec-length (.choices p))))) + (text (.label p) #:margin 4.0))))) + +(: (-> Picker string)) +(defun (picked p) (vec-ref (.choices p) (.chosen p))) + +;; --------------------------------------------------------------------------- +;; The sketch +;; --------------------------------------------------------------------------- + +(type (: Stroke (Record (: colour string) (: width double) (: points (List (Tuple double double)))))) + +(type (: Msg (Union (: Colour PickerMsg) (: Width PickerMsg) + (: Down double double) (: Drag double double) Up Clear))) + +(type (: Model (Record (: strokes (List Stroke)) (: drawing (Option Stroke)) + (: colour Picker) (: width Picker)))) + +(: (-> Model double)) +(defun (width-of m) (string->double (picked (.width m)))) + +(: (-> Msg Model (Tuple Model (List (Event Msg))))) +(defun (gui-update msg m) + (next + (match msg + ((Colour pm) (record-set m (colour (picker-update pm (.colour m))))) + ((Width pm) (record-set m (width (picker-update pm (.width m))))) + ((Down x y) + (record-set m (drawing (Some (Stroke (colour (picked (.colour m))) (width (width-of m)) + (points (list (Tuple x y)))))))) + ((Drag x y) + (match (.drawing m) + ((Some s) (record-set m (drawing (Some (record-set s (points (cons (Tuple x y) (.points s)))))))) + (None m))) + (Up + (match (.drawing m) + ((Some s) (record-set m (strokes (cons s (.strokes m))) (drawing None))) + (None m))) + (Clear (record-set m (strokes '()) (drawing None)))))) + +;; A stroke as a path: a move to its first point and a line to each other. +(: (-> Stroke (View Msg))) +(defun (stroke-view s) + (def points (list-reverse (.points s))) + (def data + (loop (:for (Tuple i p) (enumerate points)) + (:let (Tuple x y) p) + (:acc parts (vecing #"${(if (= i 0) "M" "L")} ${x} ${y}")) + => (string-join parts " "))) + (path data #:stroke (.colour s) #:stroke-width (.width s))) + +(: (-> Model (View Msg))) +(defun (view m) + (def strokes (list-reverse (match (.drawing m) + ((Some s) (cons s (.strokes m))) + (None (.strokes m))))) + (column #:spacing 8.0 #:margin 12.0 + (view-map Colour (picker-view (.colour m))) + (view-map Width (picker-view (.width m))) + (border + (on-release + (on-move + (on-press + (apply canvas 460.0 260.0 #:background "white" + (vec->array (vec-map stroke-view (list->vec strokes)))) + Down) + Drag) + (fun (x y) Up)) + #:colour "#bdc3c7") + (row #:spacing 8.0 + (button "Clear" Clear) + (text #"${(length (.strokes m))} strokes")))) + +;; Strokes to show in the picture: a wave, a loop and a line. +(: (-> string double (-> double (Tuple double double)) Stroke)) +(defun (drawn colour width f) + (Stroke (colour colour) (width width) + (points (list-reverse (list-map (fun (i) (f (cast System.Double i))) (->list (range 0 61))))))) + +(defun (main) + (def wave (drawn "steelblue" 4.0 (fun (i) (Tuple (+ 20.0 (* i 7.0)) (+ 70.0 (* 35.0 (sin (/ i 6.0)))))))) + (def loop-stroke (drawn "firebrick" 6.0 (fun (i) (Tuple (+ 330.0 (* 70.0 (cos (/ i 9.55)))) + (+ 160.0 (* 60.0 (sin (/ i 9.55)))))))) + (def line-stroke (drawn "seagreen" 2.0 (fun (i) (Tuple (+ 30.0 (* i 4.0)) (+ 230.0 (* -1.5 i)))))) + (run-app (Model (strokes (list line-stroke loop-stroke wave)) (drawing None) + (colour (Picker (label "Colour") (choices ["steelblue" "firebrick" "seagreen" "black"]) (chosen 1))) + (width (Picker (label "Width") (choices ["2" "4" "6" "10"]) (chosen 2)))) + gui-update view + #:title "Sketch" #:width 500.0 #:height 420.0)) diff --git a/src/app.bjo b/src/app.bjo index 79e4918..e89ebb8 100644 --- a/src/app.bjo +++ b/src/app.bjo @@ -101,6 +101,16 @@ (DockEdge (: Avalonia.Controls.Dock)) (TreeView (: Avalonia.Controls.TreeView (-> TreeView))) (TreeViewItem (: Avalonia.Controls.TreeViewItem (-> TreeViewItem))) + (Canvas (: Avalonia.Controls.Canvas (-> Canvas))) + (RectShape (: Avalonia.Controls.Shapes.Rectangle (-> RectShape))) + (EllipseShape (: Avalonia.Controls.Shapes.Ellipse (-> EllipseShape))) + (LineShape (: Avalonia.Controls.Shapes.Line (-> LineShape))) + (PathShape (: Avalonia.Controls.Shapes.Path (-> PathShape))) + (Geometry (: Avalonia.Media.Geometry)) + (Point (: Avalonia.Point (-> double double Point))) + (PointerEventArgs (: Avalonia.Input.PointerEventArgs)) + (IPointer (: Avalonia.Input.IPointer)) + (IInputElement (: Avalonia.Input.IInputElement)) (PropertyChange (: Avalonia.AvaloniaPropertyChangedEventArgs)) (Classes (: Avalonia.Controls.Classes)) (StyledElement (: Avalonia.StyledElement)) @@ -226,6 +236,25 @@ (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)) + (canvas-left-property (: Avalonia.Controls.Canvas.LeftProperty #:get)) + (canvas-top-property (: Avalonia.Controls.Canvas.TopProperty #:get)) + (fill-property (: Avalonia.Controls.Shapes.Shape.FillProperty #:get)) + (stroke-property (: Avalonia.Controls.Shapes.Shape.StrokeProperty #:get)) + (stroke-width-property (: Avalonia.Controls.Shapes.Shape.StrokeThicknessProperty #:get)) + (radius-x-property (: Avalonia.Controls.Shapes.Rectangle.RadiusXProperty #:get)) + (radius-y-property (: Avalonia.Controls.Shapes.Rectangle.RadiusYProperty #:get)) + (line-start-property (: Avalonia.Controls.Shapes.Line.StartPointProperty #:get)) + (line-end-property (: Avalonia.Controls.Shapes.Line.EndPointProperty #:get)) + (path-data-property (: Avalonia.Controls.Shapes.Path.DataProperty #:get)) + (parse-geometry (: Avalonia.Media.Geometry.Parse (-> string Geometry))) + (pointer-pressed-event (: Avalonia.Input.InputElement.PointerPressedEvent #:get)) + (pointer-moved-event (: Avalonia.Input.InputElement.PointerMovedEvent #:get)) + (pointer-released-event (: Avalonia.Input.InputElement.PointerReleasedEvent #:get)) + (position-in (: Avalonia.Input.PointerEventArgs.GetPosition (-> PointerEventArgs Visual Point))) + (pointer-of (: Avalonia.Input.PointerEventArgs.Pointer (-> PointerEventArgs IPointer) #:get)) + (capture! (: Avalonia.Input.IPointer.Capture (-> IPointer IInputElement void))) + (point-x (: Avalonia.Point.X (-> Point double) #:get)) + (point-y (: Avalonia.Point.Y (-> Point double) #: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))) @@ -341,6 +370,7 @@ (GridKind #t) (WrapKind #t) (DockKind #t) + (CanvasKind #t) (_ #f))) (: (-> Kind bool)) @@ -364,8 +394,23 @@ (ContextKind #f) (WrapKind #f) (DockKind #f) + (CanvasKind #f) + (RectKind #f) + (EllipseKind #f) + (LineKind #f) + (PathKind #f) (_ #t))) +;; A kind that Avalonia draws as a shape, with a fill and a stroke. +(: (-> Kind bool)) +(defun (shape? kind) + (match kind + (RectKind #t) + (EllipseKind #t) + (LineKind #t) + (PathKind #t) + (_ #f))) + ;; A kind that is on or off, as a checkbox. (: (-> Kind bool)) (defun (toggle? kind) @@ -499,6 +544,14 @@ (else None))) ;; Set after the children, by `sync-tree-choice!`. ((PropChosenKey _) None) + ((PropLeft _) (Some (cast AvaloniaProperty canvas-left-property))) + ((PropTop _) (Some (cast AvaloniaProperty canvas-top-property))) + ((PropFill _) (when-kind (shape? kind) (cast AvaloniaProperty fill-property))) + ((PropStroke _) (when-kind (shape? kind) (cast AvaloniaProperty stroke-property))) + ((PropStrokeWidth _) (when-kind (shape? kind) (cast AvaloniaProperty stroke-width-property))) + ((PropStart _ _) (when-kind (= kind LineKind) (cast AvaloniaProperty line-start-property))) + ((PropEnd _ _) (when-kind (= kind LineKind) (cast AvaloniaProperty line-end-property))) + ((PropData _) (when-kind (= kind PathKind) (cast AvaloniaProperty path-data-property))) ((PropDockSide _) (Some (cast AvaloniaProperty dock-property))) ((PropClasses _) None) ((PropDate _) (when-kind (= kind DateKind) (cast AvaloniaProperty date-property))) @@ -530,6 +583,12 @@ ((PropBackground s) (brush s)) ((PropForeground s) (brush s)) ((PropBorderColour s) (brush s)) + ((PropFill s) (brush s)) + ((PropStroke s) (brush s)) + ((PropData s) + (match (try (parse-geometry s) #:catch (System.Exception)) + ((Ok g) (Some (cast Object g))) + ((Err _) None))) ((PropShortcut s) (match (gesture s) ((Some g) (Some (cast Object g))) (None None))) ;; No date, or one that cannot be read, is none: the property is cleared. ((PropDate s) @@ -588,6 +647,14 @@ ((PropDate s) (text s)) ((PropClasses _) (cast Object 0)) ((PropChosenKey s) (text s)) + ((PropLeft d) (cast Object d)) + ((PropTop d) (cast Object d)) + ((PropFill s) (text s)) + ((PropStroke s) (text s)) + ((PropStrokeWidth d) (cast Object d)) + ((PropStart x y) (cast Object (Point. x y))) + ((PropEnd x y) (cast Object (Point. x y))) + ((PropData s) (text s)) ((PropDockSide i) (cast Object (match i (0 DockEdge.Left) (1 DockEdge.Top) (2 DockEdge.Right) (_ DockEdge.Bottom)))))) @@ -604,7 +671,9 @@ ((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) ((PropChosenKey _) 36))) + ((PropDockSide _) 34) ((PropClasses _) 35) ((PropChosenKey _) 36) + ((PropLeft _) 37) ((PropTop _) 38) ((PropFill _) 39) ((PropStroke _) 40) + ((PropStrokeWidth _) 41) ((PropStart _ _) 42) ((PropEnd _ _) 43) ((PropData _) 44))) (: (-> Control AvaloniaProperty Object void)) (defun (put! control property value) @@ -632,6 +701,9 @@ ((Tuple WrapKind (PropSpacing d)) (put! control (cast AvaloniaProperty wrap-item-spacing) (cast Object d)) (put! control (cast AvaloniaProperty wrap-line-spacing) (cast Object d))) + ((Tuple RectKind (PropRadius d)) + (put! control (cast AvaloniaProperty radius-x-property) (cast Object d)) + (put! control (cast AvaloniaProperty radius-y-property) (cast Object d))) ((Tuple _ (PropClasses names)) (def classes (classes-of (cast StyledElement control))) (classes-clear! classes) @@ -667,6 +739,9 @@ (clear (cast AvaloniaProperty wrap-item-spacing)) (clear (cast AvaloniaProperty wrap-line-spacing))) ((Tuple _ (PropClasses _)) (classes-clear! (classes-of (cast StyledElement control)))) + ((Tuple RectKind (PropRadius _)) + (clear (cast AvaloniaProperty radius-x-property)) + (clear (cast AvaloniaProperty radius-y-property))) (_ (match (slot kind p) ((Some property) (clear property)) @@ -1024,6 +1099,22 @@ (fun () (def d (range-value (cast RangeBase control))) (fun (on) (match on ((OnSlide f) (Some (f d))) (_ None)))))) +;; A canvas's pointer events come from whatever is drawn on it as well, + ;; so they are taken from any source, at the place on the canvas. + (CanvasKind + (defun (on-pointer (: event RoutedEvent) pick (: press? bool)) + (on! control event + (fun (sender e) + (def p (cast PointerEventArgs e)) + (when press? (capture! (pointer-of p) (cast IInputElement control))) + (def at (position-in p (cast Visual control))) + (fire! app live (pick (point-x at) (point-y at)))))) + (on-pointer (cast RoutedEvent pointer-pressed-event) + (fun (x y) (fun (on) (match on ((OnPress f) (Some (f x y))) (_ None)))) #t) + (on-pointer (cast RoutedEvent pointer-moved-event) + (fun (x y) (fun (on) (match on ((OnMove f) (Some (f x y))) (_ None)))) #f) + (on-pointer (cast RoutedEvent pointer-released-event) + (fun (x y) (fun (on) (match on ((OnRelease f) (Some (f x y))) (_ None)))) #f)) (TreeKind (on-event (cast RoutedEvent selection-changed-event) (fun () @@ -1083,6 +1174,16 @@ (ContextKind (cast Control (Border.))) (ContextMenuKind (cast Control (ContextMenu.))) (TreeKind (cast Control (TreeView.))) + (CanvasKind + ;; Transparent and not empty: a panel with no background takes no + ;; pointer where nothing is drawn. + (def c (cast Control (Canvas.))) + (put! c (cast AvaloniaProperty panel-background) (cast Object (parse-brush "Transparent"))) + c) + (RectKind (cast Control (RectShape.))) + (EllipseKind (cast Control (EllipseShape.))) + (LineKind (cast Control (LineShape.))) + (PathKind (cast Control (PathShape.))) (TreeNodeKind (cast Control (TreeViewItem.))) (ToggleSwitchKind (cast Control (ToggleSwitch.))) (RadioKind (cast Control (RadioButton.))) diff --git a/src/view.bjo b/src/view.bjo index 5b63129..903046b 100644 --- a/src/view.bjo +++ b/src/view.bjo @@ -23,9 +23,14 @@ menu-bar submenu menu-item menu-separator toggle-switch radio number-input date-picker auto-complete expander tree-view tree-node + canvas rect ellipse line path column row grid scroll border flow dock ;; Wrappers. keyed at paint with-context-menu tooltip at-top at-bottom at-left at-right classed + at-point on-press on-move on-release + + ;; Views of another message type. + view-map ;; Keyboard shortcuts. Key key key-gesture key-message @@ -46,6 +51,7 @@ MenuBarKind MenuItemKind SeparatorKind ContextKind ContextMenuKind ToggleSwitchKind RadioKind NumberKind DateKind AutoCompleteKind ExpanderKind WrapKind DockKind TreeKind TreeNodeKind + CanvasKind RectKind EllipseKind LineKind PathKind ColumnKind RowKind GridKind ScrollKind BorderKind))) ;; A property of a control. A kind takes only some of them; the app ignores @@ -86,7 +92,15 @@ (: 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 - (: PropChosenKey string)))) ; the key of a tree's chosen node + (: PropChosenKey string) ; the key of a tree's chosen node + (: PropLeft double) ; where a view is on its canvas + (: PropTop double) + (: PropFill string) ; the colour inside a shape + (: PropStroke string) ; the colour of a shape's line + (: PropStrokeWidth double) + (: PropStart double double) ; the ends of a line + (: PropEnd double double) + (: PropData string)))) ; a path, as "M 0 0 L 10 10 Z" ;; An event, and the message it sends. (type (: (On %m) (Union (: OnClick %m) @@ -94,7 +108,10 @@ (: OnToggle (-> bool %m)) (: OnSlide (-> double %m)) (: OnSelect (-> int %m)) - (: OnPick (-> string %m))))) + (: OnPick (-> string %m)) + (: OnPress (-> double double %m)) + (: OnMove (-> double double %m)) + (: OnRelease (-> double double %m))))) ;; One control and what is in it. ;; @@ -153,6 +170,59 @@ (defun (classed v #:rest names) (record-set v (props (vec-add (.props v) (PropClasses (array->vec names)))))) +;; The view, at `x`, `y` on its canvas, counted from the top left corner. +(: at-point (-> double double (View %m) (View %m))) +(defun (at-point x y v) + (record-set v (props (vec-add (vec-add (.props v) (PropLeft x)) (PropTop y))))) + +(: (-> (On %m) (View %m) (View %m))) +(defun (with-event on v) + (record-set v (events (vec-add (.events v) on)))) + +;; The canvas, sending `(f x y)` when a button is pressed on it, with the +;; place on the canvas. The canvas takes the pointer until the button is let +;; go, so a drag that leaves it still reaches it. Only a canvas has these. +(: on-press (-> (View %m) (-> double double %m) (View %m))) +(defun (on-press v f) (with-event (OnPress f) v)) + +;; The canvas, sending `(f x y)` each time the pointer moves over it, or +;; anywhere while a button pressed on it is held. +(: on-move (-> (View %m) (-> double double %m) (View %m))) +(defun (on-move v f) (with-event (OnMove f) v)) + +;; The canvas, sending `(f x y)` when the button pressed on it is let go. +(: on-release (-> (View %m) (-> double double %m) (View %m))) +(defun (on-release v f) (with-event (OnRelease f) v)) + +;; --------------------------------------------------------------------------- +;; Views of another message type +;; --------------------------------------------------------------------------- + +;; The view, sending `(f msg)` for each message it would send. This is how a +;; part of a program with messages of its own is put in a larger view: its +;; view is mapped with the case of the larger message that carries its own. +;; +;; (view-map Picker (date-range-view (.range m))) +(: view-map (-> (-> %a %b) (View %a) (View %b))) +(defun (view-map f v) + (: (-> (On %a) (On %b))) + (defun (map-on on) + (match on + ((OnClick m) (OnClick (f m))) + ((OnInput g) (OnInput (fun (s) (f (g s))))) + ((OnToggle g) (OnToggle (fun (b) (f (g b))))) + ((OnSlide g) (OnSlide (fun (d) (f (g d))))) + ((OnSelect g) (OnSelect (fun (i) (f (g i))))) + ((OnPick g) (OnPick (fun (k) (f (g k))))) + ((OnPress g) (OnPress (fun (x y) (f (g x y))))) + ((OnMove g) (OnMove (fun (x y) (f (g x y))))) + ((OnRelease g) (OnRelease (fun (x y) (f (g x y))))))) + (View (kind (.kind v)) + (props (.props v)) + (events (vec-map map-on (.events v))) + (children (vec-map (fun (c) (view-map f c)) (.children v))) + (key (.key v)))) + ;; The view, with `text` shown when the pointer rests on it. (: tooltip (-> (View %m) string (View %m))) (defun (tooltip v text) @@ -486,6 +556,57 @@ (defun (dock #:margin 0.0 #:rest children) (stack DockKind 0.0 margin children)) +;; A surface to draw on, `width` by `height`, with shapes and other views at +;; places of their own: a shape by its numbers, a view by `at-point`. Its +;; pointer messages come from `on-press`, `on-move` and `on-release`. +(: (-> double double (#:background string) (#:margin double) #:rest (View %m) (View %m))) +(defun (canvas width height #:background "" #:margin 0.0 #:rest children) + (container CanvasKind + (with-layout (if (string-empty? background) [] [(PropBackground background)]) + width height margin 0.0) + (array->vec children))) + +(: (-> (Vec Prop) string string double (Vec Prop))) +(defun (with-paint props fill stroke stroke-width) + (defun (with ps make c) (if (string-empty? c) ps (vec-add ps (make c)))) + (with (with (if (> stroke-width 0.0) (vec-add props (PropStrokeWidth stroke-width)) props) + PropFill fill) + PropStroke stroke)) + +;; A rectangle at `x`, `y`, `width` by `height`, with corners as round as +;; `#:radius`. +(: (-> double double double double (#:fill string) (#:stroke string) (#:stroke-width double) + (#:radius double) + (View %m))) +(defun (rect x y width height #:fill "" #:stroke "" #:stroke-width 1.0 #:radius 0.0) + (leaf RectKind + (with-paint (with-size [(PropLeft x) (PropTop y) (PropWidth width) (PropHeight height)] + PropRadius radius) + fill stroke stroke-width) + [])) + +;; An ellipse in the rectangle at `x`, `y`, `width` by `height`. +(: (-> double double double double (#:fill string) (#:stroke string) (#:stroke-width double) + (View %m))) +(defun (ellipse x y width height #:fill "" #:stroke "" #:stroke-width 1.0) + (leaf EllipseKind + (with-paint [(PropLeft x) (PropTop y) (PropWidth width) (PropHeight height)] + fill stroke stroke-width) + [])) + +;; A line from `x1`, `y1` to `x2`, `y2`. +(: (-> double double double double (#:stroke string) (#:stroke-width double) (View %m))) +(defun (line x1 y1 x2 y2 #:stroke "black" #:stroke-width 1.0) + (leaf LineKind + (with-paint [(PropStart x1 y1) (PropEnd x2 y2)] "" stroke stroke-width) + [])) + +;; A path, written as SVG writes one: "M 10 10 L 50 10 L 30 40 Z" is a +;; triangle. Its numbers are places on the canvas. +(: (-> string (#:fill string) (#:stroke string) (#:stroke-width double) (View %m))) +(defun (path data #:fill "" #:stroke "" #:stroke-width 1.0) + (leaf PathKind (with-paint [(PropData data)] fill stroke stroke-width) [])) + ;; A view that scrolls when it is taller than the space it has. (: (-> (View %m) (#:width double) (#:height double) (#:margin double) (View %m))) (defun (scroll child #:width 0.0 #:height 0.0 #:margin 0.0) diff --git a/tests/canvas.bjo b/tests/canvas.bjo new file mode 100644 index 0000000..4215c35 --- /dev/null +++ b/tests/canvas.bjo @@ -0,0 +1,176 @@ +;; 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/canvas.bjo — views of another message type, and canvases. +;; +;; bjo run tests/canvas.bjo +;; +;; No window is opened. Pointer events are made as Avalonia makes them, and +;; raised on the canvas and on a shape on it. + +(import (std prelude)) +(import (std simpletest)) +(import (bjogui view)) +(import (bjogui app)) + +(import/class + (Object (: System.Object)) + (ClrString (: System.String)) + (Control (: Avalonia.Controls.Control)) + (AvaloniaObject (: Avalonia.AvaloniaObject)) + (Layoutable (: Avalonia.Layout.Layoutable)) + (Visual (: Avalonia.Visual)) + (Interactive (: Avalonia.Interactivity.Interactive)) + (RoutedEvent (: Avalonia.Interactivity.RoutedEvent)) + (RoutedEventArgs (: Avalonia.Interactivity.RoutedEventArgs (-> RoutedEvent RoutedEventArgs))) + (TextBox (: Avalonia.Controls.TextBox)) + (Shape (: Avalonia.Controls.Shapes.Shape)) + (LineShape (: Avalonia.Controls.Shapes.Line)) + (PathShape (: Avalonia.Controls.Shapes.Path)) + (IBrush (: Avalonia.Media.IBrush)) + (Geometry (: Avalonia.Media.Geometry)) + (Point (: Avalonia.Point (-> double double Point))) + (Pointer (: Avalonia.Input.Pointer (-> int PointerType bool Pointer))) + (PointerType (: Avalonia.Input.PointerType)) + (IPointer (: Avalonia.Input.IPointer)) + (PointProps (: Avalonia.Input.PointerPointProperties (-> PointProps))) + (KeyModifiers (: Avalonia.Input.KeyModifiers)) + (MouseButton (: Avalonia.Input.MouseButton)) + (Pressed (: Avalonia.Input.PointerPressedEventArgs + (-> Object IPointer Visual Point ulong PointProps KeyModifiers int Pressed))) + (Moved (: Avalonia.Input.PointerEventArgs + (-> RoutedEvent Object IPointer Visual Point ulong PointProps KeyModifiers Moved))) + (Released (: Avalonia.Input.PointerReleasedEventArgs + (-> Object IPointer Visual Point ulong PointProps KeyModifiers MouseButton Released)))) + +(import/extern + (click-event (: Avalonia.Controls.Button.ClickEvent #:get)) + (moved-event (: Avalonia.Input.InputElement.PointerMovedEvent #:get)) + (raise-event (: Avalonia.Interactivity.Interactive.RaiseEvent (-> Interactive RoutedEventArgs void))) + (box-text! (: Avalonia.Controls.TextBox.Text (-> TextBox string void) #:set)) + (left-of* (: Avalonia.Controls.Canvas.GetLeft (-> AvaloniaObject double))) + (top-of* (: Avalonia.Controls.Canvas.GetTop (-> AvaloniaObject double))) + (fill-of (: Avalonia.Controls.Shapes.Shape.Fill (-> Shape IBrush) #:get)) + (stroke-width-of (: Avalonia.Controls.Shapes.Shape.StrokeThickness (-> Shape double) #:get)) + (line-end (: Avalonia.Controls.Shapes.Line.EndPoint (-> LineShape Point) #:get)) + (path-data (: Avalonia.Controls.Shapes.Path.Data (-> PathShape Geometry) #:get)) + (point-x (: Avalonia.Point.X (-> Point double) #:get)) + (width-of (: Avalonia.Layout.Layoutable.Width (-> Layoutable double) #:get))) + +;; --------------------------------------------------------------------------- +;; A component: a counter with a name, which knows nothing of who uses it +;; --------------------------------------------------------------------------- + +(type (: CounterMsg (Union Inc (: Named string)))) +(type (: Counter (Record (: count int) (: name string)))) + +(: (-> CounterMsg Counter Counter)) +(defun (counter-update msg c) + (match msg + (Inc (record-set c (count (+ (.count c) 1)))) + ((Named s) (record-set c (name s))))) + +(: (-> Counter (View CounterMsg))) +(defun (counter-view c) + (row (button "+" Inc) (text-input (.name c) Named) (text #"${(.count c)}"))) + +;; Two of them, each a case of the larger message. +(type (: PairMsg (Union (: Left CounterMsg) (: Right CounterMsg)))) +(type (: Pair (Record (: left Counter) (: right Counter)))) + +(: (-> PairMsg Pair Pair)) +(defun (pair-update msg p) + (match msg + ((Left m) (record-set p (left (counter-update m (.left p))))) + ((Right m) (record-set p (right (counter-update m (.right p))))))) + +(: (-> Pair (View PairMsg))) +(defun (pair-view p) + (column (view-map Left (counter-view (.left p))) (view-map Right (counter-view (.right p))))) + +;; And the whole app: the pair, mapped once more, and a canvas. +(type (: Msg (Union (: OfPair PairMsg) (: Down double double) (: Drag double double) + (: Up double double)))) +(type (: Model (Record (: pair Pair) (: log (List string))))) + +(: (-> Msg Model (Tuple Model (List (Event Msg))))) +(defun (gui-update msg m) + (defun (log s) (record-set m (log (cons s (.log m))))) + (next (match msg + ((OfPair pm) (record-set m (pair (pair-update pm (.pair m))))) + ((Down x y) (log #"down ${x} ${y}")) + ((Drag x y) (log #"drag ${x} ${y}")) + ((Up x y) (log #"up ${x} ${y}"))))) + +(: (-> Model (View Msg))) +(defun (view m) + (column + (view-map OfPair (pair-view (.pair m))) ; 0 + (on-release + (on-move + (on-press + (canvas 200.0 100.0 #:background "white" ; 1 + (rect 10.0 20.0 30.0 40.0 #:fill "red" #:stroke "black" #:radius 4.0) ; 1 0 + (ellipse 50.0 10.0 20.0 20.0 #:fill "blue") ; 1 1 + (line 0.0 0.0 80.0 60.0 #:stroke "green" #:stroke-width 3.0) ; 1 2 + (path "M 100 10 L 140 10 L 120 40 Z" #:fill "orange") ; 1 3 + (at-point 150.0 60.0 (text "hi"))) ; 1 4 + Down) + Drag) + Up))) + +(defun (main) + (def app (start-app (Model (pair (Pair (left (Counter (count 0) (name ""))) + (right (Counter (count 0) (name ""))))) + (log '())) + gui-update view (fun (c) unit))) + (defun (ctl path) (app-control-at app path)) + (defun (click path) + (raise-event (cast Interactive (ctl path)) (RoutedEventArgs. (cast RoutedEvent click-event)))) + (defun (both) (.pair (app-model app))) + (defun (left-of (: c Control)) (left-of* (cast AvaloniaObject c))) + (defun (top-of (: c Control)) (top-of* (cast AvaloniaObject c))) + + ;; --- view-map --------------------------------------------------------------------- + (click '(0 1 0)) + (expect "a click in a mapped view, mapped twice" (.count (.right (both))) 1) + (expect "and not the other one" (.count (.left (both))) 0) + (box-text! (cast TextBox (ctl '(0 0 1))) "Ada") + (expect "a mapped function message" (.name (.left (both))) "Ada") + (click '(0 0 0)) + (click '(0 0 0)) + (expect "the view shows the component's model" (.count (.left (both))) 2) + + ;; --- shapes ----------------------------------------------------------------------- + (def r (ctl '(1 0))) + (expect "a rectangle's place" (list (left-of r) (top-of r)) '(10.0 20.0)) + (expect "its size" (width-of (cast Layoutable r)) 30.0) + (expect "its fill" (.ToString (cast Object (fill-of (cast Shape r)))) "Red") + (expect "a line's end" (point-x (line-end (cast LineShape (ctl '(1 2))))) 80.0) + (expect "its width" (stroke-width-of (cast Shape (ctl '(1 2)))) 3.0) + (expect "a path's geometry" (eq? (cast Object (path-data (cast PathShape (ctl '(1 3))))) (cast Object 0)) #f) + (expect "a view at a point" (left-of (ctl '(1 4))) 150.0) + + ;; --- pointer ---------------------------------------------------------------------- + (def canvas-control (ctl '(1))) + (def pointer (cast IPointer (Pointer. 1 PointerType.Mouse #t))) + (defun (at x y) (Point. x y)) + (raise-event (cast Interactive canvas-control) + (cast RoutedEventArgs (Pressed. (cast Object canvas-control) pointer (cast Visual canvas-control) + (at 30.0 40.0) 0UL (PointProps.) KeyModifiers.None 1))) + (expect "a press on the canvas" (.log (app-model app)) '("down 30 40")) + ;; On a shape, the event comes from the shape, and the place is still the + ;; canvas's. + (raise-event (cast Interactive r) + (cast RoutedEventArgs (Moved. (cast RoutedEvent moved-event) (cast Object r) pointer + (cast Visual canvas-control) (at 35.0 45.0) 1UL (PointProps.) + KeyModifiers.None))) + (expect "a move over a shape" (list-ref (.log (app-model app)) 0) "drag 35 45") + (raise-event (cast Interactive canvas-control) + (cast RoutedEventArgs (Released. (cast Object canvas-control) pointer (cast Visual canvas-control) + (at 36.0 46.0) 2UL (PointProps.) KeyModifiers.None + MouseButton.Left))) + (expect "a release" (list-ref (.log (app-model app)) 0) "up 36 46") + + (println "bjogui canvas ok"))