mirror of
https://github.com/penpot/penpot.git
synced 2026-06-15 20:02:17 +00:00
🎉 Edit wasm guides by double clicking them
This commit is contained in:
parent
c3491b9403
commit
995ba0fc0f
@ -607,21 +607,99 @@
|
||||
(when (>= index 0)
|
||||
(nth (vec (vals guides)) index nil)))
|
||||
|
||||
(mf/defc guide-drag-preview*
|
||||
[{:keys [guide position vbox zoom]}]
|
||||
(mf/defc guide-overlay*
|
||||
"Temporary SVG rendering of a guide that's being interacted with (drag or
|
||||
inline edit). In :edit mode it also shows the pill with an editable number."
|
||||
[{:keys [guide position vbox zoom mode frame-offset
|
||||
on-input-commit on-input-cancel]}]
|
||||
(let [axis (:axis guide)
|
||||
guide-color (or (:color guide) default-guide-color)
|
||||
stroke-width (/ guide-width zoom)
|
||||
{:keys [x1 y1 x2 y2]} (guide-line-axis position vbox axis)]
|
||||
[:line {:x1 x1
|
||||
:y1 y1
|
||||
:x2 x2
|
||||
:y2 y2
|
||||
:style {:stroke guide-color
|
||||
:stroke-width stroke-width
|
||||
:stroke-opacity guide-opacity-hover}}]))
|
||||
{:keys [x1 y1 x2 y2]} (guide-line-axis position vbox axis)
|
||||
input-ref (mf/use-ref nil)
|
||||
|
||||
(defn use-wasm-guide-drag
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-input-commit on-input-cancel)
|
||||
(fn [event]
|
||||
(cond
|
||||
(kbd/enter? event)
|
||||
(do (dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-input-commit (-> (mf/ref-val input-ref) dom/get-value)))
|
||||
|
||||
(kbd/esc? event)
|
||||
(do (dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(on-input-cancel)))))
|
||||
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
(mf/deps on-input-commit)
|
||||
(fn []
|
||||
(on-input-commit (-> (mf/ref-val input-ref) dom/get-value))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(some-> (mf/ref-val input-ref) dom/select-text!))
|
||||
|
||||
[:g.guide-overlay
|
||||
[:line {:x1 x1
|
||||
:y1 y1
|
||||
:x2 x2
|
||||
:y2 y2
|
||||
:style {:stroke guide-color
|
||||
:stroke-width stroke-width
|
||||
:stroke-opacity guide-opacity-hover}}]
|
||||
|
||||
(when (= mode :edit)
|
||||
(let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]}
|
||||
(guide-pill-axis position vbox zoom axis)
|
||||
corner-radius (/ guide-pill-corner-radius zoom)
|
||||
display-value (fmt/format-number (- position frame-offset))
|
||||
input-w (/ guide-pill-width zoom)
|
||||
input-h (/ guide-pill-height zoom)]
|
||||
[:g.guide-pill
|
||||
[:rect {:x rect-x
|
||||
:y rect-y
|
||||
:width rect-width
|
||||
:height rect-height
|
||||
:rx corner-radius
|
||||
:ry corner-radius
|
||||
:style {:fill guide-color}}]
|
||||
[:foreignObject {:x (- text-x (/ input-w 2))
|
||||
:y (- text-y (/ input-h 2))
|
||||
:width input-w
|
||||
:height input-h
|
||||
:transform (when (= axis :y)
|
||||
(str "rotate(-90 " text-x "," text-y ")"))}
|
||||
[:input {:ref input-ref
|
||||
:type "number"
|
||||
:step "any"
|
||||
:default-value display-value
|
||||
:auto-focus true
|
||||
:on-key-down on-key-down
|
||||
:on-blur on-blur
|
||||
:on-pointer-down dom/stop-propagation
|
||||
:style {:width "100%"
|
||||
:height "100%"
|
||||
:border "none"
|
||||
:outline "none"
|
||||
:padding 0
|
||||
:margin 0
|
||||
:background "transparent"
|
||||
:color colors/white
|
||||
:font-family rulers/font-family
|
||||
:font-size (str (/ rulers/font-size zoom) "px")
|
||||
:text-align "center"
|
||||
:-moz-appearance "textfield"}}]]]))]))
|
||||
|
||||
(defn use-wasm-guide-interaction
|
||||
"Owns both drag and inline-edit lifecycles for WASM-rendered guides.
|
||||
|
||||
Returns a map with the live overlay `state` (`{:guide ... :new-position ...
|
||||
:mode :drag|:edit ...}` or nil) plus callbacks the overlay needs in edit
|
||||
mode: `commit-edit` (commits the parsed input value) and `cancel-edit`
|
||||
(drops the edit without committing)."
|
||||
[{:keys [guides zoom wasm-guides? disabled-guides? on-guide-change
|
||||
on-guide-drag get-hover-frame focus]}]
|
||||
(let [dragging-ref (mf/use-ref false)
|
||||
@ -630,7 +708,7 @@
|
||||
guide-ref (mf/use-ref nil)
|
||||
pending-ref (mf/use-ref nil)
|
||||
drag-listeners-ref (mf/use-ref nil)
|
||||
drag-state (mf/use-state nil)
|
||||
state (mf/use-state nil)
|
||||
|
||||
snap-pixel?
|
||||
(mf/deref refs/snap-pixel?)
|
||||
@ -638,11 +716,11 @@
|
||||
read-only?
|
||||
(mf/use-ctx ctx/workspace-read-only?)]
|
||||
|
||||
;; The drag handlers are defined here so they close directly over the refs
|
||||
;; and the current render's props (guides, zoom, ...). The pointerdown
|
||||
;; listener is re-registered by the effect below whenever those props
|
||||
;; change, so it always sees fresh values without a mutable context ref.
|
||||
(let [remove-drag-listeners!
|
||||
;; The handlers are defined here so they close directly over the refs and
|
||||
;; the current render's props (guides, zoom, ...). The pointerdown /
|
||||
;; dblclick listeners are re-registered by the effect below whenever those
|
||||
;; props change, so they always see fresh values.
|
||||
(let [remove-drag-listeners
|
||||
(fn []
|
||||
(when-let [{:keys [on-move on-up]} (mf/ref-val drag-listeners-ref)]
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
@ -651,20 +729,19 @@
|
||||
(.removeEventListener viewport "pointercancel" on-up true))
|
||||
(mf/set-ref-val! drag-listeners-ref nil)))
|
||||
|
||||
reset-drag!
|
||||
reset-state
|
||||
(fn []
|
||||
(remove-drag-listeners!)
|
||||
(when (mf/ref-val moved-ref)
|
||||
(when (some? on-guide-drag)
|
||||
(on-guide-drag nil)))
|
||||
(remove-drag-listeners)
|
||||
(when (some? on-guide-drag)
|
||||
(on-guide-drag nil))
|
||||
(mf/set-ref-val! dragging-ref false)
|
||||
(mf/set-ref-val! moved-ref false)
|
||||
(mf/set-ref-val! start-ref nil)
|
||||
(mf/set-ref-val! guide-ref nil)
|
||||
(mf/set-ref-val! pending-ref nil)
|
||||
(reset! drag-state nil))
|
||||
(reset! state nil))
|
||||
|
||||
finish-drag!
|
||||
finish-drag
|
||||
(fn [event]
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(when (and (mf/ref-val moved-ref) (some? on-guide-change))
|
||||
@ -677,9 +754,9 @@
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(when (.-pointerId event)
|
||||
(.releasePointerCapture viewport (.-pointerId event))))
|
||||
(reset-drag!)))
|
||||
(reset-state)))
|
||||
|
||||
drag-move!
|
||||
drag-move
|
||||
(fn [move-event]
|
||||
(when (mf/ref-val dragging-ref)
|
||||
(when-let [guide (mf/ref-val guide-ref)]
|
||||
@ -696,82 +773,141 @@
|
||||
new-frame-id (-> (get-hover-frame) (get :id))
|
||||
pending {:guide guide
|
||||
:new-position new-position
|
||||
:new-frame-id new-frame-id}]
|
||||
:new-frame-id new-frame-id
|
||||
:mode :drag}]
|
||||
(when-not (mf/ref-val moved-ref)
|
||||
(mf/set-ref-val! moved-ref true)
|
||||
(when (some? on-guide-drag)
|
||||
(on-guide-drag (:id guide))))
|
||||
(mf/set-ref-val! pending-ref pending)
|
||||
(reset! drag-state pending)))))
|
||||
(reset! state pending)))))
|
||||
|
||||
pointer-down!
|
||||
editing?
|
||||
(fn [] (= :edit (:mode @state)))
|
||||
|
||||
guide-at-event
|
||||
(fn [event]
|
||||
(when-not read-only?
|
||||
(when-let [pt (uwvv/point->viewport (dom/get-client-position event))]
|
||||
(guide-by-serialized-index guides (wasm.api/find-guide-at pt zoom))))
|
||||
|
||||
pointer-down
|
||||
(fn [event]
|
||||
(when (and (not read-only?) (not (editing?)))
|
||||
;; While editing, any click outside the input commits the edit
|
||||
;; via the input's blur handler. Don't initiate a drag on the
|
||||
;; same pointerdown.
|
||||
(when (= 0 (.-button event))
|
||||
(let [position (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport position)
|
||||
index (when pt (wasm.api/find-guide-at pt zoom))
|
||||
guide (guide-by-serialized-index guides index)]
|
||||
(when (and guide (guide-draggable-in-focus? focus guide))
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(.setPointerCapture viewport (.-pointerId event)))
|
||||
(dom/stop-propagation event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! moved-ref false)
|
||||
(mf/set-ref-val! start-ref position)
|
||||
(mf/set-ref-val! guide-ref guide)
|
||||
(mf/set-ref-val! pending-ref
|
||||
{:guide guide
|
||||
:new-position (:position guide)
|
||||
:new-frame-id (:frame-id guide)})
|
||||
;; Pointer capture (above) routes all subsequent pointer
|
||||
;; events to the viewport, so we listen on the viewport
|
||||
;; itself rather than window. This keeps events flowing
|
||||
;; even outside the browser window.
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(let [on-move #(drag-move! %)
|
||||
on-up #(finish-drag! %)]
|
||||
(mf/set-ref-val! drag-listeners-ref
|
||||
{:on-move on-move :on-up on-up})
|
||||
(.addEventListener viewport "pointermove" on-move true)
|
||||
(.addEventListener viewport "pointerup" on-up true)
|
||||
(.addEventListener viewport "pointercancel" on-up true))))))))]
|
||||
(let [position (dom/get-client-position event)
|
||||
guide (guide-at-event event)]
|
||||
(when (and guide (guide-draggable-in-focus? focus guide))
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(.setPointerCapture viewport (.-pointerId event)))
|
||||
(dom/stop-propagation event)
|
||||
(mf/set-ref-val! dragging-ref true)
|
||||
(mf/set-ref-val! moved-ref false)
|
||||
(mf/set-ref-val! start-ref position)
|
||||
(mf/set-ref-val! guide-ref guide)
|
||||
(mf/set-ref-val! pending-ref
|
||||
{:guide guide
|
||||
:new-position (:position guide)
|
||||
:new-frame-id (:frame-id guide)
|
||||
:mode :drag})
|
||||
;; Pointer capture (above) routes all subsequent pointer
|
||||
;; events to the viewport, so we listen on the viewport
|
||||
;; itself rather than window. This keeps events flowing
|
||||
;; even outside the browser window.
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(let [on-move #(drag-move %)
|
||||
on-up #(finish-drag %)]
|
||||
(mf/set-ref-val! drag-listeners-ref
|
||||
{:on-move on-move :on-up on-up})
|
||||
(.addEventListener viewport "pointermove" on-move true)
|
||||
(.addEventListener viewport "pointerup" on-up true)
|
||||
(.addEventListener viewport "pointercancel" on-up true))))))))
|
||||
|
||||
double-click
|
||||
(fn [event]
|
||||
(when (and (not read-only?) (not (editing?)))
|
||||
(let [guide (guide-at-event event)]
|
||||
(when (and guide (guide-draggable-in-focus? focus guide))
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when (some? on-guide-drag)
|
||||
(on-guide-drag (:id guide)))
|
||||
(mf/set-ref-val! guide-ref guide)
|
||||
(let [frame (some-> (:frame-id guide) refs/object-by-id deref)
|
||||
offset (if frame
|
||||
(if (= :x (:axis guide)) (:x frame) (:y frame))
|
||||
0)]
|
||||
(reset! state {:guide guide
|
||||
:new-position (:position guide)
|
||||
:new-frame-id (:frame-id guide)
|
||||
:frame-offset offset
|
||||
:mode :edit}))))))
|
||||
|
||||
commit-edit
|
||||
(fn [raw-value]
|
||||
(when (editing?)
|
||||
(let [{:keys [guide new-frame-id frame-offset]} @state
|
||||
parsed (some-> raw-value str/trim d/parse-double)]
|
||||
(when (and (some? parsed) (some? on-guide-change))
|
||||
(on-guide-change (assoc guide
|
||||
:position (+ parsed frame-offset)
|
||||
:frame-id new-frame-id)))
|
||||
(reset-state))))
|
||||
|
||||
cancel-edit
|
||||
(fn []
|
||||
(when (editing?)
|
||||
(reset-state)))]
|
||||
|
||||
(mf/with-effect [wasm-guides? disabled-guides? read-only?
|
||||
guides zoom focus snap-pixel?
|
||||
on-guide-change on-guide-drag get-hover-frame]
|
||||
(when (and wasm-guides? (not disabled-guides?) (not read-only?))
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(.addEventListener viewport "pointerdown" pointer-down! true)
|
||||
(.addEventListener viewport "pointerdown" pointer-down true)
|
||||
(.addEventListener viewport "dblclick" double-click true)
|
||||
(fn []
|
||||
(.removeEventListener viewport "pointerdown" pointer-down! true)
|
||||
;; Only tear down drag state on real teardown. If this cleanup is
|
||||
;; triggered by a dependency change mid-drag, leave the active
|
||||
;; drag (and its listeners) untouched so it can finish.
|
||||
(when-not (mf/ref-val dragging-ref)
|
||||
(reset-drag!)))))))
|
||||
(.removeEventListener viewport "pointerdown" pointer-down true)
|
||||
(.removeEventListener viewport "dblclick" double-click true)
|
||||
;; Only tear down state on real teardown. If this cleanup is
|
||||
;; triggered by a dependency change mid-interaction, leave the
|
||||
;; active drag/edit (and its listeners) untouched so it can
|
||||
;; finish.
|
||||
(when-not (or (mf/ref-val dragging-ref) (editing?))
|
||||
(reset-state))))))
|
||||
|
||||
drag-state))
|
||||
{:state state
|
||||
:commit-edit commit-edit
|
||||
:cancel-edit cancel-edit})))
|
||||
|
||||
(mf/defc wasm-guide-drag-layer*
|
||||
"Owns WASM guide drag state and preview rendering so updates are not
|
||||
(mf/defc wasm-guide-overlay-layer*
|
||||
"Owns WASM guide drag/edit state and overlay rendering so updates are not
|
||||
blocked by memoization on `viewport-guides*`."
|
||||
[{:keys [guides zoom wasm-guides? disabled-guides? on-guide-change
|
||||
on-guide-drag get-hover-frame focus vbox]}]
|
||||
(let [drag-state
|
||||
(use-wasm-guide-drag {:guides guides
|
||||
:zoom zoom
|
||||
:wasm-guides? wasm-guides?
|
||||
:disabled-guides? disabled-guides?
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-drag on-guide-drag
|
||||
:get-hover-frame get-hover-frame
|
||||
:focus focus})]
|
||||
(when-let [{:keys [guide new-position]} @drag-state]
|
||||
[:> guide-drag-preview* {:guide guide
|
||||
:position new-position
|
||||
:vbox vbox
|
||||
:zoom zoom}])))
|
||||
(let [{:keys [state commit-edit cancel-edit]}
|
||||
(use-wasm-guide-interaction {:guides guides
|
||||
:zoom zoom
|
||||
:wasm-guides? wasm-guides?
|
||||
:disabled-guides? disabled-guides?
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-drag on-guide-drag
|
||||
:get-hover-frame get-hover-frame
|
||||
:focus focus})
|
||||
|
||||
{:keys [guide new-position mode frame-offset]} @state]
|
||||
|
||||
(when (some? guide)
|
||||
[:> guide-overlay* {:guide guide
|
||||
:position new-position
|
||||
:vbox vbox
|
||||
:zoom zoom
|
||||
:mode mode
|
||||
:frame-offset (or frame-offset 0)
|
||||
:on-input-commit commit-edit
|
||||
:on-input-cancel cancel-edit}])))
|
||||
|
||||
(mf/defc viewport-guides*
|
||||
{::mf/wrap [mf/memo]}
|
||||
@ -853,15 +989,15 @@
|
||||
:disabled-guides disabled-guides}]
|
||||
|
||||
(when wasm-guides?
|
||||
[:> wasm-guide-drag-layer* {:guides guides
|
||||
:zoom zoom
|
||||
:wasm-guides? wasm-guides?
|
||||
:disabled-guides? disabled-guides
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-drag on-guide-drag
|
||||
:get-hover-frame get-hover-frame
|
||||
:focus focus
|
||||
:vbox vbox}])
|
||||
[:> wasm-guide-overlay-layer* {:guides guides
|
||||
:zoom zoom
|
||||
:wasm-guides? wasm-guides?
|
||||
:disabled-guides? disabled-guides
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-drag on-guide-drag
|
||||
:get-hover-frame get-hover-frame
|
||||
:focus focus
|
||||
:vbox vbox}])
|
||||
|
||||
(when-not wasm-guides?
|
||||
(for [{:keys [id frame-id] :as guide} visible-guides]
|
||||
|
||||
@ -524,7 +524,6 @@ impl Surfaces {
|
||||
tile_viewbox: &TileViewbox,
|
||||
background: skia::Color,
|
||||
) {
|
||||
// self.backbuffer.canvas().clear(background);
|
||||
self.tiles.update(viewbox, tile_viewbox);
|
||||
let atlas_image = self.tile_atlas.image_snapshot();
|
||||
let canvas = self.backbuffer.canvas();
|
||||
|
||||
@ -59,7 +59,7 @@ impl GuidePool {
|
||||
return None;
|
||||
}
|
||||
|
||||
// FIXME: do binary search instead
|
||||
// NOTE: `partition_point` is a binary search, so this is O(log n)
|
||||
let idx = guides.partition_point(|guide| guide.position() < coord);
|
||||
let mut closest: Option<&Guide> = None;
|
||||
let mut closest_dist = world_tolerance;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user