#lang scribble/manual @(require planet/scribble planet/version planet/resolver scribble/eval scribble/bnf racket/sandbox racket/port (only-in racket/contract any/c) racket/runtime-path "scribble-helpers.rkt" "../js-assembler/get-js-vm-implemented-primitives.rkt") @(require (for-label (this-package-in resource)) (for-label (this-package-in web-world))) @(define-runtime-path whalesong-path "..") @title{CS19 instructions for Whalesong} @author+email["Danny Yoo" "dyoo@hashcollision.org"] @section{Installation} Racket 5.1.2 or greater is a prerequisite for Whalesong. Brown CS maintains its own installation of Racket 5.1.3 in @filepath{/local/projects/racket/releases/5.1.3}. To access it, you can add the following to your @filepath{.environment}: @filebox[".environment"]{ @verbatim|{ pathprependifdir PATH "/local/projects/racket/releases/5.1.3/bin" }|} Hopefully, this should already be configured to be the default for the @tt{cs19} group by the time you read this. We'll install a local development copy of Whalesong in a @filepath{whalesong} subdirectory. On the very first time we install Whalesong: @verbatim|{ $ git clone git://github.com/dyoo/whalesong.git $ cd whalesong $ make }| The @filepath{make} step make take a minute or two, and creates a command-line program called @filepath{whalesong} that we'll use to build Whalesong programs. Whenever we need to update whalesong, we should do the following @verbatim|{ $ git pull $ make }| @subsection{Warning on Firefox} Firefox unfortunately has a @link["https://bugzilla.mozilla.org/show_bug.cgi?id=676343"]{JavaScript bug} that prevents it from reliably evaluating Whalesong programs. As of this writing, I have not been able to find a workaround. You should probably use Google Chrome instead when testing your programs; Google Chrome should be in @filepath{/contrib/bin/google-chrome}. @section{Usage} The @filepath{whalesong} launcher in the subdirectory will compile programs to @filepath{.html} and @filepath{.js} files. Example usage: using @litchar{whalesong build} to compile a whalesong program. @verbatim|{ fermi ~/whalesong $ cd examples fermi ~/whalesong/examples $ cat hello.rkt #lang planet dyoo/whalesong (display "hello world") (newline) fermi ~/whalesong/examples $ ../whalesong build hello.rkt fermi ~/whalesong/examples $ google-chrome hello.html Created new window in existing browser session. fermi ~/whalesong/examples $ }| @section{Examples} There are examples in the @link["https://github.com/dyoo/whalesong/tree/master/examples"]{@filepath{whalesong/examples}} and @link["https://github.com/dyoo/whalesong/tree/master/web-world/examples"]{@filepath{whalesong/web-world/examples}}. Let's look at a few of them. @subsection{Hello world} Let's try making a simple, standalone executable. At the moment, the program must be written in the base language of @racket[(planet dyoo/whalesong)]. This restriction unfortunately prevents arbitrary @racketmodname[racket/base] programs from compiling at the moment; the developers (namely, dyoo) will be working to remove this restriction as quickly as possible. Write a @filepath{hello.rkt} with the following content @filebox["hello.rkt"]{ @codeblock{ #lang planet dyoo/whalesong (display "hello world") (newline) }} This program is a regular Racket program, and can be executed normally, @verbatim|{ $ racket hello.rkt hello world $ }| However, it can also be packaged with @filepath{whalesong}. @verbatim|{ $ whalesong build hello.rkt Writing program # Writing html # $ ls -l hello.html -rw-r--r-- 1 dyoo dyoo 3817 2011-09-10 15:02 hello.html $ ls -l hello.js -rw-r--r-- 1 dyoo dyoo 2129028 2011-09-10 15:02 hello.js }| Running @tt{whalesong build} on a Racket program will produce @filepath{.html} and @filepath{.js} files. If we open this file in our favorite web browser, we should see a triumphant message show on screen. @subsection{Tick tock} Let's do something a little more interesting, and create a ticker that counts on the screen. The first thing we can do is mock up a web page with a user interface, like this. @filebox["index.html"]{ @verbatim|{ My simple program

The current counter is: fill-me-in

}| } We can even look at this in a standard web browser. Once we're happy with the statics of our program, we can inject dynamic behavior. Write a file called @filepath{tick-tock.rkt} with the following content. @filebox["tick-tock.rkt"]{ @codeblock|{ #lang planet dyoo/whalesong (require (planet dyoo/whalesong/web-world) (planet dyoo/whalesong/resource)) (define-resource index.html) ;; draw: world view -> view (define (draw world dom) (update-view-text (view-focus dom "counter") world)) ;; tick: world view -> world (define (tick world dom) (add1 world)) ;; stop?: world view -> boolean (define (stop? world dom) (> world 10)) (big-bang 0 (initial-view index.html) (to-draw draw) (on-tick tick 1) (stop-when stop?)) }| } Several things are happening here. @itemize[ @item{We @racket[require] a few libraries to get us some additional behavior; in particular, @racketmodname/this-package[web-world] to let us write event-driven web-based programs, and @racketmodname/this-package[resource] to give us access to external @tech{resource}s.} @item{We use @racket[define-resource] to refer to external files, like @filepath{index.html} that we'd like to include in our program.} @item{We use @racket[big-bang] to start up a computation that responses to events. In this example, that's clock ticks introduced by @racket[on-tick], though because we're on the web, we can bind to many other kinds of web events (by using @racket[view-bind]).} ] @subsection{More web-world examples} Here are a collection of web-world demos: @itemize[ @item{@link["http://hashcollision.org/whalesong/examples/attr-animation/attr-animation.html"]{attr-animation.html} [@link["http://hashcollision.org/whalesong/examples/attr-animation/attr-animation.rkt"]{src}] Uses @racket[update-view-attr] and @racket[on-tick] to perform a simple color animation.} @item{@link["http://hashcollision.org/whalesong/examples/boid/boid.html"]{boid.html} [@link["http://hashcollision.org/whalesong/examples/boid/boid.rkt"]{src}] Uses @racket[update-view-css] and @racket[on-tick] to perform an animation of a flock of @link["http://en.wikipedia.org/wiki/Boids"]{boids}.} @item{@link["http://hashcollision.org/whalesong/examples/dwarves/dwarves.html"]{dwarves.html} [@link["http://hashcollision.org/whalesong/examples/dwarves/dwarves.rkt"]{src}] Uses @racket[view-show] and @racket[view-hide] to manipulate a view. Click on a dwarf to make them hide. } @item{@link["http://hashcollision.org/whalesong/examples/dwarves-with-remove/dwarves-with-remove.html"]{dwarves-with-remove.html} [@link["http://hashcollision.org/whalesong/examples/dwarves-with-remove/dwarves-with-remove.rkt"]{src}] Uses @racket[view-focus?] and @racket[view-remove] to see if a dwarf should be removed from the view. } @item{@link["http://hashcollision.org/whalesong/examples/field/field.html"]{field.html} [@link["http://hashcollision.org/whalesong/examples/field/field.rkt"]{src}] Uses @racket[view-bind] to read a text field, and @racket[update-view-text] to change the text content of an element. } @item{@link["http://hashcollision.org/whalesong/examples/phases/phases.html"]{phases.html} [@link["http://hashcollision.org/whalesong/examples/phases/phases.rkt"]{src}] Switches out one view entirely in place of another. Different views can correspond to phases in a program. } @item{@link["http://hashcollision.org/whalesong/examples/tick-tock/tick-tock.html"]{tick-tock.html} [@link["http://hashcollision.org/whalesong/examples/tick-tock/tick-tock.rkt"]{src}] Uses @racket[on-tick] to show a timer counting up. } @item{@link["http://hashcollision.org/whalesong/examples/redirected/redirected.html"]{redirected.html} [@link["http://hashcollision.org/whalesong/examples/redirected/redirected.rkt"]{src}] Uses @racket[on-tick] to show a timer counting up, and also uses @racket[open-output-element] to pipe side-effecting @racket[printf]s to a hidden @tt{div}. } @item{@link["http://hashcollision.org/whalesong/examples/todo/todo.html"]{todo.html} [@link["http://hashcollision.org/whalesong/examples/todo/todo.rkt"]{src}] A simple TODO list manager. } @item{@link["http://hashcollision.org/whalesong/examples/where-am-i/where-am-i.html"]{where-am-i.html} [@link["http://hashcollision.org/whalesong/examples/where-am-i/where-am-i.rkt"]{src}] Uses @racket[on-location-change] and @racket[on-mock-location-change] to demonstrate location services. } ] @section{API} @defmodule/this-package[web-world] For the purposes of tour-guide, we'll be focusing on the @racketmodname/this-package[web-world] library in Whalesong. Like the big-bang in regular world, the callbacks are world-to-world functions. One difference introduced by the web is the web page itself: because the page itself is a source of state, it too will be passed to callbacks. This library presents a functional version of the DOM in the form of a @tech{view}. The world-updating callbacks may optionally take an @tech{event} object, which provides additional information about the event that triggered the callback. @defproc[(big-bang [w world] [h big-bang-handler] ...) world]{ Start a big bang computation. } @defproc[(initial-view [x any]) big-bang-handler]{ Provide an initial view for the big-bang. Normally, @racket[x] will be a @tech{resource} to a web page. @codeblock|{ ... (define-resource page1.html) ... (big-bang ... (initial-view page1.html)) }| } @defproc[(stop-when [stop? ([w world] [dom view] -> boolean)]) big-bang-handler]{ Tells @racket[big-bang] when to stop. @codeblock|{ ... (define-struct world (given expected)) ... ;; stop?: world view -> boolean (define (stop? world dom) (string=? (world-given world) (world-expected world))) (big-bang ... (stop-when stop?)) }| } @defproc*[(((on-tick [tick-f ([w world] [v view] [e event]? -> world)] [delay real]) big-bang-handler) ((on-tick [tick-f ([w world] [v view] [e event]? -> world)]) big-bang-handler))]{ Tells @racket[big-bang] to update the world during clock ticks. By default, this will send a clock tick 28 times a second, but if given @racket[delay], it will use that instead. @codeblock|{ ... ;; tick: world dom -> world (define (tick world view) (add1 world)) (big-bang ... (on-tick tick 5)) ;; tick every five seconds }| } @defproc[(on-mock-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{ Tells @racket[big-bang] to update the world during simulated movement. During the extent of a big-bang, a form widget will appear in the @tt{document.body} to allow us to manually send location-changing events. The optional @tech{event} argument will contain numbers for @racket["latitude"] and @racket["longitude"]. @codeblock|{ ... ;; move: world view event -> world (define (move world dom event) (list (event-ref event "latitude") (event-ref event "longitude"))) ... (big-bang ... (on-mock-location-change move)) }| } @defproc[(on-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{ Tells @racket[big-bang] to update when the location changes, as received by the @link["http://dev.w3.org/geo/api/spec-source.html"]{Geolocation API}. The optional @tech{event} argument will contain numbers for @racket["latitude"] and @racket["longitude"]. @codeblock|{ ... ;; move: world view event -> world (define (move world dom event) (list (event-ref event "latitude") (event-ref event "longitude"))) ... (big-bang ... (on-location-change move)) }| } @defproc[(to-draw [draw-f ([w world] [v view] -> view)]) big-bang-handler]{ Tells @racket[big-bang] how to update the rendering of the world. The draw function will be called every time an event occurs. @codeblock|{ ... (define-struct world (name age)) ;; draw: world view -> view (define (draw world dom) (update-view-text (view-focus dom "name-span") (world-name world))) ... (big-bang ... (to-draw draw)) }| } @subsection{Views} A @deftech{view} is a functional representation of the browser DOM tree. A view is always focused on an element, and the functions in this subsection show how to traverse and manipulate the view. @defproc[(->view [x any]) view]{ Coerse a value into a view whose focus is on the topmost element. Common values for @racket[x] include @tech{resource}s. } @defproc[(view-focus? [v view] [id String]) boolean]{ Return true if the view can be focused using the given id. } @defproc[(view-focus [v view] [id String]) view]{ Focuses the view on an element, given the @racket[id]. The view will be searched starting from the toplevelmost node. } Once we have a view, we can refocus the view to different elements by using @racket[view-left], @racket[view-right], @racket[view-up], and @racket[view-down]. @defproc[(view-left? [v view]) boolean]{ See if the view can be moved to the previous sibling. } @defproc[(view-left [v view]) view]{ Move the focus to the previous sibling. } @defproc[(view-right? [v view]) boolean]{ See if the view can be moved to the next sibling. } @defproc[(view-right [v view]) view]{ Move the focus to the next sibling.} @defproc[(view-up? [v view]) boolean]{ See if the view can be moved to the parent. } @defproc[(view-up [v view]) view]{ Move the focus to the parent.} @defproc[(view-down? [v view]) boolean]{ See if the view can be moved to the first child. } @defproc[(view-down [v view]) view]{ Move the view to the first child.} Once we focus the view on an element, we can bind a world handler to it that responds to events. @defproc[(view-bind [v view] [type string] [world-updater ([w world] [v view] [e event]? -> world)]) view]{ Attach a world-updating event to the focus. When the world-updater is called, the view will be focused on the element that triggered the event. Common event types include @racket["click"], @racket["mouseenter"], @racket["change"].} When the view is on an element that we'd like to query or update, we can use several functions: @defproc[(view-text [v view]) string]{ Get the textual content at the focus. } @defproc[(update-view-text [v view] [s string]) view]{ Update the textual content at the focus.} @defproc[(view-show [v view]) view]{ Show the element at the focus. } @defproc[(view-hide [v view]) view]{ Hide the element at the focus. } @defproc[(view-attr [v view] [name String]) view]{ Get the attribute @racket[name] at the focus. } @defproc[(update-view-attr [v view] [name String] [value String]) view]{ Update the attribute @racket[n] with the value @racket[v] at the focus. } @defproc[(view-css [v view] [name String]) view]{ Get the css value @racket[name] at the focus. } @defproc[(update-view-css [v view] [name String] [value String]) view]{ Update the css value @racket[n] with the value @racket[v] at the focus. } @defproc[(view-id [v view]) world]{ Get the unique identifier of the node at the focus. } @defproc[(view-form-value [v view]) view]{ Get the form value of the node at the focus.} @defproc[(update-view-form-value [v view] [value String]) view]{ Update the form value of the node at the focus.} @defproc[(view-append-child [d dom]) view]{ Add the dom node @racket[d] as the last child of the focused node. Focus moves to the appended child. Dom nodes can be created by using @racket[xexp->dom], which converts a @tech{xexp} to a node. } @defproc[(view-remove [v view]) view]{ Remove the dom node at the focus from the view @racket[v]. Focus tries to move to the right, if there's a next sibling. If that fails, focus then moves to the left, if there's a previous sibling. If that fails too, then focus moves to the parent.} @subsection{Events} An @deftech{event} is a structure that holds name-value pairs. Whenever an event occurs in web-world, it may include some auxiliary information about the event. As a concrete example, location events from @racket[on-location-change] and @racket[on-mock-location-change] can send latitude and longitude values, as long as the world callback can accept the event as an argument. @defstruct[event ([kvpairs (listof (list symbol (or/c string number)))])]{} @defproc[(event-ref [evt event?] [name (or/c symbol string)]) value]{ Get an value from the event, given its @racket[name]. } @defproc[(event-keys [evt event?]) (listof symbol)]{ Get an list of the event's keys. } @section{Dynamic DOM generation with xexps} @declare-exporting/this-package[web-world] We often need to dynamically inject new dom nodes into an existing view. As an example where the UI is entirely in code: @codeblock|{ #lang planet dyoo/whalesong (require (planet dyoo/whalesong/web-world)) ;; tick: world view -> world (define (tick world view) (add1 world)) ;; draw: world view -> view (define (draw world view) (view-append-child view (xexp->dom `(p "hello, can you see this? " ,(number->string world))))) (big-bang 0 (initial-view (xexp->dom '(html (head) (body)))) (on-tick tick 1) (to-draw draw)) }| Normally, we'll want to do as much of the statics as possible with @filepath{.html} resources, but when nothing else will do, we can generate DOM nodes programmatically. We can create new DOMs from an @tech{xexp}, which is a s-expression representation for a DOM node. Here are examples of expressions that evaluate to xexps: @racketblock["hello world"] @racketblock['(p "hello, this" "is an item")] @racketblock[ (local [(define name "josh")] `(p "hello" (i ,name)))] @racketblock[ '(div (\@ (id "my-div-0")) (span "This is a span in a div"))] @racketblock[ `(div (\@ ,(fresh-id)) (span "This is another span in a div whose id is dynamically generated"))] More formally, a @deftech{xexp} is: @(let ([open @litchar{(}] [close @litchar{)}] [at @litchar[(symbol->string '\@)]]) @BNF[(list @nonterm{xexp} @nonterm{string} @BNF-seq[open @nonterm{id} @kleenestar[@nonterm{xexp}] close] @BNF-seq[open @nonterm{id} open at @kleenestar[@nonterm{key-value}] close @kleenestar[@nonterm{xexp}] close]) (list @nonterm{key-value} @BNF-seq[open @nonterm{symbol} @nonterm{string} close]) ]) To check to see if something is a xexp, use @racket[xexp?]: @defproc[(xexp? [x any]) boolean]{ Return true if @racket[x] is a xexp. } @defproc[(xexp->dom [an-xexp xexp]) dom]{ Return a dom from the xexp. } When creating xexps, we may need to create unique ids for the nodes. The web-world library provides a @racket[fresh-id] form to create these. @defproc[(fresh-id) string]{ Return a string that can be used as a DOM node id. } @section{Including external resources} @defmodule/this-package[resource] Programs may need to use an external file @deftech{resource} that isn't itself a Racket program, but instead some other kind of data. Graphical programs will often use @filepath{.png}s, and web-related programs @filepath{.html}s, for example. Whalesong provides the @racketmodname/this-package[resource] library to refer and use these external resources. When Whalesong compiles a program into a package, these resources will be bundled alongside the JavaScript-compiled output. @defform[(define-resource id [path-string])]{ Defines a resource with the given path name. For example, @codeblock|{ #lang planet dyoo/whalesong (require (planet dyoo/whalesong/resource)) (define-resource my-whale-image-resource "humpback.png") }| } As a convenience, we can also write @codeblock|{ #lang planet dyoo/whalesong (require (planet dyoo/whalesong/resource)) (define-resource humpback.png) }| which defines a variable named @racket[humpback.png] whose resource is @filepath{humpback.png}. @defproc[(resource->url [a-resource resource?]) string?]{ Given a resource, gets a URL. For example, @codeblock|{ #lang planet dyoo/whalesong (require (planet dyoo/whalesong/resource) (planet dyoo/whalesong/image)) (define-resource my-whale-image-resource "humpback.png") (define WHALE-IMAGE (bitmap/url (resource->url my-whale-image-resource))) }| } @section{Tips and Tricks} @subsection{Hiding standard output or directing it to an element} @declare-exporting/this-package[web-world] For a web-world program, output is normally done by using @racket[to-draw]. However, side effecting functions, such as @racket[printf] or @racket[display], are still available, and will append to @tt{document.body}. We may want to disable such printing or redirect it to a particular element on the page. For such purposes, use a combination of @racket[current-output-port] and @racket[open-output-element] to redirect the output of these side effect functions to somewhere else. For example: @codeblock|{ ... ;; Redirect standard output to a div called "stdout-div". (current-output-port (open-output-element "stdout-div")) ... (big-bang ... (on-tick (lambda (world dom) (begin (printf "Tick!\n") (add1 world)))) ...) }| All subsequent I/O side effects after the call to @racket[current-output-port] will be written out to the @tt{stdout-div}, which can be easily styled with @tt{display: none} to hide it from normal browser display. @defproc[(open-output-element [id string]) output-port]{ Opens an output port that will be directed to write to the DOM element whose id is @racket[id]. Note: writing to this port shouldn't fail, even if the id does not currently exist on the page. }