0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

Merge pull request #2114 from penpot/andrewzhurov-2645-hovering-layers-bounding-box

Layer outlines hightlight on hovering
This commit is contained in:
Alejandro 2022-07-28 11:43:43 +02:00 committed by GitHub
commit aa95114860
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 201 additions and 109 deletions

View file

@ -5,12 +5,15 @@
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
### :sparkles: New features ### :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 ### :bug: Bugs fixed
### :arrow_up: Deps updates ### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
- To @andrewzhurov for many code contributions on this release.
## 1.15.0-beta ## 1.15.0-beta

View file

@ -35,6 +35,7 @@
[app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.fix-bool-contents :as fbc]
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.guides :as dwgu] [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.interactions :as dwi]
[app.main.data.workspace.layers :as dwly] [app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout] [app.main.data.workspace.layout :as layout]
@ -1732,6 +1733,10 @@
(dm/export dws/select-shape) (dm/export dws/select-shape)
(dm/export dws/shift-select-shapes) (dm/export dws/shift-select-shapes)
;; Highlight
(dm/export dwh/highlight-shape)
(dm/export dwh/dehighlight-shape)
;; Groups ;; Groups
(dm/export dwg/mask-group) (dm/export dwg/mask-group)
(dm/export dwg/unmask-group) (dm/export dwg/unmask-group)

View 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))))

View file

@ -251,6 +251,8 @@
[page-id] [page-id]
(l/derived #(wsh/lookup-page-objects % page-id) st/state =)) (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 (def workspace-page-objects
(l/derived wsh/lookup-page-objects st/state =)) (l/derived wsh/lookup-page-objects st/state =))

View file

@ -8,6 +8,7 @@
"A workspace specific context menu (mouse right click)." "A workspace specific context menu (mouse right click)."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.main.data.events :as ev] [app.main.data.events :as ev]
@ -38,7 +39,7 @@
(dom/stop-propagation event)) (dom/stop-propagation event))
(mf/defc menu-entry (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) (let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false) hovering? (mf/use-ref false)
@ -48,7 +49,8 @@
(mf/set-ref-val! hovering? true) (mf/set-ref-val! hovering? true)
(let [submenu-node (mf/ref-val submenu-ref)] (let [submenu-node (mf/ref-val submenu-ref)]
(when (some? submenu-node) (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 on-pointer-leave
(mf/use-callback (mf/use-callback
@ -59,7 +61,8 @@
(timers/schedule (timers/schedule
200 200
#(when-not (mf/ref-val hovering?) #(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 set-dom-node
(mf/use-callback (mf/use-callback
@ -68,6 +71,11 @@
(when (and (some? dom) (some? submenu-node)) (when (and (some? dom) (some? submenu-node))
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))] (dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
(mf/use-effect
(mf/deps on-unmount)
(constantly on-unmount))
(if icon (if icon
[:li.icon-menu-item {:ref set-dom-node [:li.icon-menu-item {:ref set-dom-node
:on-click on-click :on-click on-click
@ -123,19 +131,34 @@
[:& menu-separator]])) [:& menu-separator]]))
(mf/defc context-menu-layer-position (mf/defc context-menu-layer-position
[{:keys [hover-objs shapes]}] [{:keys [shapes]}]
(let [do-bring-forward #(st/emit! (dw/vertical-order-selected :up)) (let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
do-bring-to-front #(st/emit! (dw/vertical-order-selected :top)) do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top)))
do-send-backward #(st/emit! (dw/vertical-order-selected :down)) do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down)))
do-send-to-back #(st/emit! (dw/vertical-order-selected :bottom)) do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom)))
select-shapes (fn [id] #(st/emit! (dws/select-shape id)))]
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) (when (> (count hover-objs) 1)
[:& menu-entry {:title (tr "workspace.shape.menu.select-layer")} [:& menu-entry {:title (tr "workspace.shape.menu.select-layer")}
(for [object hover-objs] (for [object hover-objs]
[:& menu-entry {:title (:name object) [:& menu-entry {:title (:name object)
:key (dm/str (:id object))
:selected? (some #(= object %) shapes) :selected? (some #(= object %) shapes)
:on-click (select-shapes (:id object)) :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})}])]) :icon (si/element-icon {:shape object})}])])
[:& menu-entry {:title (tr "workspace.shape.menu.forward") [:& menu-entry {:title (tr "workspace.shape.menu.forward")
:shortcut (sc/get-tooltip :bring-forward) :shortcut (sc/get-tooltip :bring-forward)
@ -435,14 +458,11 @@
:on-click do-delete}])) :on-click do-delete}]))
(mf/defc shape-context-menu (mf/defc shape-context-menu
{::mf/wrap [mf/memo]}
[{:keys [mdata] :as props}] [{:keys [mdata] :as props}]
(let [{:keys [disable-booleans? disable-flatten?]} mdata (let [{:keys [disable-booleans? disable-flatten?]} mdata
shapes (mf/deref refs/selected-objects) 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 props #js {:shapes shapes
:hover-objs hover-objs
:disable-booleans? disable-booleans? :disable-booleans? disable-booleans?
:disable-flatten? disable-flatten?}] :disable-flatten? disable-flatten?}]
(when-not (empty? shapes) (when-not (empty? shapes)

View file

@ -84,91 +84,114 @@
(:name shape "") (:name shape "")
(when (seq (:touched shape)) " *")]))) (when (seq (:touched shape)) " *")])))
(defn- make-collapsed-iref
[id]
#(-> (l/in [:expanded id])
(l/derived refs/workspace-local)))
(mf/defc layer-item (mf/defc layer-item
[{:keys [index item selected objects] :as props}] [{:keys [index item selected objects] :as props}]
(let [id (:id item) (let [id (:id item)
selected? (contains? selected id) blocked? (:blocked item)
container? (or (cph/frame-shape? item) hidden? (:hidden item)
(cph/group-shape? item))
disable-drag (mf/use-state false) disable-drag (mf/use-state false)
scroll-to-middle? (mf/use-var true) 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 expanded? (mf/deref expanded-iref)
(mf/deps id) selected? (contains? selected id)
(make-collapsed-iref id)) container? (or (cph/frame-shape? item)
(cph/group-shape? item))
expanded? (mf/deref expanded-iref)
toggle-collapse toggle-collapse
(fn [event] (mf/use-fn
(dom/stop-propagation event) (mf/deps expanded?)
(if (and expanded? (kbd/shift? event)) (fn [event]
(st/emit! (dwc/collapse-all)) (dom/stop-propagation event)
(st/emit! (dwc/toggle-collapse id)))) (if (and expanded? (kbd/shift? event))
(st/emit! (dwc/collapse-all))
(st/emit! (dwc/toggle-collapse id)))))
toggle-blocking toggle-blocking
(fn [event] (mf/use-fn
(dom/stop-propagation event) (mf/deps id blocked?)
(if (:blocked item) (fn [event]
(st/emit! (dw/update-shape-flags [id] {:blocked false})) (dom/stop-propagation event)
(st/emit! (dw/update-shape-flags [id] {:blocked true}) (if blocked?
(dw/deselect-shape id)))) (st/emit! (dw/update-shape-flags [id] {:blocked false}))
(st/emit! (dw/update-shape-flags [id] {:blocked true})
(dw/deselect-shape id)))))
toggle-visibility toggle-visibility
(fn [event] (mf/use-fn
(dom/stop-propagation event) (mf/deps hidden?)
(if (:hidden item) (fn [event]
(st/emit! (dw/update-shape-flags [id] {:hidden false})) (dom/stop-propagation event)
(st/emit! (dw/update-shape-flags [id] {:hidden true})))) (if hidden?
(st/emit! (dw/update-shape-flags [id] {:hidden false}))
(st/emit! (dw/update-shape-flags [id] {:hidden true})))))
select-shape select-shape
(fn [event] (mf/use-fn
(dom/prevent-default event) (mf/deps id)
(reset! scroll-to-middle? false) (fn [event]
(let [id (:id item)] (dom/prevent-default event)
(cond (reset! scroll-to-middle? false)
(kbd/shift? event) (cond
(st/emit! (dw/shift-select-shapes id)) (kbd/shift? event)
(st/emit! (dw/shift-select-shapes id))
(kbd/mod? event) (kbd/mod? event)
(st/emit! (dw/select-shape id true)) (st/emit! (dw/select-shape id true))
(> (count selected) 1) (> (count selected) 1)
(st/emit! (dw/select-shape id)) (st/emit! (dw/select-shape id))
:else
(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 on-context-menu
(fn [event] (mf/use-fn
(dom/prevent-default event) (mf/deps item)
(dom/stop-propagation event) (fn [event]
(let [pos (dom/get-client-position event)] (dom/prevent-default event)
(st/emit! (dw/show-shape-context-menu {:position pos (dom/stop-propagation event)
:shape item})))) (let [pos (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position pos :shape item})))))
on-drag on-drag
(fn [{:keys [id]}] (mf/use-fn
(when (not (contains? selected id)) (mf/deps id selected)
(st/emit! (dw/select-shape id)))) (fn [{:keys [id]}]
(when (not (contains? selected id))
(st/emit! (dw/select-shape id)))))
on-drop on-drop
(fn [side _data] (mf/use-fn
(if (= side :center) (mf/deps id)
(st/emit! (dw/relocate-selected-shapes (:id item) 0)) (fn [side _data]
(let [to-index (if (= side :top) (inc index) index) (if (= side :center)
parent-id (cph/get-parent-id objects (:id item))] (st/emit! (dw/relocate-selected-shapes id 0))
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))) (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 on-hold
(fn [] (mf/use-fn
(when-not expanded? (mf/deps id expanded?)
(st/emit! (dwc/toggle-collapse (:id item))))) (fn []
(when-not expanded?
(st/emit! (dwc/toggle-collapse id)))))
[dprops dref] (hooks/use-sortable [dprops dref] (hooks/use-sortable
:data-type "penpot/layer" :data-type "penpot/layer"
@ -183,25 +206,23 @@
ref (mf/use-ref)] ref (mf/use-ref)]
(mf/use-effect (mf/with-effect [selected? selected]
(mf/deps selected? selected) (let [single? (= (count selected) 1)
(fn [] node (mf/ref-val ref)
(let [single? (= (count selected) 1)
node (mf/ref-val ref)
subid subid
(when (and single? selected?) (when (and single? selected?)
(let [scroll-to @scroll-to-middle?] (let [scroll-to @scroll-to-middle?]
(ts/schedule (ts/schedule
100 100
#(if scroll-to #(if scroll-to
(dom/scroll-into-view! node #js {:block "center", :behavior "smooth"}) (dom/scroll-into-view! node #js {:block "center", :behavior "smooth"})
(do (do
(dom/scroll-into-view-if-needed! node #js {:block "center", :behavior "smooth"}) (dom/scroll-into-view-if-needed! node #js {:block "center", :behavior "smooth"})
(reset! scroll-to-middle? true))))))] (reset! scroll-to-middle? true))))))]
#(when (some? subid) #(when (some? subid)
(rx/dispose! subid))))) (rx/dispose! subid))))
[:li {:on-context-menu on-context-menu [:li {:on-context-menu on-context-menu
:ref dref :ref dref
@ -217,6 +238,8 @@
[:div.element-list-body {:class (dom/classnames :selected selected? [:div.element-list-body {:class (dom/classnames :selected selected?
:icon-layer (= (:type item) :icon)) :icon-layer (= (:type item) :icon))
:on-click select-shape :on-click select-shape
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-double-click #(dom/stop-propagation %)} :on-double-click #(dom/stop-propagation %)}
[:& si/element-icon {:shape item}] [:& si/element-icon {:shape item}]
[:& layer-name {:shape item [:& layer-name {:shape item
@ -373,7 +396,7 @@
(and (and
(:show-search-box @filter-state) (:show-search-box @filter-state)
(or (d/not-empty? (:search-text @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 search-and-filters
(fn [[id shape]] (fn [[id shape]]
@ -426,7 +449,7 @@
[filtered-objects [filtered-objects
handle-show-more handle-show-more
(mf/html (mf/html
(if (:show-search-box @filter-state) (if (:show-search-box @filter-state)
[:* [:*
@ -487,7 +510,7 @@
(fn [entries] (fn [entries]
(when (and (.-isIntersecting (first entries)) (some? show-more)) (when (and (.-isIntersecting (first entries)) (some? show-more))
(show-more))) (show-more)))
on-render-container on-render-container
(fn [element] (fn [element]
(let [options #js {:root element} (let [options #js {:root element}

View file

@ -53,6 +53,7 @@
panning panning
selrect selrect
transform transform
highlighted
vbox vbox
vport vport
zoom zoom
@ -286,6 +287,7 @@
{:objects base-objects {:objects base-objects
:selected selected :selected selected
:hover #{(:id @hover) @frame-hover} :hover #{(:id @hover) @frame-hover}
:highlighted highlighted
:edition edition :edition edition
:zoom zoom}]) :zoom zoom}])

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.viewport.outline (ns app.main.ui.workspace.viewport.outline
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.util.object :as obj] [app.util.object :as obj]
@ -75,26 +76,33 @@
:zoom zoom :zoom zoom
:color color}]))) :color color}])))
(defn- show-outline?
[shape]
(and (not (:hidden shape))
(not (:blocked shape))))
(mf/defc shape-outlines (mf/defc shape-outlines
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [selected (or (obj/get props "selected") #{}) (let [selected (or (obj/get props "selected") #{})
hover (or (obj/get props "hover") #{}) hover (or (obj/get props "hover") #{})
highlighted (or (obj/get props "highlighted") #{})
objects (obj/get props "objects") objects (obj/get props "objects")
edition (obj/get props "edition") edition (obj/get props "edition")
zoom (obj/get props "zoom") zoom (obj/get props "zoom")
outlines-ids (set/union selected hover) lookup (d/getf objects)
show-outline? (fn [shape] (and (not (:hidden shape)) edition? (fn [o] (= edition o))
(not (:blocked shape))))
shapes (->> outlines-ids shapes (-> #{}
(filter #(not= edition %)) (into (comp (remove edition?)
(map #(get objects %)) (keep lookup)
(filterv show-outline?) (filter show-outline?))
(filter some?))] (set/union selected hover))
(into (comp (remove edition?)
(keep lookup))
highlighted))]
[:g.outlines [:g.outlines
[:& shape-outlines-render {:shapes shapes [:& shape-outlines-render {:shapes shapes :zoom zoom}]]))
:zoom zoom}]]))