diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index a37912ede0..2113f76ea1 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1473,12 +1473,17 @@ (rx/filter (complement :skip-component-sync?)) ;; Keep waits pending while component changes are checked. (rx/map start-sync-barrier) - (rx/observe-on :async)) + (rx/share)) - get-component-events - (fn [[event old-data]] - (let [{:keys [file-id changes save-undo? undo-group]} event - changed-components + ;; Buffers commits until a timer turn passes with no new ones. The + ;; debounce timer (not a microtask) queues behind any timers already + ;; due, so a burst of commits is inspected as one batch in one task. + commit-batches-s + (rx/buffer-until (rx/debounce 0 commits-s) commits-s) + + get-component-changes + (fn [old-data {:keys [file-id changes save-undo? undo-group]}] + (let [changed-components (when (and old-data (or (nil? file-id) (= file-id (:id old-data)))) (into #{} @@ -1486,29 +1491,39 @@ changes))] (cond (empty? changed-components) - (rx/empty) + nil save-undo? (do (log/info :hint "detected component changes" :ids (map str changed-components) :undo-group undo-group) - (->> (rx/from changed-components) - (rx/map #(component-changed - % (:id old-data) undo-group)))) + (map #(vector ::component-changed % undo-group) + changed-components)) :else ;; Undos only bump :modified-at. - (->> (rx/from changed-components) - (rx/map touch-component))))) + (map #(vector ::touch-component %) changed-components)))) + + ;; One event per distinct component change in the batch + get-component-events + (fn [batch old-data] + (->> batch + (mapcat #(get-component-changes old-data (first %))) + (distinct) + (map (fn [[type component-id undo-group]] + (if (= type ::component-changed) + (component-changed component-id (:id old-data) undo-group) + (touch-component component-id)))) + (rx/from))) component-events-s - (->> commits-s + (->> commit-batches-s (rx/with-latest-from workspace-buffer-s) (rx/mapcat - (fn [[[event task] old-data]] - (->> (get-component-events [event old-data]) - (rx/finalize #(finish-sync-barrier! task))))) + (fn [[batch old-data]] + (->> (get-component-events batch old-data) + (rx/finalize #(run! (comp finish-sync-barrier! second) batch))))) ;; Close barriers left behind when the page shuts down. (rx/finalize #(wrf/finish-tasks! @pending-sync-barriers*)) (rx/share)) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index 474c40d60d..39bb3f0786 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -202,12 +202,6 @@ file-id (mf/use-ctx ctx/current-file-id) - current-colors* (mf/use-state []) - current-colors (deref current-colors*) - - grouped-colors* (mf/use-state {}) - grouped-colors (deref grouped-colors*) - open-groups* (mf/use-state #{}) open-groups (deref open-groups*) @@ -221,13 +215,15 @@ [{:value "recent" :label (tr "workspace.libraries.colors.recent-colors") :id "recent"} {:value "file" :label (tr "workspace.libraries.colors.file-library") :id "file"}]) + ;; Equal-stable, since `select*` resets its selection when options change options - (mf/with-memo [library-options libraries file-id] - (into library-options - (comp - (map val) - (map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))}))) - (dissoc libraries file-id))) + (h/use-equal-memo + (mf/with-memo [library-options libraries file-id] + (into library-options + (comp + (map val) + (map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))}))) + (dissoc libraries file-id)))) on-library-change (mf/use-fn @@ -281,49 +277,49 @@ (fn [s] (if (contains? s path) (disj s path) - (conj s path))))))] + (conj s path)))))) - ;; Load library colors when the selected library (or filter options) change. - ;; - ;; flat current-colors* -- used for the grid view and the recent list view. - ;; grouped grouped-colors* -- used for the library grouped list view. - ;; - ;; Library colors are fully converted with `library-color->color` here so - ;; the render path never needs to do it. `flat-colors` is materialised as - ;; an eager vector so realisation does not leak into render time. - ;; open-groups* is reset to #{} (all groups expanded) on every library switch. - (mf/with-effect [selected recent-colors libraries file-id valid-color?] - (let [resolved-file-id (if (= selected :file) file-id selected)] - (reset! open-groups* #{}) - (if (= selected :recent) - (let [colors (into [] - (comp - (filter valid-color?) - (map-indexed (fn [index color] - (let [color (if (map? color) color {:color color})] - (vary-meta color assoc ::id (dm/str index))))) - (take-while some?)) - (sort ctc/sort-colors (reverse recent-colors)))] - (reset! current-colors* colors) - (reset! grouped-colors* {})) + resolved-file-id + (if (= selected :file) file-id selected) - (let [raw-colors (->> (dm/get-in libraries [resolved-file-id :data :colors]) - (vals) - (filter valid-color?) - (sort-by :name)) + ;; Colors of the selected library; stable across unrelated commits + library-colors + (when (not= selected :recent) + (dm/get-in libraries [resolved-file-id :data :colors])) - ;; Eager vector for the grid view -- index-based ::id for keying. - flat-colors (into [] - (map-indexed (fn [index color] - (-> (ctc/library-color->color color resolved-file-id) - (vary-meta assoc ::id (dm/str index))))) - raw-colors) + ;; Flat (grid and recent list) and grouped (library list) colors, converted for rendering + [current-colors grouped-colors] + (mf/with-memo [selected recent-colors library-colors resolved-file-id valid-color?] + (if (= selected :recent) + [(into [] + (comp + (filter valid-color?) + (map-indexed (fn [index color] + (let [color (if (map? color) color {:color color})] + (vary-meta color assoc ::id (dm/str index))))) + (take-while some?)) + (sort ctc/sort-colors (reverse recent-colors))) + {}] - ;; Group tree with colors already converted -- no conversions at render time. - grouped (some-> (grp/group-assets raw-colors false) - (convert-grouped-colors resolved-file-id))] - (reset! current-colors* flat-colors) - (reset! grouped-colors* (or grouped {})))))) + (let [raw-colors (->> (vals library-colors) + (filter valid-color?) + (sort-by :name)) + + ;; Eager vector for the grid view -- index-based ::id for keying. + flat-colors (into [] + (map-indexed (fn [index color] + (-> (ctc/library-color->color color resolved-file-id) + (vary-meta assoc ::id (dm/str index))))) + raw-colors) + + ;; Group tree with colors already converted -- no conversions at render time. + grouped (some-> (grp/group-assets raw-colors false) + (convert-grouped-colors resolved-file-id))] + [flat-colors (or grouped {})])))] + + ;; Expands all groups when the selected library changes + (mf/with-effect [selected] + (reset! open-groups* #{})) [:div {:class (stl/css :libraries)} [:div {:class (stl/css :select-wrapper)} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index fc4861678e..a8ddf66070 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -121,10 +121,15 @@ (reset! internal-color* color) (on-change color))))] + ;; Syncs with color changes made outside the ramp; the colors the ramp + ;; emits come back with the same components and are skipped (mf/use-effect (mf/deps color) (fn [] - (reset! internal-color* (enrich-color-map color)))) + (let [color (enrich-color-map color)] + (when (not= (select-keys color [:h :s :v :alpha]) + (select-keys internal-color [:h :s :v :alpha])) + (reset! internal-color* color))))) [:* [:> value-saturation-selector* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 9229b36f70..b753def734 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -100,6 +100,21 @@ (when (= (:type panel) :component-swap) [:> component-menu* {:shapes (:shapes panel) :is-swap-opened true}])) +(defn- get-shapes-with-children + "Returns the shapes of `selected` together with all their descendants." + [objects selected] + (loop [queue (into #queue [] selected) + visited selected] + (if-let [id (peek queue)] + (let [shape (get objects id) + children (:shapes shape)] + (if (seq children) + (let [new-children (remove visited children)] + (recur (into (pop queue) new-children) + (into visited new-children))) + (recur (pop queue) visited))) + (sequence (keep (d/getf objects)) visited)))) + (mf/defc design-menu* {::mf/private true} [{:keys [selected objects page-id file-id shapes]}] @@ -145,29 +160,16 @@ (->> (dm/get-in grid-edition [edition :selected]) (map #(dm/get-in objects [edition :layout-grid-cells %]))) - shapes-with-children* - (mf/use-state nil) + ;; Deferred, so the subtree walk runs in a background render + deferred-selected + (mf/use-deferred selected) - _ (mf/use-effect - (mf/deps selected objects shapes) - (fn [] - (reset! shapes-with-children* nil) - (let [result - (loop [queue (into #queue [] selected) - visited selected] - (if-let [id (peek queue)] - (let [shape (get objects id) - children (:shapes shape)] - (if (seq children) - (let [new-children (remove visited children)] - (recur (into (pop queue) new-children) - (into visited new-children))) - (recur (pop queue) visited))) - (sequence (keep (d/getf objects)) visited)))] - (reset! shapes-with-children* result)))) + deferred-objects + (mf/use-deferred objects) shapes-with-children - (deref shapes-with-children*) + (mf/with-memo [deferred-selected deferred-objects] + (get-shapes-with-children deferred-objects deferred-selected)) total-selected (count selected)]