From 78d7fe3e1057522d9e226d89b781f2d7aa451216 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 23 Feb 2022 16:30:01 +0100 Subject: [PATCH] :sparkles: New focus mode in workspace --- CHANGES.md | 1 + common/src/app/common/pages.cljc | 7 + common/src/app/common/pages/focus.cljc | 51 ++++++ common/src/app/common/pages/helpers.cljc | 11 ++ .../styles/main/partials/sidebar.scss | 37 +++++ frontend/src/app/main/data/workspace.cljs | 149 +++--------------- .../app/main/data/workspace/drawing/box.cljs | 5 +- .../app/main/data/workspace/selection.cljs | 71 ++++++++- .../app/main/data/workspace/shortcuts.cljs | 10 +- .../app/main/data/workspace/transforms.cljs | 9 +- .../src/app/main/data/workspace/zoom.cljs | 123 +++++++++++++++ frontend/src/app/main/refs.cljs | 3 + frontend/src/app/main/snap.cljs | 25 +-- frontend/src/app/main/ui/hooks.cljs | 13 ++ .../app/main/ui/workspace/context_menu.cljs | 26 ++- .../app/main/ui/workspace/shapes/frame.cljs | 3 +- .../app/main/ui/workspace/sidebar/layers.cljs | 23 ++- .../src/app/main/ui/workspace/viewport.cljs | 19 ++- .../ui/workspace/viewport/frame_grid.cljs | 9 +- .../main/ui/workspace/viewport/guides.cljs | 21 ++- .../app/main/ui/workspace/viewport/hooks.cljs | 11 +- .../ui/workspace/viewport/snap_points.cljs | 7 +- .../main/ui/workspace/viewport/widgets.cljs | 4 +- frontend/src/app/util/snap_data.cljs | 1 + frontend/translations/en.po | 12 ++ frontend/translations/es.po | 12 ++ 26 files changed, 484 insertions(+), 179 deletions(-) create mode 100644 common/src/app/common/pages/focus.cljc create mode 100644 frontend/src/app/main/data/workspace/zoom.cljs diff --git a/CHANGES.md b/CHANGES.md index 8f9429d144..713eb58eba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797) - Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394) - Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283) +- New focus mode in workspace [Taiga #2748](https://tree.taiga.io/project/penpot/us/2748) - Changed text shapes to be displayed as natives SVG text elements [Taiga #2759](https://tree.taiga.io/project/penpot/us/2759) - Texts now can have strokes, multiple fills and can be used as masks diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc index dce949453b..496c7ccb7c 100644 --- a/common/src/app/common/pages.cljc +++ b/common/src/app/common/pages.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.pages.changes :as changes] [app.common.pages.common :as common] + [app.common.pages.focus :as focus] [app.common.pages.indices :as indices] [app.common.pages.init :as init])) @@ -19,6 +20,11 @@ (dm/export common/default-color) (dm/export common/component-sync-attrs) +;; Focus +(dm/export focus/focus-objects) +(dm/export focus/filter-not-focus) +(dm/export focus/is-in-focus?) + ;; Indices (dm/export indices/calculate-z-index) (dm/export indices/update-z-index) @@ -36,3 +42,4 @@ (dm/export init/make-minimal-shape) (dm/export init/make-minimal-group) (dm/export init/empty-file-data) + diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/pages/focus.cljc new file mode 100644 index 0000000000..df0f2d351f --- /dev/null +++ b/common/src/app/common/pages/focus.cljc @@ -0,0 +1,51 @@ +;; 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.common.pages.focus + (:require + [app.common.data :as d] + [app.common.pages.helpers :as cph] + [app.common.pages.indices :as cpi] + [app.common.uuid :as uuid])) + +(defn focus-objects + [objects focus] + (let [[ids-with-children z-index] + (when (d/not-empty? focus) + [(into (conj focus uuid/zero) + (mapcat (partial cph/get-children-ids objects)) + focus) + (cpi/calculate-z-index objects)]) + + sort-by-z-index + (fn [coll] + (->> coll (sort-by (fn [a b] (- (get z-index a) (get z-index b))))))] + + (cond-> objects + (some? ids-with-children) + (-> (select-keys ids-with-children) + (assoc-in [uuid/zero :shapes] (sort-by-z-index focus)))))) + +(defn filter-not-focus + [objects focus ids] + + (let [focused-ids + (when (d/not-empty? focus) + (into focus + (mapcat (partial cph/get-children-ids objects)) + focus))] + + (if (some? focused-ids) + (into (d/ordered-set) + (filter #(contains? focused-ids %)) + ids) + ids))) + +(defn is-in-focus? + [objects focus id] + (d/seek + #(contains? focus %) + (cph/get-parents-seq objects id))) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 8fef5abc85..838349bd4f 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -74,6 +74,16 @@ [objects id] (-> objects (get id) :parent-id)) +(defn get-parents-seq + [objects shape-id] + + (cond + (nil? shape-id) + nil + + :else + (lazy-seq (cons shape-id (get-parents-seq objects (get-in objects [shape-id :parent-id])))))) + (defn get-parent-ids "Returns a vector of parents of the specified shape." [objects shape-id] @@ -463,3 +473,4 @@ [path name] (let [path-split (split-path path)] (merge-path-item (first path-split) name))) + diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index e086a1d06b..f239f225ec 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -113,6 +113,43 @@ } } } + + & .focus-title { + width: 100%; + height: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + grid-column-gap: 8px; + + & .back-button { + cursor: pointer; + background: none; + border: none; + transform: rotate(180deg); + padding: 0; + + &:hover { + svg { + fill: $color-primary; + } + } + + & svg { + fill: $color-white; + } + } + + & .focus-mode { + color: $color-primary; + border: 1px solid $color-primary; + border-radius: 3px; + font-size: 10px; + text-transform: uppercase; + padding: 0px 4px; + display: flex; + align-items: center; + } + } } .assets-bar .tool-window { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index f24f605469..6874f0ee16 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -10,7 +10,6 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.align :as gal] - [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] @@ -44,6 +43,7 @@ [app.main.data.workspace.svg-upload :as svg] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.zoom :as dwz] [app.main.repo :as rp] [app.main.streams :as ms] [app.main.worker :as uw] @@ -294,7 +294,7 @@ exit-workspace? (not= :workspace (get-in state [:route :data :name]))] (cond-> (assoc-in state [:workspace-cache page-id] local) :always - (dissoc :current-page-id :workspace-local :trimmed-page) + (dissoc :current-page-id :workspace-local :trimmed-page :workspace-focus-selected) exit-workspace? (dissoc :workspace-drawing)))))) @@ -478,11 +478,6 @@ ;; --- Viewport Sizing -(declare increase-zoom) -(declare decrease-zoom) -(declare set-zoom) -(declare zoom-to-fit-all) - (defn initialize-viewport [{:keys [width height] :as size}] (letfn [(update* [{:keys [vport] :as local}] @@ -612,114 +607,8 @@ (-> state (update :workspace-local dissoc :panning))))) -(defn start-zooming [pt] - (ptk/reify ::start-zooming - ptk/WatchEvent - (watch [_ state stream] - (let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))] - (when-not (get-in state [:workspace-local :zooming]) - (rx/concat - (rx/of #(-> % (assoc-in [:workspace-local :zooming] true))) - (->> stream - (rx/filter ms/pointer-event?) - (rx/filter #(= :delta (:source %))) - (rx/map :pt) - (rx/take-until stopper) - (rx/map (fn [delta] - (let [scale (+ 1 (/ (:y delta) 100))] ;; this number may be adjusted after user testing - (set-zoom pt scale))))))))))) -(defn finish-zooming [] - (ptk/reify ::finish-zooming - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-local dissoc :zooming))))) -;; --- Zoom Management - -(defn- impl-update-zoom - [{:keys [vbox] :as local} center zoom] - (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) - old-zoom (:zoom local) - center (if center center (gsh/center-rect vbox)) - scale (/ old-zoom new-zoom) - mtx (gmt/scale-matrix (gpt/point scale) center) - vbox' (gsh/transform-rect vbox mtx)] - (-> local - (assoc :zoom new-zoom) - (update :vbox merge (select-keys vbox' [:x :y :width :height]))))) - -(defn increase-zoom - [center] - (ptk/reify ::increase-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))) - -(defn decrease-zoom - [center] - (ptk/reify ::decrease-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))) - -(defn set-zoom - [center scale] - (ptk/reify ::set-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (-> (* z scale) - (max 0.01) - (min 200)))))))) - -(def reset-zoom - (ptk/reify ::reset-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % nil 1))))) - -(def zoom-to-fit-all - (ptk/reify ::zoom-to-fit-all - ptk/UpdateEvent - (update [_ state] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shapes (cph/get-immediate-children objects) - srect (gsh/selection-rect shapes)] - (if (empty? shapes) - state - (update state :workspace-local - (fn [{:keys [vport] :as local}] - (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) - zoom (/ (:width vport) (:width srect))] - (-> local - (assoc :zoom zoom) - (update :vbox merge srect)))))))))) - -(def zoom-to-selected-shape - (ptk/reify ::zoom-to-selected-shape - ptk/UpdateEvent - (update [_ state] - (let [selected (wsh/lookup-selected state)] - (if (empty? selected) - state - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - srect (->> selected - (map #(get objects %)) - (gsh/selection-rect))] - (update state :workspace-local - (fn [{:keys [vport] :as local}] - (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) - zoom (/ (:width vport) (:width srect))] - (-> local - (assoc :zoom zoom) - (update :vbox merge srect))))))))))) ;; --- Update Shape Attrs @@ -1887,21 +1776,25 @@ (dm/export dwp/clone-media-object) (dm/export dwc/image-uploaded) -;; Selection - -(dm/export dws/select-shape) -(dm/export dws/deselect-shape) -(dm/export dws/select-all) -(dm/export dws/deselect-all) +;; Common +(dm/export dwc/add-shape) +(dm/export dwc/clear-edition-mode) (dm/export dwc/select-shapes) -(dm/export dws/shift-select-shapes) +(dm/export dwc/start-edition-mode) + +;; Drawing +(dm/export dwd/select-for-drawing) + +;; Selection +(dm/export dws/toggle-focus-mode) +(dm/export dws/deselect-all) +(dm/export dws/deselect-shape) (dm/export dws/duplicate-selected) (dm/export dws/handle-area-selection) +(dm/export dws/select-all) (dm/export dws/select-inside-group) -(dm/export dwd/select-for-drawing) -(dm/export dwc/clear-edition-mode) -(dm/export dwc/add-shape) -(dm/export dwc/start-edition-mode) +(dm/export dws/select-shape) +(dm/export dws/shift-select-shapes) ;; Groups @@ -1924,3 +1817,11 @@ (dm/export dwgu/remove-guide) (dm/export dwgu/set-hover-guide) +;; Zoom +(dm/export dwz/reset-zoom) +(dm/export dwz/zoom-to-selected-shape) +(dm/export dwz/start-zooming) +(dm/export dwz/finish-zooming) +(dm/export dwz/zoom-to-fit-all) +(dm/export dwz/decrease-zoom) +(dm/export dwz/increase-zoom) diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 561a372b87..f8c9a75925 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -60,6 +60,7 @@ page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) layout (get state :workspace-layout) + focus (:workspace-focus-selected state) zoom (get-in state [:workspace-local :zoom] 1) frames (cph/get-frames objects) @@ -80,7 +81,7 @@ (rx/of #(assoc-in state [:workspace-drawing :object] shape)) ;; Initial SNAP - (->> (snap/closest-snap-point page-id [shape] layout zoom initial) + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus initial) (rx/map move-drawing)) (->> ms/mouse-position @@ -88,7 +89,7 @@ (rx/with-latest vector ms/mouse-position-shift) (rx/switch-map (fn [[point :as current]] - (->> (snap/closest-snap-point page-id [shape] layout zoom point) + (->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point) (rx/map #(conj current %))))) (rx/map (fn [[_ shift? point]] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 4905c3916b..ac7d6907a5 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] [app.common.math :as mth] + [app.common.pages :as cp] [app.common.pages.changes-builder :as pcb] [app.common.pages.helpers :as cph] [app.common.spec :as us] @@ -20,10 +21,13 @@ [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.zoom :as dwz] + [app.main.refs :as refs] [app.main.streams :as ms] [app.main.worker :as uw] [beicon.core :as rx] [cljs.spec.alpha :as s] + [clojure.set :as set] [linked.set :as lks] [potok.core :as ptk])) @@ -161,7 +165,12 @@ (ptk/reify ::select-shapes ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :selected] ids)) + (let [objects (wsh/lookup-page-objects state) + focus (:workspace-focus-selected state) + ids (if (d/not-empty? focus) + (cp/filter-not-focus objects focus ids) + ids)] + (assoc-in state [:workspace-local :selected] ids))) ptk/WatchEvent (watch [_ state _] @@ -173,8 +182,9 @@ (ptk/reify ::select-all ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [focus (:workspace-focus-selected state) + objects (-> (wsh/lookup-page-objects state) + (cp/focus-objects focus)) selected (let [frame-ids (into #{} (comp (map (d/getf objects)) @@ -484,8 +494,8 @@ id-duplicated (when (= (count selected) 1) (first selected))] - (rx/of (select-shapes selected) - (dch/commit-changes changes) + (rx/of (dch/commit-changes changes) + (select-shapes selected) (memorize-duplicated id-original id-duplicated))))))) (defn change-hover-state @@ -495,3 +505,54 @@ (update [_ state] (let [hover-value (if value #{id} #{})] (assoc-in state [:workspace-local :hover] hover-value))))) + +(defn update-focus-shapes + [added removed] + (ptk/reify ::update-focus-shapes + ptk/UpdateEvent + (update [_ state] + + (let [objects (wsh/lookup-page-objects state) + + focus (-> (:workspace-focus-selected state) + (set/union added) + (set/difference removed)) + focus (cph/clean-loops objects focus)] + + (-> state + (assoc :workspace-focus-selected focus)))))) + +(defn toggle-focus-mode + [] + (ptk/reify ::toggle-focus-mode + ptk/UpdateEvent + (update [_ state] + (let [selected (wsh/lookup-selected state)] + (cond-> state + (and (empty? (:workspace-focus-selected state)) + (d/not-empty? selected)) + (assoc :workspace-focus-selected selected) + + (d/not-empty? (:workspace-focus-selected state)) + (dissoc :workspace-focus-selected)))) + + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (rx/filter #(or (= ::toggle-focus-mode (ptk/type %)) + (= :app.main.data.workspace/finalize-page (ptk/type %))) stream)] + (when (d/not-empty? (:workspace-focus-selected state)) + (rx/merge + (rx/of dwz/zoom-to-selected-shape + (deselect-all)) + (->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) + (rx/take-until stopper) + (rx/map (comp set keys)) + (rx/buffer 2 1) + (rx/merge-map + (fn [[old-keys new-keys]] + (let [removed (set/difference old-keys new-keys) + added (set/difference new-keys old-keys)] + + (if (or (d/not-empty? added) (d/not-empty? removed)) + (rx/of (update-focus-shapes added removed)) + (rx/empty)))))))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 64fcba6299..3b825a3474 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -352,11 +352,13 @@ :command (ds/c-mod "alt+g") :fn #(st/emit! (dw/create-artboard-from-selection))} - :hide-ui {:tooltip "\\" - :command "\\" - :fn #(st/emit! (dw/toggle-layout-flags :hide-ui))} + :hide-ui {:tooltip "\\" + :command "\\" + :fn #(st/emit! (dw/toggle-layout-flags :hide-ui))} - }) + :toggle-focus-mode {:command "f" + :tooltip "F" + :fn #(st/emit! (dw/toggle-focus-mode))}}) (def opacity-shortcuts (into {} (->> diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index d81e2bc775..330c325130 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -375,6 +375,7 @@ stoper (rx/filter ms/mouse-up? stream) layout (:workspace-layout state) page-id (:current-page-id state) + focus (:workspace-focus-selected state) zoom (get-in state [:workspace-local :zoom] 1) objects (wsh/lookup-page-objects state page-id) resizing-shapes (map #(get objects %) ids) @@ -387,7 +388,7 @@ (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) (rx/map normalize-proportion-lock) (rx/switch-map (fn [[point _ _ :as current]] - (->> (snap/closest-snap-point page-id resizing-shapes layout zoom point) + (->> (snap/closest-snap-point page-id resizing-shapes objects layout zoom focus point) (rx/map #(conj current %))))) (rx/mapcat (partial resize shape initial-position layout)) (rx/take-until stoper)) @@ -509,7 +510,6 @@ (rx/of (start-move initial selected))))) (rx/take-until stopper))))))) - (defn- start-move-duplicate [from-position] (ptk/reify ::start-move-duplicate @@ -544,6 +544,7 @@ stopper (rx/filter ms/mouse-up? stream) layout (get state :workspace-layout) zoom (get-in state [:workspace-local :zoom] 1) + focus (:workspace-focus-selected state) fix-axis (fn [[position shift?]] (let [delta (gpt/to-vec from-position position)] @@ -564,10 +565,10 @@ (rx/throttle 20) (rx/switch-map (fn [pos] - (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) + (->> (snap/closest-snap-move page-id shapes objects layout zoom focus pos) (rx/map #(vector pos %)))))))] (if (empty? shapes) - (rx/empty) + (rx/of (finish-transform)) (rx/concat (->> position (rx/with-latest vector snap-delta) diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs new file mode 100644 index 0000000000..9d8e19286a --- /dev/null +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -0,0 +1,123 @@ +;; 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.zoom + (:require + [app.common.geom.align :as gal] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as cph] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.streams :as ms] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn- impl-update-zoom + [{:keys [vbox] :as local} center zoom] + (let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom) + old-zoom (:zoom local) + center (if center center (gsh/center-rect vbox)) + scale (/ old-zoom new-zoom) + mtx (gmt/scale-matrix (gpt/point scale) center) + vbox' (gsh/transform-rect vbox mtx)] + (-> local + (assoc :zoom new-zoom) + (update :vbox merge (select-keys vbox' [:x :y :width :height]))))) + +(defn increase-zoom + [center] + (ptk/reify ::increase-zoom + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))) + +(defn decrease-zoom + [center] + (ptk/reify ::decrease-zoom + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))) + +(defn set-zoom + [center scale] + (ptk/reify ::set-zoom + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (-> (* z scale) + (max 0.01) + (min 200)))))))) + +(def reset-zoom + (ptk/reify ::reset-zoom + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + #(impl-update-zoom % nil 1))))) + +(def zoom-to-fit-all + (ptk/reify ::zoom-to-fit-all + ptk/UpdateEvent + (update [_ state] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shapes (cph/get-immediate-children objects) + srect (gsh/selection-rect shapes)] + (if (empty? shapes) + state + (update state :workspace-local + (fn [{:keys [vport] :as local}] + (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) + zoom (/ (:width vport) (:width srect))] + (-> local + (assoc :zoom zoom) + (update :vbox merge srect)))))))))) + +(def zoom-to-selected-shape + (ptk/reify ::zoom-to-selected-shape + ptk/UpdateEvent + (update [_ state] + (let [selected (wsh/lookup-selected state)] + (if (empty? selected) + state + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + srect (->> selected + (map #(get objects %)) + (gsh/selection-rect))] + (update state :workspace-local + (fn [{:keys [vport] :as local}] + (let [srect (gal/adjust-to-viewport vport srect {:padding 40}) + zoom (/ (:width vport) (:width srect))] + (-> local + (assoc :zoom zoom) + (update :vbox merge srect))))))))))) + +(defn start-zooming [pt] + (ptk/reify ::start-zooming + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))] + (when-not (get-in state [:workspace-local :zooming]) + (rx/concat + (rx/of #(-> % (assoc-in [:workspace-local :zooming] true))) + (->> stream + (rx/filter ms/pointer-event?) + (rx/filter #(= :delta (:source %))) + (rx/map :pt) + (rx/take-until stopper) + (rx/map (fn [delta] + (let [scale (+ 1 (/ (:y delta) 100))] ;; this number may be adjusted after user testing + (set-zoom pt scale))))))))))) + +(defn finish-zooming [] + (ptk/reify ::finish-zooming + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-local dissoc :zooming))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 2ff6c1219c..99e338e563 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -284,6 +284,9 @@ (mapv (d/getf objects) shapes)))] (l/derived selector selected-data =))) +(def workspace-focus-selected + (l/derived :workspace-focus-selected st/state)) + ;; ---- Viewer refs (def viewer-file diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 862d7bbd70..a78524c7ed 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.common.uuid :refer [zero]] [app.main.refs :as refs] @@ -32,21 +33,27 @@ (defn make-remove-snap "Creates a filter for the snap data. Used to disable certain layouts" - [layout filter-shapes] + [layout filter-shapes objects focus] - (fn [{:keys [type id]}] + (fn [{:keys [type id frame-id]}] (cond (= type :layout) (or (not (contains? layout :display-grid)) - (not (contains? layout :snap-grid))) + (not (contains? layout :snap-grid)) + (and (d/not-empty? focus) + (not (contains? focus id)))) (= type :guide) (or (not (contains? layout :rules)) - (not (contains? layout :snap-guides))) + (not (contains? layout :snap-guides)) + (and (d/not-empty? focus) + (not (contains? focus frame-id)))) :else (or (contains? filter-shapes id) - (not (contains? layout :dynamic-alignment)))))) + (not (contains? layout :dynamic-alignment)) + (and (d/not-empty? focus) + (not (cp/is-in-focus? objects focus id))))))) (defn- flatten-to-points [query-result] @@ -223,19 +230,19 @@ (rx/map snap->vector)))) (defn closest-snap-point - [page-id shapes layout zoom point] + [page-id shapes objects layout zoom focus point] (let [frame-id (snap-frame-id shapes) filter-shapes (into #{} (map :id shapes)) - remove-snap? (make-remove-snap layout filter-shapes)] + remove-snap? (make-remove-snap layout filter-shapes objects focus)] (->> (closest-snap page-id frame-id [point] remove-snap? zoom) (rx/map #(or % (gpt/point 0 0))) (rx/map #(gpt/add point %))))) (defn closest-snap-move - [page-id shapes objects layout zoom movev] + [page-id shapes objects layout zoom focus movev] (let [frame-id (snap-frame-id shapes) filter-shapes (into #{} (map :id shapes)) - remove-snap? (make-remove-snap layout filter-shapes) + remove-snap? (make-remove-snap layout filter-shapes objects focus) shape (if (> (count shapes) 1) (->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect})) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 9280fc14a6..cc30870c90 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,7 +7,9 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require + [app.common.pages :as cp] [app.main.data.shortcuts :as dsc] + [app.main.refs :as refs] [app.main.store :as st] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] @@ -235,3 +237,14 @@ (let [ret (effect-fn)] (when (fn? ret) (ret))) (mf/use-effect deps effect-fn))) + +(defn with-focus-objects + ([objects] + (let [focus (mf/deref refs/workspace-focus-selected)] + (with-focus-objects objects focus))) + + ([objects focus] + (let [objects (mf/use-memo + (mf/deps focus objects) + #(cp/focus-objects objects focus))] + objects))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index b934c64427..506726bb86 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -212,6 +212,17 @@ :on-click do-create-artboard-from-selection}] [:& menu-separator]])])) +(mf/defc context-focus-mode-menu + [{:keys []}] + (let [focus (mf/deref refs/workspace-focus-selected) + do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] + + [:& menu-entry {:title (if (empty? focus) + (tr "workspace.focus.focus-on") + (tr "workspace.focus.focus-off")) + :shortcut (sc/get-tooltip :toggle-focus-mode) + :on-click do-toggle-focus-mode}])) + (mf/defc context-menu-path [{:keys [shapes disable-flatten? disable-booleans?]}] (let [multiple? (> (count shapes) 1) @@ -426,6 +437,7 @@ [:> context-menu-layer-position props] [:> context-menu-flip props] [:> context-menu-group props] + [:> context-focus-mode-menu props] [:> context-menu-path props] [:> context-menu-layer-options props] [:> context-menu-prototype props] @@ -434,15 +446,23 @@ (mf/defc viewport-context-menu [] - (let [do-paste (st/emitf dw/paste) - do-hide-ui (st/emitf (dw/toggle-layout-flags :hide-ui))] + (let [focus (mf/deref refs/workspace-focus-selected) + + do-paste (st/emitf dw/paste) + do-hide-ui (st/emitf (dw/toggle-layout-flags :hide-ui)) + do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] [:* [:& menu-entry {:title (tr "workspace.shape.menu.paste") :shortcut (sc/get-tooltip :paste) :on-click do-paste}] [:& menu-entry {:title (tr "workspace.shape.menu.hide-ui") :shortcut (sc/get-tooltip :hide-ui) - :on-click do-hide-ui}]])) + :on-click do-hide-ui}] + + (when (d/not-empty? focus) + [:& menu-entry {:title (tr "workspace.focus.focus-off") + :shortcut (sc/get-tooltip :toggle-focus-mode) + :on-click do-toggle-focus-mode}])])) (mf/defc context-menu [] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 6ccb136363..444502d144 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.shapes.frame (:require + [app.common.colors :as cc] [app.common.data :as d] [app.common.pages.helpers :as cph] [app.main.ui.hooks :as hooks] @@ -41,7 +42,7 @@ (let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")] (if (some? (:thumbnail shape)) [:& frame/frame-thumbnail {:shape shape}] - [:rect {:x x :y y :width width :height height :style {:fill (or fill-color "var(--color-white)")}}]))) + [:rect.frame-thumbnail {:x x :y y :width width :height height :style {:fill (or fill-color cc/white)}}]))) (defn custom-deferred [component] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 9e65bbf54d..73d7acaed8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -17,6 +17,7 @@ [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.timers :as ts] @@ -314,11 +315,23 @@ (mf/defc layers-toolbox {:wrap [mf/memo]} [] - (let [page (mf/deref refs/workspace-page)] + (let [page (mf/deref refs/workspace-page) + focus (mf/deref refs/workspace-focus-selected) + objects (hooks/with-focus-objects (:objects page) focus) + title (when (= 1 (count focus)) (get-in objects [(first focus) :name]))] [:div#layers.tool-window - [:div.tool-window-bar - [:div.tool-window-icon i/layers] - [:span (:name page)]] + (if (d/not-empty? focus) + [:div.tool-window-bar + [:div.focus-title + [:button.back-button + {:on-click #(st/emit! (dw/toggle-focus-mode))} + i/arrow-slide ] + [:span (or title (tr "workspace.focus.selection"))] + [:div.focus-mode (tr "workspace.focus.focus-mode")]]] + + [:div.tool-window-bar + [:span (:name page)]]) + [:div.tool-window-content [:& layers-tree-wrapper {:key (:id page) - :objects (:objects page)}]]])) + :objects objects}]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index b9e5fa2e74..416e14facf 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -11,6 +11,7 @@ [app.common.geom.shapes :as gsh] [app.main.refs :as refs] [app.main.ui.context :as ctx] + [app.main.ui.hooks :as ui-hooks] [app.main.ui.measurements :as msr] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.export :as use] @@ -65,7 +66,9 @@ ;; DEREFS drawing (mf/deref refs/workspace-drawing) options (mf/deref refs/workspace-page-options) - base-objects (mf/deref refs/workspace-page-objects) + focus (mf/deref refs/workspace-focus-selected) + base-objects (-> (mf/deref refs/workspace-page-objects) + (ui-hooks/with-focus-objects focus)) modifiers (mf/deref refs/workspace-modifiers) objects-modified (mf/with-memo [base-objects modifiers] (gsh/merge-modifiers base-objects modifiers)) @@ -169,7 +172,7 @@ (hooks/setup-viewport-size viewport-ref) (hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?) (hooks/setup-keyboard alt? ctrl? space?) - (hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) + (hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? focus zoom) (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames base-objects vbox hover active-frames) @@ -253,8 +256,12 @@ [:& outline/shape-outlines {:objects base-objects :selected selected - :hover (when (or @ctrl? (not= :frame (:type @hover))) - #{(or @frame-hover (:id @hover))}) + :hover (cond + (and @hover (or @ctrl? (not= :frame (:type @hover)))) + #{(:id @hover)} + + @frame-hover + #{@frame-hover}) :edition edition :zoom zoom}]) @@ -313,7 +320,8 @@ [:& frame-grid/frame-grid {:zoom zoom :selected selected - :transform transform}]) + :transform transform + :focus focus}]) (when show-pixel-grid? [:& widgets/pixel-grid @@ -329,6 +337,7 @@ :page-id page-id :selected selected :objects base-objects + :focus focus :modifiers modifiers}]) (when show-snap-distance? diff --git a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs index aac177eec2..b3169b12c4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/frame_grid.cljs @@ -80,13 +80,14 @@ (mf/defc frame-grid {::mf/wrap [mf/memo]} - [{:keys [zoom transform selected]}] + [{:keys [zoom transform selected focus]}] (let [frames (mf/deref refs/workspace-frames) moving (when (= :move transform) selected) is-moving? #(contains? moving (:id %))] [:g.grid-display {:style {:pointer-events "none"}} (for [frame (remove is-moving? frames)] - [:& grid-display-frame {:key (str "grid-" (:id frame)) - :zoom zoom - :frame (gsh/transform-shape frame)}])])) + (when (or (empty? focus) (contains? focus (:id frame))) + [:& grid-display-frame {:key (str "grid-" (:id frame)) + :zoom zoom + :frame (gsh/transform-shape frame)}]))])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 7bfde052ab..d3495c7a84 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -428,6 +428,8 @@ (vals) (filter (guide-inside-vbox? vbox)))) + focus (mf/deref refs/workspace-focus-selected) + hover-frame-ref (mf/use-ref nil) ;; We use the ref to not redraw every guide everytime the hovering frame change @@ -464,12 +466,15 @@ :disabled-guides? disabled-guides?}] (for [current guides] - [:& guide {:key (str "guide-" (:id current)) - :guide current - :vbox vbox - :zoom zoom - :frame-modifier (get modifiers (:frame-id current)) - :get-hover-frame get-hover-frame - :on-guide-change on-guide-change - :disabled-guides? disabled-guides?}])])) + (when (or (nil? (:frame-id current)) + (empty? focus) + (contains? focus (:frame-id current))) + [:& guide {:key (str "guide-" (:id current)) + :guide current + :vbox vbox + :zoom zoom + :frame-modifier (get modifiers (:frame-id current)) + :get-hover-frame get-hover-frame + :on-guide-change on-guide-change + :disabled-guides? disabled-guides?}]))])) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 0cb64935b9..91308f8e96 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] @@ -97,13 +98,14 @@ (some #(cph/is-parent? objects % group-id)) (not)))) -(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom] +(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? focus zoom] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) ctrl-ref (mf/use-ref @ctrl?) transform-ref (mf/use-ref nil) selected-ref (mf/use-ref selected) hover-disabled-ref (mf/use-ref hover-disabled?) + focus-ref (mf/use-ref focus) query-point (mf/use-callback @@ -157,6 +159,10 @@ (mf/deps hover-disabled?) #(mf/set-ref-val! hover-disabled-ref hover-disabled?)) + (mf/use-effect + (mf/deps focus) + #(mf/set-ref-val! focus-ref focus)) + (hooks/use-stream over-shapes-stream (mf/deps page-id objects @ctrl?) @@ -166,6 +172,7 @@ (contains? #{:group :bool} (get-in objects [id :type]))) selected (mf/ref-val selected-ref) + focus (mf/ref-val focus-ref) remove-xfm (mapcat #(cph/get-parent-ids objects %)) remove-id? (cond-> (into #{} remove-xfm selected) @@ -177,6 +184,8 @@ hover-shape (->> ids (filter (comp not remove-id?)) + (filter #(or (empty? focus) + (cp/is-in-focus? objects focus %))) (first) (get objects))] (reset! hover hover-shape) diff --git a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs index e5a63b11da..d5dd3b0a53 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_points.cljs @@ -151,15 +151,16 @@ (mf/defc snap-points {::mf/wrap [mf/memo]} - [{:keys [layout zoom objects selected page-id drawing transform modifiers] :as props}] + [{:keys [layout zoom objects selected page-id drawing transform modifiers focus] :as props}] (us/assert set? selected) (let [shapes (into [] (keep (d/getf objects)) selected) filter-shapes (into selected (mapcat #(cph/get-children-ids objects %)) selected) - remove-snap? (mf/with-memo [layout filter-shapes] - (snap/make-remove-snap layout filter-shapes)) + remove-snap? + (mf/with-memo [layout filter-shapes objects focus] + (snap/make-remove-snap layout filter-shapes objects focus)) shapes (if drawing [drawing] shapes)] (when (or drawing transform) diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 507d8fc677..a9661c26f0 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace.viewport.widgets (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] @@ -165,7 +166,8 @@ [:g.frame-titles (for [frame frames] - [:& frame-title {:frame frame + [:& frame-title {:key (dm/str "frame-title-" (:id frame)) + :frame frame :selected? (contains? selected (:id frame)) :zoom zoom :show-artboard-names? show-artboard-names? diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs index 8c279836d7..5725ea826d 100644 --- a/frontend/src/app/util/snap_data.cljs +++ b/frontend/src/app/util/snap_data.cljs @@ -107,6 +107,7 @@ (mapv #(array-map :type :guide :id (:id guide) + :frame-id (:frame-id guide) :pt %)))] (if-let [frame-id (:frame-id guide)] ;; Guide inside frame, we add the information only on that frame diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 38267e234c..fae9662a49 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3543,3 +3543,15 @@ msgstr "Update" msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" + +msgid "workspace.focus.selection" +msgstr "Selection" + +msgid "workspace.focus.focus-mode" +msgstr "Focus mode" + +msgid "workspace.focus.focus-on" +msgstr "Focus on" + +msgid "workspace.focus.focus-off" +msgstr "Focus off" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 09327ba224..6fb722d97a 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3557,3 +3557,15 @@ msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + +msgid "workspace.focus.selection" +msgstr "Selección" + +msgid "workspace.focus.focus-mode" +msgstr "Modo foco" + +msgid "workspace.focus.focus-on" +msgstr "Activar modo foco" + +msgid "workspace.focus.focus-off" +msgstr "Desactivar modo foco"