From 78381873eba030882ebca1b9792c87d5c9159812 Mon Sep 17 00:00:00 2001 From: Dream <42954461+eureka0928@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:03:28 -0400 Subject: [PATCH] :sparkles: Edit ruler guide position by double-clicking the guide pill (#8987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drag-and-drop is the only way to move a ruler guide today, which makes hitting an exact pixel painful. Double-clicking the guide pill now swaps the position label for a numeric input — Enter commits, Escape cancels — so users can type a precise value relative to the guide's frame (or canvas). Closes #2311 Signed-off-by: eureka0928 --- CHANGES.md | 1 + .../main/ui/workspace/viewport/guides.cljs | 125 +++++++++++++++--- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f7ba9d16c0..f154f154bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,7 @@ - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913) - Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270) +- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index d542d983c9..bb6df6e965 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.guides (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] @@ -23,6 +24,8 @@ [app.main.ui.formats :as fmt] [app.main.ui.workspace.viewport.rulers :as rulers] [app.util.dom :as dom] + [app.util.keyboard :as kbd] + [cuerdas.core :as str] [rumext.v2 :as mf])) (def ^:const guide-width 1) @@ -286,6 +289,18 @@ (let [axis (get guide :axis) + read-only? + (mf/use-ctx ctx/workspace-read-only?) + + is-editing* + (mf/use-state false) + + is-editing + (deref is-editing*) + + input-ref + (mf/use-ref nil) + handle-change-position (mf/use-fn (mf/deps on-guide-change) @@ -329,7 +344,55 @@ frame-guide-outside? (and (some? frame) - (not (is-guide-inside-frame? (assoc guide :position pos) frame)))] + (not (is-guide-inside-frame? (assoc guide :position pos) frame))) + + frame-offset + (if (some? frame) + (if (= axis :x) (:x frame) (:y frame)) + 0) + + accept-editing + (mf/use-fn + (mf/deps frame-offset on-guide-change guide) + (fn [] + ;; Enter both fires this and triggers a blur that calls it again; + ;; bail out on the second invocation when the input is already gone. + (when-let [input (mf/ref-val input-ref)] + (let [parsed (-> input dom/get-value str/trim d/parse-double)] + (reset! is-editing* false) + (when (and (some? parsed) (some? on-guide-change)) + (on-guide-change (assoc guide :position (+ parsed frame-offset)))))))) + + cancel-editing + (mf/use-fn + #(reset! is-editing* false)) + + on-input-key-down + (mf/use-fn + (mf/deps accept-editing cancel-editing) + (fn [event] + (cond + (kbd/enter? event) + (do (dom/prevent-default event) + (dom/stop-propagation event) + (accept-editing)) + + (kbd/esc? event) + (do (dom/prevent-default event) + (dom/stop-propagation event) + (cancel-editing))))) + + on-double-click + (mf/use-fn + (mf/deps read-only?) + (fn [event] + (when-not read-only? + (dom/stop-propagation event) + (reset! is-editing* true))))] + + (mf/with-effect [is-editing] + (when is-editing + (some-> (mf/ref-val input-ref) dom/select-text!))) (when (or (nil? frame) (and (cfh/root-frame? frame) @@ -349,7 +412,8 @@ :on-pointer-down on-pointer-down :on-pointer-up on-pointer-up :on-lost-pointer-capture on-lost-pointer-capture - :on-pointer-move on-pointer-move}])) + :on-pointer-move on-pointer-move + :on-double-click on-double-click}])) (if (some? frame) (let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2 @@ -398,9 +462,12 @@ guide-opacity-hover guide-opacity)}}])) - (when (or is-hover (:hover @state)) + (when (or is-hover (:hover @state) is-editing) (let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]} - (guide-pill-axis pos vbox zoom axis)] + (guide-pill-axis pos vbox zoom axis) + display-value (fmt/format-number (- pos frame-offset)) + input-w (/ guide-pill-width zoom) + input-h (/ guide-pill-height zoom)] [:g.guide-pill [:rect {:x rect-x :y rect-y @@ -408,18 +475,46 @@ :height rect-height :rx guide-pill-corner-radius :ry guide-pill-corner-radius - :style {:fill guide-color}}] + :style {:fill guide-color} + :on-double-click on-double-click}] - [:text {:x text-x - :y text-y - :text-anchor "middle" - :dominant-baseline "middle" - :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) - :style {:font-size (/ rulers/font-size zoom) - :font-family rulers/font-family - :fill colors/white}} - ;; If the guide is associated to a frame we show the position relative to the frame - (fmt/format-number (- pos (if (= axis :x) (:x frame) (:y frame))))]]))]))) + (if is-editing + [: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-input-key-down + :on-blur accept-editing + :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"}}]] + [:text {:x text-x + :y text-y + :text-anchor "middle" + :dominant-baseline "middle" + :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) + :style {:font-size (/ rulers/font-size zoom) + :font-family rulers/font-family + :fill colors/white}} + ;; If the guide is associated to a frame we show the position relative to the frame + display-value])]))]))) (mf/defc new-guide-area* [{:keys [vbox zoom axis get-hover-frame disabled-guides]}]