317 lines
11 KiB
Racket
317 lines
11 KiB
Racket
#lang racket/base
|
|
|
|
(require racket/async-channel racket/class racket/format racket/function racket/list racket/match
|
|
ansi lux raart)
|
|
|
|
(provide activity% run-application
|
|
(struct-out app-event) fire-event
|
|
component% header/footer% scroll-pane% label% table%)
|
|
|
|
;; internal synthetic event queue
|
|
(struct app-event [from tag data] #:transparent)
|
|
(define current-app-events (make-parameter #f))
|
|
|
|
;; fires an app event to the synthetic event queue
|
|
;; from should be a component% (or perhaps activity%) that triggered the event
|
|
;; tag should be a symbol describing the event type
|
|
;; data is anything
|
|
(define (fire-event from tag data)
|
|
(async-channel-put (current-app-events) (app-event from tag data)))
|
|
|
|
;; we're making a whole new thing for writing raart/lux applications
|
|
;; because it's more flexible than the word system, and it will allow us to have a "stack" of
|
|
;; activity windows which is a more useful abstraction
|
|
;; also, user-interface code is a good place where OOP is actually useful, and it's nicer than the
|
|
;; stock lux gen:word system
|
|
(define activity%
|
|
(class object%
|
|
(init-field [fps 0.0])
|
|
(super-new)
|
|
|
|
;; returns a raart drawing
|
|
(define/public (draw size)
|
|
(error "base draw"))
|
|
|
|
;; returns 'continue 'quit or another activity to start
|
|
(define/public (on-event e)
|
|
'continue)
|
|
|
|
;; same but for fps ticks
|
|
(define/public (on-tick)
|
|
'continue)
|
|
|
|
;; called when this activity is returned to
|
|
(define/public (on-return from-data)
|
|
(void))
|
|
|
|
;; called when this activity is starting
|
|
(define/public (on-activated)
|
|
(void))
|
|
|
|
;; called when this activity is stopping. can return a value for on-return of the previous
|
|
;; activity or to be printed, if this is the last activity
|
|
(define/public (on-deactivated)
|
|
(void))))
|
|
|
|
;; steps the word wrapper based on the result of calling an activity event or tick handler
|
|
(define (application-step word e)
|
|
(define stack (application-activity-stack word))
|
|
(define result
|
|
(if e
|
|
(send (first stack) on-event e)
|
|
(send (first stack) on-tick)))
|
|
(match* (result stack)
|
|
[('continue stack) (struct-copy application word [activity-stack stack])]
|
|
[('quit (list act)) #f]
|
|
[('quit (list act act-next act-rest ...))
|
|
(send act-next on-return (send act on-deactivated))
|
|
(struct-copy application word [activity-stack (cons act-next act-rest)])]
|
|
[(act-next (list act act-rest ...))
|
|
(send act-next on-activated)
|
|
(struct-copy application word [activity-stack (cons act-next (cons act act-rest))])]))
|
|
|
|
;; an application is a word that runs an activity stack, until the last activity on the stack exits
|
|
(struct application [activity-stack size name]
|
|
#:methods gen:word
|
|
[(define (word-fps word)
|
|
(get-field fps (first (application-activity-stack word))))
|
|
(define (word-label word ft)
|
|
(application-name word))
|
|
(define (word-evt word)
|
|
(current-app-events))
|
|
(define (word-event word e)
|
|
(match e
|
|
;; intercept the screen-size-report
|
|
[(screen-size-report h w) (struct-copy application word [size (cons w h)])]
|
|
[_ (application-step word e)]))
|
|
(define (word-tick word)
|
|
(application-step word #f))
|
|
(define (word-output word)
|
|
(send (first (application-activity-stack word)) draw (application-size word)))
|
|
(define (word-return word)
|
|
(send (first (application-activity-stack word)) on-deactivated))])
|
|
|
|
;; creates and runs an application starting with a given first activity
|
|
(define (run-application name init-activity)
|
|
(define error-stream (open-output-string))
|
|
(define exit-message
|
|
(with-handlers ([exn? identity])
|
|
(parameterize ([current-error-port error-stream] [current-app-events (make-async-channel)])
|
|
(call-with-chaos
|
|
(make-raart #:mouse? #t)
|
|
(λ ()
|
|
(send init-activity on-activated)
|
|
(fiat-lux (application (list init-activity) (cons 80 24) name)))))))
|
|
|
|
;; raart is a bit fucky with the non-alternate screen so just clear it lol
|
|
(void (write-string (clear-screen/home))
|
|
(write-string (get-output-string error-stream) (current-error-port))
|
|
(displayln exit-message)))
|
|
|
|
|
|
;; ok now that there's an activity stack concept, the next enhancement is a layout engine
|
|
|
|
;; base TUI component
|
|
(define component%
|
|
(class object%
|
|
(init-field [parent #f])
|
|
(super-new)
|
|
|
|
;; draw this component with a size given by (cons w h)
|
|
;; w and h can be numbers to indicate a restriction in that dimension, or 'any to indicate any
|
|
;; size
|
|
(define/public (draw size)
|
|
(error "component draw"))
|
|
|
|
;; process an event and forward to children if necessary
|
|
;; #t if the event was consumed, otherwise #f
|
|
(define/public (on-event e)
|
|
#f)))
|
|
|
|
;; a component that vertically stacks a header, body, and footer
|
|
;; expects real width
|
|
;; forwards events to the body component
|
|
(define header/footer%
|
|
(class component%
|
|
(init-field header)
|
|
(init-field body)
|
|
(init-field footer)
|
|
(init-field [header-height 1])
|
|
(init-field [footer-height 1])
|
|
(super-new)
|
|
|
|
(set-field! parent header this)
|
|
(set-field! parent body this)
|
|
(set-field! parent footer this)
|
|
|
|
(define/override (draw size)
|
|
(match-define (cons w h) size)
|
|
(define body-h
|
|
(match h
|
|
['any 'any]
|
|
[_ (- h header-height footer-height)]))
|
|
|
|
(vappend (send header draw (cons w header-height))
|
|
(send body draw (cons w body-h))
|
|
(send footer draw (cons w footer-height))))
|
|
|
|
(define/override (on-event e)
|
|
(send body on-event e))))
|
|
|
|
;; a vertical scroll pane
|
|
;; expects real width and height
|
|
(define scroll-pane%
|
|
(class component%
|
|
(init-field body)
|
|
(super-new)
|
|
|
|
(set-field! parent body this)
|
|
|
|
(define scroll-y 0)
|
|
(define inner-size (cons 0 0))
|
|
(define screen-size (cons 80 24))
|
|
|
|
(define/public (focus-to y)
|
|
(when (> y (+ scroll-y (cdr screen-size)))
|
|
(set! scroll-y (max 0 (- y (cdr screen-size)))))
|
|
(when (< y scroll-y)
|
|
(set! scroll-y y))
|
|
(void))
|
|
|
|
(define/override (draw size)
|
|
(define body-rendered (send body draw (cons (car size) 'any)))
|
|
(set! screen-size size)
|
|
(set! inner-size (cons (raart-w body-rendered) (raart-h body-rendered)))
|
|
(when (> scroll-y (- (cdr inner-size) (cdr size)))
|
|
(set! scroll-y (max 0 (- (cdr inner-size) (cdr size)))))
|
|
(crop 0 (car size) scroll-y (cdr size) body-rendered))
|
|
|
|
(define/override (on-event e)
|
|
(or
|
|
(send body on-event e)
|
|
(match e
|
|
[(or "<up>" "k")
|
|
(set! scroll-y (max 0 (sub1 scroll-y)))
|
|
#t]
|
|
[(or "<down>" "j")
|
|
(set! scroll-y (min (cdr inner-size) (add1 scroll-y)))
|
|
#t]
|
|
[_ #f])))))
|
|
|
|
;; a basic text label
|
|
(define label%
|
|
(class component%
|
|
(init-field label-text)
|
|
(super-new)
|
|
|
|
(define/override (draw size)
|
|
(match-define (cons w h) size)
|
|
(define rendered (text label-text))
|
|
(define new-w (if (eq? w 'any) (raart-w rendered) w))
|
|
(define new-h (if (eq? h 'any) (raart-h rendered) h))
|
|
(matte new-w new-h rendered))))
|
|
|
|
(define table%
|
|
(class component%
|
|
(init-field headers)
|
|
(init-field cells)
|
|
(init-field [flex-col 0])
|
|
(super-new)
|
|
|
|
(define selected-row 0)
|
|
|
|
(define/override (draw size)
|
|
(match-define (cons w h) size)
|
|
(define rows (length cells))
|
|
(define cols (length (first cells)))
|
|
(define min-width
|
|
(+ 1 cols
|
|
(for/sum ([i (in-range cols)] #:unless (= i flex-col))
|
|
(for/fold ([max-width 0]) ([row (in-list cells)])
|
|
(max max-width (string-length (list-ref row i)))))))
|
|
(define flex-col-w
|
|
(match w
|
|
['any (for/fold ([max-width 0]) ([row (in-list cells)])
|
|
(max max-width (string-length (list-ref row flex-col))))]
|
|
[_ (max 10 (- w min-width))]))
|
|
|
|
(define rendered-cells
|
|
(for/list ([row (in-list cells)] [ri (in-naturals)])
|
|
(for/list ([cell (in-list row)] [col (in-naturals)])
|
|
(define (cell-style x) (if (= ri selected-row) (with-drawing 'bold 'green #f x) x))
|
|
(cond
|
|
[(= col flex-col)
|
|
(define trimmed-str
|
|
(if (> (string-length cell) flex-col-w)
|
|
(string-append (substring cell 0 (- flex-col-w 3)) "...")
|
|
cell))
|
|
(matte-at flex-col-w 1 0 0 (cell-style (text trimmed-str)))]
|
|
[else (cell-style (text cell))]))))
|
|
|
|
(define rendered-header
|
|
(for/list ([t (in-list headers)])
|
|
(style 'bold (text t))))
|
|
|
|
(table (cons rendered-header rendered-cells)))
|
|
|
|
(define/override (on-event e)
|
|
(match e
|
|
[(or "<up>" "k")
|
|
(set! selected-row (max 0 (sub1 selected-row)))
|
|
(when (is-a? (get-field parent this) scroll-pane%)
|
|
(send (get-field parent this) focus-to (+ 0 (* 2 selected-row))))
|
|
#t]
|
|
[(or "<down>" "j")
|
|
(set! selected-row (min (sub1 (length cells)) (add1 selected-row)))
|
|
(when (is-a? (get-field parent this) scroll-pane%)
|
|
(send (get-field parent this) focus-to (+ 5 (* 2 selected-row))))
|
|
#t]
|
|
["C-M" (fire-event this 'selection selected-row) #t]
|
|
[_ #f]))))
|
|
|
|
;; test
|
|
(module+ main
|
|
(define stacky%
|
|
(class activity%
|
|
(init-field number)
|
|
(super-new [fps 0.0])
|
|
|
|
(define selected-row #f)
|
|
|
|
(define root
|
|
(new header/footer%
|
|
[header (new label% [label-text "this is the header"])]
|
|
[body (new scroll-pane%
|
|
[body (new table%
|
|
[headers '("title" "author")]
|
|
[cells '(("meow" "meow")
|
|
("meow 2" "meow 2")
|
|
("meow 3" "meow 3")
|
|
("meow 4" "meow 4")
|
|
("meow 5" "meow 5")
|
|
("meow 6" "meow 6")
|
|
("meow 7" "meow 7")
|
|
("meow 8" "meow 8")
|
|
("meow 9" "meow 9")
|
|
("meow 10" "meow 10")
|
|
("meow 11" "meow 11")
|
|
("meow 12" "meow 12")
|
|
("meow 13" "meow 13"))])])]
|
|
[footer (new label% [label-text "this is the footer"])]))
|
|
|
|
(define/override (draw size)
|
|
(send root draw size))
|
|
|
|
(define/override (on-event e)
|
|
(match e
|
|
["q" 'quit]
|
|
["n" (new stacky% [number (add1 number)])]
|
|
[(app-event _ 'selection row) (set! selected-row row) 'quit]
|
|
[_ (send root on-event e)
|
|
'continue]))
|
|
|
|
(define/override (on-deactivated)
|
|
(format "bye, meow ~a (selected row ~a)" number selected-row))))
|
|
|
|
(run-application "demo" (new stacky% [number 0])))
|