bjogui/src/draw.bjo

438 lines
20 KiB
Text
Raw Normal View History

;; 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/src/draw.bjo — the module (bjogui draw).
;;
;; Drawing with a pen: what a component made by `component` in (bjogui view)
;; draws itself with. Each function draws at once, on top of what is drawn
;; before it, in the component's own coordinates: 0, 0 is its top left
;; corner, x grows to the right and y down, in pixels.
;;
;; A colour is a string, as everywhere in bjogui: a name such as "red", or
;; "#rgb", "#rrggbb" or "#aarrggbb". The empty string, and a colour that
;; cannot be read, draw nothing. An angle is in radians, as `sin` and `cos`
;; of (std maths) take it, and grows clockwise from the right, because y
;; grows down.
;;
;; The pen is Avalonia's DrawingContext, so a drawing is vectors: it is sharp
;; at any scale of the screen.
(export
Pen
;; Shapes.
fill-rect stroke-rect fill-circle stroke-circle fill-ellipse stroke-ellipse
stroke-line stroke-polyline fill-polygon stroke-arc fill-path stroke-path
;; Text and pictures.
draw-text text-size draw-image
;; What is drawn inside a function, moved, turned, cut or faded.
translated scaled rotated clipped faded
;; For (bjogui app): a drawing, as the picture an Image shows.
drawing-image)
(import (std prelude))
(import (std maths))
(import (std interop))
(import (std mutable map))
(import/class
(Object (: System.Object))
(Disposable (: System.IDisposable))
(Culture (: System.Globalization.CultureInfo))
(IBrush (: Avalonia.Media.IBrush))
(IPen (: Avalonia.Media.IPen))
(AvPen (: Avalonia.Media.Pen (-> IBrush double IDashStyle LineCap LineJoin double AvPen)))
(IDashStyle (: Avalonia.Media.IDashStyle))
(DashStyle (: Avalonia.Media.DashStyle (-> (Seq double) double DashStyle)))
(LineCap (: Avalonia.Media.PenLineCap))
(LineJoin (: Avalonia.Media.PenLineJoin))
(BoxShadows (: Avalonia.Media.BoxShadows))
(Rect (: Avalonia.Rect (-> double double double double Rect)))
(Point (: Avalonia.Point (-> double double Point)))
(Size (: Avalonia.Size (-> double double Size)))
(Matrix (: Avalonia.Matrix))
(Geometry (: Avalonia.Media.Geometry))
(RectGeometry (: Avalonia.Media.RectangleGeometry (-> Rect double double RectGeometry)))
(StreamGeometry (: Avalonia.Media.StreamGeometry (-> StreamGeometry)))
(GeometryContext (: Avalonia.Media.StreamGeometryContext))
(Sweep (: Avalonia.Media.SweepDirection))
(DrawingGroup (: Avalonia.Media.DrawingGroup (-> DrawingGroup)))
(DrawingImage (: Avalonia.Media.DrawingImage (-> Drawing DrawingImage)))
(Drawing (: Avalonia.Media.Drawing))
(DrawingContext (: Avalonia.Media.DrawingContext))
(PushedState (: Avalonia.Media.DrawingContext.PushedState))
(IImage (: Avalonia.Media.IImage))
(Bitmap (: Avalonia.Media.Imaging.Bitmap (-> string Bitmap)))
(FontFamily (: Avalonia.Media.FontFamily (-> string FontFamily)))
(FontStyle (: Avalonia.Media.FontStyle))
(FontWeight (: Avalonia.Media.FontWeight))
(FontStretch (: Avalonia.Media.FontStretch))
(Typeface (: Avalonia.Media.Typeface (-> FontFamily FontStyle FontWeight FontStretch Typeface)))
(FlowDirection (: Avalonia.Media.FlowDirection))
(FormattedText (: Avalonia.Media.FormattedText
(-> string Culture FlowDirection Typeface double IBrush FormattedText))))
(import/extern
(parse-brush (: Avalonia.Media.Brush.Parse (-> string IBrush)))
(parse-geometry (: Avalonia.Media.Geometry.Parse (-> string Geometry)))
(open-group (: Avalonia.Media.DrawingGroup.Open (-> DrawingGroup DrawingContext)))
(bounds-of (: Avalonia.Media.Drawing.GetBounds (-> Drawing Rect)))
(rect-x (: Avalonia.Rect.X (-> Rect double) #:get))
(rect-y (: Avalonia.Rect.Y (-> Rect double) #:get))
(open-geometry (: Avalonia.Media.StreamGeometry.Open (-> StreamGeometry GeometryContext)))
(begin-figure! (: Avalonia.Media.StreamGeometryContext.BeginFigure (-> GeometryContext Point bool void)))
(line-to! (: Avalonia.Media.StreamGeometryContext.LineTo (-> GeometryContext Point void)))
(arc-to! (: Avalonia.Media.StreamGeometryContext.ArcTo
(-> GeometryContext Point Size double bool Sweep void)))
(end-figure! (: Avalonia.Media.StreamGeometryContext.EndFigure (-> GeometryContext bool void)))
(dc-rect (: Avalonia.Media.DrawingContext.DrawRectangle
(-> DrawingContext IBrush IPen Rect double double BoxShadows void)))
(dc-ellipse (: Avalonia.Media.DrawingContext.DrawEllipse
(-> DrawingContext IBrush IPen Point double double void)))
(dc-line (: Avalonia.Media.DrawingContext.DrawLine (-> DrawingContext IPen Point Point void)))
(dc-geometry (: Avalonia.Media.DrawingContext.DrawGeometry
(-> DrawingContext IBrush IPen Geometry void)))
(dc-text (: Avalonia.Media.DrawingContext.DrawText (-> DrawingContext FormattedText Point void)))
(dc-image (: Avalonia.Media.DrawingContext.DrawImage (-> DrawingContext IImage Rect void)))
(push-transform (: Avalonia.Media.DrawingContext.PushTransform (-> DrawingContext Matrix PushedState)))
(push-clip (: Avalonia.Media.DrawingContext.PushClip (-> DrawingContext Rect PushedState)))
(push-opacity (: Avalonia.Media.DrawingContext.PushOpacity (-> DrawingContext double PushedState)))
(pop! (: Avalonia.Media.DrawingContext.PushedState.Dispose (-> PushedState void)))
(translation (: Avalonia.Matrix.CreateTranslation (-> double double Matrix)))
(scaling (: Avalonia.Matrix.CreateScale (-> double double Matrix)))
(rotation (: Avalonia.Matrix.CreateRotation (-> double Point Matrix)))
(text-width (: Avalonia.Media.FormattedText.Width (-> FormattedText double) #:get))
(text-height (: Avalonia.Media.FormattedText.Height (-> FormattedText double) #:get))
(max-text-width! (: Avalonia.Media.FormattedText.MaxTextWidth (-> FormattedText double void) #:set))
(default-family (: Avalonia.Media.FontFamily.Default #:get))
(invariant (: System.Globalization.CultureInfo.InvariantCulture #:get))
(dispose! (: System.IDisposable.Dispose (-> Disposable void))))
;; ---------------------------------------------------------------------------
;; The pen
;; ---------------------------------------------------------------------------
;; What a component draws with, for as long as its `#:draw` function runs.
(type (: Pen #:opaque (Record (: dc DrawingContext))))
;; ---------------------------------------------------------------------------
;; What is read once
;; ---------------------------------------------------------------------------
;; Colours, paths, pictures and fonts are read once each and kept, because a
;; component draws them again on each change. A table that grows past its
;; limit is emptied, so a program that makes new paths all the time does not
;; keep them all.
(def cache-limit 2000)
(: brushes (MutableMap string (Option IBrush)))
(def brushes (mutablemap))
(: geometries (MutableMap string (Option Geometry)))
(def geometries (mutablemap))
(: bitmaps (MutableMap string (Option Bitmap)))
(def bitmaps (mutablemap))
(: typefaces (MutableMap string Typeface))
(def typefaces (mutablemap))
(: (-> (MutableMap string %a) string (-> %a) %a))
(defun (cached table key make)
(match (mutablemap-try-ref table key)
((Some v) v)
(None
(when (>= (mutablemap-length table) cache-limit) (mutablemap-clear! table))
(def v (make))
(mutablemap-set! table key v)
v)))
(: (-> string (Option IBrush)))
(defun (brush colour)
(if (string-empty? colour)
None
(cached brushes colour
(fun ()
(match (try (parse-brush colour) #:catch (System.Exception))
((Ok b) (Some b))
((Err _) None))))))
(: (-> string (Option Geometry)))
(defun (geometry data)
(cached geometries data
(fun ()
(match (try (parse-geometry data) #:catch (System.Exception))
((Ok g) (Some g))
((Err _) None)))))
(: (-> string (Option Bitmap)))
(defun (bitmap path)
(cached bitmaps path
(fun ()
(match (try (Bitmap. path) #:catch (System.Exception))
((Ok b) (Some b))
((Err _) None)))))
(: (-> string bool Typeface))
(defun (typeface font bold?)
(cached typefaces #"${font}/${bold?}"
(fun ()
(Typeface. (if (string-empty? font) default-family (FontFamily. font))
FontStyle.Normal (if bold? FontWeight.Bold FontWeight.Normal) FontStretch.Normal))))
;; A line of `width`, in the brush. `cap` is "flat", "round" or "square".
;; `dash` is the lengths of the dashes and the gaps between them, in widths
;; of the line, in turn; empty for a solid line.
(: (-> IBrush double string (Vec double) IPen))
(defun (line-pen b width cap dash)
(def c (match cap ("round" LineCap.Round) ("square" LineCap.Square) (_ LineCap.Flat)))
(def d (if (vec-empty? dash)
(: (clr-default) IDashStyle)
(cast IDashStyle (DashStyle. (cast (Seq double) (: (vec->array dash) (Array double))) 0.0))))
(cast IPen (AvPen. b width d c LineJoin.Round 10.0)))
(: (-> double double Point))
(defun (pt x y) (Point. x y))
(def no-brush (: (clr-default) IBrush))
(def no-pen (: (clr-default) IPen))
;; ---------------------------------------------------------------------------
;; Shapes
;; ---------------------------------------------------------------------------
;; The rectangle at `x`, `y`, `w` by `h`, filled, with corners as round as
;; `#:radius`.
;; A rectangle with round corners is drawn as a geometry, because a drawing
;; that is kept, as a component's is, drops the corners of DrawRectangle.
(: (-> Pen IBrush IPen double double double double double void))
(defun (rect! pen b p x y w h radius)
(if (> radius 0.0)
(dc-geometry (.dc pen) b p (cast Geometry (RectGeometry. (Rect. x y w h) radius radius)))
(dc-rect (.dc pen) b p (Rect. x y w h) 0.0 0.0 (clr-default))))
(: fill-rect (-> Pen double double double double string (#:radius double) void))
(defun (fill-rect pen x y w h colour #:radius 0.0)
(match (brush colour)
((Some b) (rect! pen b no-pen x y w h radius))
(None unit)))
;; The line around a rectangle. The line is centred on the rectangle's edge.
(: stroke-rect (-> Pen double double double double string (#:width double) (#:radius double)
(#:dash (Vec double))
void))
(defun (stroke-rect pen x y w h colour #:width 1.0 #:radius 0.0 #:dash [])
(match (brush colour)
((Some b) (rect! pen no-brush (line-pen b width "flat" dash) x y w h radius))
(None unit)))
;; The circle around `cx`, `cy`, of radius `r`, filled.
(: fill-circle (-> Pen double double double string void))
(defun (fill-circle pen cx cy r colour)
(fill-ellipse pen (- cx r) (- cy r) (* 2.0 r) (* 2.0 r) colour))
;; The line around a circle.
(: stroke-circle (-> Pen double double double string (#:width double) (#:dash (Vec double)) void))
(defun (stroke-circle pen cx cy r colour #:width 1.0 #:dash [])
(stroke-ellipse pen (- cx r) (- cy r) (* 2.0 r) (* 2.0 r) colour #:width width #:dash dash))
;; The ellipse in the rectangle at `x`, `y`, `w` by `h`, filled.
(: fill-ellipse (-> Pen double double double double string void))
(defun (fill-ellipse pen x y w h colour)
(match (brush colour)
((Some b) (dc-ellipse (.dc pen) b no-pen (pt (+ x (/ w 2.0)) (+ y (/ h 2.0))) (/ w 2.0) (/ h 2.0)))
(None unit)))
;; The line around an ellipse.
(: stroke-ellipse (-> Pen double double double double string (#:width double) (#:dash (Vec double))
void))
(defun (stroke-ellipse pen x y w h colour #:width 1.0 #:dash [])
(match (brush colour)
((Some b) (dc-ellipse (.dc pen) no-brush (line-pen b width "flat" dash)
(pt (+ x (/ w 2.0)) (+ y (/ h 2.0))) (/ w 2.0) (/ h 2.0)))
(None unit)))
;; A line from `x1`, `y1` to `x2`, `y2`. `#:cap` is how its ends look:
;; "flat", "round" or "square".
(: stroke-line (-> Pen double double double double string (#:width double) (#:cap string)
(#:dash (Vec double))
void))
(defun (stroke-line pen x1 y1 x2 y2 colour #:width 1.0 #:cap "flat" #:dash [])
(match (brush colour)
((Some b) (dc-line (.dc pen) (line-pen b width cap dash) (pt x1 y1) (pt x2 y2)))
(None unit)))
;; The geometry through `points`, closed when `closed?`.
(: (-> (Vec (Tuple double double)) bool bool (Option Geometry)))
(defun (through points filled? closed?)
(if (< (vec-length points) 2)
None
(begin
(def g (StreamGeometry.))
(def ctx (open-geometry g))
(def (Tuple x0 y0) (vec-ref points 0))
(begin-figure! ctx (pt x0 y0) filled?)
(loop (:for (Tuple x y) (vec-drop points 1)) (:do (line-to! ctx (pt x y))))
(end-figure! ctx closed?)
(dispose! (cast Disposable ctx))
(Some (cast Geometry g)))))
;; Lines from each of `points` to the next.
(: stroke-polyline (-> Pen (Vec (Tuple double double)) string (#:width double) (#:cap string)
(#:dash (Vec double))
void))
(defun (stroke-polyline pen points colour #:width 1.0 #:cap "flat" #:dash [])
(match (Tuple (brush colour) (through points #f #f))
((Tuple (Some b) (Some g)) (dc-geometry (.dc pen) no-brush (line-pen b width cap dash) g))
(_ unit)))
;; The polygon with `points` at its corners, filled.
(: fill-polygon (-> Pen (Vec (Tuple double double)) string void))
(defun (fill-polygon pen points colour)
(match (Tuple (brush colour) (through points #t #t))
((Tuple (Some b) (Some g)) (dc-geometry (.dc pen) b no-pen g))
(_ unit)))
;; A piece of the circle around `cx`, `cy` of radius `r`: from the angle
;; `from`, `sweep` radians on, clockwise when `sweep` is more than 0. A sweep
;; of a whole turn or more is the whole circle.
(: stroke-arc (-> Pen double double double double double string (#:width double) (#:cap string)
(#:dash (Vec double))
void))
(defun (stroke-arc pen cx cy r from sweep colour #:width 1.0 #:cap "flat" #:dash [])
(def turn (* 2.0 3.141592653589793))
(defun (at a) (pt (+ cx (* r (cos a))) (+ cy (* r (sin a)))))
(match (brush colour)
((Some b)
(cond ((>= (abs sweep) (- turn 1e-9))
(stroke-circle pen cx cy r colour #:width width #:dash dash))
((= sweep 0.0) unit)
(else
(def g (StreamGeometry.))
(def ctx (open-geometry g))
(begin-figure! ctx (at from) #f)
(arc-to! ctx (at (+ from sweep)) (Size. r r) 0.0 (> (abs sweep) (/ turn 2.0))
(if (> sweep 0.0) Sweep.Clockwise Sweep.CounterClockwise))
(end-figure! ctx #f)
(dispose! (cast Disposable ctx))
(dc-geometry (.dc pen) no-brush (line-pen b width cap dash) (cast Geometry g)))))
(None unit)))
;; The path `data`, written as SVG writes one: "M 10 10 L 50 10 L 30 40 Z"
;; is a triangle. Filled. A path that cannot be read draws nothing.
(: fill-path (-> Pen string string void))
(defun (fill-path pen data colour)
(match (Tuple (brush colour) (geometry data))
((Tuple (Some b) (Some g)) (dc-geometry (.dc pen) b no-pen g))
(_ unit)))
;; The line along the path `data`.
(: stroke-path (-> Pen string string (#:width double) (#:cap string) (#:dash (Vec double)) void))
(defun (stroke-path pen data colour #:width 1.0 #:cap "flat" #:dash [])
(match (Tuple (brush colour) (geometry data))
((Tuple (Some b) (Some g)) (dc-geometry (.dc pen) no-brush (line-pen b width cap dash) g))
(_ unit)))
;; ---------------------------------------------------------------------------
;; Text and pictures
;; ---------------------------------------------------------------------------
(: (-> string IBrush double bool string double FormattedText))
(defun (formatted s b size bold? font max-width)
(def t (FormattedText. s invariant FlowDirection.LeftToRight (typeface font bold?) size b))
(when (> max-width 0.0) (max-text-width! t max-width))
t)
;; The text `s` with its top at `y`. `#:align` says where `x` is on it:
;; "left", "centre" or "right". `#:font` is the name of a font, "" for the
;; theme's. With `#:max-width`, the text breaks its lines at that width.
(: draw-text (-> Pen string double double string (#:size double) (#:bold bool) (#:font string)
(#:align string) (#:max-width double)
void))
(defun (draw-text pen s x y colour #:size 14.0 #:bold #f #:font "" #:align "left" #:max-width 0.0)
(match (brush colour)
((Some b)
(def t (formatted s b size bold font max-width))
(def left (match align
("centre" (- x (/ (text-width t) 2.0)))
("right" (- x (text-width t)))
(_ x)))
(dc-text (.dc pen) t (pt left y)))
(None unit)))
;; The width and the height that `draw-text` gives `s`, with the same
;; options. For a component to lay out its text.
(: text-size (-> string (#:size double) (#:bold bool) (#:font string) (#:max-width double)
(Tuple double double)))
(defun (text-size s #:size 14.0 #:bold #f #:font "" #:max-width 0.0)
(def t (formatted s (parse-brush "black") size bold font max-width))
(Tuple (text-width t) (text-height t)))
;; The picture in the file at `path`, in the rectangle at `x`, `y`, `w` by
;; `h`. A file that cannot be read draws nothing.
(: draw-image (-> Pen string double double double double void))
(defun (draw-image pen path x y w h)
(match (bitmap path)
((Some b) (dc-image (.dc pen) (cast IImage b) (Rect. x y w h)))
(None unit)))
;; ---------------------------------------------------------------------------
;; Moved, turned, cut and faded
;; ---------------------------------------------------------------------------
(: (-> PushedState (-> void) void))
(defun (inside state body)
(body)
(pop! state))
;; What `body` draws, moved by `dx`, `dy`.
(: translated (-> Pen double double (-> void) void))
(defun (translated pen dx dy body)
(inside (push-transform (.dc pen) (translation dx dy)) body))
;; What `body` draws, `sx` times as wide and `sy` times as tall, from 0, 0.
(: scaled (-> Pen double double (-> void) void))
(defun (scaled pen sx sy body)
(inside (push-transform (.dc pen) (scaling sx sy)) body))
;; What `body` draws, turned by `angle` around `cx`, `cy`.
(: rotated (-> Pen double double double (-> void) void))
(defun (rotated pen angle cx cy body)
(inside (push-transform (.dc pen) (rotation angle (pt cx cy))) body))
;; What `body` draws, only inside the rectangle at `x`, `y`, `w` by `h`.
(: clipped (-> Pen double double double double (-> void) void))
(defun (clipped pen x y w h body)
(inside (push-clip (.dc pen) (Rect. x y w h)) body))
;; What `body` draws, at `opacity`: 0 is not seen, 1 is as drawn.
(: faded (-> Pen double (-> void) void))
(defun (faded pen opacity body)
(inside (push-opacity (.dc pen) opacity) body))
;; ---------------------------------------------------------------------------
;; For the app
;; ---------------------------------------------------------------------------
;; What `draw` draws on `w` by `h`, as a picture that an Image shows, and
;; where the Image goes for 0, 0 to be the top left corner. None when there
;; is no room.
;;
;; A DrawingImage moves the top left corner of what is drawn to its own 0, 0,
;; so a drawing that starts at 10, 10, or at -5, 0, would move. The place of
;; the bounds' corner is given back, and the Image is put there, which moves
;; the drawing back. What is drawn is cut to `w` by `h`, so what is outside
;; costs nothing to show.
(: drawing-image (-> (-> Pen void) double double (Option (Tuple Object double double))))
(defun (drawing-image draw w h)
(if (or (<= w 0.0) (<= h 0.0))
None
(begin
(def group (DrawingGroup.))
(def dc (open-group group))
(def pen (Pen (dc dc)))
(clipped pen 0.0 0.0 w h (fun () (draw pen)))
(dispose! (cast Disposable dc))
(def b (bounds-of (cast Drawing group)))
(Some (Tuple (cast Object (DrawingImage. (cast Drawing group))) (rect-x b) (rect-y b))))))