New focus mode in workspace

This commit is contained in:
alonso.torres 2022-02-23 16:30:01 +01:00 committed by Andrey Antukh
parent dc18a6c3bc
commit 78d7fe3e10
26 changed files with 484 additions and 179 deletions

View file

@ -12,6 +12,7 @@
- Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797) - 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) - 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) - 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) - 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 - Texts now can have strokes, multiple fills and can be used as masks

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.pages.changes :as changes] [app.common.pages.changes :as changes]
[app.common.pages.common :as common] [app.common.pages.common :as common]
[app.common.pages.focus :as focus]
[app.common.pages.indices :as indices] [app.common.pages.indices :as indices]
[app.common.pages.init :as init])) [app.common.pages.init :as init]))
@ -19,6 +20,11 @@
(dm/export common/default-color) (dm/export common/default-color)
(dm/export common/component-sync-attrs) (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 ;; Indices
(dm/export indices/calculate-z-index) (dm/export indices/calculate-z-index)
(dm/export indices/update-z-index) (dm/export indices/update-z-index)
@ -36,3 +42,4 @@
(dm/export init/make-minimal-shape) (dm/export init/make-minimal-shape)
(dm/export init/make-minimal-group) (dm/export init/make-minimal-group)
(dm/export init/empty-file-data) (dm/export init/empty-file-data)

View file

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

View file

@ -74,6 +74,16 @@
[objects id] [objects id]
(-> objects (get id) :parent-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 (defn get-parent-ids
"Returns a vector of parents of the specified shape." "Returns a vector of parents of the specified shape."
[objects shape-id] [objects shape-id]
@ -463,3 +473,4 @@
[path name] [path name]
(let [path-split (split-path path)] (let [path-split (split-path path)]
(merge-path-item (first path-split) name))) (merge-path-item (first path-split) name)))

View file

@ -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 { .assets-bar .tool-window {

View file

@ -10,7 +10,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.align :as gal] [app.common.geom.align :as gal]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpr] [app.common.geom.proportions :as gpr]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -44,6 +43,7 @@
[app.main.data.workspace.svg-upload :as svg] [app.main.data.workspace.svg-upload :as svg]
[app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.zoom :as dwz]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.streams :as ms] [app.main.streams :as ms]
[app.main.worker :as uw] [app.main.worker :as uw]
@ -294,7 +294,7 @@
exit-workspace? (not= :workspace (get-in state [:route :data :name]))] exit-workspace? (not= :workspace (get-in state [:route :data :name]))]
(cond-> (assoc-in state [:workspace-cache page-id] local) (cond-> (assoc-in state [:workspace-cache page-id] local)
:always :always
(dissoc :current-page-id :workspace-local :trimmed-page) (dissoc :current-page-id :workspace-local :trimmed-page :workspace-focus-selected)
exit-workspace? exit-workspace?
(dissoc :workspace-drawing)))))) (dissoc :workspace-drawing))))))
@ -478,11 +478,6 @@
;; --- Viewport Sizing ;; --- Viewport Sizing
(declare increase-zoom)
(declare decrease-zoom)
(declare set-zoom)
(declare zoom-to-fit-all)
(defn initialize-viewport (defn initialize-viewport
[{:keys [width height] :as size}] [{:keys [width height] :as size}]
(letfn [(update* [{:keys [vport] :as local}] (letfn [(update* [{:keys [vport] :as local}]
@ -612,114 +607,8 @@
(-> state (-> state
(update :workspace-local dissoc :panning))))) (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 ;; --- Update Shape Attrs
@ -1887,21 +1776,25 @@
(dm/export dwp/clone-media-object) (dm/export dwp/clone-media-object)
(dm/export dwc/image-uploaded) (dm/export dwc/image-uploaded)
;; Selection ;; Common
(dm/export dwc/add-shape)
(dm/export dws/select-shape) (dm/export dwc/clear-edition-mode)
(dm/export dws/deselect-shape)
(dm/export dws/select-all)
(dm/export dws/deselect-all)
(dm/export dwc/select-shapes) (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/duplicate-selected)
(dm/export dws/handle-area-selection) (dm/export dws/handle-area-selection)
(dm/export dws/select-all)
(dm/export dws/select-inside-group) (dm/export dws/select-inside-group)
(dm/export dwd/select-for-drawing) (dm/export dws/select-shape)
(dm/export dwc/clear-edition-mode) (dm/export dws/shift-select-shapes)
(dm/export dwc/add-shape)
(dm/export dwc/start-edition-mode)
;; Groups ;; Groups
@ -1924,3 +1817,11 @@
(dm/export dwgu/remove-guide) (dm/export dwgu/remove-guide)
(dm/export dwgu/set-hover-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)

View file

@ -60,6 +60,7 @@
page-id (:current-page-id state) page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
layout (get state :workspace-layout) layout (get state :workspace-layout)
focus (:workspace-focus-selected state)
zoom (get-in state [:workspace-local :zoom] 1) zoom (get-in state [:workspace-local :zoom] 1)
frames (cph/get-frames objects) frames (cph/get-frames objects)
@ -80,7 +81,7 @@
(rx/of #(assoc-in state [:workspace-drawing :object] shape)) (rx/of #(assoc-in state [:workspace-drawing :object] shape))
;; Initial SNAP ;; 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)) (rx/map move-drawing))
(->> ms/mouse-position (->> ms/mouse-position
@ -88,7 +89,7 @@
(rx/with-latest vector ms/mouse-position-shift) (rx/with-latest vector ms/mouse-position-shift)
(rx/switch-map (rx/switch-map
(fn [[point :as current]] (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 #(conj current %)))))
(rx/map (rx/map
(fn [[_ shift? point]] (fn [[_ shift? point]]

View file

@ -10,6 +10,7 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.changes-builder :as pcb] [app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.spec :as us] [app.common.spec :as us]
@ -20,10 +21,13 @@
[app.main.data.workspace.changes :as dch] [app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.main.data.workspace.state-helpers :as wsh] [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.streams :as ms]
[app.main.worker :as uw] [app.main.worker :as uw]
[beicon.core :as rx] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[clojure.set :as set]
[linked.set :as lks] [linked.set :as lks]
[potok.core :as ptk])) [potok.core :as ptk]))
@ -161,7 +165,12 @@
(ptk/reify ::select-shapes (ptk/reify ::select-shapes
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -173,8 +182,9 @@
(ptk/reify ::select-all (ptk/reify ::select-all
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [page-id (:current-page-id state) (let [focus (:workspace-focus-selected state)
objects (wsh/lookup-page-objects state page-id) objects (-> (wsh/lookup-page-objects state)
(cp/focus-objects focus))
selected (let [frame-ids (into #{} (comp selected (let [frame-ids (into #{} (comp
(map (d/getf objects)) (map (d/getf objects))
@ -484,8 +494,8 @@
id-duplicated (when (= (count selected) 1) (first selected))] id-duplicated (when (= (count selected) 1) (first selected))]
(rx/of (select-shapes selected) (rx/of (dch/commit-changes changes)
(dch/commit-changes changes) (select-shapes selected)
(memorize-duplicated id-original id-duplicated))))))) (memorize-duplicated id-original id-duplicated)))))))
(defn change-hover-state (defn change-hover-state
@ -495,3 +505,54 @@
(update [_ state] (update [_ state]
(let [hover-value (if value #{id} #{})] (let [hover-value (if value #{id} #{})]
(assoc-in state [:workspace-local :hover] hover-value))))) (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))))))))))))

View file

@ -356,7 +356,9 @@
:command "\\" :command "\\"
:fn #(st/emit! (dw/toggle-layout-flags :hide-ui))} :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 (def opacity-shortcuts
(into {} (->> (into {} (->>

View file

@ -375,6 +375,7 @@
stoper (rx/filter ms/mouse-up? stream) stoper (rx/filter ms/mouse-up? stream)
layout (:workspace-layout state) layout (:workspace-layout state)
page-id (:current-page-id state) page-id (:current-page-id state)
focus (:workspace-focus-selected state)
zoom (get-in state [:workspace-local :zoom] 1) zoom (get-in state [:workspace-local :zoom] 1)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
resizing-shapes (map #(get objects %) ids) resizing-shapes (map #(get objects %) ids)
@ -387,7 +388,7 @@
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
(rx/map normalize-proportion-lock) (rx/map normalize-proportion-lock)
(rx/switch-map (fn [[point _ _ :as current]] (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/map #(conj current %)))))
(rx/mapcat (partial resize shape initial-position layout)) (rx/mapcat (partial resize shape initial-position layout))
(rx/take-until stoper)) (rx/take-until stoper))
@ -509,7 +510,6 @@
(rx/of (start-move initial selected))))) (rx/of (start-move initial selected)))))
(rx/take-until stopper))))))) (rx/take-until stopper)))))))
(defn- start-move-duplicate (defn- start-move-duplicate
[from-position] [from-position]
(ptk/reify ::start-move-duplicate (ptk/reify ::start-move-duplicate
@ -544,6 +544,7 @@
stopper (rx/filter ms/mouse-up? stream) stopper (rx/filter ms/mouse-up? stream)
layout (get state :workspace-layout) layout (get state :workspace-layout)
zoom (get-in state [:workspace-local :zoom] 1) zoom (get-in state [:workspace-local :zoom] 1)
focus (:workspace-focus-selected state)
fix-axis (fn [[position shift?]] fix-axis (fn [[position shift?]]
(let [delta (gpt/to-vec from-position position)] (let [delta (gpt/to-vec from-position position)]
@ -564,10 +565,10 @@
(rx/throttle 20) (rx/throttle 20)
(rx/switch-map (rx/switch-map
(fn [pos] (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 %)))))))] (rx/map #(vector pos %)))))))]
(if (empty? shapes) (if (empty? shapes)
(rx/empty) (rx/of (finish-transform))
(rx/concat (rx/concat
(->> position (->> position
(rx/with-latest vector snap-delta) (rx/with-latest vector snap-delta)

View file

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

View file

@ -284,6 +284,9 @@
(mapv (d/getf objects) shapes)))] (mapv (d/getf objects) shapes)))]
(l/derived selector selected-data =))) (l/derived selector selected-data =)))
(def workspace-focus-selected
(l/derived :workspace-focus-selected st/state))
;; ---- Viewer refs ;; ---- Viewer refs
(def viewer-file (def viewer-file

View file

@ -10,6 +10,7 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.common.uuid :refer [zero]] [app.common.uuid :refer [zero]]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -32,21 +33,27 @@
(defn make-remove-snap (defn make-remove-snap
"Creates a filter for the snap data. Used to disable certain layouts" "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 (cond
(= type :layout) (= type :layout)
(or (not (contains? layout :display-grid)) (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) (= type :guide)
(or (not (contains? layout :rules)) (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 :else
(or (contains? filter-shapes id) (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 (defn- flatten-to-points
[query-result] [query-result]
@ -223,19 +230,19 @@
(rx/map snap->vector)))) (rx/map snap->vector))))
(defn closest-snap-point (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) (let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :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) (->> (closest-snap page-id frame-id [point] remove-snap? zoom)
(rx/map #(or % (gpt/point 0 0))) (rx/map #(or % (gpt/point 0 0)))
(rx/map #(gpt/add point %))))) (rx/map #(gpt/add point %)))))
(defn closest-snap-move (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) (let [frame-id (snap-frame-id shapes)
filter-shapes (into #{} (map :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) shape (if (> (count shapes) 1)
(->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect})) (->> shapes (map gsh/transform-shape) gsh/selection-rect (gsh/setup {:type :rect}))

View file

@ -7,7 +7,9 @@
(ns app.main.ui.hooks (ns app.main.ui.hooks
"A collection of general purpose react hooks." "A collection of general purpose react hooks."
(:require (:require
[app.common.pages :as cp]
[app.main.data.shortcuts :as dsc] [app.main.data.shortcuts :as dsc]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
@ -235,3 +237,14 @@
(let [ret (effect-fn)] (let [ret (effect-fn)]
(when (fn? ret) (ret))) (when (fn? ret) (ret)))
(mf/use-effect deps effect-fn))) (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)))

View file

@ -212,6 +212,17 @@
:on-click do-create-artboard-from-selection}] :on-click do-create-artboard-from-selection}]
[:& menu-separator]])])) [:& 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 (mf/defc context-menu-path
[{:keys [shapes disable-flatten? disable-booleans?]}] [{:keys [shapes disable-flatten? disable-booleans?]}]
(let [multiple? (> (count shapes) 1) (let [multiple? (> (count shapes) 1)
@ -426,6 +437,7 @@
[:> context-menu-layer-position props] [:> context-menu-layer-position props]
[:> context-menu-flip props] [:> context-menu-flip props]
[:> context-menu-group props] [:> context-menu-group props]
[:> context-focus-mode-menu props]
[:> context-menu-path props] [:> context-menu-path props]
[:> context-menu-layer-options props] [:> context-menu-layer-options props]
[:> context-menu-prototype props] [:> context-menu-prototype props]
@ -434,15 +446,23 @@
(mf/defc viewport-context-menu (mf/defc viewport-context-menu
[] []
(let [do-paste (st/emitf dw/paste) (let [focus (mf/deref refs/workspace-focus-selected)
do-hide-ui (st/emitf (dw/toggle-layout-flags :hide-ui))]
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") [:& menu-entry {:title (tr "workspace.shape.menu.paste")
:shortcut (sc/get-tooltip :paste) :shortcut (sc/get-tooltip :paste)
:on-click do-paste}] :on-click do-paste}]
[:& menu-entry {:title (tr "workspace.shape.menu.hide-ui") [:& menu-entry {:title (tr "workspace.shape.menu.hide-ui")
:shortcut (sc/get-tooltip :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 (mf/defc context-menu
[] []

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.shapes.frame (ns app.main.ui.workspace.shapes.frame
(:require (:require
[app.common.colors :as cc]
[app.common.data :as d] [app.common.data :as d]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
@ -41,7 +42,7 @@
(let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")] (let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")]
(if (some? (:thumbnail shape)) (if (some? (:thumbnail shape))
[:& frame/frame-thumbnail {:shape 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 (defn custom-deferred
[component] [component]

View file

@ -17,6 +17,7 @@
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.timers :as ts] [app.util.timers :as ts]
@ -314,11 +315,23 @@
(mf/defc layers-toolbox (mf/defc layers-toolbox
{:wrap [mf/memo]} {: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#layers.tool-window
(if (d/not-empty? focus)
[:div.tool-window-bar [:div.tool-window-bar
[:div.tool-window-icon i/layers] [:div.focus-title
[:span (:name page)]] [: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 [:div.tool-window-content
[:& layers-tree-wrapper {:key (:id page) [:& layers-tree-wrapper {:key (:id page)
:objects (:objects page)}]]])) :objects objects}]]]))

View file

@ -11,6 +11,7 @@
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.hooks :as ui-hooks]
[app.main.ui.measurements :as msr] [app.main.ui.measurements :as msr]
[app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.export :as use] [app.main.ui.shapes.export :as use]
@ -65,7 +66,9 @@
;; DEREFS ;; DEREFS
drawing (mf/deref refs/workspace-drawing) drawing (mf/deref refs/workspace-drawing)
options (mf/deref refs/workspace-page-options) 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) modifiers (mf/deref refs/workspace-modifiers)
objects-modified (mf/with-memo [base-objects modifiers] objects-modified (mf/with-memo [base-objects modifiers]
(gsh/merge-modifiers base-objects modifiers)) (gsh/merge-modifiers base-objects modifiers))
@ -169,7 +172,7 @@
(hooks/setup-viewport-size viewport-ref) (hooks/setup-viewport-size viewport-ref)
(hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?) (hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?)
(hooks/setup-keyboard alt? ctrl? space?) (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-viewport-modifiers modifiers base-objects)
(hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-shortcuts node-editing? drawing-path?)
(hooks/setup-active-frames base-objects vbox hover active-frames) (hooks/setup-active-frames base-objects vbox hover active-frames)
@ -253,8 +256,12 @@
[:& outline/shape-outlines [:& outline/shape-outlines
{:objects base-objects {:objects base-objects
:selected selected :selected selected
:hover (when (or @ctrl? (not= :frame (:type @hover))) :hover (cond
#{(or @frame-hover (:id @hover))}) (and @hover (or @ctrl? (not= :frame (:type @hover))))
#{(:id @hover)}
@frame-hover
#{@frame-hover})
:edition edition :edition edition
:zoom zoom}]) :zoom zoom}])
@ -313,7 +320,8 @@
[:& frame-grid/frame-grid [:& frame-grid/frame-grid
{:zoom zoom {:zoom zoom
:selected selected :selected selected
:transform transform}]) :transform transform
:focus focus}])
(when show-pixel-grid? (when show-pixel-grid?
[:& widgets/pixel-grid [:& widgets/pixel-grid
@ -329,6 +337,7 @@
:page-id page-id :page-id page-id
:selected selected :selected selected
:objects base-objects :objects base-objects
:focus focus
:modifiers modifiers}]) :modifiers modifiers}])
(when show-snap-distance? (when show-snap-distance?

View file

@ -80,13 +80,14 @@
(mf/defc frame-grid (mf/defc frame-grid
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]}
[{:keys [zoom transform selected]}] [{:keys [zoom transform selected focus]}]
(let [frames (mf/deref refs/workspace-frames) (let [frames (mf/deref refs/workspace-frames)
moving (when (= :move transform) selected) moving (when (= :move transform) selected)
is-moving? #(contains? moving (:id %))] is-moving? #(contains? moving (:id %))]
[:g.grid-display {:style {:pointer-events "none"}} [:g.grid-display {:style {:pointer-events "none"}}
(for [frame (remove is-moving? frames)] (for [frame (remove is-moving? frames)]
(when (or (empty? focus) (contains? focus (:id frame)))
[:& grid-display-frame {:key (str "grid-" (:id frame)) [:& grid-display-frame {:key (str "grid-" (:id frame))
:zoom zoom :zoom zoom
:frame (gsh/transform-shape frame)}])])) :frame (gsh/transform-shape frame)}]))]))

View file

@ -428,6 +428,8 @@
(vals) (vals)
(filter (guide-inside-vbox? vbox)))) (filter (guide-inside-vbox? vbox))))
focus (mf/deref refs/workspace-focus-selected)
hover-frame-ref (mf/use-ref nil) hover-frame-ref (mf/use-ref nil)
;; We use the ref to not redraw every guide everytime the hovering frame change ;; We use the ref to not redraw every guide everytime the hovering frame change
@ -464,6 +466,9 @@
:disabled-guides? disabled-guides?}] :disabled-guides? disabled-guides?}]
(for [current guides] (for [current guides]
(when (or (nil? (:frame-id current))
(empty? focus)
(contains? focus (:frame-id current)))
[:& guide {:key (str "guide-" (:id current)) [:& guide {:key (str "guide-" (:id current))
:guide current :guide current
:vbox vbox :vbox vbox
@ -471,5 +476,5 @@
:frame-modifier (get modifiers (:frame-id current)) :frame-modifier (get modifiers (:frame-id current))
:get-hover-frame get-hover-frame :get-hover-frame get-hover-frame
:on-guide-change on-guide-change :on-guide-change on-guide-change
:disabled-guides? disabled-guides?}])])) :disabled-guides? disabled-guides?}]))]))

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.data.shortcuts :as dsc] [app.main.data.shortcuts :as dsc]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -97,13 +98,14 @@
(some #(cph/is-parent? objects % group-id)) (some #(cph/is-parent? objects % group-id))
(not)))) (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 (let [;; We use ref so we don't recreate the stream on a change
zoom-ref (mf/use-ref zoom) zoom-ref (mf/use-ref zoom)
ctrl-ref (mf/use-ref @ctrl?) ctrl-ref (mf/use-ref @ctrl?)
transform-ref (mf/use-ref nil) transform-ref (mf/use-ref nil)
selected-ref (mf/use-ref selected) selected-ref (mf/use-ref selected)
hover-disabled-ref (mf/use-ref hover-disabled?) hover-disabled-ref (mf/use-ref hover-disabled?)
focus-ref (mf/use-ref focus)
query-point query-point
(mf/use-callback (mf/use-callback
@ -157,6 +159,10 @@
(mf/deps hover-disabled?) (mf/deps hover-disabled?)
#(mf/set-ref-val! hover-disabled-ref 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 (hooks/use-stream
over-shapes-stream over-shapes-stream
(mf/deps page-id objects @ctrl?) (mf/deps page-id objects @ctrl?)
@ -166,6 +172,7 @@
(contains? #{:group :bool} (get-in objects [id :type]))) (contains? #{:group :bool} (get-in objects [id :type])))
selected (mf/ref-val selected-ref) selected (mf/ref-val selected-ref)
focus (mf/ref-val focus-ref)
remove-xfm (mapcat #(cph/get-parent-ids objects %)) remove-xfm (mapcat #(cph/get-parent-ids objects %))
remove-id? (cond-> (into #{} remove-xfm selected) remove-id? (cond-> (into #{} remove-xfm selected)
@ -177,6 +184,8 @@
hover-shape (->> ids hover-shape (->> ids
(filter (comp not remove-id?)) (filter (comp not remove-id?))
(filter #(or (empty? focus)
(cp/is-in-focus? objects focus %)))
(first) (first)
(get objects))] (get objects))]
(reset! hover hover-shape) (reset! hover hover-shape)

View file

@ -151,15 +151,16 @@
(mf/defc snap-points (mf/defc snap-points
{::mf/wrap [mf/memo]} {::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) (us/assert set? selected)
(let [shapes (into [] (keep (d/getf objects)) selected) (let [shapes (into [] (keep (d/getf objects)) selected)
filter-shapes filter-shapes
(into selected (mapcat #(cph/get-children-ids objects %)) selected) (into selected (mapcat #(cph/get-children-ids objects %)) selected)
remove-snap? (mf/with-memo [layout filter-shapes] remove-snap?
(snap/make-remove-snap layout filter-shapes)) (mf/with-memo [layout filter-shapes objects focus]
(snap/make-remove-snap layout filter-shapes objects focus))
shapes (if drawing [drawing] shapes)] shapes (if drawing [drawing] shapes)]
(when (or drawing transform) (when (or drawing transform)

View file

@ -6,6 +6,7 @@
(ns app.main.ui.workspace.viewport.widgets (ns app.main.ui.workspace.viewport.widgets
(:require (:require
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
@ -165,7 +166,8 @@
[:g.frame-titles [:g.frame-titles
(for [frame frames] (for [frame frames]
[:& frame-title {:frame frame [:& frame-title {:key (dm/str "frame-title-" (:id frame))
:frame frame
:selected? (contains? selected (:id frame)) :selected? (contains? selected (:id frame))
:zoom zoom :zoom zoom
:show-artboard-names? show-artboard-names? :show-artboard-names? show-artboard-names?

View file

@ -107,6 +107,7 @@
(mapv #(array-map (mapv #(array-map
:type :guide :type :guide
:id (:id guide) :id (:id guide)
:frame-id (:frame-id guide)
:pt %)))] :pt %)))]
(if-let [frame-id (:frame-id guide)] (if-let [frame-id (:frame-id guide)]
;; Guide inside frame, we add the information only on that frame ;; Guide inside frame, we add the information only on that frame

View file

@ -3543,3 +3543,15 @@ msgstr "Update"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the 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"

View file

@ -3557,3 +3557,15 @@ msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" 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"