summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile.am40
-rw-r--r--catbird/asset.scm241
-rw-r--r--catbird/cached-slots.scm88
-rw-r--r--catbird/camera.scm113
-rw-r--r--catbird/config.scm16
-rw-r--r--catbird/inotify.scm197
-rw-r--r--catbird/input-map.scm175
-rw-r--r--catbird/kernel.scm394
-rw-r--r--catbird/line-editor.scm312
-rw-r--r--catbird/minibuffer.scm157
-rw-r--r--catbird/mixins.scm195
-rw-r--r--catbird/mode.scm105
-rw-r--r--catbird/node-2d.scm939
-rw-r--r--catbird/node.scm160
-rw-r--r--catbird/observer.scm37
-rw-r--r--catbird/overlay.scm116
-rw-r--r--catbird/region.scm102
-rw-r--r--catbird/repl.scm349
-rw-r--r--catbird/ring-buffer.scm64
-rw-r--r--catbird/scene.scm147
-rw-r--r--configure.ac1
-rw-r--r--guix.scm4
-rw-r--r--pre-inst-env.in1
-rw-r--r--test-env.in5
-rw-r--r--tests/node.scm7
-rw-r--r--tests/scene.scm7
-rw-r--r--tests/utils.scm9
28 files changed, 3965 insertions, 17 deletions
diff --git a/.gitignore b/.gitignore
index 6494a33..2891512 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
/autom4te.cache/
/build-aux/
/Makefile
+/test-env
diff --git a/Makefile.am b/Makefile.am
index 22c997f..2769022 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -38,20 +38,32 @@ moddir=$(prefix)/share/guile/site/$(GUILE_EFFECTIVE_VERSION)
godir=$(libdir)/guile/$(GUILE_EFFECTIVE_VERSION)/site-ccache
SOURCES = \
- starling/config.scm \
- starling/ring-buffer.scm \
- starling/inotify.scm \
- starling/system.scm \
- starling/asset.scm \
- starling/node.scm \
- starling/scene.scm \
- starling/repl-server.scm \
- starling/repl.scm \
- starling/minibuffer.scm \
- starling/kernel.scm \
- starling/node-2d.scm \
- starling/gui.scm \
- starling/transition.scm
+ catbird/config.scm \
+ catbird/inotify.scm \
+ catbird/ring-buffer.scm \
+ catbird/mixins.scm \
+ catbird/cached-slots.scm \
+ catbird/observer.scm \
+ catbird/asset.scm \
+ catbird/input-map.scm \
+ catbird/mode.scm \
+ catbird/camera.scm \
+ catbird/node.scm \
+ catbird/node-2d.scm \
+ catbird/scene.scm \
+ catbird/region.scm \
+ catbird/kernel.scm \
+ catbird/line-editor.scm \
+ catbird/minibuffer.scm \
+ catbird/repl.scm \
+ catbird/overlay.scm
+
+TESTS = \
+ tests/node.scm \
+ tests/scene.scm
+
+TEST_EXTENSIONS = .scm
+SCM_LOG_COMPILER = $(top_builddir)/test-env $(GUILE)
EXTRA_DIST += \
COPYING
diff --git a/catbird/asset.scm b/catbird/asset.scm
new file mode 100644
index 0000000..a53048f
--- /dev/null
+++ b/catbird/asset.scm
@@ -0,0 +1,241 @@
+(define-module (catbird asset)
+ #:use-module (catbird config)
+ #:use-module (catbird inotify)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:export (<asset>
+ file-names
+ loader
+ artifact
+ subscribers
+ load!
+ ->asset
+ subscribe
+ unsubscribe
+ on-asset-refresh
+ define-asset
+ reload-modified-assets
+
+ <asset-container>))
+
+(define (absolute-file-name file-name)
+ (if (absolute-file-name? file-name)
+ file-name
+ (string-append (getcwd) "/" file-name)))
+
+
+;;;
+;;; Base Asset
+;;;
+
+(define-root-class <asset> ()
+ (file-names #:getter file-names #:init-keyword #:file-names)
+ (loader #:getter loader #:init-keyword #:loader)
+ (artifact #:accessor %artifact #:init-value #f)
+ (subscribers #:getter subscribers #:init-form (make-weak-key-hash-table)))
+
+(define-method (initialize (asset <asset>) initargs)
+ (next-method)
+ ;; Convert relative file names to absolute file names for
+ ;; consistency and ease of use later.
+ (slot-set! asset 'file-names (map absolute-file-name (file-names asset))))
+
+;; Allow any object to be wrapped in an asset.
+(define-method (->asset x)
+ (make <asset>
+ #:file-names '()
+ #:loader (lambda () x)))
+
+(define-method (->asset (asset <asset>))
+ asset)
+
+(define-method (subscribe (asset <asset>) obj context)
+ (let ((subs (subscribers asset)))
+ (hashq-set! subs obj (cons context (hashq-ref subs obj '())))))
+
+(define-method (unsubscribe (asset <asset>) obj context)
+ (let* ((subs (subscribers asset))
+ (contexts (delq context (hashq-ref subs obj '()))))
+ (if (null? contexts)
+ (hashq-remove! subs obj)
+ (hashq-set! subs obj contexts))))
+
+(define-method (on-asset-refresh obj context)
+ #t)
+
+(define-method (notify-refresh (asset <asset>))
+ (hash-for-each (lambda (subscriber contexts)
+ (for-each (lambda (context)
+ (on-asset-refresh subscriber context))
+ contexts))
+ (subscribers asset)))
+
+(define-method (load! (asset <asset>))
+ (let ((value (apply (loader asset) (file-names asset))))
+ (set! (%artifact asset) value)
+ (notify-refresh asset)
+ value))
+
+(define-method (reload! (asset <asset>))
+ (load! asset))
+
+(define-method (unload! (asset <asset>))
+ (set! (%artifact asset) #f))
+
+(define-method (artifact (asset <asset>))
+ (or (%artifact asset)
+ (load! asset)))
+
+
+;;;
+;;; Auto-reloading Asset
+;;;
+
+(define-class <auto-reload-asset> (<asset>)
+ ;; Do not create inotify handle until it is needed.
+ (inotify #:allocation #:class #:init-form (delay (make-inotify)))
+ ;; List of all auto-reloadable assets stored as a weak key hash
+ ;; table
+ (assets #:allocation #:class #:init-thunk make-weak-key-hash-table))
+
+(define (asset-inotify)
+ (force (class-slot-ref <auto-reload-asset> 'inotify)))
+
+(define (auto-reload-assets)
+ (class-slot-ref <auto-reload-asset> 'assets))
+
+(define (register-auto-reload-asset! asset)
+ (hashq-set! (auto-reload-assets) asset #t))
+
+(define-method (load! (asset <auto-reload-asset>))
+ ;; These are both no-ops if the asset and file are already being
+ ;; watched.
+ (register-auto-reload-asset! asset)
+ (for-each (lambda (file-name)
+ (inotify-add-watch! (asset-inotify) file-name '(close-write)))
+ (file-names asset))
+ (next-method))
+
+(define (assets-for-event event)
+ (let ((f (inotify-watch-file-name (inotify-event-watch event))))
+ (hash-fold (lambda (asset dummy-value memo)
+ (if (member f (file-names asset))
+ (cons asset memo)
+ memo))
+ '()
+ (auto-reload-assets))))
+
+;; Needs to be called periodically in the game loop to reload modified
+;; assets.
+(define (reload-modified-assets)
+ "Reload all assets whose files have been modified."
+ (let ((inotify (asset-inotify)))
+ (while (inotify-pending-events? inotify)
+ (let* ((event (inotify-read-event inotify))
+ (assets (assets-for-event event)))
+ (if (null? assets)
+ ;; There are no assets associated with this file anymore
+ ;; (they've been redefined with new file names or GC'd),
+ ;; so remove the watch.
+ (inotify-watch-remove! (inotify-event-watch event))
+ ;; Reload all assets associated with the file.
+ (for-each reload! assets))))))
+
+
+;;;
+;;; Syntax
+;;;
+
+(define-syntax-rule (define-asset name ((var file-name) ...) body ...)
+ (define name
+ (let ((file-names (list file-name ...))
+ (proc (lambda (var ...) body ...)))
+ (if (and (defined? 'name) (is-a? name <asset>))
+ (begin
+ (initialize name
+ #:file-names file-names
+ #:loader proc)
+ name)
+ (make (if developer-mode? <auto-reload-asset> <asset>)
+ #:file-names file-names
+ #:loader proc)))))
+
+
+;;;
+;;; Asset Metaclass
+;;;
+
+(define-class <asset-slot-class> (<catbird-metaclass>))
+
+(define-method (asset-slot? (slot <slot>))
+ (get-keyword #:asset? (slot-definition-options slot)))
+
+(define (slot-ref* obj slot-name)
+ (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name)))
+
+(define-method (compute-getter-method (class <asset-slot-class>) slot)
+ (if (asset-slot? slot)
+ ;; Wrap the original getter procedure with a new procedure that
+ ;; extracts the current value from the asset object.
+ (make <method>
+ #:specializers (list class)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj)
+ (artifact (proc obj)))))
+ (next-method)))
+
+(define-method (compute-setter-method (class <asset-slot-class>) slot)
+ (if (asset-slot? slot)
+ ;; Wrap the original setter procedure with a new procedure that
+ ;; manages asset update notifications.
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj new)
+ (let ((old (slot-ref* obj slot-name))
+ (new* (->asset new)))
+ (unless (eq? old new)
+ (when old
+ (unsubscribe old obj slot-name))
+ (subscribe new* obj slot-name)
+ (proc obj new*))))))
+ (next-method)))
+
+(define (map-initargs proc initargs)
+ (let loop ((initargs initargs))
+ (match initargs
+ (() '())
+ ((slot-name value . rest)
+ (cons* slot-name (proc slot-name value) (loop rest))))))
+
+(define (for-each-initarg proc initargs)
+ (let loop ((initargs initargs))
+ (match initargs
+ (() '())
+ ((slot-name value . rest)
+ (proc slot-name value)
+ (loop rest)))))
+
+(define (coerce-asset obj slot-name)
+ (let ((value (slot-ref* obj slot-name)))
+ (if (is-a? value <asset>)
+ value
+ (let ((asset (->asset value)))
+ (slot-set! obj slot-name asset)
+ asset))))
+
+(define-class <asset-container> ()
+ #:metaclass <asset-slot-class>)
+
+(define-method (initialize (instance <asset-container>) initargs)
+ (next-method)
+ ;; Subscribe for updates to all asset slots.
+ (for-each (lambda (slot)
+ (when (asset-slot? slot)
+ (let* ((slot-name (slot-definition-name slot))
+ (value (coerce-asset instance slot-name)))
+ (subscribe value instance slot-name))))
+ (class-slots (class-of instance))))
diff --git a/catbird/cached-slots.scm b/catbird/cached-slots.scm
new file mode 100644
index 0000000..be77103
--- /dev/null
+++ b/catbird/cached-slots.scm
@@ -0,0 +1,88 @@
+(define-module (catbird cached-slots)
+ #:use-module (catbird config)
+ #:use-module (oop goops)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-9)
+ #:export (<cacheable>
+ slot-expired?
+ expire-slot!))
+
+(define-record-type <cached-value>
+ (%make-cached-value value expired? proc)
+ cached-value?
+ (value %cached-value-ref set-cached-value!)
+ (expired? cached-value-expired? set-cached-value-expired!)
+ (proc cached-value-proc))
+
+(define (make-cached-value init proc)
+ (%make-cached-value init #t proc))
+
+(define (refresh-cached-value! cache)
+ (let ((x ((cached-value-proc cache) (%cached-value-ref cache))))
+ (set-cached-value! cache x)
+ (set-cached-value-expired! cache #f)))
+
+(define (cached-value-ref cache)
+ (when (cached-value-expired? cache)
+ (refresh-cached-value! cache))
+ (%cached-value-ref cache))
+
+(define (expire-cached-value! cache)
+ (set-cached-value-expired! cache #t))
+
+(define (expire-slot! obj slot-name)
+ (expire-cached-value! (slot-ref obj slot-name)))
+
+(define (slot-expired? obj slot-name)
+ (cached-value-expired? (slot-ref obj slot-name)))
+
+(define-class <cached-slot-class> (<catbird-metaclass>))
+
+(define (slot-ref* obj slot-name)
+ (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name)))
+
+(define-method (cached-slot? (slot <slot>))
+ (get-keyword #:cached? (slot-definition-options slot)))
+
+(define-method (slot-refresh-proc (slot <slot>))
+ (get-keyword #:refresh (slot-definition-options slot)))
+
+(define-method (compute-getter-method (class <cached-slot-class>) slot)
+ (if (cached-slot? slot)
+ ;; Wrap the original getter procedure with a new procedure that
+ ;; extracts the current value from the cached value, recomputing
+ ;; it if necessary.
+ (make <method>
+ #:specializers (list class)
+ #:procedure (let ((proc (method-procedure (next-method))))
+ (lambda (obj)
+ (cached-value-ref (proc obj)))))
+ (next-method)))
+
+(define-method (compute-setter-method (class <cached-slot-class>) slot)
+ (if (cached-slot? slot)
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (lambda (obj new)
+ (raise-exception
+ (make-exception-with-message "cached slots cannot be set"))))
+ (next-method)))
+
+(define-class <cacheable> ()
+ #:metaclass <cached-slot-class>)
+
+(define-method (initialize (instance <cacheable>) initargs)
+ (next-method)
+ ;; Setup cached values.
+ (for-each (lambda (slot)
+ (when (cached-slot? slot)
+ (let* ((slot-name (slot-definition-name slot))
+ (refresh-proc (slot-refresh-proc slot))
+ (cached-value (make-cached-value
+ (slot-ref* instance slot-name)
+ (lambda (prev)
+ (refresh-proc instance prev)))))
+ (slot-set! instance slot-name cached-value))))
+ (class-slots (class-of instance))))
diff --git a/catbird/camera.scm b/catbird/camera.scm
new file mode 100644
index 0000000..ad7425c
--- /dev/null
+++ b/catbird/camera.scm
@@ -0,0 +1,113 @@
+(define-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (chickadee math)
+ #:use-module (chickadee math matrix)
+ #:use-module (chickadee math rect)
+ #:use-module (chickadee math vector)
+ #:use-module (oop goops)
+ #:export (<camera>
+ projection-matrix
+ view-matrix
+ width
+ height
+ current-camera
+
+ <camera-2d>
+ view-bounding-box
+ move-to
+ move-by
+
+ <camera-3d>
+ field-of-vision
+ near-clip
+ far-clip
+ direction
+ up))
+
+(define-root-class <camera> ()
+ (width #:accessor width #:init-keyword #:width)
+ (height #:accessor height #:init-keyword #:height)
+ (projection-matrix #:getter projection-matrix #:init-thunk make-identity-matrix4)
+ (view-matrix #:getter view-matrix #:init-thunk make-identity-matrix4))
+
+(define-generic refresh-projection)
+(define-generic refresh-view)
+
+(define-method (initialize (camera <camera>) args)
+ (next-method)
+ (refresh-projection camera)
+ (refresh-view camera))
+
+(define current-camera (make-parameter #f))
+
+
+;;;
+;;; 2D Camera
+;;;
+
+(define-class <camera-2d> (<camera> <movable-2d>)
+ (view-bounding-box #:accessor view-bounding-box #:init-thunk make-null-rect))
+
+(define-method (initialize (camera <camera-2d>) initargs)
+ (next-method)
+ (let ((bb (view-bounding-box camera)))
+ (set-rect-width! bb (width camera))
+ (set-rect-height! bb (height camera))))
+
+(define-method (refresh-projection (camera <camera-2d>))
+ (orthographic-projection! (projection-matrix camera)
+ 0.0 (width camera)
+ (height camera) 0.0
+ 0.0 1.0))
+
+(define-method (refresh-view (camera <camera-2d>))
+ (let ((p (position camera))
+ (bb (view-bounding-box camera)))
+ (matrix4-translate! (view-matrix camera) p)
+ (set-rect-x! bb (vec2-x p))
+ (set-rect-y! bb (vec2-y p))))
+
+(define-method (move-to (camera <camera-2d>) p)
+ (vec2-copy! p (position camera))
+ (refresh-view camera))
+
+(define-method (move-by (camera <camera-2d>) d)
+ (vec2-add! (position camera) d)
+ (refresh-view camera))
+
+
+;;;
+;;; 3D Camera
+;;;
+
+(define-class <camera-3d> (<camera> <movable-3d>)
+ (field-of-vision #:getter field-of-vision #:init-keyword #:field-of-vision
+ #:init-value (degrees->radians 60))
+ (near-clip #:getter near-clip #:init-keyword #:near-clip #:init-value 0.1)
+ (far-clip #:getter far-clip #:init-keyword #:far-clip #:init-value 5.0)
+ (direction #:getter direction #:init-keyword #:direction
+ #:init-form (vec3 0.0 0.0 -1.0))
+ (up #:getter up #:init-keyword #:up
+ #:init-form (vec3 0.0 1.0 0.0)))
+
+(define-method (refresh-projection (camera <camera-3d>))
+ (perspective-projection! (projection-matrix camera)
+ (field-of-vision camera)
+ (/ (width camera) (height camera))
+ (near-clip camera)
+ (far-clip camera)))
+
+(define-method (refresh-view (camera <camera-3d>))
+ (look-at! (view-matrix camera)
+ (position camera)
+ (direction camera)
+ (up camera)))
+
+(define-method (move-to (camera <camera-3d>) p)
+ (vec3-copy! p (position camera))
+ (refresh-view camera))
+
+(define-method (move-by (camera <camera-3d>) d)
+ (vec3-add! (position camera) d)
+ (refresh-view camera))
diff --git a/catbird/config.scm b/catbird/config.scm
new file mode 100644
index 0000000..a010bc1
--- /dev/null
+++ b/catbird/config.scm
@@ -0,0 +1,16 @@
+(define-module (catbird config)
+ #:use-module (oop goops)
+ #:export (developer-mode?
+ <catbird-metaclass>
+ define-root-class))
+
+(define developer-mode?
+ (equal? (getenv "CATBIRD_DEV_MODE") "1"))
+
+(define <catbird-metaclass>
+ (if developer-mode? <redefinable-class> <class>))
+
+(define-syntax-rule (define-root-class name (supers ...) args ...)
+ (define-class name (supers ...)
+ args ...
+ #:metaclass <catbird-metaclass>))
diff --git a/catbird/inotify.scm b/catbird/inotify.scm
new file mode 100644
index 0000000..28a92b5
--- /dev/null
+++ b/catbird/inotify.scm
@@ -0,0 +1,197 @@
+(define-module (catbird inotify)
+ #:use-module (ice-9 binary-ports)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 match)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-9 gnu)
+ #:use-module (system foreign)
+ #:export (make-inotify
+ inotify?
+ inotify-watches
+ inotify-add-watch!
+ inotify-pending-events?
+ inotify-read-event
+ inotify-watch?
+ inotify-watch-id
+ inotify-watch-file-name
+ inotify-watch-remove!
+ inotify-event?
+ inotify-event-watch
+ inotify-event-type
+ inotify-event-cookie
+ inotify-event-file-name))
+
+(define libc (dynamic-link))
+
+(define inotify-init
+ (pointer->procedure int (dynamic-func "inotify_init" libc) '()))
+
+(define inotify-add-watch
+ (pointer->procedure int (dynamic-func "inotify_add_watch" libc)
+ (list int '* uint32)))
+
+(define inotify-rm-watch
+ (pointer->procedure int (dynamic-func "inotify_rm_watch" libc)
+ (list int int)))
+
+(define IN_ACCESS #x00000001) ; file was accessed.
+(define IN_MODIFY #x00000002) ; file was modified.
+(define IN_ATTRIB #x00000004) ; metadata changed
+(define IN_CLOSE_WRITE #x00000008) ; file opened for writing closed
+(define IN_CLOSE_NOWRITE #x00000010) ; file not opened for writing closed
+(define IN_OPEN #x00000020) ; file was opened
+(define IN_MOVED_FROM #x00000040) ; file was moved from X
+(define IN_MOVED_TO #x00000080) ; file was moved to Y
+(define IN_CREATE #x00000100) ; subfile was created
+(define IN_DELETE #x00000200) ; subfile was deleted
+(define IN_DELETE_SELF #x00000400) ; self was deleted
+(define IN_MOVE_SELF #x00000800) ; self was moved
+;; Kernel flags
+(define IN_UNMOUNT #x00002000) ; backing fs was unmounted
+(define IN_Q_OVERFLOW #x00004000) ; event queue overflowed
+(define IN_IGNORED #x00008000) ; file was ignored
+;; Special flags
+(define IN_ONLYDIR #x01000000) ; only watch if directory
+(define IN_DONT_FOLLOW #x02000000) ; do not follow symlink
+(define IN_EXCL_UNLINK #x04000000) ; exclude events on unlinked objects
+(define IN_MASK_ADD #x20000000) ; add to the mask of an existing watch
+(define IN_ISDIR #x40000000) ; event occurred against directory
+(define IN_ONESHOT #x80000000) ; only send event once
+
+(define mask/symbol (make-hash-table))
+(define symbol/mask (make-hash-table))
+
+(for-each (match-lambda
+ ((sym mask)
+ (hashq-set! symbol/mask sym mask)
+ (hashv-set! mask/symbol mask sym)))
+ `((access ,IN_ACCESS)
+ (modify ,IN_MODIFY)
+ (attrib ,IN_ATTRIB)
+ (close-write ,IN_CLOSE_WRITE)
+ (close-no-write ,IN_CLOSE_NOWRITE)
+ (open ,IN_OPEN)
+ (moved-from ,IN_MOVED_FROM)
+ (moved-to ,IN_MOVED_TO)
+ (create ,IN_CREATE)
+ (delete ,IN_DELETE)
+ (delete-self ,IN_DELETE_SELF)
+ (move-self ,IN_MOVE_SELF)
+ (only-dir ,IN_ONLYDIR)
+ (dont-follow ,IN_DONT_FOLLOW)
+ (exclude-unlink ,IN_EXCL_UNLINK)
+ (is-directory ,IN_ISDIR)
+ (once ,IN_ONESHOT)))
+
+(define (symbol->mask sym)
+ (hashq-ref symbol/mask sym))
+
+(define (mask->symbol sym)
+ (hashq-ref mask/symbol sym))
+
+(define-record-type <inotify>
+ (%make-inotify port buffer buffer-pointer watches)
+ inotify?
+ (port inotify-port)
+ (buffer inotify-buffer)
+ (buffer-pointer inotify-buffer-pointer)
+ (watches inotify-watches))
+
+(define-record-type <inotify-watch>
+ (make-inotify-watch id file-name owner)
+ inotify-watch?
+ (id inotify-watch-id)
+ (file-name inotify-watch-file-name)
+ (owner inotify-watch-owner))
+
+(define-record-type <inotify-event>
+ (make-inotify-event watch type cookie file-name)
+ inotify-event?
+ (watch inotify-event-watch)
+ (type inotify-event-type)
+ (cookie inotify-event-cookie)
+ (file-name inotify-event-file-name))
+
+(define (display-inotify inotify port)
+ (format port "#<inotify port: ~a>" (inotify-port inotify)))
+
+(define (display-inotify-watch watch port)
+ (format port "#<inotify-watch id: ~d file-name: ~a>"
+ (inotify-watch-id watch)
+ (inotify-watch-file-name watch)))
+
+(define (display-inotify-event event port)
+ (format port "#<inotify-event type: ~s cookie: ~d file-name: ~a watch: ~a>"
+ (inotify-event-type event)
+ (inotify-event-cookie event)
+ (inotify-event-file-name event)
+ (inotify-event-watch event)))
+
+(set-record-type-printer! <inotify> display-inotify)
+(set-record-type-printer! <inotify-watch> display-inotify-watch)
+(set-record-type-printer! <inotify-event> display-inotify-event)
+
+(define (make-inotify)
+ (let ((fd (inotify-init))
+ (buffer (make-bytevector 4096)))
+ (%make-inotify (fdopen fd "r")
+ buffer
+ (bytevector->pointer buffer)
+ (make-hash-table))))
+
+(define (inotify-fd inotify)
+ (port->fdes (inotify-port inotify)))
+
+(define (absolute-file-name file-name)
+ (if (absolute-file-name? file-name)
+ file-name
+ (string-append (getcwd) "/" file-name)))
+
+(define (inotify-add-watch! inotify file-name modes)
+ (let* ((watches (inotify-watches inotify))
+ (abs-file-name (absolute-file-name file-name))
+ (wd (inotify-add-watch (inotify-fd inotify)
+ (string->pointer abs-file-name)
+ (apply logior (map symbol->mask modes)))))
+ (or (hashv-ref watches wd)
+ (let ((new-watch (make-inotify-watch wd abs-file-name inotify)))
+ (hashv-set! watches wd new-watch)
+ new-watch))))
+
+(define (inotify-watch-remove! watch)
+ (inotify-rm-watch (inotify-fd (inotify-watch-owner watch))
+ (inotify-watch-id watch))
+ (hashv-remove! (inotify-watches (inotify-watch-owner watch))
+ (inotify-watch-id watch)))
+
+(define (inotify-pending-events? inotify)
+ ;; Sometimes an interrupt happens during the char-ready? call and an
+ ;; exception is thrown. Just return #f in that case and move on
+ ;; with life.
+ (false-if-exception (char-ready? (inotify-port inotify))))
+
+(define (read-int port buffer)
+ (get-bytevector-n! port buffer 0 (sizeof int))
+ (bytevector-sint-ref buffer 0 (native-endianness) (sizeof int)))
+
+(define (read-uint32 port buffer)
+ (get-bytevector-n! port buffer 0 (sizeof uint32))
+ (bytevector-uint-ref buffer 0 (native-endianness) (sizeof uint32)))
+
+(define (read-string port buffer buffer-pointer length)
+ (and (> length 0)
+ (begin
+ (get-bytevector-n! port buffer 0 length)
+ (pointer->string buffer-pointer))))
+
+(define (inotify-read-event inotify)
+ (let* ((port (inotify-port inotify))
+ (buffer (inotify-buffer inotify))
+ (wd (read-int port buffer))
+ (event-mask (read-uint32 port buffer))
+ (cookie (read-uint32 port buffer))
+ (len (read-uint32 port buffer))
+ (name (read-string port buffer (inotify-buffer-pointer inotify) len)))
+ (make-inotify-event (hashv-ref (inotify-watches inotify) wd)
+ (mask->symbol event-mask) cookie name)))
diff --git a/catbird/input-map.scm b/catbird/input-map.scm
new file mode 100644
index 0000000..43ba57a
--- /dev/null
+++ b/catbird/input-map.scm
@@ -0,0 +1,175 @@
+(define-module (catbird input-map)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-1)
+ #:export (make-input-map
+ add-input
+ remove-input
+ key-press
+ key-release
+ text-input
+ mouse-press
+ mouse-release
+ mouse-move
+ mouse-wheel
+ controller-press
+ controller-release
+ controller-move
+ key-press-handler
+ key-release-handler
+ text-input-handler
+ mouse-press-handler
+ mouse-release-handler
+ mouse-move-handler
+ mouse-wheel-handler
+ controller-press-handler
+ controller-release-handler
+ controller-move-handler))
+
+(define (make-input-map)
+ '())
+
+(define (add-input input-map spec handler)
+ (cons (cons spec handler)
+ input-map))
+
+(define (remove-input input-map spec)
+ (delete spec input-map
+ (match-lambda
+ ((s . _) (equal? s spec)))))
+
+(define* (key-press key #:optional (modifiers '()))
+ `(keyboard ,key ,modifiers down))
+
+(define* (key-release key #:optional (modifiers '()))
+ `(keyboard ,key ,modifiers up))
+
+(define (text-input)
+ '(text-input))
+
+(define (mouse-press button)
+ `(mouse button ,button down))
+
+(define (mouse-release button)
+ `(mouse button ,button up))
+
+(define* (mouse-move #:optional (buttons '()))
+ `(mouse move ,buttons))
+
+(define* (mouse-wheel)
+ '(mouse wheel))
+
+(define (controller-press id button)
+ `(controller button ,id ,button down))
+
+(define (controller-release id button)
+ `(controller button ,id ,button up))
+
+(define (controller-move id axis)
+ `(controller axis ,id ,axis))
+
+;; Chickadee is specific about which modifier keys are pressed and
+;; makes distinctions between left and right ctrl, for example. For
+;; convenience, we want users to be able to specify simply 'ctrl' and
+;; it will match both left and right.
+(define (modifiers-match? spec-modifiers modifiers)
+ (every (lambda (k)
+ (case k
+ ;; The specification is looking for a specific modifier
+ ;; key.
+ ((left-ctrl right-ctrl left-alt right-alt left-shift right-shift)
+ (memq k modifiers))
+ ;; The specification is looking for either left/right
+ ;; modifier key.
+ ((ctrl)
+ (or (memq 'left-control modifiers)
+ (memq 'right-control modifiers)))
+ ((alt)
+ (or (memq 'left-alt modifiers)
+ (memq 'right-alt modifiers)))
+ ((shift)
+ (or (memq 'left-shift modifiers)
+ (memq 'right-shift modifiers)))))
+ spec-modifiers))
+
+(define (key-press-handler input-map key modifiers)
+ (any (match-lambda
+ ((('keyboard key* modifiers* 'down) . handler)
+ (and (eq? key key*)
+ (modifiers-match? modifiers* modifiers)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (key-release-handler input-map key modifiers)
+ (any (match-lambda
+ ((('keyboard key* modifiers* 'up) . handler)
+ (and (eq? key key*)
+ (modifiers-match? modifiers modifiers*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (text-input-handler input-map)
+ (any (match-lambda
+ ((('text-input) . handler) handler)
+ (_ #f))
+ input-map))
+
+(define (mouse-press-handler input-map button)
+ (any (match-lambda
+ ((('mouse 'button button* 'down) . handler)
+ (and (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-release-handler input-map button)
+ (any (match-lambda
+ ((('mouse 'button button* 'up) . handler)
+ (and (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-move-handler input-map buttons)
+ (any (match-lambda
+ ((('mouse 'move buttons*) . handler)
+ (and (= (length buttons) (length buttons*))
+ (every (lambda (b) (memq b buttons*)) buttons)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-wheel-handler input-map)
+ (any (match-lambda
+ ((('mouse 'wheel) . handler)
+ handler)
+ (_ #f))
+ input-map))
+
+(define (controller-press-handler input-map controller-id button)
+ (any (match-lambda
+ ((('controller 'button controller-id* button* 'down) . handler)
+ (and (= controller-id controller-id*)
+ (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (controller-release-handler input-map controller-id button)
+ (any (match-lambda
+ ((('controller 'button controller-id* button* 'up) . handler)
+ (and (= controller-id controller-id*)
+ (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (controller-move-handler input-map controller-id axis)
+ (any (match-lambda
+ ((('controller 'axis controller-id* axis*) . handler)
+ (and (= controller-id controller-id*)
+ (eq? axis axis*)
+ handler))
+ (_ #f))
+ input-map))
diff --git a/catbird/kernel.scm b/catbird/kernel.scm
new file mode 100644
index 0000000..4ed642a
--- /dev/null
+++ b/catbird/kernel.scm
@@ -0,0 +1,394 @@
+(define-module (catbird kernel)
+ #:use-module (catbird asset)
+ #:use-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird input-map)
+ #:use-module (catbird mixins)
+ #:use-module (catbird mode)
+ #:use-module (catbird region)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee data array-list)
+ #:use-module (chickadee math rect)
+ #:use-module (ice-9 atomic)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:use-module (system repl coop-server)
+ #:export (all-regions
+ create-full-region
+ create-region
+ find-region-by-name
+ frames-per-second
+ kill-region
+ current-keyboard-focus
+ take-keyboard-focus
+ current-controller-focus
+ take-controller-focus
+ bind-input/global
+ unbind-input/global
+ run-catbird
+ exit-catbird))
+
+
+;;;
+;;; Kernel
+;;;
+
+(define-root-class <kernel> ()
+ (controllers #:getter controllers #:init-thunk make-array-list)
+ (regions #:accessor regions #:init-value '())
+ (input-map #:accessor input-map #:init-thunk make-input-map)
+ (keyboard-focus #:accessor keyboard-focus #:init-value #f)
+ (controller-focus #:getter controller-focus #:init-thunk make-hash-table)
+ (repl #:accessor repl #:init-value #f)
+ (frame-start-time #:accessor frame-start-time #:init-value 0.0)
+ (average-frame-time #:accessor average-frame-time #:init-value 0.0))
+
+(define-method (load* (kernel <kernel>))
+ (when developer-mode?
+ (set! (repl kernel) (spawn-coop-repl-server))))
+
+;; Add the system notification and debugging overlay.
+(define-method (add-overlay (kernel <kernel>))
+ (let ((region (create-full-region #:name 'overlay #:rank 9999)))
+ (set! (camera region)
+ (make <camera-2d>
+ #:width (rect-width (area region))
+ #:height (rect-height (area region))))
+ ;; Use resolve-module to avoid a circular dependency.
+ (replace-scene region
+ ((module-ref (resolve-module '(catbird overlay))
+ 'make-overlay)))))
+
+(define-method (overlay-scene (kernel <kernel>))
+ (scene (lookup-region kernel 'overlay)))
+
+(define-method (notify (kernel <kernel>) message)
+ (let ((notify (module-ref (resolve-module '(catbird overlay)) 'notify)))
+ (notify (overlay-scene kernel) message)))
+
+(define-method (update (kernel <kernel>) dt)
+ (when developer-mode?
+ (poll-coop-repl-server (repl kernel))
+ (reload-modified-assets))
+ (for-each (lambda (region) (update/around region dt))
+ (regions kernel)))
+
+(define-method (render (kernel <kernel>) alpha)
+ (for-each (lambda (region)
+ (render/around region alpha))
+ (regions kernel))
+ ;; Compute FPS.
+ (let ((current-time (elapsed-time)))
+ (set! (average-frame-time kernel)
+ (+ (* (- current-time (frame-start-time kernel)) 0.1)
+ (* (average-frame-time kernel) 0.9)))
+ (set! (frame-start-time kernel) current-time)))
+
+(define-method (lookup-region (kernel <kernel>) region-name)
+ (find (lambda (region)
+ (eq? (name region) region-name))
+ (regions kernel)))
+
+(define-method (add-region (kernel <kernel>) (region <region>))
+ (let ((r (regions kernel)))
+ ;; The first region added gets keyboard focus by default.
+ (when (null? r)
+ (set! (keyboard-focus kernel) region))
+ (set! (regions kernel)
+ (sort-by-rank/ascending (cons region (regions kernel))))))
+
+(define-method (bind-input (kernel <kernel>) spec handler)
+ (set! (input-map kernel) (add-input (input-map kernel) spec handler)))
+
+(define-method (unbind-input (kernel <kernel>) spec)
+ (set! (input-map kernel) (remove-input (input-map kernel) spec)))
+
+
+;;;
+;;; Keyboard
+;;;
+
+(define-method (on-key-press (kernel <kernel>) key modifiers)
+ (or (let ((handler (key-press-handler (input-map kernel) key modifiers)))
+ (and handler (handler)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-key-press s key modifiers)))))
+
+(define-method (on-key-release (kernel <kernel>) key modifiers)
+ (or (let ((handler (key-release-handler (input-map kernel) key modifiers)))
+ (and handler (handler)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-key-release s key modifiers)))))
+
+(define-method (on-text-input (kernel <kernel>) text)
+ (or (let ((handler (text-input-handler (input-map kernel))))
+ (and handler (handler text)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-text-input s text)))))
+
+
+;;;
+;;; Mouse
+;;;
+
+(define (mouse-search kernel proc)
+ (let loop ((regions* (regions kernel)))
+ (match regions*
+ (() #f)
+ ((r . rest)
+ (or (loop rest)
+ (let ((s (scene r)))
+ (and s (proc s))))))))
+
+(define-method (on-mouse-press (kernel <kernel>) button x y)
+ (or (let ((handler (mouse-press-handler (input-map kernel) button)))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-press s button x y)))))
+
+(define-method (on-mouse-release (kernel <kernel>) button x y)
+ (or (let ((handler (mouse-release-handler (input-map kernel) button)))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-release s button x y)))))
+
+(define-method (on-mouse-move (kernel <kernel>) x y x-rel y-rel buttons)
+ (or (let ((handler (mouse-move-handler (input-map kernel) buttons)))
+ (and handler (handler x y x-rel y-rel)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-move s x y x-rel y-rel buttons)))))
+
+(define-method (on-mouse-wheel (kernel <kernel>) x y)
+ (or (let ((handler (mouse-wheel-handler (input-map kernel))))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-wheel s x y)))))
+
+
+;;;
+;;; Controllers
+;;;
+
+(define-method (controller-focus (kernel <kernel>) slot)
+ (hashv-ref (controller-focus kernel) (controller-slot-id slot)))
+
+(define (make-controller-slot id)
+ (vector id #f))
+
+(define (controller-slot-id slot)
+ (vector-ref slot 0))
+
+(define (controller-slot-controller slot)
+ (vector-ref slot 1))
+
+(define (controller-slot-empty? slot)
+ (not (controller-slot-controller slot)))
+
+(define (fill-controller-slot! slot controller)
+ (vector-set! slot 1 controller))
+
+(define (clear-controller-slot! slot)
+ (fill-controller-slot! slot #f))
+
+(define-method (empty-controller-slot (kernel <kernel>))
+ (let* ((c (controllers kernel))
+ (n (array-list-size c)))
+ (let loop ((i 0))
+ (if (= i n)
+ (let ((slot (make-controller-slot i)))
+ (array-list-push! c slot)
+ slot)
+ (let ((slot (array-list-ref c i)))
+ (if (controller-slot-empty? slot)
+ slot
+ (loop (+ i 1))))))))
+
+(define-method (find-controller-slot (kernel <kernel>) controller)
+ (let* ((c (controllers kernel))
+ (n (array-list-size c)))
+ (let loop ((i 0))
+ (if (= i n)
+ #f
+ (let ((slot (array-list-ref c i)))
+ (if (eq? (controller-slot-controller slot) controller)
+ slot
+ (loop (+ i 1))))))))
+
+(define-method (on-controller-add (kernel <kernel>) controller)
+ (let ((slot (empty-controller-slot kernel)))
+ (notify kernel (string-append "Controller "
+ (number->string
+ (+ (controller-slot-id slot) 1))
+ " connected: "
+ (controller-name controller)))
+ (fill-controller-slot! slot controller)))
+
+(define-method (on-controller-remove (kernel <kernel>) controller)
+ (let ((slot (find-controller-slot kernel controller)))
+ (notify kernel (string-append "Controller "
+ (number->string
+ (+ (controller-slot-id slot) 1))
+ " disconnected: "
+ (controller-name controller)))
+ (clear-controller-slot! (find-controller-slot kernel controller))))
+
+(define-method (on-controller-press (kernel <kernel>) controller button)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-press-handler (input-map kernel)
+ (controller-slot-id slot)
+ button)))
+ (and handler (handler)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and r (on-controller-press s
+ (controller-slot-id slot)
+ button))))))
+
+(define-method (on-controller-release (kernel <kernel>) controller button)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-release-handler (input-map kernel)
+ (controller-slot-id slot)
+ button)))
+ (and handler (handler)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and s (on-controller-release s
+ (controller-slot-id slot)
+ button))))))
+
+(define-method (on-controller-move (kernel <kernel>) controller axis value)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-move-handler (input-map kernel)
+ (controller-slot-id slot)
+ axis)))
+ (and handler (handler value)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and s (on-controller-move s
+ (controller-slot-id slot)
+ axis
+ value))))))
+
+
+;;;
+;;; Global kernel API
+;;;
+
+(define current-kernel (make-parameter #f))
+
+(define (unique-region-name)
+ (gensym "region-"))
+
+(define* (create-region area #:key (rank 0) (name (unique-region-name)))
+ (let ((region (make-region area name rank)))
+ (add-region (current-kernel) region)
+ region))
+
+(define* (create-full-region #:key (rank 0) (name (unique-region-name)))
+ (let ((w (window-width (current-window)))
+ (h (window-height (current-window))))
+ (create-region (make-rect 0.0 0.0 w h) #:rank rank #:name name)))
+
+(define (kill-region region)
+ (let ((k (current-kernel)))
+ (set! (regions k) (delq region (regions k)))))
+
+(define (all-regions)
+ (regions (current-kernel)))
+
+(define (find-region-by-name name)
+ (lookup-region (current-kernel) name))
+
+(define (current-keyboard-focus)
+ (keyboard-focus (current-kernel)))
+
+(define (take-keyboard-focus region)
+ (set! (keyboard-focus (current-kernel)) region))
+
+(define (current-controller-focus controller-id)
+ (hashv-ref (controller-focus (current-kernel)) controller-id))
+
+(define (take-controller-focus controller-id region)
+ (hashv-set! (controller-focus (current-kernel)) controller-id region))
+
+(define (bind-input/global spec handler)
+ (bind-input (current-kernel) spec handler))
+
+(define (unbind-input/global spec handler)
+ (unbind-input (current-kernel) spec handler))
+
+(define (frames-per-second)
+ (/ 1.0 (average-frame-time (current-kernel))))
+
+(define* (run-catbird thunk #:key (width 1366) (height 768)
+ (title "^~Catbird~^") (fullscreen? #f)
+ (resizable? #t) (update-hz 60))
+ (let ((kernel (make <kernel>)))
+ (parameterize ((current-kernel kernel))
+ (run-game #:window-title title
+ #:window-width width
+ #:window-height height
+ #:window-fullscreen? fullscreen?
+ #:window-resizable? resizable?
+ #:update-hz update-hz
+ #:load
+ (lambda ()
+ (load* kernel)
+ (thunk)
+ (add-overlay kernel))
+ #:draw
+ (lambda (alpha)
+ (render kernel alpha))
+ #:update
+ (lambda (dt)
+ (update kernel dt))
+ #:key-press
+ (lambda (key modifiers repeat?)
+ (on-key-press kernel key modifiers))
+ #:key-release
+ (lambda (key modifiers)
+ (on-key-release kernel key modifiers))
+ #:text-input
+ (lambda (text)
+ (on-text-input kernel text))
+ #:mouse-press
+ ;; TODO: Handle click counter?
+ (lambda (button clicks x y)
+ (on-mouse-press kernel button x y))
+ #:mouse-release
+ (lambda (button x y)
+ (on-mouse-release kernel button x y))
+ #:mouse-move
+ (lambda (x y x-rel y-rel buttons)
+ (on-mouse-move kernel x y x-rel y-rel buttons))
+ #:mouse-wheel
+ (lambda (x y)
+ (on-mouse-wheel kernel x y))
+ #:controller-add
+ (lambda (controller)
+ (on-controller-add kernel controller))
+ #:controller-remove
+ (lambda (controller)
+ (on-controller-remove kernel controller))
+ #:controller-press
+ (lambda (controller button)
+ (on-controller-press kernel controller button))
+ #:controller-release
+ (lambda (controller button)
+ (on-controller-release kernel controller button))
+ #:controller-move
+ (lambda (controller axis value)
+ (on-controller-move kernel controller axis value))))))
+
+(define (exit-catbird)
+ "Stop the Catbird engine."
+ (abort-game))
diff --git a/catbird/line-editor.scm b/catbird/line-editor.scm
new file mode 100644
index 0000000..74ced0b
--- /dev/null
+++ b/catbird/line-editor.scm
@@ -0,0 +1,312 @@
+(define-module (catbird line-editor)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird observer)
+ #:use-module (catbird region)
+ #:use-module (catbird ring-buffer)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<line-editor>
+ <line-edit-mode>
+ backward-char
+ backward-delete-char
+ forward-delete-char
+ backward-history
+ beginning-of-line
+ clear-line
+ end-of-line
+ forward-char
+ forward-history
+ get-line
+ history-enabled?
+ insert-char
+ invert-color
+ kill-line
+ overwrite
+ prompt
+ save-to-history)
+ #:re-export (color
+ font))
+
+;; TODO: Matching paren/quote highlighting.
+(define-class <line-editor> (<node-2d>)
+ (chars-before #:accessor chars-before #:init-value '())
+ (chars-after #:accessor chars-after #:init-value '())
+ (cached-line #:accessor cached-line #:init-value #f)
+ (prompt #:accessor prompt #:init-keyword #:prompt #:init-value ""
+ #:observe? #t)
+ ;; TODO: Allow customizable history length.
+ (history #:accessor history #:init-form (make-ring-buffer 128))
+ (history-enabled? #:accessor history-enabled?
+ #:init-keyword #:history-enabled? #:init-value #t)
+ (history-index #:accessor history-index #:init-value 0)
+ (font #:accessor font #:init-keyword #:font #:init-thunk default-font
+ #:asset? #t)
+ (color #:accessor color #:init-keyword #:color #:init-value white)
+ (invert-color #:accessor invert-color #:init-keyword #:invert-color
+ #:init-value black)
+ (accepting-input? #:accessor accepting-input? #:init-value #t))
+
+(define-method (on-change (editor <line-editor>) slot old new)
+ (update-visual editor))
+
+(define-method (on-boot (editor <line-editor>))
+ (attach-to editor
+ (make <label>
+ #:name 'prompt
+ #:font (font editor)
+ #:text (prompt editor)
+ #:color (color editor))
+ (make <label>
+ #:name 'before-cursor
+ #:rank 1
+ #:font (font editor)
+ #:color (color editor))
+ (make <label>
+ #:name 'on-cursor
+ #:rank 1
+ #:font (font editor))
+ (make <label>
+ #:name 'after-cursor
+ #:rank 1
+ #:font (font editor)
+ #:color (color editor))
+ (make <canvas>
+ #:name 'cursor
+ #:painter
+ (with-style ((fill-color (color editor)))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ (font-line-width (font editor) "_")
+ (font-line-height (font editor)))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+;; Whenever a command key sequence is pressed while the line editor is
+;; active we have to stop accepting text input for one tick.
+;; Otherwise, an errant character shows up. For example, if the user
+;; presses alt+p, and that is bound to (backward-history editor 1),
+;; the 'p' character shows up at the end of the line. This is due to
+;; the fact that SDL generates a key event *and* an input event for
+;; the same key press.
+(define-method (discard-next-char (editor <line-editor>))
+ (run-script editor
+ (set! (accepting-input? editor) #f)
+ (sleep (current-timestep))
+ (set! (accepting-input? editor) #t)))
+
+(define-method (update-visual (editor <line-editor>))
+ (let* ((lprompt (& editor prompt))
+ (cursor (& editor cursor))
+ (before (& editor before-cursor))
+ (on (& editor on-cursor))
+ (after (& editor after-cursor)))
+ (set! (cached-line editor) #f)
+ ;; Stop cursor blink. The cursor should only blink when the user
+ ;; is idle.
+ (stop-scripts cursor)
+ ;; Ensure the cursor is visible in case we stopped the script
+ ;; during a time when it was hidden.
+ (show cursor)
+ ;; Put the proper text in the proper labels
+ (set! (text lprompt) (prompt editor))
+ (set! (text before)
+ (list->string (reverse (chars-before editor))))
+ (set! (text on)
+ (match (chars-after editor)
+ (() "")
+ ((c . _)
+ (string c))))
+ (set! (text after)
+ (match (chars-after editor)
+ (() "")
+ ((_ . chars)
+ (list->string chars))))
+ ;; Line everything up.
+ (place-right lprompt before)
+ (place-right before on)
+ (place-right on after)
+ (align-left on cursor)
+ ;; Adjust size
+ (set! (width editor)
+ (+ (width lprompt) (width before) (width on) (width after)))
+ (set! (height editor) (height cursor))
+ ;; Resume blinking cursor after a short idle timeout.
+ (run-script cursor
+ (forever
+ (set! (color on) (invert-color editor))
+ (sleep 0.5)
+ (hide cursor)
+ (set! (color on) (color editor))
+ (sleep 0.5)
+ (show cursor)))))
+
+(define-method (get-line (editor <line-editor>))
+ (or (cached-line editor)
+ (let ((line (list->string
+ (append (reverse (chars-before editor))
+ (chars-after editor)))))
+ (set! (cached-line editor) line)
+ line)))
+
+(define-method (overwrite (editor <line-editor>) str)
+ (set! (chars-before editor) (reverse (string->list str)))
+ (set! (chars-after editor) '())
+ (update-visual editor))
+
+(define-method (clear-line (editor <line-editor>))
+ (discard-next-char editor)
+ (overwrite editor "")
+ (newest-history editor))
+
+(define-method (insert-char (editor <line-editor>) char)
+ (when (accepting-input? editor)
+ (set! (chars-before editor) (cons char (chars-before editor)))
+ (update-visual editor)))
+
+(define-method (backward-delete-char (editor <line-editor>) n)
+ (unless (<= n 0)
+ (set! (chars-before editor)
+ (drop (chars-before editor)
+ (min n (length (chars-before editor))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (forward-delete-char (editor <line-editor>) n)
+ (unless (<= n 0)
+ (set! (chars-after editor)
+ (drop (chars-after editor)
+ (min n (length (chars-after editor))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (kill-line (editor <line-editor>))
+ (forward-delete-char editor (length (chars-after editor))))
+
+(define-method (backward-char (editor <line-editor>) n)
+ (let loop ((n n)
+ (before (chars-before editor))
+ (after (chars-after editor)))
+ (if (or (<= n 0) (null? before))
+ (begin
+ (set! (chars-before editor) before)
+ (set! (chars-after editor) after))
+ (loop (- n 1)
+ (cdr before)
+ (cons (car before) after))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (forward-char (editor <line-editor>) n)
+ (let loop ((n n)
+ (before (chars-before editor))
+ (after (chars-after editor)))
+ (if (or (<= n 0) (null? after))
+ (begin
+ (set! (chars-before editor) before)
+ (set! (chars-after editor) after))
+ (loop (- n 1)
+ (cons (car after) before)
+ (cdr after))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (beginning-of-line (editor <line-editor>))
+ (backward-char editor (length (chars-before editor))))
+
+(define-method (end-of-line (editor <line-editor>))
+ (forward-char editor (length (chars-after editor))))
+
+(define-method (save-to-history (editor <line-editor>))
+ (ring-buffer-put! (history editor) (get-line editor)))
+
+(define-method (history-ref (editor <line-editor>) i)
+ (ring-buffer-ref (history editor) i))
+
+(define-method (go-to-history (editor <line-editor>) i)
+ (when (and (history-enabled? editor)
+ (>= i 0)
+ (< i (ring-buffer-length (history editor))))
+ (set! (history-index editor) i)
+ (overwrite editor (history-ref editor i))))
+
+(define-method (backward-history (editor <line-editor>) n)
+ (discard-next-char editor)
+ (go-to-history editor (max (- (history-index editor) n) 0)))
+
+(define-method (forward-history (editor <line-editor>) n)
+ (discard-next-char editor)
+ (go-to-history editor
+ (min (+ (history-index editor) n)
+ (- (ring-buffer-length (history editor)) 1))))
+
+(define-method (newest-history (editor <line-editor>))
+ (set! (history-index editor) (ring-buffer-length (history editor))))
+
+
+;;;
+;;; Line editing minor mode
+;;;
+
+(define-class <line-edit-mode> (<minor-mode>)
+ (editor #:accessor editor #:init-keyword #:editor))
+
+(define-method (insert-text (mode <line-edit-mode>) new-text)
+ (let ((e (editor mode)))
+ (string-for-each (lambda (char)
+ (insert-char e char))
+ new-text)))
+
+(define-method (backward-delete-char (mode <line-edit-mode>))
+ (backward-delete-char (editor mode) 1))
+
+(define-method (forward-delete-char (mode <line-edit-mode>))
+ (forward-delete-char (editor mode) 1))
+
+(define-method (backward-char (mode <line-edit-mode>))
+ (backward-char (editor mode) 1))
+
+(define-method (forward-char (mode <line-edit-mode>))
+ (forward-char (editor mode) 1))
+
+(define-method (beginning-of-line (mode <line-edit-mode>))
+ (beginning-of-line (editor mode)))
+
+(define-method (end-of-line (mode <line-edit-mode>))
+ (end-of-line (editor mode)))
+
+(define-method (backward-history (mode <line-edit-mode>))
+ (backward-history (editor mode) 1))
+
+(define-method (forward-history (mode <line-edit-mode>))
+ (forward-history (editor mode) 1))
+
+(define-method (kill-line (mode <line-edit-mode>))
+ (kill-line (editor mode)))
+
+(bind-input <line-edit-mode> (key-press 'backspace) backward-delete-char)
+(bind-input <line-edit-mode> (key-press 'delete) forward-delete-char)
+(bind-input <line-edit-mode> (key-press 'd '(ctrl)) forward-delete-char)
+(bind-input <line-edit-mode> (key-press 'left) backward-char)
+(bind-input <line-edit-mode> (key-press 'b '(ctrl)) backward-char)
+(bind-input <line-edit-mode> (key-press 'right) forward-char)
+(bind-input <line-edit-mode> (key-press 'f '(ctrl)) forward-char)
+(bind-input <line-edit-mode> (key-press 'home) beginning-of-line)
+(bind-input <line-edit-mode> (key-press 'a '(ctrl)) beginning-of-line)
+(bind-input <line-edit-mode> (key-press 'end) end-of-line)
+(bind-input <line-edit-mode> (key-press 'e '(ctrl)) end-of-line)
+(bind-input <line-edit-mode> (key-press 'up) backward-history)
+(bind-input <line-edit-mode> (key-press 'p '(alt)) backward-history)
+(bind-input <line-edit-mode> (key-press 'down) forward-history)
+(bind-input <line-edit-mode> (key-press 'n '(alt)) forward-history)
+(bind-input <line-edit-mode> (key-press 'k '(ctrl)) kill-line)
+(bind-input <line-edit-mode> (text-input) insert-text)
diff --git a/catbird/minibuffer.scm b/catbird/minibuffer.scm
new file mode 100644
index 0000000..d4ef244
--- /dev/null
+++ b/catbird/minibuffer.scm
@@ -0,0 +1,157 @@
+(define-module (catbird minibuffer)
+ #:use-module (catbird kernel)
+ #:use-module (catbird line-editor)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:export (<minibuffer>
+ <minibuffer-mode>
+ define-minibuffer-command))
+
+(define %background-color (make-color 0.0 0.0 0.0 0.8))
+(define %prompt "> ")
+(define %padding 8.0)
+
+(define-class <minibuffer> (<node-2d>)
+ (commands #:accessor commands #:allocation #:class
+ #:init-thunk make-hash-table))
+
+(define (minibuffer-commands)
+ (class-slot-ref <minibuffer> 'commands))
+
+(define (lookup-minibuffer-command name)
+ (hash-ref (minibuffer-commands) name))
+
+(define (add-minibuffer-command name thunk)
+ (hash-set! (minibuffer-commands) name thunk))
+
+(define-syntax-rule (define-minibuffer-command name body ...)
+ (add-minibuffer-command (symbol->string 'name) (lambda () body ...)))
+
+(define-method (on-boot (minibuffer <minibuffer>))
+ (attach-to minibuffer
+ (make <canvas>
+ #:name 'background)
+ (make <line-editor>
+ #:name 'editor
+ #:rank 1
+ #:position (vec2 %padding %padding)
+ #:prompt %prompt)))
+
+(define-method (resize-minibuffer (minibuffer <minibuffer>) width)
+ (set! (painter (& minibuffer background))
+ (with-style ((fill-color %background-color))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ width
+ (+ (font-line-height (font (& minibuffer editor)))
+ (* %padding 2.0)))))))
+
+(define-method (clear-minibuffer (minibuffer <minibuffer>))
+ (clear-line (& minibuffer editor)))
+
+;; TODO: The line editor should have a generic completion facility.
+(define-method (autocomplete (minibuffer <minibuffer>))
+ (let ((prefix (get-line (& minibuffer editor))))
+ ;; Auto-complete if there is a single command name that starts
+ ;; with the characters the user has already typed.
+ (match (hash-fold (lambda (key value prev)
+ (if (string-prefix? prefix key)
+ (cons key prev)
+ prev))
+ '()
+ (minibuffer-commands))
+ ((name)
+ (overwrite (& minibuffer editor) name))
+ ;; TODO: Display multiple completion options to user.
+ (_ #f))))
+
+(define-method (get-command (minibuffer <minibuffer>))
+ (lookup-minibuffer-command (get-line (& minibuffer editor))))
+
+(define-method (valid-command? (minibuffer <minibuffer>))
+ (procedure? (get-command minibuffer)))
+
+(define-method (run-command (minibuffer <minibuffer>))
+ (let ((thunk (get-command minibuffer)))
+ (save-to-history (& minibuffer editor))
+ (when (procedure? thunk)
+ (thunk))))
+
+
+;;;
+;;; Minibuffer major mode
+;;;
+
+(define-class <minibuffer-mode> (<major-mode>)
+ (prev-keyboard-focus #:accessor prev-keyboard-focus))
+
+(define-method (on-enter (mode <minibuffer-mode>))
+ (let* ((scene (parent mode))
+ (region (car (regions scene)))
+ (minibuffer (or (& scene minibuffer)
+ (make <minibuffer>
+ #:name 'minibuffer
+ #:rank 999))))
+ (if (parent minibuffer)
+ (begin
+ (clear-minibuffer minibuffer)
+ (show (& scene minibuffer)))
+ (attach-to (parent mode) minibuffer))
+ (resize-minibuffer minibuffer (area-width region))
+ (set! (prev-keyboard-focus mode) (current-keyboard-focus))
+ (take-keyboard-focus region)
+ (add-minor-mode scene (make <line-edit-mode>
+ #:editor (& scene minibuffer editor)))))
+
+(define-method (on-exit (mode <minibuffer-mode>))
+ (hide (& (parent mode) minibuffer))
+ (remove-minor-mode (parent mode) <line-edit-mode>)
+ (take-keyboard-focus (prev-keyboard-focus mode)))
+
+(define-method (close-minibuffer (mode <minibuffer-mode>))
+ (pop-major-mode (parent mode)))
+
+(define-method (autocomplete (mode <minibuffer-mode>))
+ (autocomplete (& (parent mode) minibuffer)))
+
+(define-method (run-command (mode <minibuffer-mode>))
+ ;; The minibuffer needs to be closed before running the command so
+ ;; that this mode is no longer active and we've had a chance to
+ ;; clean up the state of the overlay scene.
+ (let ((minibuffer (& (parent mode) minibuffer)))
+ (when (valid-command? minibuffer)
+ (close-minibuffer mode)
+ (run-command minibuffer))))
+
+(bind-input <minibuffer-mode> (key-press 'escape) close-minibuffer)
+(bind-input <minibuffer-mode> (key-press 'g '(ctrl)) close-minibuffer)
+(bind-input <minibuffer-mode> (key-press 'tab) autocomplete)
+(bind-input <minibuffer-mode> (key-press 'return) run-command)
+
+
+;;;
+;;; Basic minibuffer commands
+;;;
+
+(define (for-each-user-scene proc)
+ (for-each (lambda (region)
+ (unless (eq? (name region) 'overlay)
+ (let ((s (scene region)))
+ (and s (proc s)))))
+ (all-regions)))
+
+;; General purpose built-in commands.
+(define-minibuffer-command pause (for-each-user-scene pause))
+(define-minibuffer-command resume (for-each-user-scene resume))
+(define-minibuffer-command quit (exit-catbird))
diff --git a/catbird/mixins.scm b/catbird/mixins.scm
new file mode 100644
index 0000000..8f4ec7c
--- /dev/null
+++ b/catbird/mixins.scm
@@ -0,0 +1,195 @@
+(define-module (catbird mixins)
+ #:use-module (catbird config)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 exceptions)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-9)
+ #:export (<nameable>
+ name
+
+ <rankable>
+ rank
+ sort-by-rank/ascending
+
+ <containable>
+ parent
+ attach
+ detach
+ on-enter
+ on-exit
+ on-attach
+ on-detach
+
+ <updatable>
+ update
+ update/around
+
+ <scriptable>
+ agenda
+ on-pause
+ on-resume
+ paused?
+ pause
+ resume
+ run-script
+ stop-scripts
+
+ <renderable>
+ visible?
+ on-show
+ on-hide
+ show
+ hide
+ render
+ render/around
+ render/before
+
+ <movable-2d>
+ <movable-3d>
+ position)
+ #:replace (pause))
+
+(define-class <nameable> ()
+ (name #:accessor name #:init-keyword #:name #:init-value #f))
+
+;; For Z sorting objects and such.
+(define-class <rankable> ()
+ (rank #:accessor rank #:init-keyword #:rank #:init-value 0))
+
+(define (sort-by-rank/ascending lst)
+ (sort lst
+ (lambda (a b)
+ (< (rank a) (rank b)))))
+
+
+;;;
+;;; Containable
+;;;
+
+(define-class <containable> ()
+ (parent #:accessor parent #:init-form #f))
+
+(define-method (on-enter (child <containable>))
+ #t)
+
+(define-method (on-exit (child <containable>))
+ #t)
+
+(define-method (on-attach parent (child <containable>))
+ #t)
+
+(define-method (on-detach parent (child <containable>))
+ #t)
+
+(define-method (attach (obj <containable>) container)
+ (when (parent obj)
+ (raise-exception
+ (make-exception-with-message "object already has a parent")))
+ (set! (parent obj) container)
+ (on-enter obj)
+ (on-attach container obj))
+
+(define-method (detach (obj <containable>))
+ (unless (parent obj)
+ (raise-exception
+ (make-exception-with-message "object has no parent")))
+ (on-detach (parent obj) obj)
+ (on-exit obj)
+ (set! (parent obj) #f))
+
+
+;;;
+;;; Updatable
+;;;
+
+(define-class <updatable> ())
+
+(define-method (update (obj <updatable>) dt)
+ #t)
+
+(define-method (update/around (obj <updatable>) dt)
+ (update obj dt))
+
+
+;;;
+;;; Scriptable
+;;;
+
+(define-class <scriptable> (<updatable>)
+ (paused? #:accessor paused? #:init-form #f #:init-keyword #:paused?)
+ (agenda #:getter agenda #:init-thunk make-agenda))
+
+(define-method (on-pause (obj <scriptable>))
+ #t)
+
+(define-method (on-resume (obj <scriptable>))
+ #t)
+
+(define-method (pause (obj <scriptable>))
+ (unless (paused? obj)
+ (set! (paused? obj) #t)
+ (on-pause obj)))
+
+(define-method (resume (obj <scriptable>))
+ (when (paused? obj)
+ (set! (paused? obj) #f)
+ (on-resume obj)))
+
+(define-method (update/around (obj <scriptable>) dt)
+ (unless (paused? obj)
+ (with-agenda (agenda obj)
+ (update-agenda dt)
+ (next-method))))
+
+(define-syntax-rule (run-script obj body ...)
+ (with-agenda (agenda obj) (script body ...)))
+
+(define-method (stop-scripts obj)
+ (with-agenda (agenda obj) (clear-agenda)))
+
+
+;;;
+;;; Renderable
+;;;
+
+(define-class <renderable> ()
+ (visible? #:accessor visible? #:init-form #t #:init-keyword #:visible?))
+
+(define-method (on-show (obj <renderable>))
+ #t)
+
+(define-method (on-hide (obj <renderable>))
+ #t)
+
+(define-method (show (obj <renderable>))
+ (set! (visible? obj) #t)
+ (on-show obj))
+
+(define-method (hide (obj <renderable>))
+ (set! (visible? obj) #f)
+ (on-hide obj))
+
+(define-method (render (obj <renderable>) alpha)
+ #t)
+
+(define-method (render/before (obj <renderable>) alpha)
+ #t)
+
+(define-method (render/around (obj <renderable>) alpha)
+ (when (visible? obj)
+ (render/before obj alpha)
+ (render obj alpha)))
+
+
+;;;
+;;; Movable
+;;;
+
+(define-class <movable-2d> ()
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec2 0.0 0.0)))
+
+(define-class <movable-3d> ()
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec3 0.0 0.0 0.0)))
diff --git a/catbird/mode.scm b/catbird/mode.scm
new file mode 100644
index 0000000..e35146c
--- /dev/null
+++ b/catbird/mode.scm
@@ -0,0 +1,105 @@
+(define-module (catbird mode)
+ #:use-module (catbird config)
+ #:use-module (catbird input-map)
+ #:use-module (catbird mixins)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<major-mode>
+ <minor-mode>
+ bind-input
+ unbind-input
+ name-mode
+ clear-inputs
+ input-map
+ on-key-press
+ on-key-release
+ on-text-input
+ on-mouse-press
+ on-mouse-release
+ on-mouse-move
+ on-mouse-wheel
+ on-controller-press
+ on-controller-release
+ on-controller-move
+ <nothing-mode>)
+ #:re-export (controller-move
+ controller-press
+ controller-release
+ key-press
+ key-release
+ text-input
+ mouse-move
+ mouse-press
+ mouse-release
+ mouse-wheel
+ name
+ on-enter
+ on-exit
+ on-pause
+ on-resume
+ update))
+
+(define-root-class <mode> (<scriptable> <containable> <nameable>)
+ (input-map #:accessor input-map #:allocation #:each-subclass
+ #:init-thunk make-input-map))
+
+(define-method (input-map (mode-class <class>))
+ (class-slot-ref mode-class 'input-map))
+
+(define-method (bind-input mode-class spec handler)
+ (class-slot-set! mode-class 'input-map
+ (add-input (input-map mode-class) spec handler)))
+
+(define-method (unbind-input mode-class spec)
+ (class-slot-set! mode-class 'input-map
+ (remove-input (input-map mode-class) spec)))
+
+(define (clear-inputs mode-class)
+ (class-slot-set! mode-class 'input-map '()))
+
+(define-method (on-key-press (mode <mode>) key modifiers)
+ (let ((handler (key-press-handler (input-map mode) key modifiers)))
+ (and handler (handler mode))))
+
+(define-method (on-key-release (mode <mode>) key modifiers)
+ (let ((handler (key-release-handler (input-map mode) key modifiers)))
+ (and handler (handler mode))))
+
+(define-method (on-text-input (mode <mode>) text)
+ (let ((handler (text-input-handler (input-map mode))))
+ (and handler (handler mode text))))
+
+(define-method (on-mouse-press (mode <mode>) button x y)
+ (let ((handler (mouse-press-handler (input-map mode) button)))
+ (and handler (handler mode x y))))
+
+(define-method (on-mouse-release (mode <mode>) button x y)
+ (let ((handler (mouse-release-handler (input-map mode) button)))
+ (and handler (handler mode x y))))
+
+(define-method (on-mouse-move (mode <mode>) x y x-rel y-rel buttons)
+ (let ((handler (mouse-move-handler (input-map mode) buttons)))
+ (and handler (handler mode x y x-rel y-rel))))
+
+(define-method (on-mouse-wheel (mode <mode>) x y)
+ (let ((handler (mouse-wheel-handler (input-map mode))))
+ (and handler (handler mode x y))))
+
+(define-method (on-controller-press (mode <mode>) controller-id button)
+ (let ((handler (controller-press-handler (input-map mode) controller-id button)))
+ (and handler (handler mode))))
+
+(define-method (on-controller-release (mode <mode>) controller-id button)
+ (let ((handler (controller-release-handler (input-map mode) controller-id button)))
+ (and handler (handler mode))))
+
+(define-method (on-controller-move (mode <mode>) controller-id axis value)
+ (let ((handler (controller-move-handler (input-map mode) controller-id axis)))
+ (and handler (handler mode value))))
+
+(define-class <major-mode> (<mode>))
+
+(define-class <minor-mode> (<mode>))
+
+(define-class <nothing-mode> (<major-mode>))
diff --git a/catbird/node-2d.scm b/catbird/node-2d.scm
new file mode 100644
index 0000000..fc579ba
--- /dev/null
+++ b/catbird/node-2d.scm
@@ -0,0 +1,939 @@
+(define-module (catbird node-2d)
+ #:use-module (catbird asset)
+ #:use-module (catbird camera)
+ #:use-module (catbird cached-slots)
+ #:use-module (catbird mixins)
+ #:use-module (catbird node)
+ #:use-module (catbird observer)
+ #:use-module (chickadee)
+ #:use-module (chickadee math)
+ #:use-module (chickadee math bezier)
+ #:use-module (chickadee math easings)
+ #:use-module (chickadee math matrix)
+ #:use-module (chickadee math rect)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee graphics 9-patch)
+ #:use-module (chickadee graphics blend)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics engine)
+ #:use-module (chickadee graphics framebuffer)
+ #:use-module (chickadee graphics particles)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics sprite)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee graphics texture)
+ #:use-module (chickadee graphics tile-map)
+ #:use-module (chickadee graphics viewport)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (rnrs base)
+ #:export (<node-2d>
+ aggregate-bounding-box
+ align-bottom
+ align-left
+ align-right
+ align-top
+ default-height
+ default-width
+ expire-local-matrix
+ follow-bezier-path
+ local-bounding-box
+ local-matrix
+ move-by
+ move-to
+ on-child-resize
+ origin
+ origin-x
+ origin-y
+ pick
+ place-above
+ place-below
+ place-left
+ place-right
+ position-x
+ position-y
+ resize
+ rotate-by
+ rotate-to
+ rotation
+ scale
+ scale-by
+ scale-to
+ scale-x
+ scale-y
+ shear
+ shear-x
+ shear-y
+ teleport
+ world-bounding-box
+ world-matrix
+
+ <sprite>
+ texture
+ source-rect
+ blend-mode
+ tint
+
+ <atlas-sprite>
+ atlas
+ index
+
+ <animation>
+ frames
+ frame-duration
+
+ <animated-sprite>
+ animations
+ frame-duration
+ current-animation
+ start-time
+ change-animation
+
+ <9-patch>
+ top-margin
+ bottom-margin
+ left-margin
+ right-margin
+
+ <sprite-batch>
+ batch
+
+ <canvas>
+ painter
+
+ <label>
+ font
+ text
+ color
+ align
+ vertical-align
+
+ <tile-map>
+ tile-map
+ layers
+
+ <particles>
+ particles)
+ #:re-export (height
+ position
+ width))
+
+(define (refresh-local-matrix node local)
+ (matrix4-2d-transform! local
+ #:origin (origin node)
+ #:position (render-position node)
+ #:rotation (rotation node)
+ #:scale (scale node)
+ #:shear (shear node))
+ local)
+
+(define (refresh-world-matrix node world)
+ (let ((p (parent node))
+ (local (local-matrix node)))
+ (if (is-a? p <node-2d>)
+ (matrix4-mult! world local (world-matrix (parent node)))
+ (begin
+ (matrix4-identity! world)
+ (matrix4-mult! world world local)))
+ world))
+
+(define (refresh-inverse-world-matrix node inverse)
+ (matrix4-inverse! (world-matrix node) inverse)
+ inverse)
+
+(define (refresh-local-bounding-box node bb)
+ (let ((p (position node))
+ (o (origin node))
+ (r (rotation node))
+ (k (shear node))
+ (s (size node)))
+ (if (and (= r 0.0)
+ (= (vec2-x k) 0.0)
+ (= (vec2-y k) 0.0))
+ ;; Fast path: Node is axis-aligned and bounding box
+ ;; calculation is easy peasy.
+ (let ((s (scale node)))
+ (set-rect-x! bb (- (vec2-x p) (vec2-x o)))
+ (set-rect-y! bb (- (vec2-y p) (vec2-y o)))
+ (set-rect-width! bb (* (rect-width s) (vec2-x s)))
+ (set-rect-height! bb (* (rect-height s) (vec2-y s))))
+ ;; Slow path: Node is rotated, sheared, or both.
+ (let* ((m (local-matrix node))
+ (x0 0.0)
+ (y0 0.0)
+ (x1 (rect-width s))
+ (y1 (rect-height s))
+ (x2 (matrix4-transform-x m x0 y0))
+ (y2 (matrix4-transform-y m x0 y0))
+ (x3 (matrix4-transform-x m x1 y0))
+ (y3 (matrix4-transform-y m x1 y0))
+ (x4 (matrix4-transform-x m x1 y1))
+ (y4 (matrix4-transform-y m x1 y1))
+ (x5 (matrix4-transform-x m x0 y1))
+ (y5 (matrix4-transform-y m x0 y1))
+ (xmin (min x2 x3 x4 x5))
+ (ymin (min y2 y3 y4 y5))
+ (xmax (max x2 x3 x4 x5))
+ (ymax (max y2 y3 y4 y5)))
+ (set-rect-x! bb xmin)
+ (set-rect-y! bb ymin)
+ (set-rect-width! bb (- xmax xmin))
+ (set-rect-height! bb (- ymax ymin))))
+ bb))
+
+(define (refresh-world-bounding-box node bb)
+ (let* ((m (world-matrix node))
+ (s (size node))
+ (x0 0.0)
+ (y0 0.0)
+ (x1 (rect-width s))
+ (y1 (rect-height s))
+ (x2 (matrix4-transform-x m x0 y0))
+ (y2 (matrix4-transform-y m x0 y0))
+ (x3 (matrix4-transform-x m x1 y0))
+ (y3 (matrix4-transform-y m x1 y0))
+ (x4 (matrix4-transform-x m x1 y1))
+ (y4 (matrix4-transform-y m x1 y1))
+ (x5 (matrix4-transform-x m x0 y1))
+ (y5 (matrix4-transform-y m x0 y1))
+ (xmin (min x2 x3 x4 x5))
+ (ymin (min y2 y3 y4 y5))
+ (xmax (max x2 x3 x4 x5))
+ (ymax (max y2 y3 y4 y5)))
+ (set-rect-x! bb xmin)
+ (set-rect-y! bb ymin)
+ (set-rect-width! bb (- xmax xmin))
+ (set-rect-height! bb (- ymax ymin))
+ bb))
+
+(define (refresh-aggregate-bounding-box node bb)
+ ;; If the node has no children then the aggregate bounding box is
+ ;; the same as the world bounding box.
+ (rect-copy! (world-bounding-box node) bb)
+ (for-each-child (lambda (child)
+ (rect-union! bb (aggregate-bounding-box child)))
+ node)
+ bb)
+
+(define-class <node-2d> (<node> <movable-2d> <cacheable>)
+ ;; Translation of the origin. By default, the origin is at the
+ ;; bottom left corner of a node.
+ (origin #:accessor origin #:init-form (vec2 0.0 0.0) #:init-keyword #:origin
+ #:observe? #t)
+ (origin-x #:accessor origin-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (origin node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (origin node) x)
+ (expire-local-matrix node)))
+ (origin-y #:accessor origin-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (origin node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (origin node) y)
+ (expire-local-matrix node)))
+ ;; Translation
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec2 0.0 0.0) #:observe? #t)
+ (position-x #:accessor position-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (position node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (position node) x)
+ (expire-local-matrix node)))
+ (position-y #:accessor position-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (position node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (position node) y)
+ (expire-local-matrix node)))
+ ;; Rotation around the Z-axis.
+ (rotation #:accessor rotation #:init-form 0.0 #:init-keyword #:rotation
+ #:observe? #t)
+ ;; Scaling
+ (scale #:accessor scale #:init-form (vec2 1.0 1.0) #:init-keyword #:scale
+ #:observe? #t)
+ (scale-x #:accessor scale-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (scale node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (scale node) x)
+ (expire-local-matrix node)))
+ (scale-y #:accessor scale-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (scale node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (scale node) y)
+ (expire-local-matrix node)))
+ ;; Shearing
+ (shear #:accessor shear #:init-form (vec2 0.0 0.0) #:init-keyword #:shear
+ #:observe? #t)
+ (shear-x #:accessor shear-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (shear node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (shear node) x)
+ (expire-local-matrix node)))
+ (shear-y #:accessor shear-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (shear node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (shear node) y)
+ (expire-local-matrix node)))
+ ;; Some extra position vectors for defeating "temporal aliasing"
+ ;; when rendering.
+ (last-position #:getter last-position #:init-form (vec2 0.0 0.0))
+ (render-position #:getter render-position #:init-form (vec2 0.0 0.0))
+ ;; Transformation matrices:
+ ;;
+ ;; The local matrix incorporates the node-specific translation,
+ ;; rotation, scale, and shear factors.
+ (local-matrix #:getter local-matrix #:init-thunk make-identity-matrix4
+ #:cached? #t #:refresh refresh-local-matrix)
+ ;; The world matrix is defined by the multiplication of the parent's
+ ;; world matrix with the local matrix.
+ (world-matrix #:getter world-matrix #:init-thunk make-identity-matrix4
+ #:cached? #t #:refresh refresh-world-matrix)
+ ;; The inverse world matrix is useful for translating world
+ ;; coordinates into local coordinates. Using this matrix it is
+ ;; possible to detect if the mouse is over a rotated and sheared
+ ;; node, for example.
+ (inverse-world-matrix #:getter inverse-world-matrix
+ #:init-form (make-identity-matrix4)
+ #:cached? #t #:refresh refresh-inverse-world-matrix)
+ ;; Node dimensions. Stored as a rectangle for convenience, so it
+ ;; can be used as a bounding box that doesn't take any
+ ;; transformation matrix into consideration.
+ (size #:getter size #:init-thunk make-null-rect)
+ (width #:accessor width #:init-keyword #:width #:watch? #t #:allocation #:virtual
+ #:slot-ref (lambda (node) (rect-width (size node)))
+ #:slot-set! (lambda (node w)
+ (set-rect-width! (size node) w)
+ (expire-local-bounding-box node)))
+ (height #:accessor height #:init-keyword #:height #:watch? #t #:allocation #:virtual
+ #:slot-ref (lambda (node) (rect-height (size node)))
+ #:slot-set! (lambda (node h)
+ (set-rect-height! (size node) h)
+ (expire-local-bounding-box node)))
+ ;; The local bounding box is the combination of the node's
+ ;; dimensions with the local transformation matrix.
+ (local-bounding-box #:getter local-bounding-box #:init-thunk make-null-rect
+ #:cached? #t #:refresh refresh-local-bounding-box)
+ ;; The world bounding box is the combination of the node's
+ ;; dimensions with the world transformation matrix.
+ (world-bounding-box #:getter world-bounding-box #:init-thunk make-null-rect
+ #:cached? #t #:refresh refresh-world-bounding-box)
+ ;; The aggregate bounding box is the union of the node's world
+ ;; bounding boxes and the aggregate bounding boxes of all its
+ ;; children. This bounding box is used to quickly determine if a
+ ;; point in world space might be within any node in a tree. This
+ ;; bounding box can be used for render culling, mouse selection, and
+ ;; render clipping.
+ (aggregate-bounding-box #:getter aggregate-bounding-box
+ #:init-thunk make-null-rect #:cached? #t
+ #:refresh refresh-aggregate-bounding-box))
+
+(define-method (initialize (node <node-2d>) args)
+ (next-method)
+ ;; If scale is specified as a scalar value, convert it to a vector
+ ;; that applies identical scaling to both axes.
+ (let ((s (scale node)))
+ (when (number? s)
+ (slot-set! node 'scale (vec2 s s))))
+ ;; If caller doesn't specify a custom width and height, let the node
+ ;; pick a reasonable default size.
+ (when (= (width node) 0.0)
+ (set! (width node) (default-width node)))
+ (when (= (height node) 0.0)
+ (set! (height node) (default-height node)))
+ ;; Build an initial bounding box.
+ (vec2-copy! (position node) (render-position node))
+ ;; Set the initial last position to the same as the initial position
+ ;; to avoid a brief flash where the node appears at (0, 0).
+ (remember-position node))
+
+(define (expire-local-matrix node)
+ (expire-slot! node 'local-matrix)
+ (expire-world-matrix node)
+ (expire-local-bounding-box node))
+
+(define (expire-world-matrix node)
+ (unless (slot-expired? node 'world-matrix)
+ (expire-slot! node 'world-matrix)
+ (expire-slot! node 'inverse-world-matrix)
+ (for-each-child (lambda (child)
+ (expire-world-matrix child)
+ (expire-world-bounding-box child))
+ node)))
+
+(define (expire-local-bounding-box node)
+ (expire-slot! node 'local-bounding-box)
+ (expire-world-bounding-box node))
+
+(define (expire-world-bounding-box node)
+ (expire-slot! node 'world-bounding-box)
+ (expire-aggregate-bounding-box node))
+
+(define (expire-aggregate-bounding-box node)
+ (unless (slot-expired? node 'aggregate-bounding-box)
+ (expire-slot! node 'aggregate-bounding-box)
+ (let ((p (parent node)))
+ (when (is-a? p <node-2d>)
+ (expire-aggregate-bounding-box p)))))
+
+
+;;;
+;;; Bounding boxes
+;;;
+
+(define-method (default-width (node <node-2d>)) 0.0)
+
+(define-method (default-height (node <node-2d>)) 0.0)
+
+(define-method (on-child-resize node child)
+ #t)
+
+;; (define-method ((setter origin) (node <node-2d>))
+;; (dirty! node)
+;; (next-method))
+
+(define-method (on-change (node <node-2d>) slot old new)
+ (case slot
+ ((origin position rotation scale shear)
+ (expire-local-matrix node))))
+
+(define-method (resize (node <node-2d>) w h)
+ (set! (width node) w)
+ (set! (height node) h)
+ (expire-local-bounding-box node))
+
+
+;;;
+;;; Animation
+;;;
+
+(define-method (remember-position (node <node-2d>))
+ (vec2-copy! (position node) (last-position node)))
+
+(define-method (remember-position/recursive (node <node-2d>))
+ (remember-position node)
+ (for-each-child remember-position/recursive node))
+
+(define-method (move-to (node <node-2d>) x y)
+ (set! (position-x node) x)
+ (set! (position-y node) y))
+
+(define-method (move-to (node <node-2d>) x y duration ease)
+ (let ((p (position node)))
+ (move-by node (- x (vec2-x p)) (- y (vec2-y p)) duration ease)))
+
+(define-method (move-to (node <node-2d>) x y duration)
+ (move-to node x y duration smoothstep))
+
+(define-method (move-by (node <node-2d>) dx dy)
+ (let ((p (position node)))
+ (move-to node (+ (vec2-x p) dx) (+ (vec2-y p) dy))))
+
+(define-method (move-by (node <node-2d>) dx dy duration ease)
+ (let* ((p (position node))
+ (start-x (vec2-x p))
+ (start-y (vec2-y p)))
+ (tween duration 0.0 1.0
+ (lambda (n)
+ (move-to node
+ (+ start-x (* dx n))
+ (+ start-y (* dy n))))
+ #:ease ease)))
+
+(define-method (move-by (node <node-2d>) dx dy duration)
+ (move-by node dx dy duration smoothstep))
+
+(define-method (teleport (node <node-2d>) x y)
+ ;; When teleporting, we want to avoid position interpolation and odd
+ ;; looking camera jumps.
+ ;;
+ ;; Interpolation is avoided by setting all 3 position vectors to the
+ ;; same values. This prevents a visual artifact where the player
+ ;; sees 1 frame where the node is somewhere in between its former
+ ;; position and the new position.
+ ;;
+ ;; The camera jump problem occurs when a camera has a node as its
+ ;; tracking target and that node teleports. Normally, the camera's
+ ;; view matrix is updated before any nodes are rendered, and thus
+ ;; *before* the node can recompute its world matrix based on the new
+ ;; position. This creates 1 frame where the camera is improperly
+ ;; positioned at the target's old location. This 1 frame lag is not
+ ;; an issue during normal movement, but when teleporting it causes a
+ ;; noticably unsmooth blip. Forcing the matrices to be recomputed
+ ;; immediately solves this issue.
+ (set-vec2! (position node) x y)
+ (set-vec2! (last-position node) x y)
+ (set-vec2! (render-position node) x y)
+ (expire-local-matrix node))
+
+(define-method (rotate-to (node <node-2d>) theta)
+ (set! (rotation node) theta))
+
+(define-method (rotate-to (node <node-2d>) theta duration ease)
+ (tween duration (rotation node) theta
+ (lambda (r)
+ (rotate-to node r))
+ #:ease ease))
+
+(define-method (rotate-to (node <node-2d>) theta duration)
+ (rotate-to node theta duration smoothstep))
+
+(define-method (rotate-by (node <node-2d>) dtheta)
+ (rotate-to node (+ (rotation node) dtheta)))
+
+(define-method (rotate-by (node <node-2d>) dtheta duration ease)
+ (rotate-to node (+ (rotation node) dtheta) duration ease))
+
+(define-method (rotate-by (node <node-2d>) dtheta duration)
+ (rotate-by node dtheta duration smoothstep))
+
+(define-method (scale-to (node <node-2d>) sx sy)
+ (set! (scale-x node) sx)
+ (set! (scale-y node) sy))
+
+(define-method (scale-to (node <node-2d>) s)
+ (scale-to node s s))
+
+(define-method (scale-to (node <node-2d>) sx sy duration ease)
+ (scale-by node (- sx (scale-x node)) (- sy (scale-y node)) duration ease))
+
+(define-method (scale-to (node <node-2d>) sx sy duration)
+ (scale-to node sx sy duration smoothstep))
+
+(define-method (scale-by (node <node-2d>) dsx dsy)
+ (scale-to node (+ (scale-x node) dsx) (+ (scale-y node) dsy)))
+
+(define-method (scale-by (node <node-2d>) ds)
+ (scale-by node ds ds))
+
+(define-method (scale-by (node <node-2d>) dsx dsy duration ease)
+ (let ((start-x (scale-x node))
+ (start-y (scale-y node)))
+ (tween duration 0.0 1.0
+ (lambda (n)
+ (scale-to node
+ (+ start-x (* dsx n))
+ (+ start-y (* dsy n))))
+ #:ease ease)))
+
+(define-method (scale-by (node <node-2d>) dsx dsy duration)
+ (scale-by node dsx dsy duration smoothstep))
+
+(define-method (scale-by (node <node-2d>) ds duration (ease <procedure>))
+ (scale-by node ds ds duration ease))
+
+(define-method (follow-bezier-path (node <node-2d>) path duration forward?)
+ (let ((p (position node))
+ (path (if forward? path (reverse path))))
+ (for-each (lambda (bezier)
+ (tween duration
+ (if forward? 0.0 1.0)
+ (if forward? 1.0 0.0)
+ (lambda (t)
+ (bezier-curve-point-at! p bezier t)
+ (expire-local-matrix node))
+ #:ease linear))
+ path)))
+
+(define-method (follow-bezier-path (node <node-2d>) path duration)
+ (follow-bezier-path node path duration #t))
+
+(define-method (pick (node <node-2d>) p pred)
+ (and (pred node)
+ (let loop ((kids (reverse (children node))))
+ (match kids
+ (()
+ (let* ((m (inverse-world-matrix node))
+ (x (vec2-x p))
+ (y (vec2-y p))
+ (tx (matrix4-transform-x m x y))
+ (ty (matrix4-transform-y m x y)))
+ (and (>= tx 0.0)
+ (< tx (width node))
+ (>= ty 0.0)
+ (< ty (height node))
+ node)))
+ ((child . rest)
+ (let ((o (origin node)))
+ (or (pick child p pred)
+ (loop rest))))))))
+
+
+;;;
+;;; Updating/rendering
+;;;
+
+(define-method (update/around (node <node-2d>) dt)
+ (unless (paused? node)
+ (remember-position node))
+ (next-method))
+
+(define-method (pause (node <node-2d>))
+ ;; We need to set the last position of all objects in the tree to
+ ;; their current position, otherwise any moving objects will
+ ;; experience this weird jitter while paused because the last
+ ;; position will never be updated during the duration of the pause
+ ;; event.
+ (next-method)
+ (remember-position/recursive node))
+
+(define-method (tree-in-view? (node <node-2d>))
+ (rect-intersects? (aggregate-bounding-box node)
+ (view-bounding-box (current-camera))))
+
+(define-method (in-view? (node <node-2d>))
+ (rect-intersects? (world-bounding-box node)
+ (view-bounding-box (current-camera))))
+
+(define-method (render/around (node <node-2d>) alpha)
+ ;; Compute the linearly interpolated rendering position, in the case
+ ;; that node has moved since the last update.
+ (when (visible? node)
+ (let ((p (position node))
+ (lp (last-position node))
+ (rp (render-position node))
+ (beta (- 1.0 alpha)))
+ (unless (and (vec2= rp p) (vec2= lp p))
+ (set-vec2-x! rp (+ (* (vec2-x p) alpha) (* (vec2-x lp) beta)))
+ (set-vec2-y! rp (+ (* (vec2-y p) alpha) (* (vec2-y lp) beta)))
+ (expire-local-matrix node)))
+ (next-method)))
+
+
+;;;
+;;; Relative placement and alignment
+;;;
+
+;; Placement and alignment of nodes is done under the assumption that
+;; the nodes are in the same local coordinate space. If this is not
+;; the case, the results will be garbage.
+
+(define* (place-right a b #:key (padding 0.0))
+ "Adjust B's x position coordinate so that it is PADDING distance to
+the right of A."
+ (set! (position-x b) (+ (position-x a) (width a) padding)))
+
+(define* (place-left a b #:key (padding 0.0))
+ "Adjust B's x position coordinate so that it is PADDING distance to
+the left of A."
+ (set! (position-x b) (- (position-x a) (width b) padding)))
+
+(define* (place-above a b #:key (padding 0.0))
+ "Adjust B's y position coordinate so that it is PADDING distance above
+A."
+ (set! (position-y b) (+ (position-y a) (height a) padding)))
+
+(define* (place-below a b #:key (padding 0.0))
+ "Adjust B's y position coordinate so that it is PADDING distance below
+A."
+ (set! (position-y b) (- (position-y a) (height b) padding)))
+
+(define (align-left a b)
+ "Align the left side of B with the left side of A."
+ (set! (position-x b) (position-x a)))
+
+(define (align-right a b)
+ "Align the right side of B with the right side of A."
+ (set! (position-x b) (+ (position-x a) (width a))))
+
+(define (align-bottom a b)
+ "Align the bottom of B with the bottom of A."
+ (set! (position-y b) (position-y a)))
+
+(define (align-top a b)
+ "Align the top of B with the top of A."
+ (set! (position-y b) (+ (position-y a) (height a))))
+
+
+;;;
+;;; Sprite
+;;;
+
+(define-class <sprite> (<node-2d>)
+ (texture #:accessor texture #:init-keyword #:texture #:asset? #t
+ #:observe? #t)
+ (tint #:accessor tint #:init-keyword #:tint #:init-form white)
+ (blend-mode #:accessor blend-mode #:init-keyword #:blend-mode
+ #:init-form blend:alpha))
+
+(define-method (default-width (sprite <sprite>))
+ (texture-width (texture sprite)))
+
+(define-method (default-height (sprite <sprite>))
+ (texture-height (texture sprite)))
+
+(define-method (on-change (sprite <sprite>) slot-name old new)
+ (case slot-name
+ ((texture)
+ (set! (width sprite) (texture-width new))
+ (set! (height sprite) (texture-height new)))))
+
+(define-method (render (sprite <sprite>) alpha)
+ (let ((t (texture sprite)))
+ (with-graphics-state ((g:blend-mode (blend-mode sprite)))
+ (draw-sprite* t (size sprite) (world-matrix sprite)
+ #:tint (tint sprite)
+ #:texcoords (texture-gl-tex-rect t)))))
+
+
+;;;
+;;; Texture Atlas Sprite
+;;;
+
+(define-class <atlas-sprite> (<sprite>)
+ (atlas #:accessor atlas #:init-keyword #:atlas #:asset? #t #:observe? #t)
+ (index #:accessor index #:init-keyword #:index #:init-value 0 #:observe? #t))
+
+(define-method (sync-texture (sprite <atlas-sprite>))
+ (let ((t (texture-atlas-ref (atlas sprite) (index sprite))))
+ (set! (texture sprite) t)))
+
+(define-method (on-boot (sprite <atlas-sprite>))
+ (sync-texture sprite))
+
+(define-method (on-change (sprite <atlas-sprite>) slot-name old new)
+ (case slot-name
+ ((atlas index)
+ (sync-texture sprite))
+ (else
+ (next-method))))
+
+
+;;;
+;;; Animated Sprite
+;;;
+
+(define-class <animation> ()
+ (frames #:getter frames #:init-keyword #:frames)
+ (frame-duration #:getter frame-duration #:init-keyword #:frame-duration
+ #:init-form 250))
+
+(define-class <animated-sprite> (<atlas-sprite>)
+ (atlas #:accessor atlas #:init-keyword #:atlas #:asset? #t)
+ (animations #:accessor animations #:init-keyword #:animations)
+ (current-animation #:accessor current-animation
+ #:init-keyword #:default-animation
+ #:init-form 'default)
+ (start-time #:accessor start-time #:init-form 0))
+
+(define-method (on-enter (sprite <animated-sprite>))
+ (update sprite 0))
+
+(define-method (update (sprite <animated-sprite>) dt)
+ (let* ((anim (assq-ref (animations sprite) (current-animation sprite)))
+ (frame-duration (frame-duration anim))
+ (frames (frames anim))
+ (anim-duration (* frame-duration (vector-length frames)))
+ (time (mod (- (elapsed-time) (start-time sprite)) anim-duration))
+ (frame (vector-ref frames (inexact->exact
+ (floor (/ time frame-duration))))))
+ (when (not (= frame (index sprite)))
+ (set! (index sprite) frame))))
+
+(define-method (change-animation (sprite <animated-sprite>) name)
+ (set! (current-animation sprite) name)
+ (set! (start-time sprite) (elapsed-time)))
+
+
+;;;
+;;; 9-Patch
+;;;
+
+(define-class <9-patch> (<node-2d>)
+ (texture #:accessor texture #:init-keyword #:texture #:asset? #t)
+ (left-margin #:accessor left-margin #:init-keyword #:left)
+ (right-margin #:accessor right-margin #:init-keyword #:right)
+ (bottom-margin #:accessor bottom-margin #:init-keyword #:bottom)
+ (top-margin #:accessor top-margin #:init-keyword #:top)
+ (mode #:accessor mode #:init-keyword #:mode #:init-value 'stretch)
+ (blend-mode #:accessor blend-mode #:init-keyword #:blend-mode
+ #:init-value blend:alpha)
+ (tint #:accessor tint #:init-keyword #:tint #:init-value white)
+ (render-rect #:getter render-rect #:init-form (make-rect 0.0 0.0 0.0 0.0)))
+
+(define-method (initialize (9-patch <9-patch>) initargs)
+ (let ((default-margin (get-keyword #:margin initargs 0.0)))
+ (slot-set! 9-patch 'left-margin default-margin)
+ (slot-set! 9-patch 'right-margin default-margin)
+ (slot-set! 9-patch 'bottom-margin default-margin)
+ (slot-set! 9-patch 'top-margin default-margin))
+ (next-method)
+ (set-rect-width! (render-rect 9-patch) (width 9-patch))
+ (set-rect-height! (render-rect 9-patch) (height 9-patch)))
+
+;; (define-method (on-change (9-patch <9-patch>) slot-name old new)
+;; (case slot-name
+;; ((width)
+;; (set-rect-width! (render-rect 9-patch) new))
+;; ((height)
+;; (set-rect-height! (render-rect 9-patch) new)))
+;; (next-method))
+
+(define-method (render (9-patch <9-patch>) alpha)
+ (draw-9-patch* (texture 9-patch)
+ (render-rect 9-patch)
+ (world-matrix 9-patch)
+ #:top-margin (top-margin 9-patch)
+ #:bottom-margin (bottom-margin 9-patch)
+ #:left-margin (left-margin 9-patch)
+ #:right-margin (right-margin 9-patch)
+ #:mode (mode 9-patch)
+ #:blend-mode (blend-mode 9-patch)
+ #:tint (tint 9-patch)))
+
+
+;;;
+;;; Sprite Batch
+;;;
+
+(define-class <sprite-batch> (<node-2d>)
+ (batch #:accessor batch #:init-keyword #:batch)
+ (blend-mode #:accessor blend-mode
+ #:init-keyword #:blend-mode
+ #:init-form blend:alpha)
+ (clear-after-draw? #:accessor clear-after-draw?
+ #:init-keyword #:clear-after-draw?
+ #:init-form #t)
+ (batch-matrix #:accessor batch-matrix #:init-thunk make-identity-matrix4))
+
+(define-method (render (sprite-batch <sprite-batch>) alpha)
+ (let ((batch (batch sprite-batch)))
+ (draw-sprite-batch* batch (batch-matrix sprite-batch)
+ #:blend-mode (blend-mode sprite-batch))
+ (when (clear-after-draw? sprite-batch)
+ (sprite-batch-clear! batch))))
+
+
+;;;
+;;; Vector Path
+;;;
+
+(define-class <canvas> (<node-2d>)
+ (painter #:accessor painter #:init-keyword #:painter #:init-value #f
+ #:observe? #t)
+ (canvas #:accessor canvas #:init-thunk make-empty-canvas))
+
+(define-method (refresh-painter (c <canvas>))
+ (let* ((p (painter c)))
+ (when p
+ (let ((bb (painter-bounding-box p)))
+ (set-canvas-painter! (canvas c) p)
+ ;; (set! (origin-x canvas) (- (rect-x bb)))
+ ;; (set! (origin-y canvas) (- (rect-y bb)))
+ (set! (width c) (rect-width bb))
+ (set! (height c) (rect-height bb))))))
+
+(define-method (on-boot (c <canvas>))
+ (refresh-painter c))
+
+(define-method ((setter canvas) (c <canvas>))
+ (next-method)
+ (set-canvas-painter! (canvas c) (painter c)))
+
+(define-method (on-change (c <canvas>) slot-name old new)
+ (case slot-name
+ ((painter)
+ (refresh-painter c))
+ (else
+ (next-method))))
+
+(define-method (render (c <canvas>) alpha)
+ (draw-canvas* (canvas c) (world-matrix c)))
+
+
+;;;
+;;; Label
+;;;
+
+(define-class <label> (<node-2d>)
+ (font #:accessor font #:init-keyword #:font #:init-thunk default-font
+ #:asset? #t #:observe? #t)
+ (text #:accessor text #:init-value "" #:init-keyword #:text #:observe? #t)
+ (compositor #:accessor compositor #:init-thunk make-compositor)
+ (page #:accessor page #:init-thunk make-page)
+ (typeset #:accessor typeset #:init-value typeset-lrtb)
+ (align #:accessor align #:init-value 'left #:init-keyword #:align #:observe? #t)
+ (vertical-align #:accessor vertical-align #:init-value 'bottom
+ #:init-keyword #:vertical-align #:observe? #t)
+ (color #:accessor color #:init-keyword #:color #:init-value white #:observe? #t))
+
+(define-method (realign (label <label>))
+ (set! (origin-x label)
+ (case (align label)
+ ((left) 0.0)
+ ((right) (width label))
+ ((center) (/ (width label) 2.0))))
+ (set! (origin-y label)
+ (case (vertical-align label)
+ ((bottom) 0.0)
+ ((top) (height label))
+ ((center) (+ (/ (height label) 2.0) (font-descent (font label)))))))
+
+(define-method (refresh-label (label <label>))
+ (let ((c (compositor label))
+ (p (page label)))
+ (compositor-reset! c)
+ ((typeset label) c (font label) (text label) (color label))
+ (page-reset! p)
+ (page-write! p c)
+ (let ((bb (page-bounding-box p)))
+ (set! (width label) (rect-width bb))
+ (set! (height label) (rect-height bb)))))
+
+(define-method (on-boot (label <label>))
+ (refresh-label label)
+ (realign label))
+
+(define-method (on-asset-reload (label <label>) slot-name asset)
+ (case slot-name
+ ((font)
+ (refresh-label label))))
+
+(define-method (on-change (label <label>) slot-name old new)
+ (case slot-name
+ ((font text)
+ (refresh-label label)
+ (unless (eq? (align label) 'left)
+ (realign label)))
+ ((color)
+ (refresh-label label))
+ ((align vertical-align)
+ (realign label))
+ (else
+ (next-method))))
+
+(define-method (render (label <label>) alpha)
+ (draw-page (page label) (world-matrix label)))
+
+
+;;;
+;;; Tiled Map
+;;;
+
+(define-class <tile-map> (<node-2d>)
+ (tile-map #:accessor tile-map #:init-keyword #:map #:asset? #t)
+ (layers #:accessor layers #:init-keyword #:layers #:init-form #f))
+
+(define-method (render (node <tile-map>) alpha)
+ (let ((m (tile-map node)))
+ (draw-tile-map* m (world-matrix node) (tile-map-rect m)
+ #:layers (layers node))))
+
+
+;;;
+;;; Particles
+;;;
+
+(define-class <particles> (<node-2d>)
+ (particles #:accessor particles #:init-keyword #:particles))
+
+(define-method (on-boot (particles <particles>))
+ ;; Default bounding box size.
+ (when (zero? (width particles))
+ (set! (width particles) 32.0))
+ (when (zero? (height particles))
+ (set! (height particles) 32.0)))
+
+(define-method (update (node <particles>) dt)
+ (update-particles (particles node)))
+
+(define-method (render (node <particles>) alpha)
+ (draw-particles* (particles node) (world-matrix node)))
diff --git a/catbird/node.scm b/catbird/node.scm
new file mode 100644
index 0000000..99a6da3
--- /dev/null
+++ b/catbird/node.scm
@@ -0,0 +1,160 @@
+(define-module (catbird node)
+ #:use-module (catbird asset)
+ #:use-module (catbird cached-slots)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird observer)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:export (<node>
+ in-view?
+ tree-in-view?
+ children
+ for-each-child
+ on-boot
+ reboot
+ child-ref &
+ attach-to
+ replace
+ blink)
+ #:re-export (agenda
+ detach
+ hide
+ name
+ parent
+ on-enter
+ on-hide
+ on-exit
+ on-pause
+ on-resume
+ on-show
+ pause
+ paused?
+ rank
+ render
+ resume
+ run-script
+ show
+ stop-scripts
+ visible?
+ update))
+
+(define-class <node>
+ (<renderable> <scriptable> <containable> <nameable> <rankable>
+ <observer> <asset-container>)
+ ;; An integer value that determines priority order for
+ ;; updating/rendering.
+ (rank #:getter rank #:init-value 0 #:init-keyword #:rank)
+ ;; List of children, sorted by rank.
+ (children #:accessor children #:init-value '())
+ ;; Children indexed by name for fast lookup.
+ (children-by-name #:getter children-by-name #:init-thunk make-hash-table))
+
+(define-method (initialize (node <node>) initargs)
+ (next-method)
+ (on-boot node))
+
+(define-method (on-boot (node <node>))
+ #t)
+
+(define-method (reboot (node <node>))
+ (for-each-child detach node)
+ (with-agenda (agenda node) (reset-agenda))
+ (on-boot node))
+
+(define-method (write (node <node>) port)
+ (define (strip-angle-brackets str)
+ (let ((start (if (string-prefix? "<" str) 1 0))
+ (end (if (string-suffix? ">" str)
+ (- (string-length str) 1)
+ (string-length str))))
+ (substring str start end)))
+ (format port "#<~a name: ~a>"
+ (strip-angle-brackets
+ (symbol->string
+ (class-name (class-of node))))
+ (name node)))
+
+(define (for-each-child proc node)
+ (for-each proc (children node)))
+
+(define-method (update/around (node <node>) dt)
+ (unless (paused? node)
+ ;; Update children first, recursively.
+ (for-each-child (lambda (child) (update/around child dt)) node)
+ (next-method)))
+
+(define-method (tree-in-view? (node <node>))
+ #t)
+
+(define-method (in-view? (node <node>))
+ #t)
+
+(define-method (render/around (node <node>) alpha)
+ (when (and (visible? node) (tree-in-view? node))
+ (next-method)
+ (for-each-child (lambda (child) (render/around child alpha)) node)))
+
+(define-method (child-ref (parent <node>) name)
+ (hashq-ref (children-by-name parent) name))
+
+(define-syntax &
+ (syntax-rules ()
+ ((_ parent child-name)
+ (child-ref parent 'child-name))
+ ((_ parent child-name . rest)
+ (& (child-ref parent 'child-name) . rest))))
+
+(define-method (attach-to (new-parent <node>) . new-children)
+ ;; Validate all the nodes first. The whole operation will fail if
+ ;; any of them cannot be attached.
+ (for-each (lambda (child)
+ (when (parent child)
+ (raise-exception
+ (make-exception-with-message "node already has a parent")))
+ (when (child-ref new-parent (name child))
+ (raise-exception
+ (make-exception-with-message "node name taken"))))
+ new-children)
+ ;; Add named children to the name index for quick lookup later.
+ (for-each (lambda (child)
+ (when (name child)
+ (hashq-set! (children-by-name new-parent) (name child) child)))
+ new-children)
+ ;; Add the new children and sort them by their rank so that
+ ;; updating/rendering happens in the desired order.
+ (set! (children new-parent)
+ (sort-by-rank/ascending (append new-children (children new-parent))))
+ ;; Attach children to the parent, triggering initial enter/attach
+ ;; hooks.
+ (for-each (lambda (child)
+ (attach child new-parent))
+ new-children))
+
+(define-method (replace (parent-node <node>) . replacements)
+ (for-each (lambda (replacement)
+ (let ((old (child-ref parent-node (name replacement))))
+ (when old
+ (detach old))))
+ replacements)
+ (apply attach-to parent-node replacements))
+
+(define-method (detach (node <node>))
+ (let ((p (parent node)))
+ ;; Remove child from parent.
+ (set! (children p) (delq node (children p)))
+ ;; Remove from name index.
+ (when (name node)
+ (hashq-remove! (children-by-name p) (name node)))
+ (next-method)))
+
+(define-method (blink (node <node>) times interval)
+ (let loop ((i 0))
+ (when (< i times)
+ (set! (visible? node) #f)
+ (sleep interval)
+ (set! (visible? node) #t)
+ (sleep interval)
+ (loop (+ i 1)))))
diff --git a/catbird/observer.scm b/catbird/observer.scm
new file mode 100644
index 0000000..0cf01c5
--- /dev/null
+++ b/catbird/observer.scm
@@ -0,0 +1,37 @@
+(define-module (catbird observer)
+ #:use-module (catbird config)
+ #:use-module (oop goops)
+ #:export (<observer>
+ on-change))
+
+;; This is a hack to deal with the fact that specializing GOOPS
+;; accessors does not compose with inheritance.
+;;
+;; See
+;; https://dthompson.us/issues-with-object-oriented-programming-in-guile.html
+;; for details.
+
+(define-class <observer-slot-class> (<catbird-metaclass>))
+
+(define-generic on-change)
+
+(define-method (observer-slot? (slot <slot>))
+ (get-keyword #:observe? (slot-definition-options slot)))
+
+(define-method (compute-setter-method (class <observer-slot-class>) slot)
+ (if (observer-slot? slot)
+ ;; Wrap the original setter procedure with a new procedure that
+ ;; calls the on-change method.
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj new)
+ (let ((old (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name))))
+ (proc obj new)
+ (on-change obj slot-name old new)))))
+ (next-method)))
+
+(define-class <observer> ()
+ #:metaclass <observer-slot-class>)
diff --git a/catbird/overlay.scm b/catbird/overlay.scm
new file mode 100644
index 0000000..31a1442
--- /dev/null
+++ b/catbird/overlay.scm
@@ -0,0 +1,116 @@
+(define-module (catbird overlay)
+ #:use-module (catbird kernel)
+ #:use-module (catbird input-map)
+ #:use-module (catbird minibuffer)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird repl)
+ #:use-module (catbird scene)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:export (make-overlay
+ notify
+ open-minibuffer))
+
+(define %background-color (make-color 0.2 0.2 0.2 0.8))
+
+(define-class <overlay> (<scene>))
+
+(define (make-overlay)
+ (make <overlay>))
+
+(define-method (notify (scene <overlay>) message)
+ (run-script scene
+ (let* ((padding 8.0)
+ (label (make <label>
+ #:name 'message
+ #:rank 1
+ #:position (vec2 padding padding)
+ #:text message))
+ (region (car (regions scene)))
+ (bg (make <canvas>
+ #:name 'background
+ #:painter
+ (with-style ((fill-color %background-color))
+ (fill
+ (rounded-rectangle (vec2 0.0 0.0)
+ (+ (width label) padding padding)
+ (+ (height label) padding)
+ #:radius 2.0)))))
+ (notification (make <node-2d>
+ #:position (vec2 padding
+ (- (height (camera region))
+ (height bg)
+ padding)))))
+ (attach-to notification bg label)
+ (attach-to scene notification)
+ (sleep 5.0)
+ (detach notification))))
+
+(define-method (open-minibuffer)
+ (let ((r (find-region-by-name 'overlay)))
+ (push-major-mode (scene r) (make <minibuffer-mode>))))
+
+(define-class <fps-display> (<node-2d>))
+
+(define-method (on-boot (fps-display <fps-display>))
+ (let* ((font (default-font))
+ (padding 4.0)
+ (box-width (+ (font-line-width font "999.9")
+ (* padding 2.0)))
+ (box-height (+ (font-line-height font) padding)))
+ (attach-to fps-display
+ (make <canvas>
+ #:name 'background
+ #:painter
+ (with-style ((fill-color (make-color 0 0 0 0.5)))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ box-width
+ box-height))))
+ (make <label>
+ #:name 'label
+ #:rank 1
+ #:font font
+ #:position (vec2 padding padding)))
+ (set! (width fps-display) box-width)
+ (set! (height fps-display) box-height)
+ (set! (origin-y fps-display) box-height)
+ (update-fps fps-display)
+ (run-script fps-display
+ (forever
+ (sleep 1.0)
+ (update-fps fps-display)))))
+
+(define-method (update-fps (fps-display <fps-display>))
+ (set! (text (& fps-display label))
+ (format #f "~1,1f" (frames-per-second))))
+
+(define-minibuffer-command show-fps
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r))))
+ (when (and s (not (& s fps-display)))
+ (attach-to s (make <fps-display>
+ #:name 'fps-display
+ #:rank 99
+ #:position (vec2 0.0 (area-height r)))))))
+
+(define-minibuffer-command hide-fps
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r)))
+ (f (and s (& s fps-display))))
+ (when f (detach f))))
+
+(define-minibuffer-command repl
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r))))
+ (when s
+ (push-major-mode s (make <repl-mode>)))))
+
+(bind-input/global (key-press 'x '(alt)) open-minibuffer)
diff --git a/catbird/region.scm b/catbird/region.scm
new file mode 100644
index 0000000..4ae4bc9
--- /dev/null
+++ b/catbird/region.scm
@@ -0,0 +1,102 @@
+(define-module (catbird region)
+ #:use-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird node)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee data array-list)
+ #:use-module (chickadee graphics engine)
+ #:use-module (chickadee graphics viewport)
+ #:use-module (chickadee math rect)
+ #:use-module (ice-9 exceptions)
+ #:use-module (oop goops)
+ #:export (<region>
+ make-region
+ area
+ area-x
+ area-y
+ area-width
+ area-height
+ camera
+ scene
+ replace-scene
+ push-scene
+ pop-scene)
+ #:re-export (name
+ rank
+ render
+ update))
+
+(define-root-class <region> (<renderable> <updatable> <nameable> <rankable>)
+ (area #:accessor area #:init-keyword #:area)
+ (camera #:accessor camera #:init-keyword #:camera #:init-value #f)
+ (scene #:accessor scene #:init-keyword #:scene #:init-value #f)
+ (scene-stack #:getter scene-stack #:init-thunk make-array-list)
+ (viewport #:accessor viewport))
+
+(define-method (area-x (region <region>))
+ (rect-x (area region)))
+
+(define-method (area-y (region <region>))
+ (rect-y (area region)))
+
+(define-method (area-width (region <region>))
+ (rect-width (area region)))
+
+(define-method (area-height (region <region>))
+ (rect-height (area region)))
+
+(define (float->int x)
+ (inexact->exact (round x)))
+
+(define-method (initialize (region <region>) args)
+ (next-method)
+ (let ((r (area region)))
+ (set! (viewport region)
+ (make-viewport (float->int (rect-x r))
+ (float->int (rect-y r))
+ (float->int (rect-width r))
+ (float->int (rect-height r))))))
+
+(define (make-region area name rank)
+ (let* ((window (current-window))
+ (w (window-width window))
+ (h (window-height window)))
+ (when (or (< (rect-left area) 0.0)
+ (< (rect-bottom area) 0.0)
+ (> (rect-right area) w)
+ (> (rect-top area) h))
+ (raise-exception
+ (make-exception-with-message "region area exceeds window area")))
+ (make <region> #:area area #:name name #:rank rank)))
+
+(define-method (replace-scene (r <region>) (new-scene <scene>))
+ (let ((old-scene (scene r)))
+ (when old-scene (on-exit old-scene))
+ (set! (scene r) new-scene)
+ (set! (regions new-scene) (cons r (regions new-scene)))
+ (on-enter new-scene)))
+
+(define-method (push-scene (region <region>) (new-scene <scene>))
+ (let ((old-scene (scene region)))
+ (when old-scene
+ (array-list-push! (scene-stack region) old-scene))
+ (replace-scene region new-scene)))
+
+(define-method (pop-scene (region <region>))
+ (let ((stack (scene-stack region)))
+ (unless (array-list-empty? stack)
+ (replace-scene (array-list-pop! stack)))))
+
+(define-method (update (region <region>) dt)
+ (let ((s (scene region)))
+ (when s (update/around s dt))))
+
+(define-method (render (region <region>) alpha)
+ (let ((s (scene region))
+ (c (camera region)))
+ (when (and s c)
+ (parameterize ((current-camera c))
+ (with-projection (projection-matrix (camera region))
+ (render/around s alpha))))))
diff --git a/catbird/repl.scm b/catbird/repl.scm
new file mode 100644
index 0000000..d3c2197
--- /dev/null
+++ b/catbird/repl.scm
@@ -0,0 +1,349 @@
+;; TODO: Multiple values
+;; TODO: Multiple expressions
+;; TODO: Debugger
+;; TODO: Switching languages
+(define-module (catbird repl)
+ #:use-module (catbird line-editor)
+ #:use-module (catbird kernel)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird ring-buffer)
+ #:use-module (catbird scene)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (system base compile)
+ #:use-module (system base language)
+ #:export (<repl>
+ <repl-mode>
+ resize-repl))
+
+(define %background-color (make-color 0.0 0.0 0.0 0.9))
+
+(define (make-user-module)
+ (let ((module (resolve-module '(guile-user) #f)))
+ (beautify-user-module! module)
+ module))
+
+(define-class <repl> (<node-2d>)
+ (language #:accessor language #:init-value (lookup-language 'scheme))
+ (module #:accessor module #:init-thunk make-user-module)
+ (max-line-length #:accessor max-line-length #:init-value 256)
+ (log-lines #:accessor log-lines #:init-form (make-ring-buffer 64)))
+
+(define-method (on-boot (repl <repl>))
+ (attach-to repl
+ (make <canvas>
+ #:name 'background)
+ (make <label>
+ #:name 'log
+ #:rank 1)
+ (make <line-editor>
+ #:name 'editor
+ #:rank 1))
+ (log-append repl "Enter ',help' for help.")
+ (refresh-prompt repl))
+
+(define-method (log-append (repl <repl>) line)
+ (ring-buffer-put! (log-lines repl)
+ ;; Truncate long lines
+ (if (> (string-length line) (max-line-length repl))
+ (substring line 0 (max-line-length repl))
+ line))
+ (refresh-log repl))
+
+(define-method (concatenate-log (repl <repl>))
+ (let ((n (- (inexact->exact
+ (floor
+ (/ (height repl)
+ (font-line-height (font (& repl log))))))
+ 1))
+ (lines (log-lines repl)))
+ (string-join (let loop ((i (max (- (ring-buffer-length lines) n) 0)))
+ (if (< i (ring-buffer-length lines))
+ (cons (ring-buffer-ref lines i)
+ (loop (+ i 1)))
+ '()))
+ "\n")))
+
+(define-method (refresh-log (repl <repl>))
+ (let ((log (& repl log)))
+ (set! (text log) (concatenate-log repl))
+ (set! (position-y log) (- (height repl) (height log)))
+ (place-below log (& repl editor))))
+
+(define-method (refresh-prompt (repl <repl>))
+ (set! (prompt (& repl editor))
+ (format #f "~a@~a> "
+ (language-name (language repl))
+ (module-name (module repl)))))
+
+(define-method (resize-repl (repl <repl>) w h)
+ (set! (width repl) w)
+ (set! (height repl) h)
+ (set! (painter (& repl background))
+ (with-style ((fill-color %background-color))
+ (fill
+ (rectangle (vec2 0.0 0.0) w h))))
+ (refresh-log repl))
+
+(define-method (repl-read-expression (repl <repl>) line)
+ (call-with-input-string line
+ (lambda (port)
+ ((language-reader (language repl)) port (module repl)))))
+
+(define-method (with-output-to-log (repl <repl>) thunk)
+ (let* ((val *unspecified*)
+ (str (with-output-to-string
+ (lambda ()
+ (set! val (thunk))))))
+ (unless (string-null? str)
+ (for-each (lambda (line)
+ (log-append repl line))
+ (string-split str #\newline)))
+ val))
+
+(define-method (with-error-handling (repl <repl>) thunk)
+ (let ((stack #f))
+ (define (handle-error e)
+ (let ((frame (stack-ref stack 0)))
+ (log-append repl
+ (format #f "~a: In procedure: ~a:"
+ (match (frame-source frame)
+ ((_ file-name line . column)
+ (format #f "~a:~a:~a"
+ (if file-name
+ (basename file-name)
+ "unknown file")
+ line column))
+ (_ "unknown"))
+ (or (frame-procedure-name frame)
+ "unknown")))
+ (log-append repl
+ (format #f "In procedure: ~a: ~a"
+ (or (and (exception-with-origin? e)
+ (exception-origin e))
+ "unknown")
+ (if (and (exception-with-message? e)
+ (exception-with-irritants? e))
+ (apply format #f (exception-message e)
+ (exception-irritants e))
+ "")))
+ (with-output-to-log repl
+ (lambda ()
+ (display-backtrace stack
+ (current-output-port))))))
+ (define (pre-unwind-handler . args)
+ (set! stack (make-stack #t 4)))
+ (define (throw-handler)
+ (with-throw-handler #t thunk pre-unwind-handler))
+ (define (exception-handler e)
+ (if (quit-exception? e)
+ (raise-exception e)
+ (handle-error e)))
+ (with-exception-handler exception-handler throw-handler #:unwind? #t)))
+
+(define-method (repl-compile (repl <repl>) line)
+ (define (compile-line)
+ (with-output-to-log repl
+ (lambda ()
+ (compile (repl-read-expression repl line)
+ #:from (language repl)
+ #:env (module repl)))))
+ (with-error-handling repl compile-line))
+
+(define-method (write-value-to-log (repl <repl>) x)
+ (unless (unspecified? x)
+ (with-output-to-log repl (lambda () (write x)))))
+
+(define (skip-whitespace str i)
+ (let loop ((i i))
+ (cond
+ ((= i (string-length str))
+ (- i 1))
+ ((char-whitespace? (string-ref str i))
+ (loop (+ i 1)))
+ (else
+ i))))
+
+(define (find-whitespace str i)
+ (let loop ((i i))
+ (cond
+ ((= i (string-length str))
+ i)
+ ((char-whitespace? (string-ref str i))
+ i)
+ (else
+ (loop (+ i 1))))))
+
+(define (meta-command-string? str)
+ (and (not (string-null? str))
+ (eqv? (string-ref str (skip-whitespace str 0)) #\,)))
+
+(define (parse-meta-command str)
+ (let* ((i (skip-whitespace str 0))
+ (j (find-whitespace str i)))
+ (cons (substring str i j)
+ (call-with-input-string (substring str j)
+ (lambda (port)
+ (let loop ()
+ (let ((exp (read port)))
+ (if (eof-object? exp)
+ '()
+ (cons exp (loop))))))))))
+
+(define-method (meta-command (repl <repl>) line)
+ (match (parse-meta-command line)
+ ((name args ...)
+ (let ((meta (lookup-meta-command name)))
+ (if meta
+ (with-error-handling repl
+ (lambda ()
+ (apply-meta-command meta repl args)))
+ (log-append repl (string-append "Unknown meta-command: " name)))))))
+
+(define-method (repl-eval (repl <repl>))
+ (let* ((editor (& repl editor))
+ (line (get-line editor)))
+ (save-to-history editor)
+ (log-append repl (string-append (prompt editor) line))
+ (if (meta-command-string? line)
+ (meta-command repl line)
+ (write-value-to-log repl (repl-compile repl line)))
+ (clear-line editor)
+ (refresh-log repl)
+ (refresh-prompt repl)))
+
+
+;;;
+;;; Meta commands
+;;;
+
+(define-record-type <meta-command>
+ (make-meta-command name aliases category docstring proc)
+ meta-command?
+ (name meta-command-name)
+ (aliases meta-command-aliases)
+ (category meta-command-category)
+ (docstring meta-command-docstring)
+ (proc meta-command-proc))
+
+(define (apply-meta-command meta repl args)
+ (apply (meta-command-proc meta) repl args))
+
+(define *meta-commands* '())
+
+(define (lookup-meta-command name)
+ (find (lambda (m)
+ (or (string=? (meta-command-name m) name)
+ (any (lambda (alias)
+ (string=? alias name))
+ (meta-command-aliases m))))
+ *meta-commands*))
+
+(define (add-meta-command! name aliases category docstring proc)
+ (set! *meta-commands*
+ (cons (make-meta-command name aliases category docstring proc)
+ *meta-commands*)))
+
+(define (symbol->meta-command sym)
+ (string-append "," (symbol->string sym)))
+
+(define-syntax define-meta-command
+ (syntax-rules ()
+ ((_ ((name aliases ...) category repl args ...) docstring body ...)
+ (add-meta-command! (symbol->meta-command 'name)
+ (map symbol->meta-command '(aliases ...))
+ 'category
+ docstring
+ (lambda* (repl args ...)
+ body ...)))
+ ((_ (name category repl args ...) docstring body ...)
+ (add-meta-command! (symbol->meta-command 'name)
+ '()
+ 'category
+ docstring
+ (lambda* (repl args ...)
+ body ...)))))
+
+(define-meta-command (help help repl)
+ "- Show this help information."
+ (for-each (lambda (m)
+ (match (meta-command-aliases m)
+ (()
+ (log-append repl
+ (format #f "~a ~a"
+ (meta-command-name m)
+ (meta-command-docstring m))))
+ (aliases
+ (log-append repl
+ (format #f "~a ~a ~a"
+ (meta-command-name m)
+ aliases
+ (meta-command-docstring m))))))
+ (sort *meta-commands*
+ (lambda (a b)
+ (string<? (meta-command-name a)
+ (meta-command-name b))))))
+
+(define-meta-command ((quit q) system repl)
+ "- Quit program."
+ (exit-catbird))
+
+(define-meta-command ((import use) module repl module-name)
+ "MODULE - Import a module."
+ (module-use! (module repl) (resolve-module module-name)))
+
+(define-meta-command ((module m) module repl #:optional module-name)
+ "[MODULE] - Change current module or show current module."
+ (if module-name
+ (log-append repl (format #f "~a" (module-name (module repl))))
+ (set! (module repl) (resolve-module module-name))))
+
+
+;;;
+;;; REPL major mode
+;;;
+
+(define-class <repl-mode> (<major-mode>)
+ (prev-keyboard-focus #:accessor prev-keyboard-focus #:init-value #f))
+
+(define (repl mode)
+ (& (parent mode) repl))
+
+(define-method (on-enter (mode <repl-mode>))
+ (let* ((scene (parent mode))
+ (region (car (regions scene)))
+ (repl (or (& (parent mode) repl)
+ (make <repl>
+ #:name 'repl))))
+ (unless (parent repl)
+ (attach-to (parent mode) repl))
+ (show repl)
+ (resize-repl repl (area-width region) (area-height region))
+ (set! (prev-keyboard-focus mode) (current-keyboard-focus))
+ (take-keyboard-focus region)
+ (add-minor-mode scene (make <line-edit-mode>
+ #:editor (& repl editor)))))
+
+(define-method (close-repl (mode <repl-mode>))
+ (let ((scene (parent mode)))
+ (hide (& scene repl))
+ (take-keyboard-focus (prev-keyboard-focus mode))
+ (remove-minor-mode (parent mode) <line-edit-mode>)
+ (pop-major-mode scene)))
+
+(define-method (eval-expression (mode <repl-mode>))
+ (repl-eval (repl mode)))
+
+(bind-input <repl-mode> (key-press 'escape) close-repl)
+(bind-input <repl-mode> (key-press 'g '(ctrl)) close-repl)
+(bind-input <repl-mode> (key-press 'return) eval-expression)
diff --git a/catbird/ring-buffer.scm b/catbird/ring-buffer.scm
new file mode 100644
index 0000000..ce265c0
--- /dev/null
+++ b/catbird/ring-buffer.scm
@@ -0,0 +1,64 @@
+(define-module (catbird ring-buffer)
+ #:use-module (srfi srfi-9)
+ #:export (make-ring-buffer
+ ring-buffer
+ ring-buffer-length
+ ring-buffer-put!
+ ring-buffer-get!
+ ring-buffer-ref
+ ring-buffer-clear!))
+
+(define-record-type <ring-buffer>
+ (%make-ring-buffer vector length head tail)
+ ring-buffer?
+ (vector ring-buffer-vector)
+ (length ring-buffer-length set-ring-buffer-length!)
+ (head ring-buffer-head set-ring-buffer-head!)
+ (tail ring-buffer-tail set-ring-buffer-tail!))
+
+(define (make-ring-buffer size)
+ (%make-ring-buffer (make-vector size #f) 0 0 0))
+
+(define (ring-buffer-empty? ring)
+ (zero? (ring-buffer-length ring)))
+
+(define (ring-buffer-put! ring x)
+ (let* ((head (ring-buffer-head ring))
+ (tail (ring-buffer-tail ring))
+ (l (ring-buffer-length ring))
+ (v (ring-buffer-vector ring))
+ (vl (vector-length v)))
+ (vector-set! v tail x)
+ (set-ring-buffer-length! ring (min (+ l 1) vl))
+ (when (and (> l 0) (= head tail))
+ (set-ring-buffer-head! ring (modulo (+ head 1) vl)))
+ (set-ring-buffer-tail! ring (modulo (+ tail 1) vl))))
+
+(define (ring-buffer-get! ring)
+ (if (ring-buffer-empty? ring)
+ (error "ring buffer empty" ring)
+ (let* ((head (ring-buffer-head ring))
+ (v (ring-buffer-vector ring))
+ (result (vector-ref v head)))
+ (vector-set! v head #f)
+ (set-ring-buffer-head! ring (modulo (+ head 1) (vector-length v)))
+ (set-ring-buffer-length! ring (- (ring-buffer-length ring) 1))
+ result)))
+
+(define (ring-buffer-ref ring i)
+ (let ((l (ring-buffer-length ring))
+ (v (ring-buffer-vector ring)))
+ (if (>= i l)
+ (error "ring buffer index out of bounds" i)
+ (vector-ref v (modulo (+ (ring-buffer-head ring) i)
+ (vector-length v))))))
+
+(define (ring-buffer-clear! ring)
+ (let ((v (ring-buffer-vector ring)))
+ (set-ring-buffer-head! ring 0)
+ (set-ring-buffer-tail! ring 0)
+ (set-ring-buffer-length! ring 0)
+ (let loop ((i 0))
+ (when (< i (vector-length v))
+ (vector-set! v i #f)
+ (loop (+ i 1))))))
diff --git a/catbird/scene.scm b/catbird/scene.scm
new file mode 100644
index 0000000..b46a176
--- /dev/null
+++ b/catbird/scene.scm
@@ -0,0 +1,147 @@
+(define-module (catbird scene)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (chickadee data array-list)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<scene>
+ regions
+ major-mode
+ minor-modes
+ replace-major-mode
+ push-major-mode
+ pop-major-mode
+ add-minor-mode
+ remove-minor-mode))
+
+(define-root-class <scene> (<node>)
+ (regions #:accessor regions #:init-value '())
+ (major-mode #:accessor major-mode #:init-keyword #:major-mode
+ #:init-form (make <nothing-mode>))
+ (major-mode-stack #:getter major-mode-stack #:init-thunk make-array-list)
+ (minor-modes #:accessor minor-modes #:init-value '())
+ (input-map #:getter input-map #:init-value '()))
+
+(define-method (initialize (scene <scene>) args)
+ (next-method)
+ (attach (major-mode scene) scene))
+
+(define-method (replace-major-mode (scene <scene>) (mode <major-mode>))
+ (let ((old-mode (major-mode scene)))
+ (when old-mode
+ (detach old-mode))
+ (set! (major-mode scene) mode)
+ (attach mode scene)))
+
+(define-method (push-major-mode (scene <scene>) (mode <major-mode>))
+ (let ((old-mode (major-mode scene)))
+ (array-list-push! (major-mode-stack scene) old-mode)
+ (when old-mode
+ (pause old-mode))
+ (set! (major-mode scene) mode)
+ (attach mode scene)))
+
+(define-method (pop-major-mode (scene <scene>))
+ (let ((stack (major-mode-stack scene)))
+ (unless (array-list-empty? stack)
+ (let ((mode (major-mode scene))
+ (prev-mode (array-list-pop! stack)))
+ (when mode
+ (detach mode))
+ (set! (major-mode scene) prev-mode)
+ (resume prev-mode)))))
+
+(define-method (add-minor-mode (scene <scene>) (mode <minor-mode>))
+ (when (parent mode)
+ (raise-exception
+ (make-exception-with-message "mode already attached to a scene")))
+ (set! (minor-modes scene) (cons mode (minor-modes scene)))
+ (attach mode scene))
+
+(define-method (remove-minor-mode (scene <scene>) (mode <minor-mode>))
+ (unless (eq? scene (parent mode))
+ (raise-exception
+ (make-exception-with-message "mode not attached to scene")))
+ (let ((modes (minor-modes scene)))
+ (set! (minor-modes scene) (delq mode modes))
+ (detach mode)))
+
+(define-method (remove-minor-mode (scene <scene>) (mode-class <class>))
+ (let ((mode (find (lambda (mode)
+ (eq? (class-of mode) mode-class))
+ (minor-modes scene))))
+ (when mode
+ (remove-minor-mode scene mode))))
+
+(define-method (search-modes (scene <scene>) proc)
+ (or (proc (major-mode scene))
+ (find (lambda (mode)
+ (proc mode))
+ (minor-modes scene))))
+
+(define-method (on-key-press (scene <scene>) key modifiers)
+ (search-modes scene
+ (lambda (mode)
+ (on-key-press mode key modifiers))))
+
+(define-method (on-key-release (scene <scene>) key modifiers)
+ (search-modes scene
+ (lambda (mode)
+ (on-key-release mode key modifiers))))
+
+(define-method (on-text-input (scene <scene>) text)
+ (search-modes scene
+ (lambda (mode)
+ (on-text-input mode text))))
+
+(define-method (on-mouse-press (scene <scene>) button x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-press mode button x y))))
+
+(define-method (on-mouse-release (scene <scene>) button x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-release mode button x y))))
+
+(define-method (on-mouse-move (scene <scene>) x y x-rel y-rel buttons)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-move mode x y x-rel y-rel buttons))))
+
+(define-method (on-mouse-wheel (scene <scene>) x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-wheel mode x y))))
+
+(define-method (on-controller-press (scene <scene>) controller-id button)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-press mode controller-id button))))
+
+(define-method (on-controller-release (scene <scene>) controller-id button)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-release mode controller-id button))))
+
+(define-method (on-controller-move (scene <scene>) controller-id axis value)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-move mode controller-id axis value))))
+
+(define-method (update (scene <scene>) dt)
+ (update (major-mode scene) dt)
+ (for-each (lambda (mode) (update mode dt))
+ (minor-modes scene)))
+
+(define-method (pause (scene <scene>))
+ (for-each-child pause scene)
+ (next-method))
+
+(define-method (resume (scene <scene>))
+ (for-each-child resume scene)
+ (next-method))
diff --git a/configure.ac b/configure.ac
index c489791..bddce86 100644
--- a/configure.ac
+++ b/configure.ac
@@ -9,6 +9,7 @@ AM_SILENT_RULES([yes])
AC_PATH_PROG([GUILE], [guile])
AC_CONFIG_FILES([Makefile])
AC_CONFIG_FILES([pre-inst-env], [chmod +x pre-inst-env])
+AC_CONFIG_FILES([test-env], [chmod +x test-env])
GUILE_PKG([3.0 2.2])
GUILE_PROGS
diff --git a/guix.scm b/guix.scm
index 591f40a..3d3e44e 100644
--- a/guix.scm
+++ b/guix.scm
@@ -117,7 +117,7 @@ SDL2 C shared library via the foreign function interface.")
(license license:lgpl3+))))
(define chickadee
- (let ((commit "5ed490cac58d9f548bc141cc334c25dea9bfcaf2"))
+ (let ((commit "5edce04c698cd92149004ead1cad77c481c682e8"))
(package
(name "chickadee")
(version (string-append "0.8.0-1." (string-take commit 7)))
@@ -128,7 +128,7 @@ SDL2 C shared library via the foreign function interface.")
(commit commit)))
(sha256
(base32
- "1wq1v19qp2q31ybjqzw0xccyw5v03hh9y79z8rjd6l83vbp1nvkx"))))
+ "1hckx827aw3af8cbw1mfjy57wdssv1q3bs2hziymxddipwa0d425"))))
(build-system gnu-build-system)
(arguments
'(#:make-flags '("GUILE_AUTO_COMPILE=0")
diff --git a/pre-inst-env.in b/pre-inst-env.in
index 0d3b6a4..06cb204 100644
--- a/pre-inst-env.in
+++ b/pre-inst-env.in
@@ -30,6 +30,5 @@ then
fi
export PATH="$abs_top_builddir/scripts:$PATH"
-export STARLING_DEV_MODE="1"
exec "$@"
diff --git a/test-env.in b/test-env.in
new file mode 100644
index 0000000..1ab197a
--- /dev/null
+++ b/test-env.in
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+"@abs_top_builddir@/pre-inst-env" "$@"
+
+exit $?
diff --git a/tests/node.scm b/tests/node.scm
new file mode 100644
index 0000000..6cdd223
--- /dev/null
+++ b/tests/node.scm
@@ -0,0 +1,7 @@
+(define-module (tests node)
+ #:use-module (catbird node)
+ #:use-module (srfi srfi-64)
+ #:use-module (tests utils))
+
+(with-tests "node"
+ #t)
diff --git a/tests/scene.scm b/tests/scene.scm
new file mode 100644
index 0000000..20a8301
--- /dev/null
+++ b/tests/scene.scm
@@ -0,0 +1,7 @@
+(define-module (tests scene)
+ #:use-module (catbird scene)
+ #:use-module (srfi srfi-64)
+ #:use-module (tests utils))
+
+(with-tests "scene"
+ #t)
diff --git a/tests/utils.scm b/tests/utils.scm
new file mode 100644
index 0000000..b05f70c
--- /dev/null
+++ b/tests/utils.scm
@@ -0,0 +1,9 @@
+(define-module (tests utils)
+ #:use-module (srfi srfi-64)
+ #:export (with-tests))
+
+(define-syntax-rule (with-tests name body ...)
+ (begin
+ (test-begin name)
+ body ...
+ (exit (zero? (test-runner-fail-count (test-end))))))