diff --git a/CHANGES.md b/CHANGES.md index 2db27cda3..9bb6075c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 1b734551c..62dc383ad 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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) diff --git a/frontend/src/app/main/data/workspace/highlight.cljs b/frontend/src/app/main/data/workspace/highlight.cljs new file mode 100644 index 000000000..f34a1b323 --- /dev/null +++ b/frontend/src/app/main/data/workspace/highlight.cljs @@ -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)))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index ce4f0b8d5..46302a4a1 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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 =)) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 11003df3a..e77e8c0ba 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 05ad9bdfd..dc82c6deb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -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} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 3dfbe8cb8..3a1b6e05a 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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}]) diff --git a/frontend/src/app/main/ui/workspace/viewport/outline.cljs b/frontend/src/app/main/ui/workspace/viewport/outline.cljs index d1f6eaf46..87498e154 100644 --- a/frontend/src/app/main/ui/workspace/viewport/outline.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/outline.cljs @@ -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}]]))