diff --git a/syndicate/examples/gui/README.md b/syndicate/examples/gui/README.md new file mode 100644 index 0000000..f9f039b --- /dev/null +++ b/syndicate/examples/gui/README.md @@ -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) diff --git a/syndicate/examples/gui/gui.rkt b/syndicate/examples/gui/gui.rkt new file mode 100644 index 0000000..fe7f2d0 --- /dev/null +++ b/syndicate/examples/gui/gui.rkt @@ -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-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 ( $id 'root $z _)) + (heap-add! (widget-heap) (cons id z)) + (trigger-dependencies!)) + (on (retracted ( $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) diff --git a/syndicate/examples/gui/hsv.rkt b/syndicate/examples/gui/hsv.rkt new file mode 100644 index 0000000..ba915a6 --- /dev/null +++ b/syndicate/examples/gui/hsv.rkt @@ -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)) diff --git a/syndicate/examples/gui/layout/layout.rkt b/syndicate/examples/gui/layout/layout.rkt new file mode 100644 index 0000000..2f2e726 --- /dev/null +++ b/syndicate/examples/gui/layout/layout.rkt @@ -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))))) diff --git a/syndicate/examples/gui/layout/main.rkt b/syndicate/examples/gui/layout/main.rkt new file mode 100644 index 0000000..55fae0f --- /dev/null +++ b/syndicate/examples/gui/layout/main.rkt @@ -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")) diff --git a/syndicate/examples/gui/layout/sizing.rkt b/syndicate/examples/gui/layout/sizing.rkt new file mode 100644 index 0000000..283c542 --- /dev/null +++ b/syndicate/examples/gui/layout/sizing.rkt @@ -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))) diff --git a/syndicate/examples/gui/oakura-beach-20081225.jpg b/syndicate/examples/gui/oakura-beach-20081225.jpg new file mode 100644 index 0000000..2b134a7 Binary files /dev/null and b/syndicate/examples/gui/oakura-beach-20081225.jpg differ diff --git a/syndicate/examples/gui/syndicate-gui-snapshot.png b/syndicate/examples/gui/syndicate-gui-snapshot.png new file mode 100644 index 0000000..05bba99 Binary files /dev/null and b/syndicate/examples/gui/syndicate-gui-snapshot.png differ