fennel-xl/imgui.fnl
2022-12-03 23:26:07 -05:00

391 lines
16 KiB
Fennel

(local core (require :core))
(local config (require :core.config))
(local command (require :core.command))
(local keymap (require :core.keymap))
(local style (require :core.style))
(local lume (require :plugins.fennel-xl.lume))
(fn attach-imstate [view]
(set view.imstate {})
(fn view.on_mouse_pressed [self button x y clicks]
(tset self.imstate button :pressed)
(self.__index.on_mouse_pressed self button x y clicks))
(fn view.on_mouse_released [self button x y]
(tset self.imstate button :released)
(self.__index.on_mouse_released self button x y))
(fn view.on_key_pressed [self key]
(when (= self.imstate.keys nil)
(set self.imstate.keys []))
(table.insert self.imstate.keys key))
(fn view.on_text_input [self text]
(set self.imstate.text (.. (or self.imstate.text "") text))
(self.__index.on_text_input self text))
(fn view.form [self ?overrides]
(lume.merge {:x (+ self.position.x style.padding.x (- self.scroll.x))
:y (+ self.position.y style.padding.y (- self.scroll.y))
:w (- self.size.x (* style.padding.x 2))
:view self}
(or ?overrides {})))
(fn view.end-scroll [self {: y : h}]
(let [pin-to-bottom (>= self.scroll.to.y (- self.scrollheight self.size.y))]
(set self.scrollheight (- (+ y (or h 0) style.padding.y) (+ self.position.y style.padding.y (- self.scroll.y))))
(when pin-to-bottom (set self.scroll.to.y (- self.scrollheight self.size.y)))))
(fn view.draw [self]
(set self.cursor nil)
(self.__index.draw self)
(when self.imstate.postponed
(each [_ action (ipairs self.imstate.postponed)]
(action))
(set self.imstate.postponed nil))
(when (= self.cursor nil) (set self.cursor :arrow))
(set self.imstate.keys nil)
(set self.imstate.text nil)
(when (= self.imstate.left :released)
(set self.imstate.active nil))
(each [_ button (pairs [:left :middle :right])]
(tset self.imstate button
(match (. self.imstate button)
:pressed :down
:down :down
:released nil)))))
(fn register-keys [keys]
(local commands {})
(local keymaps {})
(each [_ key (ipairs keys)]
(local command-name (.. "imstate:" key))
(tset commands command-name #(core.active_view:on_key_pressed key))
(tset keymaps key command-name))
(command.add #(not= (-?> core.active_view.imstate (. :focus)) nil) commands)
(keymap.add keymaps))
(register-keys [:backspace :delete :left :right :shift+left :shift+right :home :end :shift+home :shift+end
:ctrl+left :ctrl+right :ctrl+shift+left :ctrl+shift+right :ctrl+c :ctrl+v])
(fn cmd-predicate [p]
(var p-fn p)
(when (= (type p-fn) :string) (set p-fn (require p-fn)))
(when (= (type p-fn) :table)
(local cls p-fn)
(set p-fn (fn [] (core.active_view:is cls))))
(fn [] (when (= (-?> core.active_view.imstate (. :focus)) nil)
(p-fn))))
(fn postpone [view f]
(when (= view.imstate.postponed nil)
(set view.imstate.postponed []))
(table.insert view.imstate.postponed f))
(fn make-tag [tag]
(match (type tag)
:string tag
:table (table.concat tag "::")
_ (tostring tag)))
(fn mouse-inside [x y w h]
(local (mx my) (values core.root_view.mouse.x core.root_view.mouse.y))
(and (>= mx x) (<= mx (+ x w)) (>= my y) (<= my (+ y h))))
(fn consume-pressed [view button]
(when (= (. view.imstate button) :pressed)
(tset view.imstate button :down)
true))
(fn activate [{: view : tag : x : y : w : h}]
(when (and (mouse-inside x y w h) (consume-pressed view :left))
(set view.imstate.active (make-tag tag))
true))
(fn set-cursor [view cursor]
(when (= view.cursor nil) (set view.cursor cursor)))
;; styling and layout
(fn form-defaults [form k v ...]
(when (= (. form k) nil)
(let [v (if (= (type v) :function) (v form) v)]
(tset form k v)))
(if (>= (select :# ...) 2) (form-defaults form ...)
(do (when form.tag (set form.tag (make-tag form.tag))) ; fix up tag
form)))
(fn with-style [form ...]
(form-defaults form :font style.font :color style.text :xpad style.padding.x :ypad style.padding.y ...))
(local form-preserved-keys (collect [_ key (ipairs [:view :x :y :font :color :xpad :ypad])] key true))
(fn reform [form overrides]
(if (and overrides overrides.into (not= overrides.into form))
(reform (lume.extend (lume.clear overrides.into) form) overrides)
(do (each [key (pairs form)]
(when (= (. form-preserved-keys key) nil)
(tset form key nil)))
(lume.extend form (or overrides {})))))
(fn under [form overrides] (reform form (lume.merge (or overrides {}) {:y (+ form.y (or form.h 0) (or form.ypad 0))})))
(fn right-of [form overrides] (reform form (lume.merge (or overrides {}) {:x (+ form.x (or form.w 0) (or form.xpad 0))})))
(fn group-wrapper [orig-form]
(let [group {}
update-dimension
(fn [form coord-key size-key]
(let [coord-group (. group coord-key) size-group (. group size-key)
coord-form (. form coord-key) size-form (. form size-key)]
(if (= size-form nil) ; tried to add an unsized value to the group, ignore
nil
(= coord-group nil) ; container takes on the size of its first item
(do (tset group coord-key coord-form)
(tset group size-key size-form))
(> coord-group coord-form) ; we have an item that is outside the bounds to the left / up; reduce the starting point and extend the size
(do (tset group coord-key coord-form)
(tset group size-key (- (math.max (+ coord-form size-form) (+ coord-group size-group)) coord-form)))
; extend the size if the new item is outside the bounds to the right / down
(tset group size-key (- (math.max (+ coord-form size-form) (+ coord-group size-group)) coord-group)))
form))
update-dimensions (fn [form] (update-dimension form :x :w) (update-dimension form :y :h))]
(fn [?viewfn-or-form ?form ...]
(match [(type ?viewfn-or-form) ?viewfn-or-form]
[:function viewfn] (let [result [(viewfn ?form ...)]]
(update-dimensions ?form)
(table.unpack result))
[:table form] (update-dimensions form)
[:nil] (lume.extend orig-form group)))))
(fn horiz-wrapper [{:x orig-x :w orig-w}]
(fn [{: x : y : w : h : xpad : ypad &as form} overrides]
(if (> (+ x (or w 0) xpad (or w 0)) (+ orig-x orig-w))
(reform form (lume.merge (or overrides {}) {:x orig-x :y (+ y (or h 0) (or ypad 0))}))
(right-of form overrides))))
;; widgets and widget helpers
(fn active? [view tag] (= view.imstate.active (make-tag tag)))
(fn button [{: view : tag : x : y : w : h &as form}]
(when (mouse-inside x y w h) (set-cursor view :hand))
(activate form)
(and (active? view tag) (= view.imstate.left :released) (mouse-inside x y w h)))
(fn label [form text]
(let [(_ newlines) (text:gsub "\n" "\n")
text-height (fn [font] (* (font:get_height) (+ newlines 1)))
{: x : y : w : h : halign : valign : font : color}
(with-style form
:w #($1.font:get_width text)
:h #(text-height $1.font)
:halign :left
:valign :center)
x (match halign :left x :center (+ x (/ (- w (font:get_width text)) 2)) :right (+ x w (- (font:get_width text))))
y (match valign :top y :center (+ y (/ (- h (text-height font)) 2)) :bottom (+ y h (- (text-height font))))]
(renderer.draw_text font text x y color)))
(fn textbutton [form label]
(let [{: x : y : w : h : xpad : ypad : font : color : bg}
(with-style form
:bg style.selection
:tag label
:w #(+ ($1.font:get_width label) $1.xpad)
:h #(+ ($1.font:get_height) $1.ypad))]
(renderer.draw_rect x y w h bg)
(renderer.draw_text font label (+ x (/ xpad 2)) (+ y (/ ypad 2)) color)
(button form)))
(fn checkbox [form name isset]
(let [{: x : y : w : h : font : color : bg : x-label}
(with-style form
:bg style.background
:tag name
:h (* 12 SCALE)
:x-label #(+ $1.x $1.h $1.xpad)
:w #(+ $1.x-label ($1.font:get_width name)))]
(renderer.draw_rect x y h h color)
(when (not isset) (renderer.draw_rect (+ x 2) (+ y 2) (- h 4) (- h 4)))
(renderer.draw_text font name x-label y color)
(button form))) ; whose idea was this?? should return (not isset) >:/
(fn focused? [view tag] (= (make-tag tag) (-?> view.imstate.focus (. :tag))))
(fn focus [{: view : tag : x : y : w : h &as form} opts]
(if (activate form)
(set view.imstate.focus
(doto (lume.clone (or opts {}))
(tset :tag (make-tag tag))))
(and (= view.imstate.left :released) (focused? view tag) (not (mouse-inside x y w h)))
(set view.imstate.focus nil))
(focused? view tag))
(local blink_period 0.8)
(fn x-from-i [s i xLeft font]
(if (or (<= i 1) (= s "")) xLeft
(x-from-i (s:sub 2) (- i 1) (+ xLeft (font:get_width (s:sub 1 1))) font)))
(fn i-from-x [s x xLeft font ?i]
(local i (or ?i 1))
(local w (font:get_width (s:sub 1 1)))
(local xMid (+ xLeft (/ w 2)))
(if (or (<= x xMid) (= s "")) i
(i-from-x (s:sub 2) x (+ xLeft w) font (+ i 1))))
(fn next-match [text i di pred]
(local imax (+ (length text) 1))
(local inext (+ i di))
(if (<= inext 1) 1
(> inext imax) imax
(pred (text:sub inext inext)) (if (< di 0) i inext)
(next-match text inext di pred)))
(fn is-nonword-char [char] (config.non_word_chars:find char nil true))
(fn next-word [text i di]
(let [iwordboundary (next-match text i di #(is-nonword-char $1))]
(next-match text iwordboundary di #(not (is-nonword-char $1)))))
(fn textnav [key i text]
(local imax (+ (length text) 1))
(match key
:left (math.max 1 (- i 1))
:right (math.min imax (+ i 1))
:ctrl+left (next-word text i -1)
:ctrl+right (next-word text i 1)
:home 1
:end imax))
(fn selection-span [view]
(let [f view.imstate.focus
iStart (math.min f.i f.iAnchor)
iLim (math.max f.i f.iAnchor)]
(values iStart iLim)))
(fn selection-text [view text]
(local (iStart iLim) (selection-span view))
(text:sub iStart (- iLim 1)))
(fn replace-selection [view s replacement ?iStart ?iLim]
(local (iStart iLim) (if ?iLim (values ?iStart ?iLim) (selection-span view)))
(local text
(.. (s:sub 1 (- iStart 1))
replacement
(s:sub iLim)))
(local iNew (+ iStart (length replacement)))
(set view.imstate.focus.i iNew)
(set view.imstate.focus.iAnchor iNew)
text)
(fn textbox [form text]
(local {: font : color : w : h : x : y : xpad : ypad : color : bg : view : tag}
(with-style form :h #(+ ($1.font:get_height) $1.ypad)
:bg style.background))
(var textNew (or text ""))
(local (hText xText yText) (values (font:get_height) (+ x (/ xpad 2)) (+ y (/ ypad 2))))
(local initial-press (= view.imstate.left :pressed))
; handle key events
(when (focus form {:i 1 :iAnchor 1 :blink (system.get_time)})
(local f view.imstate.focus)
(when (> f.i (+ (length textNew) 1)) (set f.i (+ (length textNew) 1)))
(when (> f.iAnchor (+ (length textNew) 1)) (set f.iAnchor (+ (length textNew) 1)))
(when view.imstate.text
(set textNew (replace-selection view textNew view.imstate.text)))
(each [_ key (ipairs (or view.imstate.keys []))]
(set view.imstate.focus.blink (system.get_time))
(if (= key :ctrl+c) (system.set_clipboard (selection-text view textNew))
(= key :ctrl+v) (set textNew (replace-selection view textNew (system.get_clipboard)))
(key:find "shift%+") (set f.i (or (textnav (key:gsub "shift%+" "") f.i textNew) f.i))
(let [iNav (textnav key f.i textNew)]
(when iNav
(set f.i iNav)
(set f.iAnchor iNav))
(when (or (= key :delete) (= key :backspace))
(local (iStartDel iLimDel)
(if (not= f.i f.iAnchor) (selection-span view)
(= key :delete) (values f.i (+ f.i 1))
(= key :backspace) (values (math.max 1 (- f.i 1)) f.i)))
(set textNew (replace-selection view textNew "" iStartDel iLimDel)))))))
; handle mouse events
(when (mouse-inside x y w h) (set-cursor view :ibeam))
(when (and (focused? view tag) (active? view tag) (mouse-inside x y w h))
(local mouse-i (i-from-x textNew core.root_view.mouse.x x style.font))
(when initial-press
(set view.imstate.focus.iAnchor mouse-i))
(set view.imstate.focus.i mouse-i))
; draw box
(renderer.draw_rect x y w h color)
(renderer.draw_rect (+ x 1) (+ y 1) (- w 2) (- h 2) bg)
(if (focused? view tag)
; draw text with selection + caret
(let [(iStart iLim) (selection-span view)
xSelect (renderer.draw_text font (textNew:sub 1 (- iStart 1)) xText yText color)
sSelect (textNew:sub iStart (- iLim 1))
wSelect (font:get_width sSelect)
xTail (+ xSelect wSelect)]
(when (> wSelect 0)
(renderer.draw_rect xSelect yText wSelect hText style.selection)
(renderer.draw_text font sSelect xSelect yText color))
(renderer.draw_text font (textNew:sub iLim) xTail yText color)
(when (or (active? view tag)
(< (% (- (system.get_time) view.imstate.focus.blink) (* blink_period 2)) blink_period))
(renderer.draw_rect (x-from-i textNew view.imstate.focus.i xText font) yText style.caret_width hText style.caret)))
; just draw the text
(renderer.draw_text font textNew xText yText color))
textNew)
(fn textfield [form label text]
(let [{: x : y : w : wlabel : wtext : font : color}
(with-style form :wlabel #(+ ($1.font:get_width label) $1.xpad)
:wtext (* 150 SCALE)
:w #(+ $1.wlabel $1.wtext)
:tag label)
form-textbox (lume.merge form {:w wtext :x (+ x wlabel)})
_ (renderer.draw_text font label x y color)
text (textbox form-textbox text)]
(set form.h form-textbox.h)
text))
(fn option-text [option]
(match (type option)
:string option
:table (or option.label (tostring option))
_ (tostring option)))
(fn dropdown [form selection options]
(let [{: x : y : w :h row-h : font : color : bg : xpad : ypad : view : tag}
(with-style form :w (* 150 SCALE)
:h #(+ ($1.font:get_height) $1.ypad)
:bg style.selection)]
(var new-selection nil)
(renderer.draw_rect x y w row-h bg)
(renderer.draw_text style.font (option-text selection) (+ x xpad) (+ y (/ ypad 2)) color)
(renderer.draw_text style.icon_font "-" (+ x w (- xpad)) (+ y (/ ypad 2)) color)
(when (focused? view tag)
(var row-y (+ y row-h))
(each [i option (ipairs options)]
(when (button (lume.merge form {:tag [(make-tag tag) i] :y row-y}))
(set new-selection option))
(set row-y (+ row-y row-h)))
(postpone view (fn []
(var row-y (+ y row-h))
(each [i option (ipairs options)]
(renderer.draw_rect x row-y w row-h bg)
(renderer.draw_text font (option-text option) (+ x xpad) (+ row-y (/ ypad 2)) color)
(set row-y (+ row-y row-h))))))
(focus form)
(or new-selection selection)))
(fn labelled-dropdown [form label selection options]
(let [{: x : y : wlabel : wdropdown : font : color}
(with-style form :wlabel #(+ ($1.font:get_width label) $1.xpad)
:wdropdown (* 150 SCALE)
:w #(+ $1.wlabel $1.wdropdown)
:tag label)
form-dropdown (lume.merge form {:x (+ x wlabel) :w wdropdown})
_ (renderer.draw_text font label x y color)
selection (dropdown form-dropdown selection options)]
(set form.h form-dropdown.h)
selection))
{: attach-imstate : cmd-predicate : postpone : mouse-inside : activate : active?
: button : checkbox : textbox : textfield : textbutton : dropdown : labelled-dropdown : label
: reform : under : right-of : horiz-wrapper : group-wrapper
: with-style : form-defaults}