clog/clog.lisp
2020-12-21 23:34:39 -05:00

456 lines
13 KiB
Common Lisp

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; CLOG - The Common Lisp Omnificent GUI ;;;;
;;;; (c) 2020-2021 David Botton ;;;;
;;;; License BSD 3 Clause ;;;;
;;;; ;;;;
;;;; clog.lisp ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports - clog
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mgl-pax:define-package :clog
(:documentation "The Common List Omnificent GUI - Parent package")
(:local-nicknames (:cc :clog-connection))
(:use #:cl #:mgl-pax))
(in-package :clog)
(defsection @clog-manual (:title "The CLOG manual")
"The Common Lisp Omnificient GUI, CLOG for short, uses web technology
to produce graphical user interfaces for applications locally or
remotely. The CLOG package starts up the connectivity to the browser
or other websocket client (often a browser embedded in a native
application."
(clog asdf:system)
(@clog-system section)
(@clog-objs section))
(defsection @clog-system (:title "CLOG System")
"CLOG Startup and Shutdown"
(initialize function)
(shutdown function)
"CLOG Low Level binding functions"
(attach function)
(create-with-html function)
"CLOG utilities"
(open-browser function))
(defsection @clog-objs (:title "CLOG Objects")
"CLOG-Obj"
(clog-obj class)
"CLOG-Obj - General Properties"
(property generic-function)
(style generic-function)
(attribute generic-function)
(height generic-function)
(width generic-function)
"CLOG-Obj - General Methods"
(focus generic-function)
(blur generic-function)
"CLOG-Obj - Placement"
(place-after generic-function)
(place-before generic-function)
(place-inside-top-of generic-function)
(place-inside-bottom-of generic-function)
"CLOG-Obj - Low Level"
(create-child generic-function)
(attach-as-child generic-function)
(connection-data generic-function)
(validp generic-function)
"CLOG-Obj - Event Handling"
(set-on-click generic-function))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - clog-obj
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defclass clog-obj ()
((connection-id
:reader connection-id
:initarg :connection-id)
(html-id
:reader html-id
:initarg :html-id)
(event-handlers
:accessor event-handlers
:initform (make-hash-table :test #'equal)))
(:documentation "CLOG objects (clog-obj) encapsulate the connection between
lisp and the HTML DOM element."))
(defgeneric connection-id (clog-obj)
(:documentation "Reader for connection-id slot. (Private)"))
(defgeneric html-id (clog-obj)
(:documentation "Reader for html-id slot. (Private)"))
(defgeneric event-handlers (clog-obj)
(:documentation "Reader/writer for even-handler hash. (Private)"))
;;;;;;;;;;;;;;;;;;;
;; make-clog-obj ;;
;;;;;;;;;;;;;;;;;;;
(defun make-clog-obj (connection-id html-id)
"Construct a new clog-obj. (Private)"
(make-instance 'clog-obj :connection-id connection-id
:html-id html-id))
;;;;;;;;;;;;;;;
;; script-id ;;
;;;;;;;;;;;;;;;
(defgeneric script-id (clog-obj)
(:documentation "Return the script id for OBJ based on the html-id set
during attachment. (Private)"))
(defmethod script-id ((obj clog-obj))
(if (eql (html-id obj) 0)
"'body'"
(format nil "clog['~A']" (html-id obj))))
;;;;;;;;;;;;
;; jquery ;;
;;;;;;;;;;;;
(defgeneric jquery (clog-obj)
(:documentation "Return the jquery accessor for OBJ. (Private)"))
(defmethod jquery ((obj clog-obj))
(format nil "$(~A)" (script-id obj)))
;;;;;;;;;;;;;;;;;;;;
;; jquery-execute ;;
;;;;;;;;;;;;;;;;;;;;
(defgeneric jquery-execute (clog-obj method)
(:documentation "Execute the jquery METHOD on OBJ. Result is
dicarded. (Private)"))
(defmethod jquery-execute ((obj clog-obj) method)
(cc:execute (connection-id obj)
(format nil "~A.~A" (jquery obj) method)))
;;;;;;;;;;;;;;;;;;
;; jquery-query ;;
;;;;;;;;;;;;;;;;;;
(defgeneric jquery-query (clog-obj method)
(:documentation "Execute the jquery METHOD on OBJ and return
result. (Private)"))
(defmethod jquery-query ((obj clog-obj) method)
(cc:query (connection-id obj)
(format nil "~A.~A" (jquery obj) method)))
;;;;;;;;;;;;;;
;; property ;;
;;;;;;;;;;;;;;
(defgeneric property (clog-obj property-name)
(:documentation "Get/Setf html property. (eg. draggable)"))
(defmethod property ((obj clog-obj) property-name)
(jquery-query obj (format nil "prop('~A')" property-name)))
(defgeneric set-property (clog-obj property-name value)
(:documentation "Set html property."))
(defmethod set-property ((obj clog-obj) property-name value)
(jquery-execute obj (format nil "prop('~A','~A')" property-name value)))
(defsetf property set-property)
;;;;;;;;;;;
;; style ;;
;;;;;;;;;;;
(defgeneric style (clog-obj style-name)
(:documentation "Get/Setf css style."))
(defmethod style ((obj clog-obj) style-name)
(jquery-query obj (format nil "css('~A')" style-name)))
(defgeneric set-style (clog-obj style-name value)
(:documentation "Set css style."))
(defmethod set-style ((obj clog-obj) style-name value)
(jquery-execute obj (format nil "css('~A','~A')" style-name value)))
(defsetf style set-style)
;;;;;;;;;;;;;;;
;; attribute ;;
;;;;;;;;;;;;;;;
(defgeneric attribute (clog-obj attribute-name)
(:documentation "Get/Setf html tag attribute. (eg. src on img tag)"))
(defmethod attribute ((obj clog-obj) attribute-name)
(jquery-query obj (format nil "attr('~A')" attribute-name)))
(defgeneric set-attribute (clog-obj attribute-name value)
(:documentation "Set html tag attribute."))
(defmethod set-attribute ((obj clog-obj) attribute-name value)
(jquery-execute obj (format nil "attr('~A','~A')" attribute-name value)))
(defsetf attribute set-attribute)
;;;;;;;;;;;;
;; height ;;
;;;;;;;;;;;;
(defgeneric height (clog-obj)
(:documentation "Get/Setf html height in pixels."))
(defmethod height ((obj clog-obj))
(jquery-query obj "height()"))
(defgeneric set-height (clog-obj value)
(:documentation "Set height VALUE for CLOG-OBJ"))
(defmethod set-height ((obj clog-obj) value)
(jquery-execute obj (format nil "height('~A')" value)))
(defsetf height set-height)
;;;;;;;;;;;
;; width ;;
;;;;;;;;;;;
(defgeneric width (clog-obj)
(:documentation "Get/Setf html width in pixels."))
(defmethod width ((obj clog-obj))
(jquery-query obj "width()"))
(defgeneric set-width (clog-obj value)
(:documentation "Set width VALUE for CLOG-OBJ"))
(defmethod set-width ((obj clog-obj) value)
(jquery-execute obj (format nil "width('~A')" value)))
(defsetf width set-width)
;;;;;;;;;;;
;; focus ;;
;;;;;;;;;;;
(defgeneric focus (clog-obj)
(:documentation "Focus on CLOG-OBJ"))
(defmethod focus ((obj clog-obj))
(jquery-execute obj "focus()"))
;;;;;;;;;;
;; blur ;;
;;;;;;;;;;
(defgeneric blur (clog-obj)
(:documentation "Remove focus from CLOG-OBJ"))
(defmethod focus ((obj clog-obj))
(jquery-execute "blur()"))
;;;;;;;;;;;;;;;;;;
;; create-child ;;
;;;;;;;;;;;;;;;;;;
(defgeneric create-child (clog-obj html &key auto-place)
(:documentation "Create a new CLOG-OBJ from HTML element as child of OBJ and if :AUTO-PLACE (default t)
place-inside-bottom-of OBJ"))
(defmethod create-child ((obj clog-obj) html &key (auto-place t))
(let ((child (create-with-html (connection-id obj) html)))
(if auto-place
(place-inside-bottom-of obj child)
child)))
;;;;;;;;;;;;;;;;;;;;;
;; attach-as-child ;;
;;;;;;;;;;;;;;;;;;;;;
(defgeneric attach-as-child (clog-obj html-id)
(:documentation "Create a new CLOG-OBJ and attach an existing element with HTML-ID. The
HTML-ID must be unique."))
(defmethod attach-as-child ((obj clog-obj) html-id)
(cc:execute (connection-id obj) (format nil "clog['~A']=$('#~A')" html-id html-id))
(make-clog-obj (connection-id obj) html-id))
;;;;;;;;;;;;
;; validp ;;
;;;;;;;;;;;;
(defgeneric validp (clog-obj)
(:documentation "Returns true of connection is valid on this CLOG-OBJ."))
(defmethod validp ((obj clog-obj))
(cc:validp (connection-id obj)))
;;;;;;;;;;;;;;;;;;;;;
;; connection-data ;;
;;;;;;;;;;;;;;;;;;;;;
(defgeneric connection-data (clog-obj)
(:documentation "Get connection-data that is associated with
clog-obj that will persist regardless of thread."))
(defmethod connection-data ((obj clog-obj))
(cc:get-connection-data (connection-id obj)))
;;;;;;;;;;;;;;;;;
;; place-after ;;
;;;;;;;;;;;;;;;;;
(defgeneric place-after (clog-obj next-obj)
(:documentation "Places NEXT-OBJ after CLOG-OBJ in DOM"))
(defmethod place-after ((obj clog-obj) next-obj)
(jquery-execute obj (format nil "after(~A)" (script-id next-obj)))
next-obj)
;;;;;;;;;;;;;;;;;;
;; place-before ;;
;;;;;;;;;;;;;;;;;;
(defgeneric place-before (clog-obj next-obj)
(:documentation "Places NEXT-OBJ before CLOG-OBJ in DOM"))
(defmethod place-before ((obj clog-obj) next-obj)
(jquery-execute obj (format nil "before(~A)" (script-id next-obj)))
next-obj)
;;;;;;;;;;;;;;;;;;;;;;;;;
;; place-inside-top-of ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric place-inside-top-of (clog-obj next-obj)
(:documentation "Places NEXT-OBJ inside top of CLOG-OBJ in DOM"))
(defmethod place-inside-top-of ((obj clog-obj) next-obj)
(jquery-execute obj (format nil "prepend(~A)" (script-id next-obj)))
next-obj)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; place-inside-bottom-of ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defgeneric place-inside-bottom-of (clog-obj next-obj)
(:documentation "Places NEXT-OBJ inside bottom of CLOG-OBJ in DOM"))
(defmethod place-inside-bottom-of ((obj clog-obj) next-obj)
(jquery-execute obj (format nil "append(~A)" (script-id next-obj)))
next-obj)
;;;;;;;;;;;;;;;;;;
;; set-on-click ;;
;;;;;;;;;;;;;;;;;;
(defmethod bind-event-script ((obj clog-obj) event call-back)
(jquery-execute
obj (format nil "on('~A',function (e, data){~A})" event call-back)))
(defmethod set-event ((obj clog-obj) event handler)
;; meeds mutex
(let ((hook (format nil "~A:~A" (html-id obj) event)))
(cond (handler
(bind-event-script obj event
(format nil "ws.send('E:~A-')" hook))
(setf (gethash hook (connection-data obj)) handler))
(t
(remhash hook (connection-data obj))))))
(defgeneric set-on-click (clog-obj on-click-handler)
(:documentation "Set the ON-CLICK-HANDLER for CLOG-OBJ. If ON-CLICK-HANDLER
is nil unbind the event."))
(defmethod set-on-click ((obj clog-obj) on-click-handler)
(set-event obj "click" on-click-handler))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - clog
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;
;; initialize ;;
;;;;;;;;;;;;;;;;
(defvar *on-new-window* nil "Store the on-new-window handler")
(defun on-connect (id)
(when cc:*verbose-output*
(format t "Start new window handler on connection-id - ~A" id))
(let ((body (make-clog-obj id 0)))
(funcall *on-new-window* body)))
(defun initialize (on-new-window
&key
(host "0.0.0.0")
(port 8080)
(boot-file "/boot.html")
(static-root #P"./static-files/"))
"Inititalze CLOG on a socket using HOST and PORT to serve BOOT-FILE as
the default route to establish web-socket connections and static files
located at STATIC-ROOT."
(setf *on-new-window* on-new-window)
(cc:initialize #'on-connect
:host host
:port port
:boot-file boot-file
:static-root static-root))
;;;;;;;;;;;;;;
;; shutdown ;;
;;;;;;;;;;;;;;
(defun shutdown ()
"Shutdown CLOG."
(cc:shutdown-clog))
;;;;;;;;;;;;
;; attach ;;
;;;;;;;;;;;;
(defun attach (connection-id html-id)
"Create a new clog-obj and attach an existing element with HTML-ID on
CONNECTION-ID to it and then return it. The HTML-ID must be unique."
(cc:execute connection-id (format nil "clog['~A']=$('#~A')" html-id html-id))
(make-clog-obj connection-id html-id))
;;;;;;;;;;;;;;;;;;;;;;
;; create-with-html ;;
;;;;;;;;;;;;;;;;;;;;;;
(defun create-with-html (connection-id html)
"Create a new clog-obj and attach it to HTML on CONNECTION-ID. There must be
a single outer block that will be set to an internal id. The returned clog-obj
requires placement or will not be visible, ie. place-after, etc"
(let ((web-id (cc:generate-id)))
(cc:execute
connection-id
(format nil "clog['~A']=$(\"~A\"); clog['~A'].first().prop('id','~A')"
web-id html web-id web-id))
(make-clog-obj connection-id web-id)))
;;;;;;;;;;;;;;;;;;
;; open-browser ;;
;;;;;;;;;;;;;;;;;;
(defun open-browser (&key (url "http://127.0.0.1:8080"))
"Open a web browser to URL."
(trivial-open-browser:open-browser url))