mirror of
https://github.com/penpot/penpot.git
synced 2026-05-23 00:43:41 +00:00
🔧 Improve layer items lag
This commit is contained in:
parent
3264bc746f
commit
7ea99d8494
@ -44,13 +44,13 @@
|
|||||||
(defn- render-component-pixels
|
(defn- render-component-pixels
|
||||||
"Renders a component frame using the workspace WASM context.
|
"Renders a component frame using the workspace WASM context.
|
||||||
Returns an observable that emits a data-uri string.
|
Returns an observable that emits a data-uri string.
|
||||||
Deferred by one animation frame so that process-shape-changes!
|
Deferred with setTimeout so that React can paint pending state
|
||||||
has time to sync all child shapes to WASM memory first."
|
updates before the synchronous WASM render blocks the main thread."
|
||||||
[frame-id]
|
[frame-id]
|
||||||
(rx/create
|
(rx/create
|
||||||
(fn [subs]
|
(fn [subs]
|
||||||
(js/requestAnimationFrame
|
(js/setTimeout
|
||||||
(fn [_]
|
(fn []
|
||||||
(try
|
(try
|
||||||
(let [png-bytes (wasm.api/render-shape-pixels frame-id 1)]
|
(let [png-bytes (wasm.api/render-shape-pixels frame-id 1)]
|
||||||
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
|
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
|
||||||
@ -63,7 +63,8 @@
|
|||||||
(fn [err]
|
(fn [err]
|
||||||
(rx/error! subs err)))))
|
(rx/error! subs err)))))
|
||||||
(catch :default err
|
(catch :default err
|
||||||
(rx/error! subs err)))))
|
(rx/error! subs err))))
|
||||||
|
0)
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defn render-thumbnail
|
(defn render-thumbnail
|
||||||
|
|||||||
@ -201,41 +201,22 @@
|
|||||||
|
|
||||||
(mf/defc layer-item*
|
(mf/defc layer-item*
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [index item selected objects rename-id
|
[{:keys [index item-id selected rename-id
|
||||||
is-sortable is-filtered depth is-component-child
|
is-sortable is-filtered depth is-component-child
|
||||||
highlighted style render-children parent-size]
|
style render-children parent-size]
|
||||||
:or {render-children true}}]
|
:or {render-children true}}]
|
||||||
(let [id (get item :id)
|
(let [item-iref (mf/with-memo [item-id]
|
||||||
|
(l/derived (fn [objects] (get objects item-id)) refs/workspace-page-objects))
|
||||||
|
item (mf/deref item-iref)
|
||||||
|
|
||||||
|
id item-id
|
||||||
blocked? (get item :blocked)
|
blocked? (get item :blocked)
|
||||||
hidden? (get item :hidden)
|
hidden? (get item :hidden)
|
||||||
|
|
||||||
shapes (get item :shapes)
|
;; child shape IDs reversed for display (top z-order first in layer panel)
|
||||||
shapes (mf/with-memo [shapes objects]
|
child-ids-raw (get item :shapes)
|
||||||
(loop [counter 0
|
child-ids (mf/with-memo [child-ids-raw]
|
||||||
shapes (seq shapes)
|
(some-> child-ids-raw rseq vec))
|
||||||
result (list)]
|
|
||||||
|
|
||||||
(if-let [id (first shapes)]
|
|
||||||
(if-let [obj (get objects id)]
|
|
||||||
(do
|
|
||||||
;; NOTE: this is a bit hacky, but reduces substantially
|
|
||||||
;; the allocation; If we use enumeration, we allocate
|
|
||||||
;; new sequence and add one iteration on each render,
|
|
||||||
;; independently if objects are changed or not. If we
|
|
||||||
;; store counter on metadata, we still need to create a
|
|
||||||
;; new allocation for each shape; with this method we
|
|
||||||
;; bypass this by mutating a private property on the
|
|
||||||
;; object removing extra allocation and extra iteration
|
|
||||||
;; on every request.
|
|
||||||
(unchecked-set obj "__$__counter" counter)
|
|
||||||
(recur (inc counter)
|
|
||||||
(rest shapes)
|
|
||||||
(conj result obj)))
|
|
||||||
(recur (inc counter)
|
|
||||||
(rest shapes)
|
|
||||||
result))
|
|
||||||
|
|
||||||
(-> result vec not-empty))))
|
|
||||||
|
|
||||||
drag-disabled* (mf/use-state false)
|
drag-disabled* (mf/use-state false)
|
||||||
drag-disabled? (deref drag-disabled*)
|
drag-disabled? (deref drag-disabled*)
|
||||||
@ -245,8 +226,11 @@
|
|||||||
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
|
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
|
||||||
is-expanded (mf/deref expanded-iref)
|
is-expanded (mf/deref expanded-iref)
|
||||||
|
|
||||||
|
highlighted-iref (mf/with-memo [id]
|
||||||
|
(l/derived #(contains? (get % :highlighted) id) refs/workspace-local))
|
||||||
|
|
||||||
is-selected (contains? selected id)
|
is-selected (contains? selected id)
|
||||||
is-highlighted (contains? highlighted id)
|
is-highlighted (mf/deref highlighted-iref)
|
||||||
|
|
||||||
container? (or (cfh/frame-shape? item)
|
container? (or (cfh/frame-shape? item)
|
||||||
(cfh/group-shape? item))
|
(cfh/group-shape? item))
|
||||||
@ -303,14 +287,15 @@
|
|||||||
|
|
||||||
select-shape
|
select-shape
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id is-filtered objects)
|
(mf/deps id is-filtered)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(mf/set-ref-val! scroll-middle-ref false)
|
(mf/set-ref-val! scroll-middle-ref false)
|
||||||
(cond
|
(cond
|
||||||
(kbd/shift? event)
|
(kbd/shift? event)
|
||||||
(if is-filtered
|
(if is-filtered
|
||||||
(st/emit! (dw/shift-select-shapes id objects))
|
(let [objects (deref refs/workspace-page-objects)]
|
||||||
|
(st/emit! (dw/shift-select-shapes id objects)))
|
||||||
(st/emit! (dw/shift-select-shapes id)))
|
(st/emit! (dw/shift-select-shapes id)))
|
||||||
|
|
||||||
(kbd/mod? event)
|
(kbd/mod? event)
|
||||||
@ -363,13 +348,14 @@
|
|||||||
|
|
||||||
on-drop
|
on-drop
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id objects is-expanded selected)
|
(mf/deps id is-expanded selected)
|
||||||
(fn [side _data]
|
(fn [side _data]
|
||||||
(let [single? (= (count selected) 1)
|
(let [single? (= (count selected) 1)
|
||||||
same? (and single? (= (first selected) id))]
|
same? (and single? (= (first selected) id))]
|
||||||
(when-not same?
|
(when-not same?
|
||||||
(let [files (deref refs/files)
|
(let [objects (deref refs/workspace-page-objects)
|
||||||
shape (get objects id)
|
files (deref refs/files)
|
||||||
|
shape (get objects id)
|
||||||
|
|
||||||
parent-id
|
parent-id
|
||||||
(cond
|
(cond
|
||||||
@ -421,20 +407,22 @@
|
|||||||
:on-hold on-hold
|
:on-hold on-hold
|
||||||
:disabled drag-disabled?
|
:disabled drag-disabled?
|
||||||
:detect-center? container?
|
:detect-center? container?
|
||||||
:data {:id (:id item)
|
:data {:id id
|
||||||
:index index
|
:index index
|
||||||
:name (:name item)}
|
:name (:name item)}
|
||||||
;; We don't want to change the structure of component copies
|
;; We don't want to change the structure of component copies
|
||||||
:draggable? (and ^boolean is-sortable
|
:draggable? (and ^boolean is-sortable
|
||||||
^boolean (not is-read-only)
|
^boolean (not is-read-only)
|
||||||
^boolean (not (ctn/has-any-copy-parent? objects item))))
|
^boolean (not (let [objects (deref refs/workspace-page-objects)]
|
||||||
|
(ctn/has-any-copy-parent? objects item)))))
|
||||||
|
|
||||||
on-tab-press
|
on-tab-press
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id objects)
|
(mf/deps id)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when (contains? cf/flags :canary)
|
(when (contains? cf/flags :canary)
|
||||||
(let [shift? (kbd/shift? event)
|
(let [objects (deref refs/workspace-page-objects)
|
||||||
|
shift? (kbd/shift? event)
|
||||||
shape (get objects id)
|
shape (get objects id)
|
||||||
parent (get objects (:parent-id shape))
|
parent (get objects (:parent-id shape))
|
||||||
siblings (:shapes parent)
|
siblings (:shapes parent)
|
||||||
@ -473,15 +461,15 @@
|
|||||||
|
|
||||||
;; Setup scroll-driven lazy loading when expanded
|
;; Setup scroll-driven lazy loading when expanded
|
||||||
;; and ensures selected children are loaded immediately
|
;; and ensures selected children are loaded immediately
|
||||||
(mf/with-effect [is-expanded shapes selected]
|
(mf/with-effect [is-expanded child-ids selected]
|
||||||
(let [total (count shapes)]
|
(let [total (count child-ids)]
|
||||||
(if ^boolean is-expanded
|
(if ^boolean is-expanded
|
||||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
||||||
;; Find if any selected id is a direct child and get its render index
|
;; Find if any selected id is a direct child and get its render index
|
||||||
selected-child-render-idx
|
selected-child-render-idx
|
||||||
(when (> total default-chunk-size)
|
(when (> total default-chunk-size)
|
||||||
(some (fn [sel-id]
|
(some (fn [sel-id]
|
||||||
(let [idx (.indexOf shapes sel-id)]
|
(let [idx (.indexOf child-ids sel-id)]
|
||||||
(when (>= idx 0) idx)))
|
(when (>= idx 0) idx)))
|
||||||
selected))
|
selected))
|
||||||
|
|
||||||
@ -508,9 +496,9 @@
|
|||||||
(mf/set-ref-val! obs nil)))))
|
(mf/set-ref-val! obs nil)))))
|
||||||
|
|
||||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||||
;; and (shapes item) to reconnect observer after shape changes
|
;; and child-ids to reconnect observer after shape changes
|
||||||
(mf/with-effect [children-count is-expanded shapes]
|
(mf/with-effect [children-count is-expanded child-ids]
|
||||||
(let [total (count shapes)
|
(let [total (count child-ids)
|
||||||
name-node (mf/ref-val name-node-ref)
|
name-node (mf/ref-val name-node-ref)
|
||||||
scroll-node (dom/get-parent-with-data name-node "scroll-container")
|
scroll-node (dom/get-parent-with-data name-node "scroll-container")
|
||||||
lazy-node (mf/ref-val lazy-ref)]
|
lazy-node (mf/ref-val lazy-ref)]
|
||||||
@ -565,27 +553,26 @@
|
|||||||
:style style}
|
:style style}
|
||||||
|
|
||||||
(when (and ^boolean render-children
|
(when (and ^boolean render-children
|
||||||
^boolean shapes
|
^boolean (seq child-ids)
|
||||||
^boolean is-expanded)
|
^boolean is-expanded)
|
||||||
[:div {:class (stl/css-case
|
[:div {:class (stl/css-case
|
||||||
:element-children true
|
:element-children true
|
||||||
:parent-selected is-selected
|
:parent-selected is-selected
|
||||||
:sticky-children root-board?)
|
:sticky-children root-board?)
|
||||||
:data-testid (dm/str "children-" id)}
|
:data-testid (dm/str "children-" id)}
|
||||||
(for [item (take children-count shapes)]
|
(let [total (count child-ids)]
|
||||||
[:> layer-item*
|
(for [[display-idx child-id] (d/enumerate (take children-count child-ids))]
|
||||||
{:item item
|
[:> layer-item*
|
||||||
:rename-id rename-id
|
{:item-id child-id
|
||||||
:highlighted highlighted
|
:rename-id rename-id
|
||||||
:selected selected
|
:selected selected
|
||||||
:index (unchecked-get item "__$__counter")
|
:index (- total 1 display-idx)
|
||||||
:objects objects
|
:key (dm/str child-id)
|
||||||
:key (dm/str (get item :id))
|
:is-sortable is-sortable
|
||||||
:is-sortable is-sortable
|
:depth depth
|
||||||
:depth depth
|
:parent-size parent-size
|
||||||
:parent-size parent-size
|
:is-component-child is-component-tree}]))
|
||||||
:is-component-child is-component-tree}])
|
|
||||||
|
|
||||||
(when (< children-count (count shapes))
|
(when (< children-count (count child-ids))
|
||||||
[:div {:ref lazy-ref
|
[:div {:ref lazy-ref
|
||||||
:class (stl/css :lazy-load-sentinel)}])])]))
|
:class (stl/css :lazy-load-sentinel)}])])]))
|
||||||
|
|||||||
@ -34,13 +34,6 @@
|
|||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def ^:private ref:highlighted-shapes
|
|
||||||
(l/derived (fn [local]
|
|
||||||
(-> local
|
|
||||||
(get :highlighted)
|
|
||||||
(not-empty)))
|
|
||||||
refs/workspace-local))
|
|
||||||
|
|
||||||
(def ^:private ref:shape-for-rename
|
(def ^:private ref:shape-for-rename
|
||||||
(l/derived (l/key :shape-for-rename) refs/workspace-local))
|
(l/derived (l/key :shape-for-rename) refs/workspace-local))
|
||||||
|
|
||||||
@ -78,95 +71,61 @@
|
|||||||
|
|
||||||
[:> layer-item* props]))
|
[:> layer-item* props]))
|
||||||
|
|
||||||
|
(defn- use-root-shape-ids
|
||||||
|
"Subscribe to the root shape's children IDs only, using = equality
|
||||||
|
so the component only re-renders when the list of IDs actually changes."
|
||||||
|
[]
|
||||||
|
(let [ref (mf/with-memo []
|
||||||
|
(l/derived (fn [objects]
|
||||||
|
(get-in objects [uuid/zero :shapes]))
|
||||||
|
refs/workspace-page-objects
|
||||||
|
=))
|
||||||
|
ids (mf/deref ref)]
|
||||||
|
ids))
|
||||||
|
|
||||||
(mf/defc layers-tree*
|
(mf/defc layers-tree*
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [objects is-filtered parent-size] :as props}]
|
[{:keys [is-filtered parent-size]}]
|
||||||
(let [selected (use-selected-shapes)
|
(let [selected (use-selected-shapes)
|
||||||
highlighted (mf/deref ref:highlighted-shapes)
|
|
||||||
root (get objects uuid/zero)
|
|
||||||
|
|
||||||
rename-id (mf/deref ref:shape-for-rename)
|
rename-id (mf/deref ref:shape-for-rename)
|
||||||
|
root-ids (use-root-shape-ids)
|
||||||
shapes (get root :shapes)
|
objects (deref refs/workspace-page-objects)]
|
||||||
shapes (mf/with-memo [shapes objects]
|
|
||||||
(loop [counter 0
|
|
||||||
shapes (seq shapes)
|
|
||||||
result (list)]
|
|
||||||
(if-let [id (first shapes)]
|
|
||||||
(if-let [obj (get objects id)]
|
|
||||||
(do
|
|
||||||
;; NOTE: this is a bit hacky, but reduces substantially
|
|
||||||
;; the allocation; If we use enumeration, we allocate
|
|
||||||
;; new sequence and add one iteration on each render,
|
|
||||||
;; independently if objects are changed or not. If we
|
|
||||||
;; store counter on metadata, we still need to create a
|
|
||||||
;; new allocation for each shape; with this method we
|
|
||||||
;; bypass this by mutating a private property on the
|
|
||||||
;; object removing extra allocation and extra iteration
|
|
||||||
;; on every request.
|
|
||||||
(unchecked-set obj "__$__counter" counter)
|
|
||||||
(recur (inc counter)
|
|
||||||
(rest shapes)
|
|
||||||
(conj result obj)))
|
|
||||||
(recur (inc counter)
|
|
||||||
(rest shapes)
|
|
||||||
result))
|
|
||||||
result)))]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
|
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
|
||||||
[:> hooks/sortable-container* {}
|
[:> hooks/sortable-container* {}
|
||||||
(for [obj shapes]
|
;; Layers display top z-order first, so reverse the shapes vector
|
||||||
(if (cfh/frame-shape? obj)
|
(let [total (count root-ids)]
|
||||||
[:> frame-wrapper*
|
(for [[display-idx id] (d/enumerate (reverse root-ids))]
|
||||||
{:item obj
|
(let [index (- total 1 display-idx)
|
||||||
:rename-id rename-id
|
obj (get objects id)]
|
||||||
:selected selected
|
(when obj
|
||||||
:highlighted highlighted
|
(if (cfh/frame-shape? obj)
|
||||||
:index (unchecked-get obj "__$__counter")
|
[:> frame-wrapper*
|
||||||
:objects objects
|
{:item-id id
|
||||||
:key (dm/str (get obj :id))
|
:rename-id rename-id
|
||||||
:is-sortable true
|
:selected selected
|
||||||
:is-filtered is-filtered
|
:index index
|
||||||
:parent-size parent-size
|
:key (dm/str id)
|
||||||
:depth -1}]
|
:is-sortable true
|
||||||
[:> layer-item*
|
:is-filtered is-filtered
|
||||||
{:item obj
|
:parent-size parent-size
|
||||||
:rename-id rename-id
|
:depth -1}]
|
||||||
:selected selected
|
[:> layer-item*
|
||||||
:highlighted highlighted
|
{:item-id id
|
||||||
:index (unchecked-get obj "__$__counter")
|
:rename-id rename-id
|
||||||
:objects objects
|
:selected selected
|
||||||
:key (dm/str (get obj :id))
|
:index index
|
||||||
:is-sortable true
|
:key (dm/str id)
|
||||||
:is-filtered is-filtered
|
:is-sortable true
|
||||||
:depth -1
|
:is-filtered is-filtered
|
||||||
:parent-size parent-size}]))]]))
|
:depth -1
|
||||||
|
:parent-size parent-size}])))))]]))
|
||||||
(mf/defc layers-tree-wrapper*
|
(mf/defc layers-tree-wrapper*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [objects] :as props}]
|
[props]
|
||||||
;; This is a performance sensitive componet, so we use lower-level primitives for
|
;; layers-tree* now self-subscribes to objects per-item; no need to
|
||||||
;; reduce residual allocation for this specific case
|
;; debounce the full objects map. Just pass through.
|
||||||
(let [state-tmp (mf/useState objects)
|
[:> layers-tree* props])
|
||||||
objects' (aget state-tmp 0)
|
|
||||||
set-objects (aget state-tmp 1)
|
|
||||||
|
|
||||||
subject-s (mf/with-memo []
|
|
||||||
(rx/subject))
|
|
||||||
changes-s (mf/with-memo [subject-s]
|
|
||||||
(->> subject-s
|
|
||||||
(rx/debounce 500)))
|
|
||||||
|
|
||||||
props (mf/spread-props props {:objects objects'})]
|
|
||||||
|
|
||||||
(mf/with-effect [objects subject-s]
|
|
||||||
(rx/push! subject-s objects))
|
|
||||||
|
|
||||||
(mf/with-effect [changes-s]
|
|
||||||
(let [sub (rx/subscribe changes-s set-objects)]
|
|
||||||
#(rx/dispose! sub)))
|
|
||||||
|
|
||||||
[:> layers-tree* props]))
|
|
||||||
|
|
||||||
(mf/defc filters-tree*
|
(mf/defc filters-tree*
|
||||||
{::mf/wrap [mf/memo #(mf/throttle % 300)]
|
{::mf/wrap [mf/memo #(mf/throttle % 300)]
|
||||||
@ -176,12 +135,11 @@
|
|||||||
root (get objects uuid/zero)]
|
root (get objects uuid/zero)]
|
||||||
[:ul {:class (stl/css :element-list)}
|
[:ul {:class (stl/css :element-list)}
|
||||||
(for [[index id] (d/enumerate (:shapes root))]
|
(for [[index id] (d/enumerate (:shapes root))]
|
||||||
(when-let [obj (get objects id)]
|
(when (some? (get objects id))
|
||||||
[:> layer-item*
|
[:> layer-item*
|
||||||
{:item obj
|
{:item-id id
|
||||||
:selected selected
|
:selected selected
|
||||||
:index index
|
:index index
|
||||||
:objects objects
|
|
||||||
:key id
|
:key id
|
||||||
:is-sortable false
|
:is-sortable false
|
||||||
:is-filtered true
|
:is-filtered true
|
||||||
@ -602,8 +560,7 @@
|
|||||||
:data-scroll-container true
|
:data-scroll-container true
|
||||||
:style {:display (when (some? filtered-objects) "none")}}
|
:style {:display (when (some? filtered-objects) "none")}}
|
||||||
|
|
||||||
[:> layers-tree-wrapper* {:objects filtered-objects
|
[:> layers-tree-wrapper* {:key (dm/str page-id)
|
||||||
:key (dm/str page-id)
|
|
||||||
:is-filtered true
|
:is-filtered true
|
||||||
:parent-size size-parent}]]]
|
:parent-size size-parent}]]]
|
||||||
|
|
||||||
@ -611,7 +568,6 @@
|
|||||||
:class (stl/css :tool-window-content)
|
:class (stl/css :tool-window-content)
|
||||||
:data-scroll-container true
|
:data-scroll-container true
|
||||||
:style {:display (when (some? filtered-objects) "none")}}
|
:style {:display (when (some? filtered-objects) "none")}}
|
||||||
[:> layers-tree-wrapper* {:objects objects
|
[:> layers-tree-wrapper* {:key (dm/str page-id)
|
||||||
:key (dm/str page-id)
|
|
||||||
:is-filtered false
|
:is-filtered false
|
||||||
:parent-size size-parent}]])]))
|
:parent-size size-parent}]])]))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user