+91
-11
@@ -109,6 +109,19 @@
|
|||||||
;; Unknown type - dispatch to update function
|
;; Unknown type - dispatch to update function
|
||||||
(put! msg-chan event)))))
|
(put! msg-chan event)))))
|
||||||
|
|
||||||
|
;; === Mouse Hit Testing ===
|
||||||
|
|
||||||
|
(defn- zone-handler-at
|
||||||
|
"Find the handler for event-type at (x, y) in zones.
|
||||||
|
Children render before parents, so they register first in the list.
|
||||||
|
Walking forward finds the innermost (child) handler first."
|
||||||
|
[zones x y event-type]
|
||||||
|
(some (fn [{zx :x zy :y zw :w zh :h :keys [handlers]}]
|
||||||
|
(when (and (<= zx x (+ zx zw -1))
|
||||||
|
(<= zy y (+ zy zh -1)))
|
||||||
|
(get handlers event-type)))
|
||||||
|
zones))
|
||||||
|
|
||||||
;; === Input Loop ===
|
;; === Input Loop ===
|
||||||
(defn- start-input-loop!
|
(defn- start-input-loop!
|
||||||
"Start thread that reads input and puts events on channel.
|
"Start thread that reads input and puts events on channel.
|
||||||
@@ -135,9 +148,10 @@
|
|||||||
- :init-events - Vector of events to dispatch at startup
|
- :init-events - Vector of events to dispatch at startup
|
||||||
- :fps - Target frames per second (default 60)
|
- :fps - Target frames per second (default 60)
|
||||||
- :alt-screen - Use alternate screen buffer (default true)
|
- :alt-screen - Use alternate screen buffer (default true)
|
||||||
|
- :mouse - Enable mouse tracking (default false)
|
||||||
|
|
||||||
Returns the final model."
|
Returns the final model."
|
||||||
[{:keys [init update view init-events fps alt-screen]
|
[{:keys [init update view init-events fps alt-screen mouse]
|
||||||
:or {fps 60 alt-screen true}}]
|
:or {fps 60 alt-screen true}}]
|
||||||
(let [msg-chan (chan 256)
|
(let [msg-chan (chan 256)
|
||||||
running? (atom true)
|
running? (atom true)
|
||||||
@@ -147,6 +161,7 @@
|
|||||||
(term/raw-mode!)
|
(term/raw-mode!)
|
||||||
(term/init-input!)
|
(term/init-input!)
|
||||||
(when alt-screen (term/alt-screen!))
|
(when alt-screen (term/alt-screen!))
|
||||||
|
(when mouse (term/enable-mouse!))
|
||||||
(term/clear!)
|
(term/clear!)
|
||||||
|
|
||||||
(try
|
(try
|
||||||
@@ -160,13 +175,18 @@
|
|||||||
|
|
||||||
;; Initial render
|
;; Initial render
|
||||||
(let [size (term/get-terminal-size)
|
(let [size (term/get-terminal-size)
|
||||||
ctx {:available-height (:height size)
|
ctx (cond-> {:available-height (:height size)
|
||||||
:available-width (:width size)}]
|
:available-width (:width size)}
|
||||||
(term/render! (render/render (view init) ctx)))
|
mouse (assoc :mouse true))
|
||||||
|
render-result (render/render (view init) ctx)
|
||||||
|
output (if (map? render-result) (:output render-result) render-result)
|
||||||
|
initial-zones (when (map? render-result) (:zones render-result))]
|
||||||
|
(term/render! output)
|
||||||
|
|
||||||
;; Main loop
|
;; Main loop
|
||||||
(loop [model init
|
(loop [model init
|
||||||
last-render (System/currentTimeMillis)]
|
last-render (System/currentTimeMillis)
|
||||||
|
zones initial-zones]
|
||||||
(let [;; Wait for message with timeout for frame limiting
|
(let [;; Wait for message with timeout for frame limiting
|
||||||
remaining (max 1 (- frame-time (- (System/currentTimeMillis) last-render)))
|
remaining (max 1 (- frame-time (- (System/currentTimeMillis) last-render)))
|
||||||
event (alt!!
|
event (alt!!
|
||||||
@@ -175,19 +195,76 @@
|
|||||||
|
|
||||||
(if (or (nil? event) (not @running?))
|
(if (or (nil? event) (not @running?))
|
||||||
;; No message, just continue
|
;; No message, just continue
|
||||||
(recur model (System/currentTimeMillis))
|
(recur model (System/currentTimeMillis) zones)
|
||||||
|
|
||||||
;; Check for quit
|
;; Check for quit
|
||||||
(if (= (:type event) :quit)
|
(if (= (:type event) :quit)
|
||||||
;; Quit - return final model
|
;; Quit - return final model
|
||||||
model
|
model
|
||||||
|
|
||||||
;; Update model
|
;; Handle mouse events
|
||||||
|
(if (= (:type event) :mouse)
|
||||||
|
(cond
|
||||||
|
;; Left click → find :on-click handler, dispatch directly
|
||||||
|
(and zones (= (:button event) :left) (= (:action event) :press))
|
||||||
|
(if-let [handler (zone-handler-at zones (:x event) (:y event) :on-click)]
|
||||||
|
(let [click-event (assoc handler :mouse-x (:x event) :mouse-y (:y event))
|
||||||
|
result (update {:model model :event click-event})
|
||||||
|
new-model (:model result)
|
||||||
|
size (term/get-terminal-size)
|
||||||
|
ctx (cond-> {:available-height (:height size)
|
||||||
|
:available-width (:width size)}
|
||||||
|
mouse (assoc :mouse true))
|
||||||
|
now (System/currentTimeMillis)]
|
||||||
|
(when-let [events (:events result)]
|
||||||
|
(doseq [e events] (execute-event! e msg-chan)))
|
||||||
|
(let [render-result (render/render (view new-model) ctx)
|
||||||
|
output (if (map? render-result) (:output render-result) render-result)
|
||||||
|
new-zones (when (map? render-result) (:zones render-result))]
|
||||||
|
(term/render! output)
|
||||||
|
(recur new-model now new-zones)))
|
||||||
|
(recur model (System/currentTimeMillis) zones))
|
||||||
|
|
||||||
|
;; Scroll wheel → process directly, drain queued scrolls
|
||||||
|
(and zones (#{:wheel-up :wheel-down} (:button event)))
|
||||||
|
(if-let [handler (zone-handler-at zones (:x event) (:y event) :on-scroll)]
|
||||||
|
(let [;; Apply this scroll event
|
||||||
|
scroll-event (assoc handler :direction (:button event))
|
||||||
|
result (update {:model model :event scroll-event})
|
||||||
|
;; Drain and apply all queued scroll events
|
||||||
|
final-model (loop [m (:model result)]
|
||||||
|
(if-let [next (async/poll! msg-chan)]
|
||||||
|
(if (and (= (:type next) :mouse)
|
||||||
|
(#{:wheel-up :wheel-down} (:button next)))
|
||||||
|
(if-let [h (zone-handler-at zones (:x next) (:y next) :on-scroll)]
|
||||||
|
(recur (:model (update {:model m
|
||||||
|
:event (assoc h :direction (:button next))})))
|
||||||
|
(do (put! msg-chan next) m))
|
||||||
|
(do (put! msg-chan next) m))
|
||||||
|
m))
|
||||||
|
size (term/get-terminal-size)
|
||||||
|
ctx (cond-> {:available-height (:height size)
|
||||||
|
:available-width (:width size)}
|
||||||
|
mouse (assoc :mouse true))
|
||||||
|
now (System/currentTimeMillis)]
|
||||||
|
(let [render-result (render/render (view final-model) ctx)
|
||||||
|
output (if (map? render-result) (:output render-result) render-result)
|
||||||
|
new-zones (when (map? render-result) (:zones render-result))]
|
||||||
|
(term/render! output)
|
||||||
|
(recur final-model now new-zones)))
|
||||||
|
(recur model (System/currentTimeMillis) zones))
|
||||||
|
|
||||||
|
;; Other mouse events (release, etc.) → ignore
|
||||||
|
:else
|
||||||
|
(recur model (System/currentTimeMillis) zones))
|
||||||
|
|
||||||
|
;; Update model for non-mouse events
|
||||||
(let [result (update {:model model :event event})
|
(let [result (update {:model model :event event})
|
||||||
new-model (:model result)
|
new-model (:model result)
|
||||||
size (term/get-terminal-size)
|
size (term/get-terminal-size)
|
||||||
ctx {:available-height (:height size)
|
ctx (cond-> {:available-height (:height size)
|
||||||
:available-width (:width size)}
|
:available-width (:width size)}
|
||||||
|
mouse (assoc :mouse true))
|
||||||
now (System/currentTimeMillis)]
|
now (System/currentTimeMillis)]
|
||||||
|
|
||||||
;; Execute events
|
;; Execute events
|
||||||
@@ -196,15 +273,18 @@
|
|||||||
(execute-event! e msg-chan)))
|
(execute-event! e msg-chan)))
|
||||||
|
|
||||||
;; Render with context for flex layouts
|
;; Render with context for flex layouts
|
||||||
(term/render! (render/render (view new-model) ctx))
|
(let [render-result (render/render (view new-model) ctx)
|
||||||
|
output (if (map? render-result) (:output render-result) render-result)
|
||||||
(recur new-model now))))))
|
new-zones (when (map? render-result) (:zones render-result))]
|
||||||
|
(term/render! output)
|
||||||
|
(recur new-model now new-zones)))))))))
|
||||||
|
|
||||||
(finally
|
(finally
|
||||||
;; Cleanup
|
;; Cleanup
|
||||||
(reset! running? false)
|
(reset! running? false)
|
||||||
(reset! debounce-timers {})
|
(reset! debounce-timers {})
|
||||||
(close! msg-chan)
|
(close! msg-chan)
|
||||||
|
(when mouse (term/disable-mouse!))
|
||||||
(when alt-screen (term/exit-alt-screen!))
|
(when alt-screen (term/exit-alt-screen!))
|
||||||
(term/restore!)
|
(term/restore!)
|
||||||
(term/close-input!)
|
(term/close-input!)
|
||||||
|
|||||||
+55
-3
@@ -16,7 +16,8 @@
|
|||||||
{:type :key, :key \\c, :modifiers #{:ctrl}} ; Ctrl+C
|
{:type :key, :key \\c, :modifiers #{:ctrl}} ; Ctrl+C
|
||||||
{:type :key, :key :enter} ; Enter key
|
{:type :key, :key :enter} ; Enter key
|
||||||
{:type :key, :key :f1, :modifiers #{:alt}} ; Alt+F1"
|
{:type :key, :key :f1, :modifiers #{:alt}} ; Alt+F1"
|
||||||
(:require [tui.terminal :as term]))
|
(:require [tui.terminal :as term]
|
||||||
|
[clojure.string :as str]))
|
||||||
|
|
||||||
;; === Control Key Mappings ===
|
;; === Control Key Mappings ===
|
||||||
;; Maps byte codes 0-31 to either:
|
;; Maps byte codes 0-31 to either:
|
||||||
@@ -83,8 +84,59 @@
|
|||||||
(make-key-event :escape)
|
(make-key-event :escape)
|
||||||
|
|
||||||
(= c2 \[)
|
(= c2 \[)
|
||||||
;; CSI sequence
|
;; CSI sequence - check for SGR mouse (ESC [ <)
|
||||||
|
(let [c3 (term/read-char-timeout 50)]
|
||||||
|
(cond
|
||||||
|
(nil? c3)
|
||||||
|
(make-key-event :escape)
|
||||||
|
|
||||||
|
;; SGR mouse sequence: ESC [ < Cb ; Cx ; Cy M/m
|
||||||
|
(= c3 \<)
|
||||||
(loop [buf []]
|
(loop [buf []]
|
||||||
|
(let [c (term/read-char-timeout 50)]
|
||||||
|
(cond
|
||||||
|
(nil? c)
|
||||||
|
(make-key-event :unknown)
|
||||||
|
|
||||||
|
;; Final byte M (press) or m (release)
|
||||||
|
(or (= c \M) (= c \m))
|
||||||
|
(let [params (str/split (apply str buf) #";")]
|
||||||
|
(if (= (count params) 3)
|
||||||
|
(let [cb (parse-long (nth params 0))
|
||||||
|
cx (parse-long (nth params 1))
|
||||||
|
cy (parse-long (nth params 2))
|
||||||
|
action (if (= c \M) :press :release)
|
||||||
|
;; Decode button: bits 0-1 = button, bit 6 = wheel
|
||||||
|
wheel? (bit-test cb 6)
|
||||||
|
btn-bits (bit-and cb 3)
|
||||||
|
button (if wheel?
|
||||||
|
(if (zero? btn-bits) :wheel-up :wheel-down)
|
||||||
|
(case btn-bits
|
||||||
|
0 :left
|
||||||
|
1 :middle
|
||||||
|
2 :right
|
||||||
|
:unknown))]
|
||||||
|
{:type :mouse
|
||||||
|
:button button
|
||||||
|
:action action
|
||||||
|
:x (dec cx) ; SGR is 1-indexed
|
||||||
|
:y (dec cy)})
|
||||||
|
(make-key-event :unknown)))
|
||||||
|
|
||||||
|
;; Accumulate digits and semicolons
|
||||||
|
:else
|
||||||
|
(recur (conj buf c)))))
|
||||||
|
|
||||||
|
;; Regular CSI sequence — c3 is first char after "ESC ["
|
||||||
|
:else
|
||||||
|
(if (<= 0x40 (int c3) 0x7E)
|
||||||
|
;; c3 is already a final byte (e.g. ESC [ A for arrow up)
|
||||||
|
(let [seq-str (str c3)]
|
||||||
|
(if-let [key (get csi-sequences seq-str)]
|
||||||
|
(make-key-event key)
|
||||||
|
(make-key-event :unknown)))
|
||||||
|
;; c3 is a parameter/intermediate, read more chars
|
||||||
|
(loop [buf [c3]]
|
||||||
(let [c (term/read-char-timeout 50)]
|
(let [c (term/read-char-timeout 50)]
|
||||||
(cond
|
(cond
|
||||||
(nil? c)
|
(nil? c)
|
||||||
@@ -103,7 +155,7 @@
|
|||||||
(make-key-event :unknown)))
|
(make-key-event :unknown)))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(make-key-event :unknown))))
|
(make-key-event :unknown)))))))
|
||||||
|
|
||||||
(= c2 \O)
|
(= c2 \O)
|
||||||
;; SS3 sequence (F1-F4 on some terminals)
|
;; SS3 sequence (F1-F4 on some terminals)
|
||||||
|
|||||||
+148
-36
@@ -117,6 +117,23 @@
|
|||||||
parsed)]
|
parsed)]
|
||||||
constrained))
|
constrained))
|
||||||
|
|
||||||
|
;; === Mouse Zone Tracking ===
|
||||||
|
|
||||||
|
(def ^:dynamic *zones* nil)
|
||||||
|
|
||||||
|
(defn- extract-handlers
|
||||||
|
"Extract mouse handler attrs (:on-click, :on-scroll) from an element's attrs."
|
||||||
|
[attrs]
|
||||||
|
(cond-> {}
|
||||||
|
(:on-click attrs) (assoc :on-click (:on-click attrs))
|
||||||
|
(:on-scroll attrs) (assoc :on-scroll (:on-scroll attrs))))
|
||||||
|
|
||||||
|
(defn- register-zone!
|
||||||
|
"Register a mouse zone with its bounding box and handlers."
|
||||||
|
[x y width height handlers]
|
||||||
|
(when (and *zones* (seq handlers))
|
||||||
|
(swap! *zones* conj {:x x :y y :w width :h height :handlers handlers})))
|
||||||
|
|
||||||
;; === Hiccup Parsing ===
|
;; === Hiccup Parsing ===
|
||||||
(defn- flatten-children
|
(defn- flatten-children
|
||||||
"Flatten sequences in children (but not vectors, which are hiccup elements)."
|
"Flatten sequences in children (but not vectors, which are hiccup elements)."
|
||||||
@@ -155,9 +172,14 @@
|
|||||||
|
|
||||||
(defn- render-text
|
(defn- render-text
|
||||||
"Render :text element."
|
"Render :text element."
|
||||||
[attrs children]
|
[attrs children ctx]
|
||||||
(let [content (apply str (flatten children))]
|
(let [content (apply str (flatten children))
|
||||||
(apply-style content attrs)))
|
result (apply-style content attrs)]
|
||||||
|
(let [handlers (extract-handlers attrs)]
|
||||||
|
(when (seq handlers)
|
||||||
|
(register-zone! (or (:x ctx) 0) (or (:y ctx) 0)
|
||||||
|
(count content) 1 handlers)))
|
||||||
|
result))
|
||||||
|
|
||||||
;; === Layout Primitives ===
|
;; === Layout Primitives ===
|
||||||
(declare render-element)
|
(declare render-element)
|
||||||
@@ -175,18 +197,32 @@
|
|||||||
\"N%\" (percentage), \"Nfr\" (fractional unit), or nil (auto).
|
\"N%\" (percentage), \"Nfr\" (fractional unit), or nil (auto).
|
||||||
Example: [:row {:widths [20 :flex :flex]} child1 child2 child3]
|
Example: [:row {:widths [20 :flex :flex]} child1 child2 child3]
|
||||||
Example: [:row {:widths [\"30%\" \"2fr\" \"1fr\"]} child1 child2 child3]"
|
Example: [:row {:widths [\"30%\" \"2fr\" \"1fr\"]} child1 child2 child3]"
|
||||||
[{:keys [gap widths] :or {gap 0}} children ctx]
|
[{:keys [gap widths] :as attrs :or {gap 0}} children ctx]
|
||||||
(let [available-width (or (:available-width ctx) 120)
|
(let [available-width (or (:available-width ctx) 120)
|
||||||
available-height (or (:available-height ctx) 100)
|
available-height (or (:available-height ctx) 100)
|
||||||
|
parent-x (or (:x ctx) 0)
|
||||||
|
parent-y (or (:y ctx) 0)
|
||||||
;; Use new enhanced sizing system
|
;; Use new enhanced sizing system
|
||||||
calculated-widths (when widths
|
calculated-widths (when widths
|
||||||
(calculate-sizes widths children available-width gap))
|
(calculate-sizes widths children available-width gap))
|
||||||
|
;; Pre-calculate x offsets for each child
|
||||||
|
child-x-offsets (if calculated-widths
|
||||||
|
(reductions + parent-x
|
||||||
|
(map-indexed
|
||||||
|
(fn [idx _]
|
||||||
|
(+ (or (nth calculated-widths idx 0) 0)
|
||||||
|
(if (pos? idx) gap 0)))
|
||||||
|
children))
|
||||||
|
nil)
|
||||||
;; Render each child with its allocated width in context
|
;; Render each child with its allocated width in context
|
||||||
rendered (map-indexed
|
rendered (map-indexed
|
||||||
(fn [idx child]
|
(fn [idx child]
|
||||||
(let [child-width (when calculated-widths
|
(let [child-width (when calculated-widths
|
||||||
(nth calculated-widths idx nil))
|
(nth calculated-widths idx nil))
|
||||||
child-ctx (cond-> ctx
|
child-x (if child-x-offsets
|
||||||
|
(nth (vec child-x-offsets) idx parent-x)
|
||||||
|
parent-x)
|
||||||
|
child-ctx (cond-> (assoc ctx :x child-x :y parent-y)
|
||||||
child-width (assoc :available-width child-width))]
|
child-width (assoc :available-width child-width))]
|
||||||
(render-element child child-ctx)))
|
(render-element child child-ctx)))
|
||||||
children)
|
children)
|
||||||
@@ -214,9 +250,12 @@
|
|||||||
width (get child-widths child-idx 0)]
|
width (get child-widths child-idx 0)]
|
||||||
(ansi/pad-right line (or width 0))))
|
(ansi/pad-right line (or width 0))))
|
||||||
child-lines)))]
|
child-lines)))]
|
||||||
|
;; Register zone for row itself if it has handlers
|
||||||
|
(let [handlers (extract-handlers attrs)
|
||||||
|
total-width (+ (reduce + 0 child-widths) (* gap (max 0 (dec (count children)))))]
|
||||||
|
(register-zone! parent-x parent-y total-width max-height handlers))
|
||||||
(str/join "\n" combined-lines)))
|
(str/join "\n" combined-lines)))
|
||||||
|
|
||||||
|
|
||||||
(defn- render-col
|
(defn- render-col
|
||||||
"Render :col - vertical layout.
|
"Render :col - vertical layout.
|
||||||
Supports :heights for distributing vertical space.
|
Supports :heights for distributing vertical space.
|
||||||
@@ -224,32 +263,49 @@
|
|||||||
\"N%\" (percentage), \"Nfr\" (fractional unit), or nil (auto).
|
\"N%\" (percentage), \"Nfr\" (fractional unit), or nil (auto).
|
||||||
Example: [:col {:heights [3 :flex :flex 4]} child1 child2 child3 child4]
|
Example: [:col {:heights [3 :flex :flex 4]} child1 child2 child3 child4]
|
||||||
Example: [:col {:heights [\"10%\" \"2fr\" \"1fr\"]} child1 child2 child3]"
|
Example: [:col {:heights [\"10%\" \"2fr\" \"1fr\"]} child1 child2 child3]"
|
||||||
[{:keys [gap heights width height] :or {gap 0}} children ctx]
|
[{:keys [gap heights width height] :as attrs :or {gap 0}} children ctx]
|
||||||
(let [;; Use explicit width/height if provided, otherwise from context
|
(let [;; Use explicit width/height if provided, otherwise from context
|
||||||
available-width (or width (:available-width ctx) 120)
|
available-width (or width (:available-width ctx) 120)
|
||||||
available-height (or height (:available-height ctx) 100)
|
available-height (or height (:available-height ctx) 100)
|
||||||
|
parent-x (or (:x ctx) 0)
|
||||||
|
parent-y (or (:y ctx) 0)
|
||||||
;; Use new enhanced sizing system
|
;; Use new enhanced sizing system
|
||||||
calculated-heights (when heights
|
calculated-heights (when heights
|
||||||
(calculate-sizes heights children available-height gap))
|
(calculate-sizes heights children available-height gap))
|
||||||
;; Render each child with its allocated height in context
|
;; Render each child, tracking y offset
|
||||||
rendered (map-indexed
|
rendered (loop [idx 0
|
||||||
(fn [idx child]
|
y-offset parent-y
|
||||||
(let [child-height (when calculated-heights
|
results []]
|
||||||
|
(if (>= idx (count children))
|
||||||
|
results
|
||||||
|
(let [child (nth children idx)
|
||||||
|
child-height (when calculated-heights
|
||||||
(nth calculated-heights idx nil))
|
(nth calculated-heights idx nil))
|
||||||
child-ctx (cond-> (assoc ctx :available-width available-width)
|
child-ctx (cond-> (assoc ctx :available-width available-width
|
||||||
child-height (assoc :available-height child-height))]
|
:x parent-x :y y-offset)
|
||||||
(render-element child child-ctx)))
|
child-height (assoc :available-height child-height))
|
||||||
children)
|
result (render-element child child-ctx)
|
||||||
|
result-height (if child-height
|
||||||
|
child-height
|
||||||
|
(inc (count (re-seq #"\n" result))))]
|
||||||
|
(recur (inc idx)
|
||||||
|
(+ y-offset result-height gap)
|
||||||
|
(conj results result)))))
|
||||||
separator (str/join (repeat gap "\n"))]
|
separator (str/join (repeat gap "\n"))]
|
||||||
|
;; Register zone for col itself if it has handlers
|
||||||
|
(let [handlers (extract-handlers attrs)]
|
||||||
|
(register-zone! parent-x parent-y available-width available-height handlers))
|
||||||
(str/join (str "\n" separator) rendered)))
|
(str/join (str "\n" separator) rendered)))
|
||||||
|
|
||||||
(defn- render-box
|
(defn- render-box
|
||||||
"Render :box - bordered container.
|
"Render :box - bordered container.
|
||||||
Supports :width/:height as number or :fill (uses available size from ctx)."
|
Supports :width/:height as number or :fill (uses available size from ctx)."
|
||||||
[{:keys [border title padding width height]
|
[{:keys [border title padding width height] :as attrs
|
||||||
:or {border :rounded padding 0}}
|
:or {border :rounded padding 0}}
|
||||||
children ctx]
|
children ctx]
|
||||||
(let [;; Calculate target dimensions first (for passing to children)
|
(let [parent-x (or (:x ctx) 0)
|
||||||
|
parent-y (or (:y ctx) 0)
|
||||||
|
;; Calculate target dimensions first (for passing to children)
|
||||||
target-width (cond
|
target-width (cond
|
||||||
(number? width) width
|
(number? width) width
|
||||||
(= width :fill) (or (:available-width ctx) 80)
|
(= width :fill) (or (:available-width ctx) 80)
|
||||||
@@ -258,8 +314,8 @@
|
|||||||
(number? height) height
|
(number? height) height
|
||||||
(= height :fill) (or (:available-height ctx) 24)
|
(= height :fill) (or (:available-height ctx) 24)
|
||||||
:else nil)
|
:else nil)
|
||||||
;; Pass constrained dimensions to children
|
;; Pass constrained dimensions to children (offset by border)
|
||||||
child-ctx (cond-> ctx
|
child-ctx (cond-> (assoc ctx :x (+ parent-x 1) :y (+ parent-y 1))
|
||||||
target-width (assoc :available-width (- target-width 2))
|
target-width (assoc :available-width (- target-width 2))
|
||||||
target-height (assoc :available-height (- target-height 2)))
|
target-height (assoc :available-height (- target-height 2)))
|
||||||
chars (get ansi/box-chars border (:rounded ansi/box-chars))
|
chars (get ansi/box-chars border (:rounded ansi/box-chars))
|
||||||
@@ -278,11 +334,28 @@
|
|||||||
[0 0 0 0])
|
[0 0 0 0])
|
||||||
:else [0 0 0 0])
|
:else [0 0 0 0])
|
||||||
|
|
||||||
|
;; Render title - string or vector of strings/[:text] elements
|
||||||
|
;; Each [:text] in a vector title renders via render-text for automatic zone registration
|
||||||
|
title-rendered (when title
|
||||||
|
(if (string? title)
|
||||||
|
title
|
||||||
|
(loop [parts title, out "", x (+ parent-x 3)]
|
||||||
|
(if (empty? parts)
|
||||||
|
out
|
||||||
|
(let [part (first parts)]
|
||||||
|
(if (string? part)
|
||||||
|
(recur (rest parts) (str out part) (+ x (count part)))
|
||||||
|
(let [[_ attrs & children] part
|
||||||
|
content (apply str (flatten children))
|
||||||
|
rendered (render-text attrs children (assoc ctx :x x :y parent-y))]
|
||||||
|
(recur (rest parts) (str out rendered) (+ x (count content))))))))))
|
||||||
|
title-visible-len (when title-rendered (ansi/visible-length title-rendered))
|
||||||
|
|
||||||
;; Calculate content width
|
;; Calculate content width
|
||||||
max-content-width (apply max 0 (map ansi/visible-length lines))
|
max-content-width (apply max 0 (map ansi/visible-length lines))
|
||||||
inner-width (+ max-content-width pad-left pad-right)
|
inner-width (+ max-content-width pad-left pad-right)
|
||||||
;; Title needs: "─ title " = title-length + 3
|
;; Title needs: "─ title " = title-length + 3
|
||||||
title-width (if title (+ (count title) 3) 0)
|
title-width (if title-rendered (+ title-visible-len 3) 0)
|
||||||
box-width (or target-width (+ (max inner-width title-width) 2))
|
box-width (or target-width (+ (max inner-width title-width) 2))
|
||||||
content-width (- box-width 2)
|
content-width (- box-width 2)
|
||||||
|
|
||||||
@@ -312,9 +385,9 @@
|
|||||||
|
|
||||||
;; Build box
|
;; Build box
|
||||||
top-line (str (:tl chars)
|
top-line (str (:tl chars)
|
||||||
(if title
|
(if title-rendered
|
||||||
(str (:h chars) " " title " "
|
(str (:h chars) " " title-rendered " "
|
||||||
(apply str (repeat (max 0 (- content-width (count title) 3)) (:h chars))))
|
(apply str (repeat (max 0 (- content-width title-visible-len 3)) (:h chars))))
|
||||||
(apply str (repeat content-width (:h chars))))
|
(apply str (repeat content-width (:h chars))))
|
||||||
(:tr chars))
|
(:tr chars))
|
||||||
bottom-line (str (:bl chars)
|
bottom-line (str (:bl chars)
|
||||||
@@ -323,7 +396,11 @@
|
|||||||
body-lines (for [line all-lines]
|
body-lines (for [line all-lines]
|
||||||
(str (:v chars)
|
(str (:v chars)
|
||||||
(ansi/fit-width line content-width)
|
(ansi/fit-width line content-width)
|
||||||
(:v chars)))]
|
(:v chars)))
|
||||||
|
total-height (+ 2 (count (vec all-lines)))]
|
||||||
|
;; Register zone for the box
|
||||||
|
(let [handlers (extract-handlers attrs)]
|
||||||
|
(register-zone! parent-x parent-y box-width total-height handlers))
|
||||||
(str/join "\n" (concat [top-line] body-lines [bottom-line]))))
|
(str/join "\n" (concat [top-line] body-lines [bottom-line]))))
|
||||||
|
|
||||||
(defn- render-space
|
(defn- render-space
|
||||||
@@ -383,11 +460,18 @@
|
|||||||
(let [available-width (or (:available-width ctx) 120)
|
(let [available-width (or (:available-width ctx) 120)
|
||||||
available-height (or (:available-height ctx) 30)
|
available-height (or (:available-height ctx) 30)
|
||||||
[bg-child modal-child] children
|
[bg-child modal-child] children
|
||||||
;; Render background
|
;; Render background with position
|
||||||
bg-rendered (render-element bg-child ctx)
|
bg-rendered (render-element bg-child ctx)
|
||||||
bg-lines (str/split bg-rendered #"\n" -1)
|
bg-lines (str/split bg-rendered #"\n" -1)
|
||||||
;; Render modal
|
;; Calculate modal position (centered)
|
||||||
modal-rendered (render-element modal-child ctx)
|
modal-pre-rendered (render-element modal-child ctx)
|
||||||
|
modal-lines-pre (str/split modal-pre-rendered #"\n" -1)
|
||||||
|
modal-height (count modal-lines-pre)
|
||||||
|
modal-width (apply max 0 (map ansi/visible-length modal-lines-pre))
|
||||||
|
modal-x (max 0 (quot (- available-width modal-width) 2))
|
||||||
|
modal-y (max 0 (quot (- available-height modal-height) 2))
|
||||||
|
;; Re-render modal with correct position for zone tracking
|
||||||
|
modal-rendered (render-element modal-child (assoc ctx :x modal-x :y modal-y))
|
||||||
modal-lines (str/split modal-rendered #"\n" -1)
|
modal-lines (str/split modal-rendered #"\n" -1)
|
||||||
;; Overlay
|
;; Overlay
|
||||||
result-lines (overlay-lines bg-lines modal-lines available-width available-height)]
|
result-lines (overlay-lines bg-lines modal-lines available-width available-height)]
|
||||||
@@ -430,8 +514,10 @@
|
|||||||
- :indicators - show scroll indicators when clipped (default true)
|
- :indicators - show scroll indicators when clipped (default true)
|
||||||
|
|
||||||
Example: [:scroll {:cursor 3} child0 child1 child2 child3 child4 ...]"
|
Example: [:scroll {:cursor 3} child0 child1 child2 child3 child4 ...]"
|
||||||
[{:keys [cursor indicators] :or {cursor 0 indicators true}} children ctx]
|
[{:keys [cursor indicators] :as attrs :or {cursor 0 indicators true}} children ctx]
|
||||||
(let [available-height (or (:available-height ctx) 100)
|
(let [available-height (or (:available-height ctx) 100)
|
||||||
|
parent-x (or (:x ctx) 0)
|
||||||
|
parent-y (or (:y ctx) 0)
|
||||||
total-items (count children)
|
total-items (count children)
|
||||||
;; Reserve space for indicators if enabled
|
;; Reserve space for indicators if enabled
|
||||||
indicator-height (if indicators 1 0)
|
indicator-height (if indicators 1 0)
|
||||||
@@ -441,8 +527,13 @@
|
|||||||
{:keys [start end has-above has-below]} (visible-window-calc total-items cursor max-visible)
|
{:keys [start end has-above has-below]} (visible-window-calc total-items cursor max-visible)
|
||||||
;; Get visible children
|
;; Get visible children
|
||||||
visible-children (subvec (vec children) start end)
|
visible-children (subvec (vec children) start end)
|
||||||
;; Render visible children
|
;; y offset: starts after up-indicator if present
|
||||||
rendered-lines (mapv #(render-element % ctx) visible-children)
|
y-start (+ parent-y (if (and indicators has-above) 1 0))
|
||||||
|
;; Render visible children with y position tracking
|
||||||
|
rendered-lines (vec (map-indexed
|
||||||
|
(fn [idx child]
|
||||||
|
(render-element child (assoc ctx :x parent-x :y (+ y-start idx))))
|
||||||
|
visible-children))
|
||||||
;; Build result with indicators
|
;; Build result with indicators
|
||||||
up-indicator (when (and indicators has-above)
|
up-indicator (when (and indicators has-above)
|
||||||
(ansi/style "↑" :fg :cyan))
|
(ansi/style "↑" :fg :cyan))
|
||||||
@@ -451,7 +542,19 @@
|
|||||||
all-lines (cond-> []
|
all-lines (cond-> []
|
||||||
up-indicator (conj up-indicator)
|
up-indicator (conj up-indicator)
|
||||||
true (into rendered-lines)
|
true (into rendered-lines)
|
||||||
down-indicator (conj down-indicator))]
|
down-indicator (conj down-indicator))
|
||||||
|
container-width (or (:available-width ctx) 120)]
|
||||||
|
;; Register zone for scroll container if it has handlers
|
||||||
|
(let [handlers (extract-handlers attrs)]
|
||||||
|
(register-zone! parent-x parent-y container-width available-height handlers))
|
||||||
|
;; Register clickable zones for scroll indicators
|
||||||
|
(when (and indicators has-above (:on-scroll attrs))
|
||||||
|
(register-zone! parent-x parent-y container-width 1
|
||||||
|
{:on-click (assoc (:on-scroll attrs) :direction :wheel-up)}))
|
||||||
|
(when (and indicators has-below (:on-scroll attrs))
|
||||||
|
(let [down-y (+ parent-y (dec (count all-lines)))]
|
||||||
|
(register-zone! parent-x down-y container-width 1
|
||||||
|
{:on-click (assoc (:on-scroll attrs) :direction :wheel-down)})))
|
||||||
(str/join "\n" all-lines)))
|
(str/join "\n" all-lines)))
|
||||||
|
|
||||||
;; === Grid Primitive ===
|
;; === Grid Primitive ===
|
||||||
@@ -617,10 +720,14 @@
|
|||||||
;; Calculate cell position
|
;; Calculate cell position
|
||||||
cell-y (nth (vec row-positions) row 0)
|
cell-y (nth (vec row-positions) row 0)
|
||||||
cell-x (nth (vec col-positions) col 0)
|
cell-x (nth (vec col-positions) col 0)
|
||||||
;; Render content with cell dimensions in context
|
;; Render content with cell dimensions and position in context
|
||||||
|
parent-x (or (:x ctx) 0)
|
||||||
|
parent-y (or (:y ctx) 0)
|
||||||
cell-ctx (assoc ctx
|
cell-ctx (assoc ctx
|
||||||
:available-width cell-width
|
:available-width cell-width
|
||||||
:available-height cell-height)
|
:available-height cell-height
|
||||||
|
:x (+ parent-x cell-x)
|
||||||
|
:y (+ parent-y cell-y))
|
||||||
rendered (render-element content cell-ctx)]
|
rendered (render-element content cell-ctx)]
|
||||||
(overlay-on-canvas c rendered cell-x cell-y cell-width cell-height)))
|
(overlay-on-canvas c rendered cell-x cell-y cell-width cell-height)))
|
||||||
canvas
|
canvas
|
||||||
@@ -646,7 +753,7 @@
|
|||||||
(vector? elem)
|
(vector? elem)
|
||||||
(let [[tag attrs children] (parse-element elem)]
|
(let [[tag attrs children] (parse-element elem)]
|
||||||
(case tag
|
(case tag
|
||||||
:text (render-text attrs children)
|
:text (render-text attrs children ctx)
|
||||||
:row (render-row attrs children ctx)
|
:row (render-row attrs children ctx)
|
||||||
:col (render-col attrs children ctx)
|
:col (render-col attrs children ctx)
|
||||||
:box (render-box attrs children ctx)
|
:box (render-box attrs children ctx)
|
||||||
@@ -663,10 +770,15 @@
|
|||||||
:else (str elem)))
|
:else (str elem)))
|
||||||
|
|
||||||
(defn render
|
(defn render
|
||||||
"Render hiccup to ANSI string."
|
"Render hiccup to ANSI string. When :mouse is truthy in ctx,
|
||||||
|
returns {:output string :zones [...]} instead of a plain string."
|
||||||
([hiccup] (render hiccup {}))
|
([hiccup] (render hiccup {}))
|
||||||
([hiccup ctx]
|
([hiccup ctx]
|
||||||
(render-element hiccup ctx)))
|
(if (:mouse ctx)
|
||||||
|
(binding [*zones* (atom [])]
|
||||||
|
(let [result (render-element hiccup (assoc ctx :x 0 :y 0))]
|
||||||
|
{:output result :zones @*zones*}))
|
||||||
|
(render-element hiccup ctx))))
|
||||||
|
|
||||||
;; === Convenience Components ===
|
;; === Convenience Components ===
|
||||||
(defn text
|
(defn text
|
||||||
|
|||||||
@@ -75,6 +75,20 @@
|
|||||||
(print ansi/cursor-home)
|
(print ansi/cursor-home)
|
||||||
(flush))
|
(flush))
|
||||||
|
|
||||||
|
(defn enable-mouse!
|
||||||
|
"Enable mouse tracking (SGR extended mode)."
|
||||||
|
[]
|
||||||
|
(print "\033[?1000h") ; button tracking
|
||||||
|
(print "\033[?1006h") ; SGR extended coordinates
|
||||||
|
(flush))
|
||||||
|
|
||||||
|
(defn disable-mouse!
|
||||||
|
"Disable mouse tracking."
|
||||||
|
[]
|
||||||
|
(print "\033[?1006l")
|
||||||
|
(print "\033[?1000l")
|
||||||
|
(flush))
|
||||||
|
|
||||||
(defn render!
|
(defn render!
|
||||||
"Render string to terminal."
|
"Render string to terminal."
|
||||||
[s]
|
[s]
|
||||||
|
|||||||
Reference in New Issue
Block a user