commit d45d6c523aca997977558fbfbcb3940f76b19efb
parent 920c012417317204eae9f8111b9b8202077bf840
Author: Tomas Hlavaty <tom@logand.com>
Date: Sun, 15 Sep 2013 16:28:43 +0200
refactored code from jara2wad4cl, no dependencies on postmodern and hunchentoot, separated calendar and ui
Diffstat:
A | calendar.lisp | | | 164 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | cl-rw.asd | | | 4 | +++- |
A | counter.lisp | | | 53 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | ui.lisp | | | 711 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 931 insertions(+), 1 deletion(-)
diff --git a/calendar.lisp b/calendar.lisp
@@ -0,0 +1,164 @@
+(defpackage :rw.calendar
+ (:use :cl)
+ (:export))
+
+(in-package :rw.calendar)
+
+(defun iso-date (universal-time)
+ (multiple-value-bind (ss mm hh d m y dd dl z)
+ (decode-universal-time universal-time)
+ (declare (ignore ss mm hh dd dl z))
+ (format nil "~4,'0d-~2,'0d-~2,'0d" y m d)))
+
+(defun pretty-month (month)
+ #+nil
+ (ecase month
+ (1 " 1月")
+ (2 " 2月")
+ (3 " 3月")
+ (4 " 4月")
+ (5 " 5月")
+ (6 " 6月")
+ (7 " 7月")
+ (8 " 8月")
+ (9 " 9月")
+ (10 "10月")
+ (11 "11月")
+ (12 "12月"))
+ (ecase month
+ (1 "Jan")
+ (2 "Feb")
+ (3 "Mar")
+ (4 "Apr")
+ (5 "May")
+ (6 "Jun")
+ (7 "Jul")
+ (8 "Aug")
+ (9 "Sep")
+ (10 "Oct")
+ (11 "Nov")
+ (12 "Dec")))
+
+(defun pretty-day (day)
+ #+nil
+ (ecase day
+ (0 " 月")
+ (1 " 火")
+ (2 " 水")
+ (3 " 木")
+ (4 " 金")
+ (5 " 土")
+ (6 " 日"))
+ (ecase day
+ (0 "Mo")
+ (1 "Tu")
+ (2 "We")
+ (3 "Th")
+ (4 "Fr")
+ (5 "Sa")
+ (6 "Su")))
+
+(defun pretty-date (universal-time)
+ (multiple-value-bind (se0 mi0 ho0 da0 mo0 ye0 dow0 dst0 tz0)
+ (decode-universal-time (get-universal-time))
+ (declare (ignore se0 mi0 ho0 dow0 dst0 tz0))
+ (multiple-value-bind (se mi ho da mo ye dow dst tz)
+ (decode-universal-time universal-time)
+ (declare (ignore se mi ho dow dst tz))
+ (if (= ye0 ye)
+ (if (and (= mo0 mo) (= da0 da))
+ "Today"
+ (format nil "~a ~d" (pretty-month mo) da))
+ (iso-date universal-time)))))
+
+(defun easter (year)
+ (let* ((h1 (floor year 100))
+ (h2 (floor year 400))
+ (m (- (+ 15 h1) h2 (floor (+ 13 (* 8 h1)) 25)))
+ (n (- (+ 4 h1) h2))
+ (a (mod year 19))
+ (b (mod year 4))
+ (c (mod year 7))
+ (d (mod (+ (* 19 a) m) 30))
+ (e (mod (+ (* 2 b) (* 4 c) (* 6 d) n) 7))
+ (f (+ 22 d e)))
+ (when (= 57 f)
+ (setq f 50))
+ (when (and (= 28 d) (= 6 e) (< 10 a))
+ (setq f 49))
+ (values year
+ (if (<= f 31)
+ 3
+ (progn (decf f 31) 4))
+ f)))
+
+;; http://seed7.sourceforge.net/algorith/date.htm
+(defun leap-year-p (year)
+ (or (and (zerop (mod year 4))
+ (not (zerop (mod year 100))))
+ (zerop (mod year 400))))
+
+(defun days-in-month (year month)
+ (if (member month '(1 3 5 7 8 10 12))
+ 31
+ (if (= 2 month)
+ (if (leap-year-p year) 29 28)
+ 30)))
+
+(defun day-of-year (year month day)
+ (+ day (svref (if (leap-year-p year)
+ #(0 31 60 91 121 152 182 213 244 274 305 335)
+ #(0 31 59 90 120 151 181 212 243 273 304 334))
+ (1- month))))
+
+(defun day-of-week (year month day)
+ (when (<= month 2)
+ (decf year)
+ (incf month 12))
+ (1+ (mod (+ year
+ (floor year 4)
+ (- (floor year 100))
+ (floor year 400)
+ (floor (* 31 (- month 2)) 12)
+ day
+ -1)
+ 7)))
+
+(defun week-of-year (year day-of-year)
+ (1+ (floor (+ day-of-year (day-of-week year 1 4) -5) 7)))
+
+(defun weekend (day)
+ (member day '(5 6)))
+
+(defun collect (n stream)
+ (loop
+ for i from 0 below n
+ collect (funcall stream)))
+
+(defun day-generator (year month first-weekday)
+ (let ((d (- first-weekday (day-of-week year month 1) -1))
+ (n (days-in-month year month)))
+ (lambda ()
+ (when (<= 1 (incf d) n)
+ d))))
+
+;;(collect 40 (day-generator 2012 7 0))
+;;(collect 40 (day-generator 2012 7 6))
+
+(defun weekday-generator (first-weekday)
+ (let ((x (nthcdr first-weekday '#1=(6 0 1 2 3 4 5 . #1#))))
+ (lambda ()
+ (car (setq x (cdr x))))))
+
+;;(collect 10 (weekday-generator 0))
+;;(collect 10 (weekday-generator 6))
+
+(defun week-generator (year month)
+ (let ((w (week-of-year year (day-of-year year month 1)))
+ (n (1+ (week-of-year year (day-of-year year 12 31)))))
+ (lambda ()
+ (when (<= 1 (incf w) n)
+ w))))
+
+;;(collect 15 (week-generator 2012 1))
+;;(collect 15 (week-generator 2012 12))
diff --git a/cl-rw.asd b/cl-rw.asd
@@ -42,4 +42,6 @@
(:file "email")
(:file "os")
(:file "net")
- (:file "concurrency")))
+ (:file "concurrency")
+ (:file "calendar")
+ (:file "ui")))
diff --git a/counter.lisp b/counter.lisp
@@ -0,0 +1,53 @@
+(defpackage :jara2wad4cl.counter
+ (:use :cl))
+
+(in-package :jara2wad4cl.counter)
+
+(defun counter-widget (i rvar)
+ (jara2wad4cl:slet ((n 0 rvar))
+ (lambda ()
+ `(:p ,i ": "
+ " " ,(jara2wad4cl:link "up" (lambda () (incf n)))
+ " " ,(jara2wad4cl:link "down" (lambda () (decf n)))
+ " " (:b ,n)))))
+
+(defun toplevel-widget ()
+ (let ((w (mapcar 'counter-widget '(1 2 3 4) '(i j x y)))
+ (w2 (jara2wad4cl::calendar-widget 2012 7)))
+ (lambda ()
+ `(:html
+ (:head
+ ((:meta :http-equiv "content-type"
+ :content "text/html;charset=utf-8"))
+ ((:meta :http-equiv "cache-control" :content "no-cache,no-store"))
+ ((:meta :http-equiv "pragma" :content "no-cache"))
+ ((:meta :http-equiv "expires" :content -1))
+ (:title "counter"))
+ (:body ,@(mapcar #'funcall w) ,(funcall w2))))))
+
+(defun deconstruct ()
+ ;;(print (hunchentoot:script-name*)) => parse REST rvars
+ (values (hunchentoot:get-parameter "s")
+ (hunchentoot:get-parameter "a")
+ (list 'i (hunchentoot:get-parameter "i")
+ 'j (hunchentoot:get-parameter "j")
+ 'x (hunchentoot:get-parameter "x")
+ 'y (hunchentoot:get-parameter "y"))))
+
+(defun construct (sid aid renv)
+ (let ((prefix "/counter/"))
+ (with-output-to-string (s)
+ (format s "~a?s=~a&a=~a" prefix sid aid)
+ (loop
+ for (k v) on renv by #'cddr
+ when v
+ do (format s "&~(~a~)=~a" k v)))))
+
+(hunchentoot:define-easy-handler (counter :uri "/counter/") ()
+ (jara2wad4cl:draw (lambda () (toplevel-widget)) 'deconstruct 'construct))
+
+;;http://ondoc.logand.com/d/1129/1/
+;;/d/1129/1/ => doc 1129 pg 1
+;;/product/show/1
+;;/people/new
+;;/people/1/edit
diff --git a/ui.lisp b/ui.lisp
@@ -0,0 +1,711 @@
+(defpackage :rw.ui
+ (:use :cl)
+ (:export :checkbox
+ :choice-widget
+ :combo-item1-widget
+ :combo-item2-widget
+ :combo-widget
+ :dialog-widget
+ :draw
+ :entry
+ :file
+ :form
+ :hbox-widget
+ :link
+ :password
+ :popup-widget
+ :radio
+ :reset
+ :slet
+ :spin
+ :submit
+ :text
+ :vbox-widget
+ :visible-widget))
+
+(in-package :rw.ui)
+
+(defun style (form)
+ (loop
+ for (k v) on form by #'cddr
+ for i from 0
+ when v
+ do (flet ((out (x)
+ (typecase x
+ (symbol (format t "~(~a~)" x))
+ (t (format t "~a" x)))))
+ (when (plusp i)
+ (write-char #\;))
+ (out k)
+ (write-char #\:)
+ (out v))))
+
+;;(style '(:one 1 :two 2 :three nil :four :hello))
+
+(defun css (form)
+ (dolist (x form)
+ (let ((style (with-output-to-string (*standard-output*) (style (cdr x)))))
+ (when style
+ (flet ((out (x)
+ (typecase x
+ (symbol (format t "~(~a~)" x))
+ (t (format t "~a" x)))))
+ (out (car x))
+ (write-char #\{)
+ (write-string style)
+ (write-char #\}))))))
+
+;;(css '((:pre :one 1 :two 2 :three nil :four :hello)))
+
+(defun html (form)
+ (labels ((esc (x)
+ (loop
+ for c across x
+ do (case c
+ (#\& (write-string "&"))
+ (#\< (write-string "<"))
+ (#\> (write-string ">"))
+ (t (write-char c)))))
+ (name (x)
+ (esc (etypecase x
+ (number (format nil "~a" x))
+ (string x)
+ (symbol (format nil "~(~a~)" x)))))
+ (attribute (k v)
+ (unless (or (not v)
+ (and (consp v)
+ (eq :style (car v))
+ (not (cdr v))))
+ (write-char #\space)
+ (name k)
+ (write-char #\=)
+ (write-char #\")
+ (loop
+ for c across (etypecase v
+ (string v)
+ (number (format nil "~a" v))
+ (symbol (format nil "~(~a~)" v))
+ (cons
+ (ecase (car v)
+ (:style
+ (with-output-to-string (*standard-output*)
+ (style (cdr v)))))))
+ do (case c
+ (#\& (write-string "&"))
+ (#\" (write-string """))
+ (t (write-char c))))
+ (write-char #\")))
+ (element (e a b)
+ (case e
+ (:<style
+ (element :style a
+ (list
+ (with-output-to-string (*standard-output*)
+ (css b)))))
+ (t
+ (write-char #\<)
+ (name e)
+ (loop for (k v) on a by #'cddr do (attribute k v))
+ (when b (write-char #\>))
+ (mapc #'rec b)
+ (when b (write-char #\<))
+ (write-char #\/)
+ (when b (name e))
+ (write-char #\>))))
+ (rec (x)
+ (if (atom x)
+ (when x (name x))
+ (destructuring-bind (y &rest z) x
+ (if (atom y)
+ (element y nil z)
+ (element (car y) (cdr y) z))))))
+ (princ "<!DOCTYPE html>")
+ (terpri)
+ (rec form)))
+
+(defvar *click-link*)
+(defvar *click-form*)
+
+(defun parse-nat0 (x)
+ (when (and x (not (equal "" x)) (every #'digit-char-p x))
+ (parse-integer x)))
+
+(defun parse-ismap-value (x)
+ (when (and x (not (equal "" x)) (char= #\? (char x 0)))
+ (let ((i (position #\, x)))
+ (when (plusp i)
+ (values (parse-nat0 (subseq x 1 i))
+ (parse-nat0 (subseq x (1+ i))))))))
+
+(defun html-reply (form)
+ (setf (hunchentoot:content-type*) "text/html;charset=utf-8"
+ (hunchentoot:header-out "cache-control") "no-cache, no-store"
+ (hunchentoot:header-out "pragma") "no-cache"
+ (hunchentoot:header-out "expires") "-1")
+ (with-output-to-string (*standard-output*)
+ (html form)))
+
+(defvar *register*)
+
+(defun make-state (create)
+ (let (svars svals)
+ (flet ((store (actions)
+ (let ((env (mapcar (lambda (k) (funcall (car k))) svars)))
+ (when actions
+ (push (cons env actions) svals)))))
+ (values
+ (let ((*register* (lambda (get set)
+ (push (cons get set) svars))))
+ (prog1 (funcall create)
+ (store (list 0 (lambda ())))))
+ (lambda (aid actions2 fn)
+ (let ((cached (find-if (lambda (x) (getf (cdr x) aid)) svals)))
+ ;;(print (list :@@@======== aid cached))
+ (if cached
+ (destructuring-bind (env1 &rest actions1) cached
+ (mapc (lambda (k v) (funcall (cdr k) v)) svars env1)
+ ;;(print (list :@@@-a env1 svals))
+ (unwind-protect
+ (funcall fn
+ (lambda (k p)
+ (let ((v (getf actions1 k)))
+ (print (list :@@@ k p v))
+ (when v
+ (if p
+ (funcall v p)
+ (funcall v)))))
+ (lambda ()
+ (unless env1
+ (setq svals (delete cached svals)))))
+ (store (funcall actions2))
+ #+nil(print (list :@@@-z svals))))
+ ;; TODO indicate unexpected aid when not cached?
+ (funcall fn
+ (lambda (k p) (declare (ignore k p)))
+ (lambda ())))))))))
+
+(defmacro with-state ((state aid actions2 dispatch clear) &body body)
+ `(funcall ,state ,aid ,actions2 (lambda (,dispatch ,clear) ,@body)))
+
+(defvar *renv*)
+
+(defun handle-form (form)
+ (ecase (car form)
+ (:redirect
+ (destructuring-bind (target) (cdr form)
+ (hunchentoot:redirect target)))
+ (:html (html-reply form))))
+
+(defun make-stepper (sid create construct)
+ (let ((n 0))
+ (multiple-value-bind (draw state) (make-state create)
+ (lambda (aid)
+ (let (actions2)
+ (with-state (state aid (lambda () actions2) dispatch clear)
+ ;;(print (list :@@@ (hunchentoot:query-string*)))
+ (handle-form
+ (ecase (hunchentoot:request-method*)
+ (:post
+ (dolist (x (hunchentoot:post-parameters*))
+ (destructuring-bind (k &rest v) x
+ (let ((kk (when (char= #\z (char k 0))
+ (parse36 (subseq k 1)))))
+ (funcall dispatch kk v))))
+ (funcall dispatch aid nil)
+ `(:redirect ,(funcall construct sid (pretty36 aid) *renv*)))
+ (:get
+ (funcall dispatch aid nil)
+ (funcall clear)
+ (flet ((next (v)
+ (let ((k (incf n)))
+ (push v actions2)
+ (push k actions2)
+ k)))
+ (let* ((*click-link*
+ (lambda (click &optional idempotent)
+ ;; TODO let rvars, "let explicit svars",
+ ;; funcall click idempotent in regards to
+ ;; implicit svars
+ (let ((*renv* (copy-list *renv*)))
+ ;;(funcall idempotent) TODO !!!!!!!!!!!!!!!!!
+ (funcall construct sid (pretty36 (next click))
+ *renv*))))
+ (*click-form*
+ (lambda (set)
+ (format nil "z~a" (pretty36 (next set))))))
+ (funcall draw))))))))))))
+
+(defun register (get set)
+ (funcall *register* get set))
+
+(defmacro slet (vars &body body) ;; TODO renv
+ `(let ,(mapcar (lambda (x) (subseq x 0 2)) vars)
+ ,@(mapcar (lambda (x)
+ `(register (lambda () ,(car x))
+ (lambda (v) (setq ,(car x) v))))
+ vars)
+ ,@body))
+
+(defparameter *session-lifespan* (* 60 60))
+
+(defun make-session (sid create construct)
+ (let ((lock (bt:make-lock "session ~s"))
+ (touched (get-universal-time))
+ (stepper (make-stepper sid create construct)))
+ (lambda (aid)
+ (bt:with-lock-held (lock)
+ (cond
+ ((eq t aid)
+ (< (- (get-universal-time) touched) *session-lifespan*))
+ (t
+ (setq touched (get-universal-time))
+ (funcall stepper aid)))))))
+
+(defun rd (cnt)
+ (let ((s *standard-input*))
+ (do ((n 0 (1+ n))
+ (z (read-byte s) (+ (* 256 z) (read-byte s))))
+ ((< cnt n) z))))
+
+(defun pretty36 (x)
+ (when (and (integerp x) (<= 0 x))
+ (let ((*print-base* 36))
+ (format nil "~(~a~)" x))))
+
+(defun parse36 (x)
+ (flet ((base36 (x)
+ (find x "0123456789abcdefghijklmnopqrstuvwxyz")))
+ (when (and x (not (equal "" x)) (every #'base36 x))
+ (parse-integer x :radix 36))))
+
+;;(parse36 (pretty36 123456789))
+;;(parse36 (pretty36 37))
+;;(parse36 (pretty36 109))
+
+(defun generate-sid ()
+ ;; (< (expt 36 12) (expt 2 64) (expt 36 13))
+ (pretty36
+ #+nil(random #.(expt 36 13))
+ (with-open-file (*standard-input* "/dev/urandom"
+ :element-type '(unsigned-byte 8))
+ (rd 4))))
+
+(defun make-pool ()
+ (let ((sessions (make-hash-table :test #'equal))
+ (lock (bt:make-lock "pool ~s")))
+ (lambda (create deconstruct construct)
+ (multiple-value-bind (sid aid *renv*) (funcall deconstruct)
+ (setq aid (parse36 aid))
+ (funcall
+ (bt:with-lock-held (lock)
+ (maphash (lambda (k v)
+ (unless (funcall v t)
+ (remhash k sessions)))
+ sessions)
+ (let ((x (and sid aid (gethash sid sessions))))
+ (if x
+ (lambda () (funcall x aid))
+ (do ()
+ ((not (gethash (setq sid (generate-sid)) sessions))
+ (setf (gethash sid sessions)
+ (make-session sid create construct))
+ (lambda ()
+ (hunchentoot:redirect
+ (funcall construct sid (pretty36 0) *renv*)))))))))))))
+
+(defparameter *pool* (make-pool))
+
+(defun draw (create deconstruct construct)
+ (funcall *pool* create deconstruct construct))
+
+(defun link (draw click &key style (enabled t))
+ (flet ((%draw ()
+ (if (functionp draw) (funcall draw) draw)))
+ (if enabled
+ `((:a :href ,(funcall *click-link* click) :style ,style) ,(%draw))
+ `((:span :style "color:gray") ,(%draw)))))
+
+(defun input (set type value enabled editable style size maxlength)
+ `((:input
+ :name ,(when (and set enabled editable) (funcall *click-form* set))
+ :type ,type
+ :value ,value
+ :disabled ,(unless enabled :disabled)
+ :readonly ,(unless editable :readonly)
+ :style ,style
+ :size ,size
+ :maxlength ,maxlength)))
+
+(defun submit (label set &key (enabled t) style)
+ (input set "submit" label enabled t style nil nil))
+
+(defun yes () t)
+(defun no ())
+(defun no1 (x) (declare (ignore x)))
+
+(defun reset (label &key (enabled t) style)
+ (input 'no1 "reset" label enabled t style nil nil))
+
+(defun password (set &key (enabled t) (editable t) style)
+ (input set "password" nil enabled editable style nil nil))
+
+(defun entry (set text &key (enabled t) (editable t) style size maxlength)
+ (input set "text" text enabled editable style size maxlength))
+
+(defun entry (set text &key (enabled t) (editable t) style size maxlength)
+ (input set "text" text enabled editable style size maxlength))
+
+(defun file (set &key (enabled t) style size)
+ (input set "file" nil enabled t style size nil))
+
+(defun text (set text nrows ncols &key (enabled t) (editable t) style)
+ `((:textarea
+ :name ,(funcall *click-form* set)
+ :rows ,nrows
+ :cols ,ncols
+ :disabled ,(unless enabled :disabled)
+ :readonly ,(unless editable :readonly)
+ :style ,style)
+ ,text))
+
+(defun form (draw)
+ `((:form
+ :action ,(funcall *click-link* 'no)
+ :method "post"
+ :enctype "multipart/form-data"
+ :style "padding:0;margin:0;border:0")
+ ((:div :style "width:0;height:0;overflow:hidden") ,(submit nil 'no1))
+ ,(if (functionp draw) (funcall draw) draw)))
+
+(defun visible-widget (show draw)
+ (lambda ()
+ (when (funcall show)
+ (funcall draw))))
+
+(defun popup-widget (show draw)
+ (visible-widget
+ show
+ (lambda ()
+ `((:div :style (:style
+ :position :absolute
+ :background-color :white
+ :border "solid 1px"
+ :padding "0.5em"))
+ ((:div :style (:style :position :relative))
+ ,(funcall draw))))))
+
+(defun screen-direction-combo-widget (popup selected click align)
+ (combo-widget
+ popup
+ selected
+ click
+ (list 'no 'no 'no 'no 'no 'no 'no 'no 'no)
+ (flet ((item2 (label &rest %align)
+ (combo-item2-widget label (apply align %align))))
+ (list
+ (item2 "Top Left" :top :left)
+ (item2 "Top Center" :top :center)
+ (item2 "Top Right" :top :right)
+ (item2 "Center Left" :center :left)
+ (item2 "Center" :center :center)
+ (item2 "Center Right" :center :right)
+ (item2 "Bottom Left" :bottom :left)
+ (item2 "Bottom Center" :bottom :center)
+ (item2 "Bottom Right" :bottom :right)))))
+
+(defun screen-direction-graphics-widget (popup selected click align) ;; TODO selected
+ (dropdown-widget
+ click
+ (lambda ())
+ popup
+ (let ((n (flet ((item2 (label &rest %align) ;; TODO label as hint
+ (apply align %align)))
+ (list
+ (item2 "Top Left" :top :left)
+ (item2 "Top Center" :top :center)
+ (item2 "Top Right" :top :right)
+ (item2 "Center Left" :center :left)
+ (item2 "Center" :center :center)
+ (item2 "Center Right" :center :right)
+ (item2 "Bottom Left" :bottom :left)
+ (item2 "Bottom Center" :bottom :center)
+ (item2 "Bottom Right" :bottom :right)))))
+ (lambda ()
+ `(:pre
+ ,@(loop
+ with y = n
+ ;;with s = (funcall selected)
+ for i from 0
+ for x across "X | X | X
+--+---+--
+X | X | X
+--+---+--
+X | X | X"
+ collect (cond
+ ;; ((= s i)
+ ;; `(:b (string x)))
+ ((char= #\X x)
+ (jara2wad4cl:link (string x)
+ (let ((z (pop y)))
+ (lambda () (funcall z)))))
+ (t (string x)))))))))
+
+(defun dialog-widget (show close draw1 draw2 &optional draw3)
+ (visible-widget
+ show
+ (let* ((halign :center)
+ (valign :center)
+ popup
+ (cw (flet ((align (v h)
+ (lambda ()
+ (setq popup nil
+ halign h
+ valign v))))
+ (screen-direction-graphics-widget ;;screen-direction-combo-widget
+ (lambda () popup)
+ (lambda () 4)
+ (lambda () (setq popup (not popup)))
+ #'align))))
+ (lambda ()
+ (let (#+nil(id1 (funcall *click-form* 'no1))
+ #+nil(id2 (funcall *click-form* 'no1)))
+ `((:table :style (:style :position :fixed ;;:absolute
+ :left 0
+ :top 0
+ :width "100%"
+ :height "100%"
+ :padding 0
+ :margin 0
+ :border 0
+ :pointer-events "none"))
+ (:tr
+ ((:td :align ,halign :valign ,valign)
+ ((:table :style (:style :pointer-events "auto"
+ :border "solid 1px"
+ :background-color "#eef"))
+ ((:tr :style (:style :background-color "#dde" :padding "0.3em"))
+ (:td ,(funcall cw)
+ ,(funcall draw1)
+ ((:span :style "font-family:monospace")
+ " "
+ ,(when close (link "X" close))))
+ #+nil
+ (:td ((:span :style "font-family:monospace")
+ ,(link "X" close)
+ " ")
+ ,(funcall draw1)
+ ((:span :style "font-family:monospace")
+ " "
+ ,(link "L" (lambda () (setq halign :left)))
+ ,(link "C" (lambda () (setq halign :center)))
+ ,(link "R" (lambda () (setq halign :right)))
+ "-"
+ ,(link "T" (lambda () (setq valign :top)))
+ ,(link "C" (lambda () (setq valign :center)))
+ ,(link "B" (lambda () (setq valign :bottom))))))
+ (:tr (:td ,(funcall draw2)))
+ , (when draw3
+ `(:tr (:td ,(funcall draw3))))))))))))
+ #+nil
+ (visible-widget
+ show
+ (lambda ()
+ (let (#+nil(id1 (funcall *click-form* 'no1))
+ #+nil(id2 (funcall *click-form* 'no1)))
+ `((:div :style (:style :position :absolute :left 0 :top 0))
+ ((:div :style (:style :position :relative))
+ ((:div
+ ;;:id ,id1
+ :style (:style
+ ;;:position "absolute"
+ :border "solid 1px"
+ :background-color "#eef"))
+ ((:div
+ ;;:id ,id2
+ :style (:style :background-color "#dde" :padding "0.3em"))
+ ,(funcall draw1))
+ ((:div :style (:style :padding "0.5em")) ,(funcall draw2))
+ , (when draw3
+ `((:div :style (:style :padding "0.5em"))
+ ,(funcall draw3))))
+ #+nil
+ ((:script :type "text/javascript")
+ "draggable(w('" ,id1 "'),w('" ,id2 "'),dragDialog);")))))))
+
+(defun combo-item1-widget (label &key style)
+ (lambda ()
+ (if style
+ `((:span :style (:style :padding "0.1em" ,@style)) ,label)
+ label)))
+
+(defun combo-item2-widget (label click &key style1 style2)
+ (lambda ()
+ `((:td :style (:style ,@style1))
+ ,(link label click :style `(:style ,@style2)))
+ #+nil
+ `((:li :style (:style :padding "0.1em" ,@style1))
+ ,(link label click :style `(:style ,@style2)))))
+
+(defun dropdown-widget (click label popup draw)
+ (let ((pw (popup-widget popup draw)))
+ (lambda ()
+ `(:span ,(link "^" click) " " ,(funcall label) ,(funcall pw)))))
+
+(defun combo-widget (popup selected click items1 items2 &optional (ncols 1))
+ (dropdown-widget
+ click
+ (lambda () (funcall (nth (funcall selected) items1)))
+ popup
+ (lambda ()
+ `(:table
+ ,@ (ecase ncols
+ (1 (loop
+ for x in items2
+ collect `(:tr ,(funcall x))))
+ (2 (loop
+ for (x y) on items2 by #'cddr
+ collect `(:tr ,(funcall x) ,(funcall y))))))
+ #+nil
+ `((:ul :style (:style
+ :top 0
+ :left 0
+ :list-style :none
+ :margin 0
+ :padding 0))
+ ,@(mapcar #'funcall items2)))))
+
+(defun checkbox (selected click)
+ `((:span :style (:style :font-family :monospace))
+ "["
+ ,(link (lambda () (if (funcall selected) "X" "-")) click)
+ "]"))
+
+(defun radio (selected click)
+ `((:span :style (:style :font-family :monospace))
+ "("
+ ,(link (lambda () (if (funcall selected) "o" "-")) click)
+ ")"))
+
+(defun spin (get set min max &key enabled)
+ (let ((x (funcall get)))
+ #+nil
+ `((:input
+ :name ,(funcall *click-form* set)
+ :type "number"
+ :min ,min
+ :max ,max
+ :value ,x
+ ;;:disabled ,(unless enabled :disabled)
+ ;;:readonly ,(unless editable :readonly)
+ ;;:style ,style
+ ;;:size ,size
+ ;;:maxlength ,maxlength
+ ))
+ `(:span
+ , (let ((n (max (length (princ-to-string min))
+ (length (princ-to-string max)))))
+ (entry (lambda (x)
+ (let ((y (parse-nat0 x)))
+ (when y
+ (funcall set (max min (min max y))))))
+ x
+ :enabled enabled
+ :style "text-align:right"
+ :size n
+ :maxlength n))
+ " "
+ ,(if enabled
+ (link "▼" (lambda () (funcall set (max min (1- x)))))
+ "▼")
+ " "
+ ,(if enabled
+ (link "▲" (lambda () (funcall set (min max (1+ x)))))
+ "▲"))))
+
+(defun hbox-widget (children &optional separator)
+ (if separator
+ (lambda ()
+ `(:span
+ ,@(loop
+ for x in children
+ for i from 0
+ appending (if (plusp i)
+ (list (if (eq t separator) " " separator)
+ (funcall x))
+ (list (funcall x))))))
+ (lambda ()
+ `(:table ;;(:table :border 0 :cellpadding 0 :cellspacing 0)
+ (:tr ,@(mapcar (lambda (x) `(:td ,(funcall x))) children))))))
+
+(defun vbox-widget (children &optional div)
+ (if div
+ (lambda ()
+ `(:div
+ ,@(mapcar (lambda (x) `(:div ,(funcall x))) children)))
+ (lambda ()
+ `(:table ;;(:table :border 0 :cellpadding 0 :cellspacing 0)
+ ,@(mapcar (lambda (x) `(:tr (:td ,(funcall x)))) children)))))
+
+(defun choice-widget (selected click choices &optional horizontal)
+ (let* ((radios (loop
+ for c in choices
+ for i from 0
+ collect (let ((i i))
+ (lambda ()
+ (radio
+ (lambda () (= i (funcall selected)))
+ (lambda () (funcall click i)))))))
+ (children (mapcar (lambda (r c) (hbox-widget (list r c) t))
+ radios
+ choices)))
+ (if horizontal
+ (hbox-widget children)
+ (vbox-widget children))))
+
+(defun calendar-widget (year month &key (first-weekday 0) (show-weeks t))
+ (lambda ()
+ (let ((weeks (when show-weeks (week-generator year month))))
+ `((:table :style "font-family:monospace")
+ (:tr
+ ,@(when weeks '((:td "")))
+ ((:td :colspan 3 :align "center")
+ , (let ((y year)
+ (m (1- month)))
+ (when (< m 1)
+ (decf y)
+ (setq m 12))
+ (link "<" (lambda () (setq year y month m))))
+ " " ,(pretty-month month) " "
+ , (let ((y year)
+ (m (1+ month)))
+ (when (< 12 m)
+ (incf y)
+ (setq m 1))
+ (link ">" (lambda () (setq year y month m)))))
+ ((:td :align "center") ,(link "@" (lambda ())))
+ ((:td :colspan 3 :align "center")
+ , (let ((y (1- year)))
+ (link "<" (lambda () (setq year y))))
+ " " ,year " "
+ , (let ((y (1+ year)))
+ (link ">" (lambda () (setq year y))))))
+ (:tr
+ ,@(when weeks '((:td " ")))
+ ,@(loop
+ with g = (weekday-generator first-weekday)
+ for i from 0 below 7
+ for n = (funcall g)
+ collect `((:td :style (:style :color ,(when (weekend n) "red")))
+ ,(pretty-day n))))
+ ,@(loop
+ with g = (day-generator year month first-weekday)
+ for i from 0 below 6
+ collect `(:tr
+ ,@(when weeks `(((:td :align "right") ,(funcall weeks))))
+ ,@(loop
+ for j from 0 below 7
+ for d = (funcall g)
+ collect `((:td :align "right")
+ ,(if d
+ (link d (lambda ()))
+ "")))))))))