GUI example

This commit is contained in:
Tony Garnock-Jones 2018-05-04 16:57:22 +01:00
parent 46ddb47050
commit d053edc101
8 changed files with 1053 additions and 0 deletions

View File

@ -0,0 +1,22 @@
# Simple GUI experiments using Syndicate
This directory contains UI experiments using
[Syndicate](http://syndicate-lang.org/) and its OpenGL 2D support.
Files:
- `gui.rkt`: Main entry point. Run `racket gui.rkt` to run the demo.
- `layout/`: A simple widget layout engine, loosely inspired by TeX's boxes-and-glue model.
- `sizing.rkt`: TeX-like "dimensions", including "fills"
- `layout.rkt`: Uses "dimensions" to specify "table layouts",
which are then realized in terms of specified rectangle
coordinates
- `hsv.rkt`: Utility for converting HSV colors to RGB.
Screenshot:
![Syndicate GUI screenshot](syndicate-gui-snapshot.png)

View File

@ -0,0 +1,653 @@
#lang imperative-syndicate
(require racket/set)
(require data/order)
(require srfi/19)
(require (prefix-in i: 2htdp/image))
(require (prefix-in p: pict))
(require syndicate-gl/affine)
(require "layout/main.rkt")
(require "hsv.rkt")
(require imperative-syndicate/bag)
(require imperative-syndicate/pattern)
(require/activate imperative-syndicate/drivers/gl-2d)
;;---------------------------------------------------------------------------
(define theme-font (make-parameter "Roboto"))
(define theme-font-size (make-parameter 16))
(define theme-title-font (make-parameter "Roboto Condensed"))
(define theme-title-font-size (make-parameter 20))
(define theme-title-font-color (make-parameter "white"))
(define theme-title-bar-color (make-parameter (hsv->color 260 1 0.6)))
(define theme-title-bar-selected-color (make-parameter (hsv->color 260 1 1)))
(define theme-title-bar-height (make-parameter 48))
(define theme-button-background-color (make-parameter (hsv->color 30 0.9 1)))
(define theme-button-color (make-parameter "white"))
(define theme-button-x-padding (make-parameter 40))
(define theme-button-y-padding (make-parameter 24))
(define theme-button-min-height (make-parameter 48))
(define theme-window-border-width (make-parameter 8))
(define theme-window-resize-corner-size (make-parameter 16))
(define theme-menu-item-color (make-parameter "white"))
(define theme-menu-item-background-color (make-parameter (hsv->color 240 1 0.8)))
(define theme-menu-item-selected-background-color (make-parameter (hsv->color 345 1 1)))
(define theme-menu-item-padding (make-parameter 16))
(define theme-menu-separator-width (make-parameter 2))
(define theme-menu-separator-color (make-parameter "white"))
;;---------------------------------------------------------------------------
(define (*width x)
(cond [(i:image? x) (i:image-width x)]
[(p:pict? x) (p:pict-width x)]
[else (error '*width "Neither an image nor a pict: ~v" x)]))
(define (*height x)
(cond [(i:image? x) (i:image-height x)]
[(p:pict? x) (p:pict-height x)]
[else (error '*height "Neither an image nor a pict: ~v" x)]))
(define (costume #:id [id #f] #:parent [parent-id #f] #:coordinate-map-id [coordinate-map-id #f] i)
(define iw (*width i))
(define ih (*height i))
(define iaspect (/ iw ih))
(lambda (z rect)
(match-define (rectangle left top sw sh) rect)
(define saspect (if (and (positive? sw) (positive? sh)) (/ sw sh) 1))
(define-values (scale-w scale-h translate-x translate-y)
(if (> saspect iaspect)
(let ((scale-h (/ sw iaspect)))
(values sw scale-h 0 (/ (- sh scale-h) 2)))
(let ((scale-w (* sh iaspect)))
(values scale-w sh (/ (- sw scale-w) 2) 0))))
(sprite #:id (or id (gensym 'costume))
#:parent parent-id
z
`((translate ,left ,top)
(push-matrix (scale ,sw ,sh)
,@(if id
`((touchable ,id ,in-unit-square?))
`())
,@(if coordinate-map-id
`((coordinate-map ,coordinate-map-id))
`())
(texture ,i
,(- (/ translate-x scale-w))
,(- (/ translate-y scale-h))
,(/ sw scale-w)
,(/ sh scale-h)
))
(render-children)))))
(define (draggable-mixin touching? x y id-to-raise)
(define (idle)
(react (stop-when #:when (touching?)
(message (mouse-event 'left-down (mouse-state $mx $my _ _ _)))
(dragging (- mx (x)) (- my (y))))))
(define (dragging dx dy)
(when id-to-raise (send! (raise-widget id-to-raise)))
(react (on (message (mouse-event 'motion (mouse-state $mx $my _ _ _)))
(x (- mx dx))
(y (- my dy)))
(stop-when (message (mouse-event 'left-up _)) (idle))
(stop-when (message (mouse-event 'leave _)) (idle))))
(idle))
(spawn #:name 'root-window
(define c (costume #:id 'root (i:bitmap "oakura-beach-20081225.jpg")))
(define/query-value touching? #f (touching 'root) #t)
(on #:when (touching?) (message (mouse-event 'right-down (mouse-state $x $y _ _ _)))
(send! (pop-up-menu-trigger 'system-menu x y 0 0.5 'right-up)))
(during (window $width $height)
(assert (c 0 (rectangle 0 0 width height)))))
(define (button-underlay i)
(define w (+ (*width i) (theme-button-x-padding)))
(define h (max (+ (*height i) (theme-button-y-padding)) (theme-button-min-height)))
(i:rectangle w h "solid" (theme-button-background-color)))
;;---------------------------------------------------------------------------
;; Protocol: Layout.
;;---------------------------------------------------------------------------
;; Roles:
;;
;; Layout Solver
;; Responds to assertions of interest in layout solutions by
;; computing layouts and asserting the resulting positions.
;;
;; (Observe LayoutSolution)+ ==>
;; RequestedLayoutSize ==>
;; ComputedLayoutSize ∧ LayoutSolution+
;;
;; Layout Observer
;; Supplies any initial constraints on the overall layout size,
;; and may observe the final overall computed layout size.
;;
;; RequestedLayoutSize ∧ (ComputedLayoutSize ==> 1)?
;;
;; Layout Participant
;; Supplies constraints on an individual item to be laid out
;; and monitors the resulting position decision for that item.
;;
;; LayoutSolution ==> 1
;;---------------------------------------------------------------------------
;; A LayoutSpec is one of
;; - (horizontal-layout Any)
;; - (vertical-layout Any)
;; - (tabular-layout Nat Nat)
;; where the first two use their keys for *ordering* peers relative to
;; each other using datum-order, and the last uses the given row and
;; column to place the item within an implicitly-sized grid layout.
(struct horizontal-layout (key) #:transparent)
(struct vertical-layout (key) #:transparent)
(struct tabular-layout (row col) #:transparent)
;; ASSERTION. A RequestedLayoutSize is a
;; (requested-layout-size Any (Option (box-size (Option Sizing) (Option Sizing))))
;; and describes overall constraints on the total size of the layout to be
;; constructed. Supplying `size` as `#f` means that there is no constraint at all;
;; otherwise, the `box-size` given is used as the exact dimensions of
;; the layout, unless one or both of the dimensions of the `box-size`
;; are given as `#f`, in which case there is no constraint for that
;; dimension.
(struct requested-layout-size (container-id size) #:transparent)
;; ASSERTION. A ComputedLayoutSize is a
;; (computed-layout-size Any BoxSize)
;; and gives the concrete dimensions of the layout after layout
;; computation has completed.
(struct computed-layout-size (container-id size) #:transparent)
;; ASSERTION. A LayoutSolution is a
;; (layout-solution Any LayoutSpec BoxSize Rectangle)
;; and denotes the computed bounds of a given item within a layout.
;; TODO: introduce an item ID??
(struct layout-solution (container-id
spec
size
rectangle) #:transparent)
;;---------------------------------------------------------------------------
(struct layout-item (spec size) #:transparent)
(define (layout-item-spec-key li)
(define v (layout-item-spec li))
(if (number? v) (exact->inexact v) v))
(spawn #:name 'layout-driver
(during/spawn (observe (layout-solution $container-id _ _ _))
#:name (list 'layout container-id)
(stop-when (asserted (observe (layout-solution container-id (horizontal-layout _) _ _)))
(react (solve-hv-layout #f container-id)))
(stop-when (asserted (observe (layout-solution container-id (vertical-layout _) _ _)))
(react (solve-hv-layout #t container-id)))
(stop-when (asserted (observe (layout-solution container-id (tabular-layout _ _) _ _)))
(react (solve-tabular-layout container-id)))))
(define (solve-hv-layout vertical? container-id)
(field [items (set)])
(if vertical?
(query-set* items
(observe (layout-solution container-id (vertical-layout $key) $size _))
(layout-item key size))
(query-set* items
(observe (layout-solution container-id (horizontal-layout $key) $size _))
(layout-item key size)))
(define/dataflow ordered-items (sort (set->list (items))
(order-<? datum-order)
#:key layout-item-spec-key)
#:default '())
(define/dataflow table
(if vertical?
(map list (map layout-item-size (ordered-items)))
(list (map layout-item-size (ordered-items)))))
(solve-layout* container-id
table
(lambda (layout)
(for [(item (ordered-items))
(cell (if vertical? (map car layout) (car layout)))]
(assert! (layout-solution container-id
(if vertical?
(vertical-layout (layout-item-spec item))
(horizontal-layout (layout-item-spec item)))
(layout-item-size item)
cell))))))
(define (merge-box-size existing computed)
(match existing
[#f computed]
[(box-size h v)
(box-size (or h (box-size-horizontal computed))
(or v (box-size-vertical computed)))]))
(define (solve-layout* container-id table on-layout)
(during (requested-layout-size container-id $reqsize)
(define/dataflow total-size (merge-box-size reqsize (table-sizing (table))))
(assert (computed-layout-size container-id (total-size)))
(define (total-width) (sizing-ideal (box-size-horizontal (total-size))))
(define (total-height) (sizing-ideal (box-size-vertical (total-size))))
(define/dataflow layout (table-layout (table) 0 0 (total-width) (total-height)) #:default '())
(begin/dataflow
(for [(a (in-bag (current-adhoc-assertions)))]
(match a
[(layout-solution (== container-id) _ _ _) (retract! a)]
[_ (void)]))
(on-layout (layout)))))
(define (solve-tabular-layout container-id)
(define/query-set items
(observe (layout-solution container-id (tabular-layout $row $col) $size _))
(layout-item (cons row col) size))
(define/dataflow items-table
(let* ((specs (map layout-item-spec (set->list (items))))
(row-count (+ 1 (apply max -1 (map car specs))))
(col-count (+ 1 (apply max -1 (map cdr specs))))
(mtx (for/vector [(r row-count)] (make-vector col-count #f))))
(for [(item (items))]
(vector-set! (vector-ref mtx (car (layout-item-spec item)))
(cdr (layout-item-spec item))
item))
mtx))
(define/dataflow table
(for/list [(row (items-table))]
(for/list [(item row)]
(if item (layout-item-size item) weak-fill-box-size))))
(solve-layout* container-id
table
(lambda (layout)
(define mtx (list->vector (map list->vector layout)))
(for [(item (items))]
(match-define (cons row col) (layout-item-spec item))
(assert! (layout-solution container-id
(tabular-layout row col)
(layout-item-size item)
(vector-ref (vector-ref mtx row) col)))))))
;;---------------------------------------------------------------------------
;; TODO: Having pop-up-menu-trigger be a message means that it's not
;; possible to cancel or move the menu once it has been triggered.
;; Consider using the "start" button in the corner to pop up a menu,
;; following which the screen is resized before the menu is dismissed.
;; Currently, the menu will continue to float in an incorrect location
;; rather than following the screen resize. If, however, the trigger
;; for a menu was an assertion, then the menu could track changes in
;; its triggering parameters and could be repositioned without fuss.
(struct pop-up-menu-trigger (menu-id x y x-pin y-pin release-event) #:transparent)
(struct menu-separator (menu-id order) #:transparent)
(struct menu-item (menu-id order image message) #:transparent)
(spawn #:name 'pop-up-menu-driver
(on (message (pop-up-menu-trigger $menu-id $x $y $x-pin $y-pin $release-event))
(run-pop-up-menu menu-id x y x-pin y-pin release-event)))
(define (run-pop-up-menu menu-id pop-up-cursor-x pop-up-cursor-y x-pin y-pin release-event)
(define instance-id (list menu-id (gensym 'instance)))
(define pad (theme-menu-item-padding))
(define pad2 (* pad 2))
(define normal (i:rectangle 1 1 "solid" (theme-menu-item-background-color)))
(define highlight (i:rectangle 1 1 "solid" (theme-menu-item-selected-background-color)))
(define separator (i:rectangle 1 1 "solid" (theme-menu-separator-color)))
(spawn #:name instance-id
(assert (requested-layout-size instance-id #f))
(during (menu-item menu-id $order $sealed-image $msg)
(define item-id (gensym 'item))
(define im (seal-contents sealed-image))
(define imsize (box-size (sizing (+ pad2 (*width im)) 0 0)
(sizing (+ pad2 (*height im)) 0 0)))
(during (layout-solution instance-id (vertical-layout order) imsize $rect)
(match-define (rectangle left top width height) rect)
(assert (sprite #:id item-id #:parent instance-id
0
`((translate ,left ,top)
(push-matrix
(scale ,width ,height)
(touchable (,instance-id ,item-id ,msg) ,in-unit-square?)
(texture ,(if (eq? (selected-item) item-id) highlight normal)))
(push-matrix
(translate ,pad ,pad)
(scale ,(*width im) ,(*height im))
(texture ,im)))))))
(during (menu-separator menu-id $order)
(define sep-id (gensym 'sep))
(during (layout-solution instance-id (vertical-layout order)
(box-size weak-fill-sizing
(sizing (theme-menu-separator-width) 0 0))
$rect)
(match-define (rectangle left top width height) rect)
(assert (sprite #:id sep-id #:parent instance-id
0
`((translate ,left ,top)
(scale ,width ,height)
(texture ,separator))))))
(during (computed-layout-size instance-id $menu-size)
(match-define (box-size (sizing menu-width _ _) (sizing menu-height _ _)) menu-size)
(define offset-x (- pop-up-cursor-x (* x-pin (+ menu-width 2)) -1))
(define offset-y (- pop-up-cursor-y (* y-pin (+ menu-height 2)) -1))
(assert (sprite #:id instance-id
-1
`((translate ,offset-x ,offset-y)
(render-children)))))
(define/query-value selected-item #f (touching `(,instance-id ,$i ,_)) i)
(define/query-value selected-msg #f (touching `(,instance-id ,_ ,$msg)) msg)
(stop-when (message (mouse-event release-event _))
(when (selected-item) (send! (selected-msg))))))
;;---------------------------------------------------------------------------
(define (system-text str [color #f])
(i:text/font str (theme-font-size) (or color "white")
(theme-font) 'default 'normal 'normal #f))
(define (title-font-text str)
(i:text/font str (theme-title-font-size) (theme-title-font-color)
(theme-title-font) 'default 'normal 'normal #f))
(define (menu-item/text menu-id order str message)
(menu-item menu-id order (seal (system-text str (theme-menu-item-color))) message))
;;---------------------------------------------------------------------------
(struct window-state (window-id title state) #:transparent)
(struct raise-widget (id) #:transparent)
(struct top-widget (id) #:transparent)
(define close-icon-i
(parameterize ((theme-font-size (round (* 4/3 (theme-title-font-size)))))
(system-text "×" (theme-title-font-color))))
(define (window-frame id title backdrop-color
#:close-icon? [close-icon? #t]
#:parent [parent-id 'root])
(define title-text-i (title-font-text title))
(define title-text-w (i:image-width title-text-i))
(define title-text-h (i:image-height title-text-i))
(define (title-bar-i focus?) (i:rectangle 1 1 "solid"
(if focus?
(theme-title-bar-selected-color)
(theme-title-bar-color))))
(define close-icon-w (i:image-width close-icon-i))
(define close-icon-h (i:image-height close-icon-i))
(define gap (/ (- (theme-title-bar-height) close-icon-w) 2))
(define backdrop (i:rectangle 1 1 "solid" backdrop-color))
(lambda (z rect focus?)
(match-define (rectangle left top width height) rect)
(sprite #:id id
#:parent parent-id
z
`((translate ,left ,top)
(push-matrix (translate ,(- (theme-window-border-width))
,(- (theme-title-bar-height)))
(scale ,(+ width (* 2 (theme-window-border-width)))
,(+ height (theme-title-bar-height) (theme-window-border-width)))
(touchable (,id window-backdrop) ,in-unit-square?)
(texture ,(title-bar-i focus?)))
(push-matrix (translate 0 ,(- (theme-title-bar-height)))
(scale ,width ,(theme-title-bar-height))
(touchable (,id title-bar) ,in-unit-square?))
(push-matrix (translate ,(- (+ width (theme-window-border-width))
(theme-window-resize-corner-size))
,(- (+ height (theme-window-border-width))
(theme-window-resize-corner-size)))
(scale ,(theme-window-resize-corner-size)
,(theme-window-resize-corner-size))
(touchable (,id resize-corner) ,in-unit-square?))
,@(if close-icon?
`((push-matrix
(translate ,gap ,(- (/ (+ (theme-title-bar-height) close-icon-h) 2)))
(scale ,close-icon-w ,close-icon-h)
(touchable (,id close-icon) ,in-unit-square?)
(texture ,close-icon-i)))
`())
(push-matrix (translate ,(/ (- width title-text-w) 2)
,(- (/ (+ (theme-title-bar-height) title-text-h) 2)))
(scale ,title-text-w ,title-text-h)
(texture ,title-text-i))
(push-matrix (scale ,width ,height)
(texture ,backdrop))
(render-children)))))
(define (open-window window-id window-title x y width height [backdrop-color (hsv->color 200 1 1)]
#:resizable? [resizable? #t])
(define c (window-frame window-id window-title backdrop-color))
(field [z (- (current-inexact-milliseconds))])
(define/query-value touching-title-bar?
#f (touching `(,window-id title-bar)) #t)
(on-start (draggable-mixin touching-title-bar? x y window-id))
(when resizable?
(define/query-value touching-resize-corner?
#f (touching `(,window-id resize-corner)) #t)
(on-start (draggable-mixin touching-resize-corner? width height window-id)))
(define/query-value touching-close-icon?
#f (touching `(,window-id close-icon)) #t)
(stop-when #:when (touching-close-icon?) (message (mouse-event 'left-up _)))
(on (message (raise-widget window-id))
(z (- (current-inexact-milliseconds))))
(define/query-value focus? #f (top-widget window-id) #t)
(define/dataflow bounds (rectangle (x) (y) (width) (height)) #:default zero-rectangle)
(assert (window-state window-id window-title (bounds)))
(assert (c (z) (bounds) (focus?))))
(spawn #:name 'top-widget-monitor
(local-require data/heap)
(define *widget-heap* (make-heap (lambda (a b) (<= (cdr a) (cdr b)))))
(field [widget-heap-version 0])
(define (widget-heap) (begin (widget-heap-version) *widget-heap*)) ;; gross hack
;; ^ this is to cope with the use of mutable data in a field.
;; Field update only registers damage if the field *changes*, as detected by `equal?`.
(define (trigger-dependencies!) (widget-heap-version (+ (widget-heap-version) 1)))
(on (asserted (<sprite> $id 'root $z _))
(heap-add! (widget-heap) (cons id z))
(trigger-dependencies!))
(on (retracted (<sprite> $id 'root $z _))
(heap-remove! (widget-heap) (cons id z))
(trigger-dependencies!))
(assert #:when (positive? (heap-count (widget-heap)))
(top-widget (car (heap-min (widget-heap))))))
;;---------------------------------------------------------------------------
(struct button-click (id mouse-state) #:transparent)
(begin-for-declarations
;; TODO: figure out what it is about (define (f #:x x) x) that
;; mandates begin-for-declarations to hide it from syndicate/lang's
;; local-expansion here :-(
(define (pushbutton label-str x y [w #f] [h #f]
#:shrink-x [shrink-x 0]
#:id id
#:coordinate-map-id [coordinate-map-id #f]
#:parent parent-id
#:trigger-event [trigger-event 'left-up])
(define label (system-text label-str (theme-button-color)))
(define i (i:overlay/align "middle" "middle" label (button-underlay label)))
(define c (costume #:id id #:parent parent-id #:coordinate-map-id coordinate-map-id i))
(define/query-value touching? #f (touching id) #t)
(on #:when (touching?) (message (mouse-event trigger-event $s))
(send! (button-click id s)))
(assert (c 0 (rectangle (x)
(y)
(or (and w (w)) (*width i))
(or (and h (h)) (*height i)))))
(box-size (sizing (*width i) 0 (* shrink-x (*width i))) (sizing (*height i) 0 0))))
;;---------------------------------------------------------------------------
(define (enforce-minimum f v)
(begin/dataflow (when (< (f) v) (f v))))
(begin-for-declarations
(define (message-box title init-x init-y body #:id id)
(define msg (system-text body))
(spawn #:name (list 'message-box id)
(field [x init-x]
[y init-y]
[width (max 250 (*width msg))]
[height (max 100 (*height msg))])
(open-window id title x y width height #:resizable? #f)
(assert ((costume #:parent id msg)
0
(rectangle (/ (- (width) (*width msg)) 2)
(/ (- (height) (*height msg)) 2)
(*width msg)
(*height msg)))))))
(spawn #:name 'test-window
(field [x 140] [y 140] [width 400] [height 300])
(open-window 'w "Window Title" x y width height)
(enforce-minimum width 300)
(enforce-minimum height 300)
(assert (menu-item/text 'testmenu 0 "First item" '(testmenu first)))
(assert (menu-item/text 'testmenu 1 "Second item" '(testmenu second)))
(assert (menu-item/text 'testmenu 2 "Third item" '(testmenu third)))
(during (window $width $height)
(on (message `(testmenu ,$which))
(define box-id (gensym 'box))
(message-box #:id box-id
(date->string (seconds->date (current-seconds))
"Selected at ~3")
(random width) (random height)
(format "~a" which))))
(pushbutton "Click me"
(lambda () 100)
(lambda () 100)
#:id 'click-me #:parent 'w #:trigger-event 'left-down)
(on (message (button-click 'click-me (mouse-state $x $y _ _ _)))
(send! (pop-up-menu-trigger 'testmenu x y 0 0.5 'left-up))))
;;---------------------------------------------------------------------------
(spawn #:name 'fullscreen-menu-item
(field [fullscreen? #f])
(assert (menu-item/text 'system-menu -1
(if (fullscreen?)
"Fullscreen ✓"
"Fullscreen")
'(system-menu toggle-fullscreen)))
(assert (menu-separator 'system-menu -0.9))
(on (message '(system-menu toggle-fullscreen))
(fullscreen? (not (fullscreen?))))
(assert #:when (fullscreen?) (gl-control 'fullscreen)))
(spawn #:name 'quit-menu-item
(assert (menu-item/text 'system-menu 0 "Quit" '(system-menu quit)))
(stop-when (message '(system-menu quit))
(send! (gl-control 'stop)))
(stop-when (message (key-event #\q #t _))
(send! (gl-control 'stop))))
(spawn #:name 'toolbar
(field [window-width 0] [window-height 0])
(on (asserted (window $w $h))
(window-width w)
(window-height h))
(define pad 4) ;;(theme-menu-item-padding))
(define pad2 (* pad 2))
(assert (requested-layout-size 'toolbar (box-size (sizing (- (window-width) pad2) 0 0) #f)))
(assert (observe (layout-solution 'toolbar
(horizontal-layout '(0.0 0.0))
weak-fill-box-size
(discard))))
(during (computed-layout-size 'toolbar (box-size (sizing $w _ _) (sizing $h _ _)))
(assert (sprite #:id 'toolbar #:parent #f
-0.5
`((translate 0 ,(- (window-height) h pad2))
(push-matrix (scale ,(window-width) ,(+ h pad2))
(touchable toolbar ,in-unit-square?)
(texture ,(i:rectangle 1 1 "solid" "black")))
(translate ,pad ,pad)
(render-children))))))
(spawn #:name 'start-button
(field [x 0] [y 0])
(define reqsize
(parameterize ((theme-button-y-padding 8)
(theme-button-min-height 0))
(pushbutton "Start" x y #:id 'start-button #:parent 'toolbar
#:coordinate-map-id 'start-button
#:trigger-event 'left-down)))
(during (layout-solution 'toolbar (horizontal-layout '(-10.0 0.0)) reqsize
(rectangle $l $t $w $h))
(x l)
(y t)
(during (coordinate-map 'start-button $xform)
(on (message (button-click 'start-button _))
(define pt (- (transform-point xform 0+0i) 1+4i)) ;; padding + unoffset
(send!
(pop-up-menu-trigger 'system-menu (real-part pt) (imag-part pt) 0 1 'left-up))))))
(spawn #:name 'window-list-monitor
(during/spawn (window-state $id $title _)
#:name (list 'window-list id)
(field [x 0] [y 0] [width #f] [height #f])
(define reqsize
(parameterize ((theme-button-y-padding 8)
(theme-button-min-height 0)
(theme-button-background-color (hsv->color 240 1 0.6)))
(pushbutton title x y width height #:id (list 'window-list id) #:parent 'toolbar
#:shrink-x 1
#:trigger-event 'left-down)))
(during (layout-solution 'toolbar (horizontal-layout `(-5.0 ,id)) reqsize
(rectangle $l $t $w $h))
(begin (x l) (y t) (width w) (height h))
(during (top-widget id)
(assert (sprite #:id (list 'window-list id 'highlight)
#:parent (list 'window-list id)
0
`((translate 0 ,(- h 1))
(scale ,w 1)
(texture ,(i:rectangle 1 1 "solid" "white"))))))
(on (message (button-click (list 'window-list id) _))
(send! (raise-widget id))))))
(spawn #:name 'clock
(field [now (current-seconds)])
(on (message (frame-event _ $timestamp _ _))
(define new (current-seconds))
(when (not (= new (now))) (now new)))
(define/dataflow now-im (system-text (date->string (seconds->date (now)) "~a ~b ~d, ~3"))
#:default i:empty-image)
(during (layout-solution 'toolbar (horizontal-layout '(10.0 0.0))
(box-size (sizing (*width (now-im)) 0 0)
(sizing (*height (now-im)) 0 0))
(rectangle $l $t $w $h))
(assert (sprite #:id 'clock #:parent 'toolbar
0
`((translate ,l ,(+ t (/ (- h (*height (now-im))) 2)))
(scale ,(*width (now-im)) ,(*height (now-im)))
(texture ,(now-im)))))))
(spawn-gl-2d-driver)

View File

@ -0,0 +1,29 @@
#lang racket/base
(provide fmod
hsv->color
color-by-hash)
(require 2htdp/image)
(define (fmod a b)
(- a (* b (truncate (/ a b)))))
(define (hsv->color h s v)
(define h* (fmod (/ h 60.0) 6))
(define chroma (* v s))
(define x (* chroma (- 1 (abs (- (fmod h* 2) 1)))))
(define-values (r g b)
(cond
[(< h* 1) (values chroma x 0)]
[(< h* 2) (values x chroma 0)]
[(< h* 3) (values 0 chroma x)]
[(< h* 4) (values 0 x chroma)]
[(< h* 5) (values x 0 chroma)]
[else (values chroma 0 x)]))
(define m (- v chroma))
(define (scale x) (inexact->exact (truncate (* 255 (+ x m)))))
(make-color (scale r) (scale g) (scale b)))
(define (color-by-hash v)
(hsv->color (* 360.0 (/ (bitwise-and (equal-hash-code v) 16777215) 16777216.0)) 1 1))

View File

@ -0,0 +1,191 @@
#lang racket/base
;; Tabular layout
(provide table-sizing
table-layout)
(require racket/match)
(require "sizing.rkt")
(module+ test (require rackunit))
;;---------------------------------------------------------------------------
(define (transpose rows)
(if (null? rows)
'()
(apply map list rows)))
(define (swedish-round x)
(floor (+ x 1/2)))
;;---------------------------------------------------------------------------
(define (table-sizing box-sizes)
(box-size (sizing-sum (table-column-widths box-sizes))
(sizing-sum (table-row-heights box-sizes))))
(define (table-row-heights box-sizes)
(map transverse-sizing (extract box-size-vertical box-sizes)))
(define (table-column-widths box-sizes)
(map transverse-sizing (extract box-size-horizontal (transpose box-sizes))))
(define (extract acc mtx)
(map (lambda (r) (map acc r)) mtx))
(define (transverse-sizing sizings)
(define-values (lb-v lb-f) (transverse-bound sizings sizing-shrink - max))
(define-values (ub-v ub-f) (transverse-bound sizings sizing-stretch + min))
(let* ((ideal-v (foldl max 0 (map sizing-ideal sizings)))
(ideal-v (if ub-v (min ideal-v ub-v) ideal-v))
(ideal-v (if lb-v (max ideal-v lb-v) ideal-v)))
(sizing ideal-v
(if ub-v (- ub-v ideal-v) ub-f)
(if lb-v (- ideal-v lb-v) lb-f))))
(define (transverse-bound sizings sizing-accessor minus-or-plus max-or-min)
(define vals (for/list [(s sizings) #:when (number? (sizing-accessor s))]
(minus-or-plus (sizing-ideal s) (sizing-accessor s))))
(values (and (pair? vals) (apply max-or-min vals))
(foldl fill-max 0 (filter fill? (map sizing-accessor sizings)))))
;;---------------------------------------------------------------------------
(define (table-layout box-sizes top left width height #:round [round? #t])
(define row-sizings (table-row-heights box-sizes))
(define col-sizings (table-column-widths box-sizes))
(define row-heights (compute-concrete-adjacent-sizes row-sizings height))
(define col-widths (compute-concrete-adjacent-sizes col-sizings width))
(define local-round (if round? swedish-round values))
(define-values (_bot rows-rev)
(for/fold [(top top) (rows-rev '())] [(row-height row-heights)]
(define next-top (+ top row-height))
(define rounded-top (local-round top))
(define rounded-height (- (local-round next-top) rounded-top))
(define-values (_right cells-rev)
(for/fold [(left left) (cells-rev '())] [(col-width col-widths)]
(define next-left (+ left col-width))
(define rounded-left (local-round left))
(define rounded-width (- (local-round next-left) rounded-left))
(values next-left
(cons (rectangle rounded-left
rounded-top
rounded-width
rounded-height)
cells-rev))))
(values next-top
(cons (reverse cells-rev) rows-rev))))
(reverse rows-rev))
(define (compute-concrete-adjacent-sizes sizings actual-bound)
(define ideal-total (foldl + 0 (map sizing-ideal sizings)))
(define-values (available-slop sizing-give apply-give)
(if (<= ideal-total actual-bound)
(values (- actual-bound ideal-total) sizing-stretch +)
(values (- ideal-total actual-bound) sizing-shrink -)))
(define total-give (foldl fill+ 0 (map sizing-give sizings)))
(if (number? total-give)
(let ((scale (if (zero? total-give) 0 (/ available-slop total-give))))
(map (lambda (s)
;; numeric total-give ⇒ no fills for any give in the list
(apply-give (sizing-ideal s) (* (sizing-give s) scale)))
sizings))
(let* ((weight (fill-weight total-give))
(rank (fill-rank total-give))
(scale (if (zero? weight) 0 (/ available-slop weight))))
(map (lambda (s)
(match (sizing-give s)
[(fill w (== rank)) (apply-give (sizing-ideal s) (* w scale))]
[_ (sizing-ideal s)]))
sizings))))
;;---------------------------------------------------------------------------
(module+ test
(check-equal? (transpose '((1 2 3) (4 5 6) (7 8 9))) '((1 4 7) (2 5 8) (3 6 9)))
(check-equal? (swedish-round 0.1) 0.0)
(check-equal? (swedish-round 0.5) 1.0)
(check-equal? (swedish-round 0.9) 1.0)
(check-equal? (swedish-round 1.1) 1.0)
(check-equal? (swedish-round 1.5) 2.0)
(check-equal? (swedish-round 1.9) 2.0))
(module+ test
(define s211 (sizing 2 1 1))
(define s0f0 (sizing 0 weak-fill 0))
(define b22 (box-size s211 s211))
(define b42 (box-size (sizing 4 1 1) s211))
(define b62 (box-size (sizing 6 1 1) s211))
(define b00 (box-size s0f0 s0f0))
(define t1 (list (list b22 b22 b00 b22)
(list b22 b22 b00 b22)
(list b22 b22 b00 b22)))
(define t2 (list (list b22 b22 b22)
(list b22 b00 b22)
(list b22 b22 b22)))
(define t3 (list (list b22 b42 b22)
(list b22 b00 b22)
(list b22 b22 b22)))
(define t4 (list (list b22 b62 b22)
(list b22 b00 b22)
(list b22 b22 b22)))
(check-equal? (table-sizing t1)
(box-size (sizing 6 weak-fill 3)
(sizing 6 3 3)))
(check-equal? (table-sizing t2)
(box-size (sizing 6 3 3)
(sizing 6 3 3)))
;; Is this sane?
(check-equal? (table-sizing t3)
(box-size (sizing 7 2 2)
(sizing 6 3 3)))
;; Is this sane?
(check-equal? (table-sizing t4)
(box-size (sizing 9 0 2)
(sizing 6 3 3)))
(check-equal? (table-layout t1 0 0 20 20)
(list (list (rectangle 0 0 2 7)
(rectangle 2 0 2 7)
(rectangle 4 0 14 7)
(rectangle 18 0 2 7))
(list (rectangle 0 7 2 6)
(rectangle 2 7 2 6)
(rectangle 4 7 14 6)
(rectangle 18 7 2 6))
(list (rectangle 0 13 2 7)
(rectangle 2 13 2 7)
(rectangle 4 13 14 7)
(rectangle 18 13 2 7))))
(check-equal? (table-layout t2 0 0 20 20)
(list (list (rectangle 0 0 7 7)
(rectangle 7 0 6 7)
(rectangle 13 0 7 7))
(list (rectangle 0 7 7 6)
(rectangle 7 7 6 6)
(rectangle 13 7 7 6))
(list (rectangle 0 13 7 7)
(rectangle 7 13 6 7)
(rectangle 13 13 7 7))))
;; Is this sane?
(check-equal? (table-layout t3 0 0 20 20)
(list (list (rectangle 0 0 9 7)
(rectangle 9 0 3 7)
(rectangle 12 0 8 7))
(list (rectangle 0 7 9 6)
(rectangle 9 7 3 6)
(rectangle 12 7 8 6))
(list (rectangle 0 13 9 7)
(rectangle 9 13 3 7)
(rectangle 12 13 8 7)))))

View File

@ -0,0 +1,8 @@
#lang racket/base
;; Layout, based loosely on TeX's boxes-and-glue model.
(require "sizing.rkt")
(require "layout.rkt")
(provide (all-from-out "sizing.rkt")
(all-from-out "layout.rkt"))

View File

@ -0,0 +1,150 @@
#lang racket/base
;; Dimension sizing, based loosely on TeX's boxes-and-glue model.
(provide (struct-out fill)
(struct-out sizing)
(struct-out box-size)
(struct-out rectangle)
weak-fill
zero-sizing
weak-fill-sizing
zero-box-size
weak-fill-box-size
zero-rectangle
fill+
fill-max
fill-min
fill-scale
fill-weaken
sizing-contains?
sizing-min
sizing-max
sizing-overlap?
sizing-scale
sizing-weaken
sizing-pad
sizing-adjust-ideal
sizing-sum
box-size-weaken)
(require racket/match)
;;---------------------------------------------------------------------------
;; A Fill is one of
;; - a Nat, a fixed amount of space
;; - a (fill Nat Nat), a potentially infinite amount of space
(struct fill (weight rank) #:transparent)
;; A Sizing is a (sizing Nat Fill Fill)
(struct sizing (ideal stretch shrink) #:transparent)
;; A BoxSize is a (box-size Sizing Sizing)
(struct box-size (horizontal vertical) #:transparent)
;; A Rectangle is a (rectangle Nat Nat BoxSize)
(struct rectangle (left top width height) #:transparent)
;;---------------------------------------------------------------------------
;; A very weak fill.
(define weak-fill (fill 1 -1))
(define zero-sizing (sizing 0 0 0))
(define weak-fill-sizing (sizing 0 weak-fill 0))
(define zero-box-size (box-size zero-sizing zero-sizing))
(define weak-fill-box-size (box-size weak-fill-sizing weak-fill-sizing))
(define zero-rectangle (rectangle 0 0 0 0))
;;---------------------------------------------------------------------------
;; (Nat Nat -> Nat) -> (Fill Fill -> Fill)
(define ((fill-binop op) a b)
(match* (a b)
[((? number?) (? number?)) (op a b)]
[((? number?) (? fill?)) b]
[((? fill?) (? number?)) a]
[((fill w1 r1) (fill w2 r2))
(cond [(= r1 r2) (fill (op w1 w2) r1)]
[(> r1 r2) (fill w1 r1)]
[(< r1 r2) (fill w2 r2)])]))
;; Fill Fill -> Fill
(define fill+ (fill-binop +))
(define fill-max (fill-binop max))
(define (fill-min a b)
(if (and (number? a) (number? b))
(min a b)
0))
(define (fill-scale f scale)
(if (number? f)
(* f scale)
f))
(define (fill-weaken f w r)
(if (fill? f)
(fill w r)
f))
(define (sizing-contains? s v)
(match-define (sizing x x+ x-) s)
(cond [(>= v x) (if (number? x+) (<= v (+ x x+)) #t)]
[(<= v x) (if (number? x-) (>= v (- x x-)) #t)]))
(define (sizing-min s)
(match (sizing-shrink s)
[(? number? n) (- (sizing-ideal s) n)]
[(? fill?) -inf.0]))
(define (sizing-max s)
(match (sizing-stretch s)
[(? number? n) (+ (sizing-ideal s) n)]
[(? fill?) +inf.0]))
(define (sizing-overlap? x y)
(define largest-min (max (sizing-min x) (sizing-min y)))
(define smallest-max (min (sizing-max x) (sizing-max y)))
(< largest-min smallest-max))
(define (sizing-scale s scale)
(match-define (sizing x x+ x-) s)
(sizing (* x scale) (fill-scale x+ scale) (fill-scale x- scale)))
(define (sizing-weaken s
[stretch-weight 1]
[stretch-rank 0]
[shrink-weight stretch-weight]
[shrink-rank stretch-rank])
(match-define (sizing x x+ x-) s)
(sizing x
(fill-weaken x+ stretch-weight stretch-rank)
(fill-weaken x- shrink-weight shrink-rank)))
(define (sizing-pad s amount)
(match-define (sizing x x+ x-) s)
(sizing (+ x amount) x+ x-))
(define (sizing-adjust-ideal s i)
(match-define (sizing x x+ x-) s)
(sizing i
(if (fill? x+) x+ (+ x+ (- x i)))
(if (fill? x-) x- (- x- (- x i)))))
(define (sizing-sum sizings)
(sizing (foldl + 0 (map sizing-ideal sizings))
(foldl fill+ 0 (map sizing-stretch sizings))
(foldl fill+ 0 (map sizing-shrink sizings))))
(define (box-size-weaken bs [weight 1] [rank 0])
(match-define (box-size h v) bs)
(box-size (sizing-weaken h weight rank)
(sizing-weaken v weight rank)))

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB