mirror of
https://github.com/penpot/penpot.git
synced 2025-02-09 00:28:20 -05:00
Merge pull request #2114 from penpot/andrewzhurov-2645-hovering-layers-bounding-box
Layer outlines hightlight on hovering
This commit is contained in:
commit
aa95114860
8 changed files with 201 additions and 109 deletions
|
@ -5,12 +5,15 @@
|
|||
### :boom: Breaking changes & Deprecations
|
||||
### :sparkles: New features
|
||||
|
||||
- Add some cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688)
|
||||
- Add cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688)
|
||||
- Outline highlights on layer hovering [Taiga #2645](https://tree.taiga.io/project/penpot/us/2645) by @andrewzhurov
|
||||
|
||||
### :bug: Bugs fixed
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To @andrewzhurov for many code contributions on this release.
|
||||
|
||||
|
||||
## 1.15.0-beta
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
[app.main.data.workspace.fix-bool-contents :as fbc]
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.guides :as dwgu]
|
||||
[app.main.data.workspace.highlight :as dwh]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
[app.main.data.workspace.layers :as dwly]
|
||||
[app.main.data.workspace.layout :as layout]
|
||||
|
@ -1732,6 +1733,10 @@
|
|||
(dm/export dws/select-shape)
|
||||
(dm/export dws/shift-select-shapes)
|
||||
|
||||
;; Highlight
|
||||
(dm/export dwh/highlight-shape)
|
||||
(dm/export dwh/dehighlight-shape)
|
||||
|
||||
;; Groups
|
||||
(dm/export dwg/mask-group)
|
||||
(dm/export dwg/unmask-group)
|
||||
|
|
29
frontend/src/app/main/data/workspace/highlight.cljs
Normal file
29
frontend/src/app/main/data/workspace/highlight.cljs
Normal file
|
@ -0,0 +1,29 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.highlight
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[clojure.set :as set]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
;; --- Manage shape's highlight status
|
||||
|
||||
(defn highlight-shape
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::highlight-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :highlighted] set/union #{id}))))
|
||||
|
||||
(defn dehighlight-shape
|
||||
[id]
|
||||
(us/verify ::us/uuid id)
|
||||
(ptk/reify ::dehighlight-shape
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :highlighted] disj id))))
|
|
@ -251,6 +251,8 @@
|
|||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
|
||||
;; TODO: Looks like using the `=` comparator can be pretty expensive
|
||||
;; on large pages, we are using this for some reason?
|
||||
(def workspace-page-objects
|
||||
(l/derived wsh/lookup-page-objects st/state =))
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"A workspace specific context menu (mouse right click)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.main.data.events :as ev]
|
||||
|
@ -38,7 +39,7 @@
|
|||
(dom/stop-propagation event))
|
||||
|
||||
(mf/defc menu-entry
|
||||
[{:keys [title shortcut on-click children selected? icon] :as props}]
|
||||
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave on-unmount children selected? icon] :as props}]
|
||||
(let [submenu-ref (mf/use-ref nil)
|
||||
hovering? (mf/use-ref false)
|
||||
|
||||
|
@ -48,7 +49,8 @@
|
|||
(mf/set-ref-val! hovering? true)
|
||||
(let [submenu-node (mf/ref-val submenu-ref)]
|
||||
(when (some? submenu-node)
|
||||
(dom/set-css-property! submenu-node "display" "block")))))
|
||||
(dom/set-css-property! submenu-node "display" "block")))
|
||||
(when on-pointer-enter (on-pointer-enter))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-callback
|
||||
|
@ -59,7 +61,8 @@
|
|||
(timers/schedule
|
||||
200
|
||||
#(when-not (mf/ref-val hovering?)
|
||||
(dom/set-css-property! submenu-node "display" "none")))))))
|
||||
(dom/set-css-property! submenu-node "display" "none")))))
|
||||
(when on-pointer-leave (on-pointer-leave))))
|
||||
|
||||
set-dom-node
|
||||
(mf/use-callback
|
||||
|
@ -68,6 +71,11 @@
|
|||
(when (and (some? dom) (some? submenu-node))
|
||||
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps on-unmount)
|
||||
(constantly on-unmount))
|
||||
|
||||
(if icon
|
||||
[:li.icon-menu-item {:ref set-dom-node
|
||||
:on-click on-click
|
||||
|
@ -123,19 +131,34 @@
|
|||
[:& menu-separator]]))
|
||||
|
||||
(mf/defc context-menu-layer-position
|
||||
[{:keys [hover-objs shapes]}]
|
||||
(let [do-bring-forward #(st/emit! (dw/vertical-order-selected :up))
|
||||
do-bring-to-front #(st/emit! (dw/vertical-order-selected :top))
|
||||
do-send-backward #(st/emit! (dw/vertical-order-selected :down))
|
||||
do-send-to-back #(st/emit! (dw/vertical-order-selected :bottom))
|
||||
select-shapes (fn [id] #(st/emit! (dws/select-shape id)))]
|
||||
[{:keys [shapes]}]
|
||||
(let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
|
||||
do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top)))
|
||||
do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down)))
|
||||
do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom)))
|
||||
|
||||
select-shapes (fn [id] #(st/emit! (dws/select-shape id)))
|
||||
on-pointer-enter (fn [id] #(st/emit! (dw/highlight-shape id)))
|
||||
on-pointer-leave (fn [id] #(st/emit! (dw/dehighlight-shape id)))
|
||||
on-unmount (fn [id] #(st/emit! (dw/dehighlight-shape id)))
|
||||
|
||||
;; NOTE: we use deref instead of mf/deref on objects because
|
||||
;; we really don't want rerender on object changes
|
||||
hover-ids (deref refs/current-hover-ids)
|
||||
objects (deref refs/workspace-page-objects)
|
||||
hover-objs (into [] (keep (d/getf objects)) hover-ids)]
|
||||
|
||||
[:*
|
||||
(when (> (count hover-objs) 1)
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.select-layer")}
|
||||
(for [object hover-objs]
|
||||
[:& menu-entry {:title (:name object)
|
||||
:key (dm/str (:id object))
|
||||
:selected? (some #(= object %) shapes)
|
||||
:on-click (select-shapes (:id object))
|
||||
:on-pointer-enter (on-pointer-enter (:id object))
|
||||
:on-pointer-leave (on-pointer-leave (:id object))
|
||||
:on-unmount (on-unmount (:id object))
|
||||
:icon (si/element-icon {:shape object})}])])
|
||||
[:& menu-entry {:title (tr "workspace.shape.menu.forward")
|
||||
:shortcut (sc/get-tooltip :bring-forward)
|
||||
|
@ -435,14 +458,11 @@
|
|||
:on-click do-delete}]))
|
||||
|
||||
(mf/defc shape-context-menu
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [mdata] :as props}]
|
||||
(let [{:keys [disable-booleans? disable-flatten?]} mdata
|
||||
shapes (mf/deref refs/selected-objects)
|
||||
hover-ids (mf/deref refs/current-hover-ids)
|
||||
hover-objs (mf/deref (refs/objects-by-id hover-ids))
|
||||
|
||||
props #js {:shapes shapes
|
||||
:hover-objs hover-objs
|
||||
:disable-booleans? disable-booleans?
|
||||
:disable-flatten? disable-flatten?}]
|
||||
(when-not (empty? shapes)
|
||||
|
|
|
@ -84,91 +84,114 @@
|
|||
(:name shape "")
|
||||
(when (seq (:touched shape)) " *")])))
|
||||
|
||||
(defn- make-collapsed-iref
|
||||
[id]
|
||||
#(-> (l/in [:expanded id])
|
||||
(l/derived refs/workspace-local)))
|
||||
|
||||
(mf/defc layer-item
|
||||
[{:keys [index item selected objects] :as props}]
|
||||
(let [id (:id item)
|
||||
selected? (contains? selected id)
|
||||
container? (or (cph/frame-shape? item)
|
||||
(cph/group-shape? item))
|
||||
blocked? (:blocked item)
|
||||
hidden? (:hidden item)
|
||||
|
||||
disable-drag (mf/use-state false)
|
||||
scroll-to-middle? (mf/use-var true)
|
||||
expanded-iref (mf/with-memo [id]
|
||||
(-> (l/in [:expanded id])
|
||||
(l/derived refs/workspace-local)))
|
||||
|
||||
expanded-iref (mf/use-memo
|
||||
(mf/deps id)
|
||||
(make-collapsed-iref id))
|
||||
|
||||
expanded? (mf/deref expanded-iref)
|
||||
expanded? (mf/deref expanded-iref)
|
||||
selected? (contains? selected id)
|
||||
container? (or (cph/frame-shape? item)
|
||||
(cph/group-shape? item))
|
||||
|
||||
toggle-collapse
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (and expanded? (kbd/shift? event))
|
||||
(st/emit! (dwc/collapse-all))
|
||||
(st/emit! (dwc/toggle-collapse id))))
|
||||
(mf/use-fn
|
||||
(mf/deps expanded?)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (and expanded? (kbd/shift? event))
|
||||
(st/emit! (dwc/collapse-all))
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
toggle-blocking
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (:blocked item)
|
||||
(st/emit! (dw/update-shape-flags [id] {:blocked false}))
|
||||
(st/emit! (dw/update-shape-flags [id] {:blocked true})
|
||||
(dw/deselect-shape id))))
|
||||
(mf/use-fn
|
||||
(mf/deps id blocked?)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if blocked?
|
||||
(st/emit! (dw/update-shape-flags [id] {:blocked false}))
|
||||
(st/emit! (dw/update-shape-flags [id] {:blocked true})
|
||||
(dw/deselect-shape id)))))
|
||||
|
||||
toggle-visibility
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (:hidden item)
|
||||
(st/emit! (dw/update-shape-flags [id] {:hidden false}))
|
||||
(st/emit! (dw/update-shape-flags [id] {:hidden true}))))
|
||||
(mf/use-fn
|
||||
(mf/deps hidden?)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if hidden?
|
||||
(st/emit! (dw/update-shape-flags [id] {:hidden false}))
|
||||
(st/emit! (dw/update-shape-flags [id] {:hidden true})))))
|
||||
|
||||
select-shape
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(reset! scroll-to-middle? false)
|
||||
(let [id (:id item)]
|
||||
(cond
|
||||
(kbd/shift? event)
|
||||
(st/emit! (dw/shift-select-shapes id))
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(reset! scroll-to-middle? false)
|
||||
(cond
|
||||
(kbd/shift? event)
|
||||
(st/emit! (dw/shift-select-shapes id))
|
||||
|
||||
(kbd/mod? event)
|
||||
(st/emit! (dw/select-shape id true))
|
||||
(kbd/mod? event)
|
||||
(st/emit! (dw/select-shape id true))
|
||||
|
||||
(> (count selected) 1)
|
||||
(st/emit! (dw/select-shape id))
|
||||
:else
|
||||
(st/emit! (dw/select-shape id)))))
|
||||
(> (count selected) 1)
|
||||
(st/emit! (dw/select-shape id))
|
||||
|
||||
:else
|
||||
(st/emit! (dw/select-shape id)))))
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_event]
|
||||
(st/emit! (dw/highlight-shape id))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_event]
|
||||
(st/emit! (dw/dehighlight-shape id))))
|
||||
|
||||
on-context-menu
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(st/emit! (dw/show-shape-context-menu {:position pos
|
||||
:shape item}))))
|
||||
(mf/use-fn
|
||||
(mf/deps item)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(st/emit! (dw/show-shape-context-menu {:position pos :shape item})))))
|
||||
|
||||
on-drag
|
||||
(fn [{:keys [id]}]
|
||||
(when (not (contains? selected id))
|
||||
(st/emit! (dw/select-shape id))))
|
||||
(mf/use-fn
|
||||
(mf/deps id selected)
|
||||
(fn [{:keys [id]}]
|
||||
(when (not (contains? selected id))
|
||||
(st/emit! (dw/select-shape id)))))
|
||||
|
||||
on-drop
|
||||
(fn [side _data]
|
||||
(if (= side :center)
|
||||
(st/emit! (dw/relocate-selected-shapes (:id item) 0))
|
||||
(let [to-index (if (= side :top) (inc index) index)
|
||||
parent-id (cph/get-parent-id objects (:id item))]
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [side _data]
|
||||
(if (= side :center)
|
||||
(st/emit! (dw/relocate-selected-shapes id 0))
|
||||
(let [to-index (if (= side :top) (inc index) index)
|
||||
parent-id (cph/get-parent-id objects id)]
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))
|
||||
|
||||
on-hold
|
||||
(fn []
|
||||
(when-not expanded?
|
||||
(st/emit! (dwc/toggle-collapse (:id item)))))
|
||||
(mf/use-fn
|
||||
(mf/deps id expanded?)
|
||||
(fn []
|
||||
(when-not expanded?
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
[dprops dref] (hooks/use-sortable
|
||||
:data-type "penpot/layer"
|
||||
|
@ -183,25 +206,23 @@
|
|||
|
||||
ref (mf/use-ref)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps selected? selected)
|
||||
(fn []
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
(mf/with-effect [selected? selected]
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
|
||||
subid
|
||||
(when (and single? selected?)
|
||||
(let [scroll-to @scroll-to-middle?]
|
||||
(ts/schedule
|
||||
100
|
||||
#(if scroll-to
|
||||
(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})
|
||||
(do
|
||||
(dom/scroll-into-view-if-needed! node #js {:block "center", :behavior "smooth"})
|
||||
(reset! scroll-to-middle? true))))))]
|
||||
subid
|
||||
(when (and single? selected?)
|
||||
(let [scroll-to @scroll-to-middle?]
|
||||
(ts/schedule
|
||||
100
|
||||
#(if scroll-to
|
||||
(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})
|
||||
(do
|
||||
(dom/scroll-into-view-if-needed! node #js {:block "center", :behavior "smooth"})
|
||||
(reset! scroll-to-middle? true))))))]
|
||||
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid)))))
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid))))
|
||||
|
||||
[:li {:on-context-menu on-context-menu
|
||||
:ref dref
|
||||
|
@ -217,6 +238,8 @@
|
|||
[:div.element-list-body {:class (dom/classnames :selected selected?
|
||||
:icon-layer (= (:type item) :icon))
|
||||
:on-click select-shape
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-double-click #(dom/stop-propagation %)}
|
||||
[:& si/element-icon {:shape item}]
|
||||
[:& layer-name {:shape item
|
||||
|
@ -373,7 +396,7 @@
|
|||
(and
|
||||
(:show-search-box @filter-state)
|
||||
(or (d/not-empty? (:search-text @filter-state))
|
||||
(d/not-empty? (:active-filters @filter-state))))
|
||||
(d/not-empty? (:active-filters @filter-state))))
|
||||
|
||||
search-and-filters
|
||||
(fn [[id shape]]
|
||||
|
@ -426,7 +449,7 @@
|
|||
|
||||
[filtered-objects
|
||||
handle-show-more
|
||||
|
||||
|
||||
(mf/html
|
||||
(if (:show-search-box @filter-state)
|
||||
[:*
|
||||
|
@ -487,7 +510,7 @@
|
|||
(fn [entries]
|
||||
(when (and (.-isIntersecting (first entries)) (some? show-more))
|
||||
(show-more)))
|
||||
|
||||
|
||||
on-render-container
|
||||
(fn [element]
|
||||
(let [options #js {:root element}
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
panning
|
||||
selrect
|
||||
transform
|
||||
highlighted
|
||||
vbox
|
||||
vport
|
||||
zoom
|
||||
|
@ -286,6 +287,7 @@
|
|||
{:objects base-objects
|
||||
:selected selected
|
||||
:hover #{(:id @hover) @frame-hover}
|
||||
:highlighted highlighted
|
||||
:edition edition
|
||||
:zoom zoom}])
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.workspace.viewport.outline
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.util.object :as obj]
|
||||
|
@ -75,26 +76,33 @@
|
|||
:zoom zoom
|
||||
:color color}])))
|
||||
|
||||
(defn- show-outline?
|
||||
[shape]
|
||||
(and (not (:hidden shape))
|
||||
(not (:blocked shape))))
|
||||
|
||||
(mf/defc shape-outlines
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [selected (or (obj/get props "selected") #{})
|
||||
hover (or (obj/get props "hover") #{})
|
||||
(let [selected (or (obj/get props "selected") #{})
|
||||
hover (or (obj/get props "hover") #{})
|
||||
highlighted (or (obj/get props "highlighted") #{})
|
||||
|
||||
objects (obj/get props "objects")
|
||||
edition (obj/get props "edition")
|
||||
zoom (obj/get props "zoom")
|
||||
objects (obj/get props "objects")
|
||||
edition (obj/get props "edition")
|
||||
zoom (obj/get props "zoom")
|
||||
|
||||
outlines-ids (set/union selected hover)
|
||||
show-outline? (fn [shape] (and (not (:hidden shape))
|
||||
(not (:blocked shape))))
|
||||
lookup (d/getf objects)
|
||||
edition? (fn [o] (= edition o))
|
||||
|
||||
shapes (->> outlines-ids
|
||||
(filter #(not= edition %))
|
||||
(map #(get objects %))
|
||||
(filterv show-outline?)
|
||||
(filter some?))]
|
||||
shapes (-> #{}
|
||||
(into (comp (remove edition?)
|
||||
(keep lookup)
|
||||
(filter show-outline?))
|
||||
(set/union selected hover))
|
||||
(into (comp (remove edition?)
|
||||
(keep lookup))
|
||||
highlighted))]
|
||||
|
||||
[:g.outlines
|
||||
[:& shape-outlines-render {:shapes shapes
|
||||
:zoom zoom}]]))
|
||||
[:& shape-outlines-render {:shapes shapes :zoom zoom}]]))
|
||||
|
|
Loading…
Add table
Reference in a new issue