diff --git a/CHANGES.md b/CHANGES.md index 17114bc6a..2c2f173c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -49,6 +49,7 @@ - Fix unexpected removal of guides on copy&paste frames [Taiga #3887](https://tree.taiga.io/project/penpot/issue/3887) by @andrewzhurov - Fix props preserving on copy&paste texts [Taiga #3629](https://tree.taiga.io/project/penpot/issue/3629) by @andrewzhurov - Fix unexpected layers ungrouping on moving it [Taiga #3932](https://tree.taiga.io/project/penpot/issue/3932) by @andrewzhurov +- Fix unexpected exception and behavior on colorpicker with gradients [Taiga #3448](https://tree.taiga.io/project/penpot/issue/3448) diff --git a/frontend/resources/styles/main/partials/color-palette.scss b/frontend/resources/styles/main/partials/color-palette.scss index 78eeed013..3ef34c52a 100644 --- a/frontend/resources/styles/main/partials/color-palette.scss +++ b/frontend/resources/styles/main/partials/color-palette.scss @@ -82,6 +82,7 @@ .color-palette-actions-button { cursor: pointer; + display: flex; & svg { width: 1rem; height: 1rem; diff --git a/frontend/src/app/main/broadcast.cljs b/frontend/src/app/main/broadcast.cljs new file mode 100644 index 000000000..ef50e4b31 --- /dev/null +++ b/frontend/src/app/main/broadcast.cljs @@ -0,0 +1,52 @@ +;; 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.broadcast + "BroadcastChannel API." + (:require + [app.common.transit :as t] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defrecord BroadcastMessage [id type data] + cljs.core/IDeref + (-deref [_] data)) + +(def ^:const default-topic "penpot") + +;; The main broadcast channel instance, used for emit data +(defonce default-channel + (js/BroadcastChannel. default-topic)) + +(defonce stream + (->> (rx/create (fn [subs] + (let [chan (js/BroadcastChannel. default-topic)] + (unchecked-set chan "onmessage" #(rx/push! subs (unchecked-get % "data"))) + (fn [] (.close ^js chan))))) + (rx/map t/decode-str) + (rx/map map->BroadcastMessage) + (rx/share))) + +(defn emit! + ([type data] + (.postMessage ^js default-channel (t/encode-str {:id nil :type type :data data})) + nil) + ([id type data] + (.postMessage ^js default-channel (t/encode-str {:id id :type type :data data})) + nil)) + +(defn type? + ([type] + (fn [obj] (= (:type obj) type))) + ([obj type] + (= (:type obj) type))) + +(defn event + [type data] + (ptk/reify ::event + ptk/EffectEvent + (effect [_ _ _] + (emit! type data)))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 904412bd2..61f91a062 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -6,55 +6,32 @@ (ns app.main.data.workspace.colors (:require - [app.common.colors :as clr] + [app.common.colors :as colors] [app.common.data :as d] [app.common.pages.helpers :as cph] + [app.main.broadcast :as mbc] [app.main.data.modal :as md] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.layout :as layout] + [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] [app.util.color :as uc] [beicon.core :as rx] [potok.core :as ptk])) -(defn change-palette-selected - "Change the library used by the general palette tool" - [selected] - (ptk/reify ::change-palette-selected - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :selected-palette] selected)) - - ptk/EffectEvent - (effect [_ state _] - (let [wglobal (:workspace-global state)] - (layout/persist-layout-state! wglobal))))) - -(defn change-palette-selected-colorpicker - "Change the library used by the color picker" - [selected] - (ptk/reify ::change-palette-selected-colorpicker - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :selected-palette-colorpicker] selected)) - - ptk/EffectEvent - (effect [_ state _] - (let [wglobal (:workspace-global state)] - (layout/persist-layout-state! wglobal))))) +;; A set of keys that are used for shared state identifiers +(def ^:const colorpicker-selected-broadcast-key ::colorpicker-selected) +(def ^:const colorpalette-selected-broadcast-key ::colorpalette-selected) (defn show-palette "Show the palette tool and change the library it uses" [selected] (ptk/reify ::show-palette - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :selected-palette] selected)) - ptk/WatchEvent (watch [_ _ _] - (rx/of (layout/toggle-layout-flag :colorpalette :force? true))) + (rx/of (layout/toggle-layout-flag :colorpalette :force? true) + (mbc/event colorpalette-selected-broadcast-key selected))) ptk/EffectEvent (effect [_ state _] @@ -158,10 +135,10 @@ ptk/WatchEvent (watch [_ state _] (let [change-fn (fn [shape attrs] - (-> shape - (cond-> (not (contains? shape :fills)) - (assoc :fills [])) - (assoc-in [:fills position] (into {} attrs))))] + (-> shape + (cond-> (not (contains? shape :fills)) + (assoc :fills [])) + (assoc-in [:fills position] (into {} attrs))))] (transform-fill state ids color change-fn))))) (defn change-fill-and-clear @@ -342,45 +319,11 @@ (-> state (assoc-in [:workspace-global :picking-color?] true) (assoc ::md/modal {:id (random-uuid) - :data {:color clr/black :opacity 1} + :data {:color colors/black :opacity 1} :type :colorpicker :props {:on-change handle-change-color} :allow-click-outside true}))))))) -(defn start-gradient - [gradient] - (ptk/reify ::start-gradient - ptk/UpdateEvent - (update [_ state] - (let [id (-> state wsh/lookup-selected first)] - (-> state - (assoc-in [:workspace-global :current-gradient] gradient) - (assoc-in [:workspace-global :current-gradient :shape-id] id)))))) - -(defn stop-gradient - [] - (ptk/reify ::stop-gradient - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-global dissoc :current-gradient))))) - -(defn update-gradient - [changes] - (ptk/reify ::update-gradient - ptk/UpdateEvent - (update [_ state] - (-> state - (update-in [:workspace-global :current-gradient] merge changes))))) - -(defn select-gradient-stop - [spot] - (ptk/reify ::select-gradient-stop - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-global :editing-stop] spot))))) - (defn color-att->text [color] {:fill-color (:color color) @@ -409,7 +352,9 @@ :fill (change-fill [(:shape-id shape)] new-color (:index shape)) :stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) :shadow (change-shadow [(:shape-id shape)] new-color (:index shape)) - :content (dwt/update-text-with-function (:shape-id shape) (partial change-text-color old-color new-color (:index shape)))))))))) + :content (dwt/update-text-with-function + (:shape-id shape) + (partial change-text-color old-color new-color (:index shape)))))))))) (defn apply-color-from-palette [color is-alt?] @@ -431,3 +376,177 @@ (if is-alt? (rx/of (change-stroke ids (merge uc/empty-color color) 0)) (rx/of (change-fill ids (merge uc/empty-color color) 0))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; COLORPICKER STATE MANAGEMENT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn split-color-components + [{:keys [color opacity] :as data}] + (let [value (if (uc/hex? color) color colors/black) + [r g b] (uc/hex->rgb value) + [h s v] (uc/hex->hsv value)] + (merge data + {:hex (or value "000000") + :alpha (or opacity 1) + :r r :g g :b b + :h h :s s :v v}))) + +(defn materialize-color-components + [{:keys [hex alpha] :as data}] + (-> data + (assoc :color hex) + (assoc :opacity alpha))) + +(defn clear-color-components + [data] + (dissoc data :hex :alpha :r :g :b :h :s :v)) + +(defn- create-gradient + [type] + {:start-x 0.5 + :start-y (if (= type :linear-gradient) 0.0 0.5) + :end-x 0.5 + :end-y 1 + :width 1.0}) + +(defn get-color-from-colorpicker-state + [{:keys [type current-color stops gradient] :as state}] + (if (= type :color) + (clear-color-components current-color) + {:gradient (-> gradient + (assoc :type (case type + :linear-gradient :linear + :radial-gradient :radial)) + (assoc :stops (mapv clear-color-components stops)) + (dissoc :shape-id))})) + +(defn- colorpicker-onchange-runner + "Effect event that runs the on-change callback with the latest + colorpicker state converted to color object." + [on-change] + (ptk/reify ::colorpicker-onchange-runner + ptk/WatchEvent + (watch [_ state _] + (when-let [color (some-> state :colorpicker get-color-from-colorpicker-state)] + (on-change color) + (rx/of (dwl/add-recent-color color)))))) + +(defn initialize-colorpicker + [on-change] + (ptk/reify ::initialize-colorpicker + ptk/WatchEvent + (watch [_ _ stream] + (let [stoper (rx/merge + (rx/filter (ptk/type? ::finalize-colorpicker) stream) + (rx/filter (ptk/type? ::initialize-colorpicker) stream))] + + (->> (rx/merge + (->> stream + (rx/filter (ptk/type? ::update-colorpicker-gradient)) + (rx/debounce 200)) + (rx/filter (ptk/type? ::update-colorpicker-color) stream) + (rx/filter (ptk/type? ::activate-colorpicker-gradient) stream)) + (rx/map (constantly (colorpicker-onchange-runner on-change))) + (rx/take-until stoper)))))) + +(defn finalize-colorpicker + [] + (ptk/reify ::finalize-colorpicker + ptk/UpdateEvent + (update [_ state] + (dissoc state :colorpicker)))) + +(defn update-colorpicker + [{:keys [gradient] :as data}] + (ptk/reify ::update-colorpicker + ptk/UpdateEvent + (update [_ state] + (let [shape-id (-> state wsh/lookup-selected first)] + (update state :colorpicker + (fn [state] + (if (some? gradient) + (let [stop (or (:editing-stop state) 0) + stops (mapv split-color-components (:stops gradient)) + type (case (:type gradient) + :linear :linear-gradient + :radial :radial-gradient)] + (-> state + (assoc :type type) + (assoc :current-color (nth stops stop)) + (assoc :stops stops) + (assoc :gradient (-> gradient + (dissoc :stops) + (assoc :shape-id shape-id))) + (assoc :editing-stop stop))) + + (-> state + (assoc :type :color) + (assoc :current-color (split-color-components (dissoc data :gradient))) + (dissoc :editing-stop) + (dissoc :gradient) + (dissoc :stops))))))))) + +(defn update-colorpicker-color + [changes] + (ptk/reify ::update-colorpicker-color + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (let [state (-> state + (update :current-color merge changes) + (update :current-color materialize-color-components))] + (if-let [stop (:editing-stop state)] + (update-in state [:stops stop] (fn [data] (->> changes + (merge data) + (materialize-color-components)))) + (-> state + (assoc :type :color) + (dissoc :gradient :stops :editing-stop))))))))) + +(defn update-colorpicker-gradient + [changes] + (ptk/reify ::update-colorpicker-gradient + ptk/UpdateEvent + (update [_ state] + (update-in state [:colorpicker :gradient] merge changes)))) + +(defn select-colorpicker-gradient-stop + [stop] + (ptk/reify ::select-colorpicket-gradient-stop + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (if-let [color (get-in state [:stops stop])] + (assoc state + :current-color color + :editing-stop stop) + state)))))) + +(defn activate-colorpicker-gradient + [type] + (ptk/reify ::activate-colorpicker-gradient + ptk/UpdateEvent + (update [_ state] + (update state :colorpicker + (fn [state] + (if (= type (:type state)) + (do + (-> state + (assoc :type :color) + (dissoc :editing-stop :stops :gradient))) + (let [gradient (create-gradient type) + color (:current-color state)] + (-> state + (assoc :type type) + (assoc :gradient gradient) + (cond-> (not (:stops state)) + (assoc :editing-stop 0 + :stops [(assoc color :offset 0) + (-> color + (assoc :alpha 0) + (assoc :offset 1) + (materialize-color-components))])))))))))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 7663a0585..5b37502eb 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -113,7 +113,7 @@ (defn add-recent-color [color] - (us/assert ::ctc/recent-color color) + (us/assert! ::ctc/recent-color color) (ptk/reify ::add-recent-color ptk/WatchEvent (watch [it _ _] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 112d4b001..9040ee831 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -410,3 +410,7 @@ (defn workspace-text-modifier-by-id [id] (l/derived #(get % id) workspace-text-modifier =)) + +(def colorpicker + (l/derived :colorpicker st/state)) + diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index aa177e094..5fd3acd0f 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -17,23 +17,31 @@ :radial (tr "workspace.gradients.radial") nil)) -(mf/defc color-bullet [{:keys [color on-click]}] - (if (uc/multiple? color) - [:div.color-bullet.multiple {:on-click #(when on-click (on-click %))}] +(mf/defc color-bullet + {::mf/wrap [mf/memo]} + [{:keys [color on-click]}] + (let [on-click (mf/use-fn + (mf/deps color on-click) + (fn [event] + (when (fn? on-click) + (^function on-click color event))))] - ;; No multiple selection - (let [color (if (string? color) {:color color :opacity 1} color)] - [:div.color-bullet.tooltip.tooltip-right - {:class (dom/classnames :is-library-color (some? (:id color)) - :is-not-library-color (nil? (:id color)) - :is-gradient (some? (:gradient color))) - :on-click #(when on-click (on-click %)) - :alt (or (:name color) (:color color) (gradient-type->string (:type (:gradient color))))} - (if (:gradient color) - [:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}] - [:div.color-bullet-wrapper - [:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}] - [:div.color-bullet-right {:style {:background (uc/color->background color)}}]])]))) + (if (uc/multiple? color) + [:div.color-bullet.multiple {:on-click on-click}] + + ;; No multiple selection + (let [color (if (string? color) {:color color :opacity 1} color)] + [:div.color-bullet.tooltip.tooltip-right + {:class (dom/classnames :is-library-color (some? (:id color)) + :is-not-library-color (nil? (:id color)) + :is-gradient (some? (:gradient color))) + :on-click on-click + :alt (or (:name color) (:color color) (gradient-type->string (:type (:gradient color))))} + (if (:gradient color) + [:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}] + [:div.color-bullet-wrapper + [:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}] + [:div.color-bullet-right {:style {:background (uc/color->background color)}}]])])))) (mf/defc color-name [{:keys [color size on-click on-double-click]}] (let [color (if (string? color) {:color color :opacity 1} color) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index d9c2b72ba..6fc2c6850 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -7,16 +7,26 @@ (ns app.main.ui.hooks "A collection of general purpose react hooks." (:require + [app.common.data.macros :as dm] [app.common.pages :as cp] + [app.common.uuid :as uuid] + [app.main.broadcast :as mbc] [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] + [app.util.storage :refer [storage]] [app.util.timers :as ts] [beicon.core :as rx] + [goog.functions :as f] [rumext.alpha :as mf])) +(defn use-id + "Get a stable id value across rerenders." + [] + (mf/use-memo #(dm/str (uuid/next)))) + (defn use-rxsub [ob] (let [[state reset-state!] (mf/useState @ob)] @@ -191,7 +201,6 @@ [(deref state) ref])) - (defn use-stream "Wraps the subscription to a stream into a `use-effect` call" ([stream on-subscribe] @@ -205,6 +214,7 @@ ;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state (defn use-previous + "Returns the value from previuous render cycle." [value] (let [ref (mf/use-ref value)] (mf/use-effect @@ -214,13 +224,27 @@ (mf/ref-val ref))) (defn use-update-var + "Returns a var pointer what automatically updates with latest values." [value] - (let [ref (mf/use-var value)] - (mf/use-effect - (mf/deps value) - (fn [] - (reset! ref value))) - ref)) + (let [ptr (mf/use-var value)] + (mf/with-effect [value] + (reset! ptr value)) + ptr)) + +(defn use-ref-callback + "Returns a stable callback pointer what calls the interned + callback. The interned callback will be automatically updated on + each reander if the reference changes and works as noop if the + pointer references to nil value." + [f] + (let [ptr (mf/use-ref nil)] + (mf/with-effect [f] + (mf/set-ref-val! ptr #js {:f f})) + (mf/use-fn + (fn [& args] + (let [obj (mf/ref-val ptr)] + (when ^boolean obj + (apply (.-f obj) args))))))) (defn use-equal-memo [val] @@ -258,4 +282,34 @@ #(cp/focus-objects objects focus))] objects))) +(defn use-debounce + [ms value] + (let [[state update-state-fn] (mf/useState value) + update-fn (mf/use-memo (mf/deps ms) #(f/debounce update-state-fn ms))] + (mf/with-effect [value] + (update-fn value)) + state)) +(defn use-shared-state + "A specialized hook that adds persistence and inter-context reactivity + to the default mf/use-state hook. + + The state is automatically persisted under the provided key on + localStorage. And it will keep watching events with type equals to + `key` for new values." + [key default] + (let [id (use-id) + state (mf/use-state (get @storage key default)) + stream (mf/with-memo [] + (->> mbc/stream + (rx/filter #(= (:type %) key)) + (rx/filter #(not= (:id %) id)) + (rx/map deref)))] + + (mf/with-effect [@state key] + (mbc/emit! id key @state) + (swap! storage assoc key @state)) + + (use-stream stream (partial reset! state)) + + state)) diff --git a/frontend/src/app/main/ui/hooks/resize.cljs b/frontend/src/app/main/ui/hooks/resize.cljs index d640fd81b..101d84b21 100644 --- a/frontend/src/app/main/ui/hooks/resize.cljs +++ b/frontend/src/app/main/ui/hooks/resize.cljs @@ -8,6 +8,7 @@ (:require [app.common.geom.point :as gpt] [app.common.logging :as log] + [app.common.spec :as us] [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.util.dom :as dom] @@ -73,43 +74,38 @@ (defn use-resize-observer [callback] - (assert (some? callback)) + (us/assert! (some? callback) "the `callback` is mandatory") (let [prev-val-ref (mf/use-ref nil) - current-observer-ref (mf/use-ref nil) - - callback-ref (hooks/use-update-var {:callback callback}) + observer-ref (mf/use-ref nil) + callback (hooks/use-ref-callback callback) ;; We use the ref as a callback when the dom node is ready (or change) - node-ref - (mf/use-callback - (fn [^js node] - (when (some? node) - (let [^js current-observer (mf/ref-val current-observer-ref) - ^js prev-val (mf/ref-val prev-val-ref)] + node-ref (mf/use-fn + (fn [^js node] + (when (some? node) + (let [^js observer (mf/ref-val observer-ref) + ^js prev-val (mf/ref-val prev-val-ref)] - (when (and (not= prev-val node) (some? current-observer)) - (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) - (.disconnect current-observer) - (mf/set-ref-val! current-observer-ref nil)) + (when (and (not= prev-val node) (some? observer)) + (log/debug :action "disconnect" :js/prev-val prev-val :js/node node) + (.disconnect observer) + (mf/set-ref-val! observer-ref nil)) - (when (and (not= prev-val node) (some? node)) - (let [^js observer - (js/ResizeObserver. - #(let [callback (get @callback-ref :callback)] - (callback last-resize-type (dom/get-client-size node))))] - (mf/set-ref-val! current-observer-ref observer) - (log/debug :action "observe" :js/node node :js/observer observer) - (.observe observer node)))) + (when (and (not= prev-val node) (some? node)) + (let [^js observer (js/ResizeObserver. + #(callback last-resize-type (dom/get-client-size node)))] + (mf/set-ref-val! observer-ref observer) + (log/debug :action "observe" :js/node node :js/observer observer) + (.observe observer node)))) - (mf/set-ref-val! prev-val-ref node))))] + (mf/set-ref-val! prev-val-ref node))))] + + (mf/with-effect [] + ;; On dismount we need to disconnect the current observer + (fn [] + (when-let [observer (mf/ref-val observer-ref)] + (log/debug :action "disconnect") + (.disconnect ^js observer)))) - (mf/use-effect - (fn [] - ;; On dismount we need to disconnect the current observer - (fn [] - (let [current-observer (mf/ref-val current-observer-ref)] - (when (some? current-observer) - (log/debug :action "disconnect") - (.disconnect current-observer)))))) node-ref)) diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index c46863230..39b0e17bb 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -6,11 +6,13 @@ (ns app.main.ui.workspace.colorpalette (:require + [app.common.data.macros :as dm] [app.main.data.workspace.colors :as mdc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :as cb] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.hooks :as h] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] [app.util.dom :as dom] @@ -19,36 +21,21 @@ [app.util.object :as obj] [cuerdas.core :as str] [goog.events :as events] - [okulary.core :as l] [rumext.alpha :as mf])) -;; --- Refs - -(def palettes-ref - (-> (l/in [:library :palettes]) - (l/derived st/state))) - -(def selected-palette-ref - (-> (l/in [:workspace-global :selected-palette]) - (l/derived st/state))) - -(def selected-palette-size-ref - (-> (l/in [:workspace-global :selected-palette-size]) - (l/derived st/state))) - ;; --- Components -(mf/defc palette-item - [{:keys [color]}] - (let [select-color - (fn [event] - (st/emit! (mdc/apply-color-from-palette color (kbd/alt? event))))] +(mf/defc palette-item + {::mf/wrap [mf/memo]} + [{:keys [color]}] + (letfn [(select-color [event] + (st/emit! (mdc/apply-color-from-palette color (kbd/alt? event))))] [:div.color-cell {:on-click select-color} [:& cb/color-bullet {:color color}] [:& cb/color-name {:color color}]])) (mf/defc palette - [{:keys [current-colors recent-colors file-colors shared-libs selected]}] + [{:keys [current-colors recent-colors file-colors shared-libs selected on-select]}] (let [state (mf/use-state {:show-menu false}) width (:width @state 0) @@ -97,54 +84,66 @@ (fn [_] (let [dom (mf/ref-val container) width (obj/get dom "clientWidth")] - (swap! state assoc :width width))))] + (swap! state assoc :width width)))) + on-select-palette + (mf/use-fn + (mf/deps on-select) + (fn [event] + (let [node (dom/get-current-target event) + value (dom/get-attribute node "data-palette")] + (on-select (if (or (= "file" value) (= "recent" value)) + (keyword value) + (parse-uuid value))))))] (mf/use-layout-effect #(let [dom (mf/ref-val container) width (obj/get dom "clientWidth")] (swap! state assoc :width width))) - (mf/use-effect - #(let [key1 (events/listen js/window "resize" on-resize)] - (fn [] - (events/unlistenByKey key1)))) + (mf/with-effect [] + (let [key1 (events/listen js/window "resize" on-resize)] + #(events/unlistenByKey key1))) [:div.color-palette {:ref parent-ref :class (dom/classnames :no-text (< size 72)) - :style #js {"--height" (str size "px") - "--bullet-size" (str (if (< size 72) (- size 15) (- size 30)) "px")}} + :style #js {"--height" (dm/str size "px") + "--bullet-size" (dm/str (if (< size 72) (- size 15) (- size 30)) "px")}} [:div.resize-area {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture :on-mouse-move on-mouse-move}] [:& dropdown {:show (:show-menu @state) :on-close #(swap! state assoc :show-menu false)} [:ul.workspace-context-menu.palette-menu - (for [[idx cur-library] (map-indexed vector (vals shared-libs))] - (let [colors (-> cur-library (get-in [:data :colors]) vals)] + (for [{:keys [data id] :as library} (vals shared-libs)] + (let [colors (-> data :colors vals)] [:li.palette-library - {:key (str "library-" idx) - :on-click #(st/emit! (mdc/change-palette-selected (:id cur-library)))} - (when (= selected (:id cur-library)) i/tick) - [:div.library-name (str (:name cur-library) " " (str/format "(%s)" (count colors)))] + {:key (dm/str "library-" id) + :on-click on-select-palette + :data-palette (dm/str id)} + (when (= selected id) i/tick) + [:div.library-name (str (:name library) " " (str/ffmt "(%)" (count colors)))] [:div.color-sample - (for [[idx {:keys [color]}] (map-indexed vector (take 7 colors))] - [:& cb/color-bullet {:key (str "color-" idx) + (for [[i {:keys [color]}] (map-indexed vector (take 7 colors))] + [:& cb/color-bullet {:key (dm/str "color-" i) :color color}])]])) [:li.palette-library - {:on-click #(st/emit! (mdc/change-palette-selected :file))} + {:on-click on-select-palette + :data-palette "file"} (when (= selected :file) i/tick) - [:div.library-name (str (tr "workspace.libraries.colors.file-library") - (str/format " (%s)" (count file-colors)))] + [:div.library-name (dm/str + (tr "workspace.libraries.colors.file-library") + (str/ffmt " (%)" (count file-colors)))] [:div.color-sample - (for [[idx color] (map-indexed vector (take 7 (vals file-colors))) ] - [:& cb/color-bullet {:key (str "color-" idx) + (for [[i color] (map-indexed vector (take 7 (vals file-colors))) ] + [:& cb/color-bullet {:key (dm/str "color-" i) :color color}])]] [:li.palette-library - {:on-click #(st/emit! (mdc/change-palette-selected :recent))} + {:on-click on-select-palette + :data-palette "recent"} (when (= selected :recent) i/tick) [:div.library-name (str (tr "workspace.libraries.colors.recent-colors") (str/format " (%s)" (count recent-colors)))] @@ -178,34 +177,32 @@ (let [recent-colors (mf/deref refs/workspace-recent-colors) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) - selected (or (mf/deref selected-palette-ref) :recent) - current-library-colors (mf/use-state [])] + selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) - (mf/use-effect - (mf/deps selected) - (fn [] - (reset! current-library-colors - (into [] - (cond - (= selected :recent) (reverse recent-colors) - (= selected :file) (->> (vals file-colors) (sort-by :name)) - :else (->> (library->colors shared-libs selected) (sort-by :name))))))) + colors (mf/use-state []) + on-select (mf/use-fn #(reset! selected %))] - (mf/use-effect - (mf/deps recent-colors) - (fn [] - (when (= selected :recent) - (reset! current-library-colors (reverse recent-colors))))) + (mf/with-effect [@selected] + (fn [] + (reset! colors + (into [] + (cond + (= @selected :recent) (reverse recent-colors) + (= @selected :file) (->> (vals file-colors) (sort-by :name)) + :else (->> (library->colors shared-libs @selected) (sort-by :name))))))) - (mf/use-effect - (mf/deps file-colors) - (fn [] - (when (= selected :file) - (reset! current-library-colors (into [] (->> (vals file-colors) - (sort-by :name))))))) + (mf/with-effect [recent-colors @selected] + (when (= @selected :recent) + (reset! colors (reverse recent-colors)))) - [:& palette {:current-colors @current-library-colors + (mf/with-effect [file-colors @selected] + (when (= @selected :file) + (reset! colors (into [] (->> (vals file-colors) + (sort-by :name)))))) + + [:& palette {:current-colors @colors :recent-colors recent-colors :file-colors file-colors :shared-libs shared-libs - :selected selected}])) + :selected @selected + :on-select on-select}])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index 12829dd12..da975ea1d 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.colorpicker (:require - [app.common.colors :as clr] [app.main.data.modal :as modal] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.libraries :as dwl] @@ -41,247 +40,102 @@ (def viewport (l/derived :vport refs/workspace-local)) -(def editing-spot-state-ref - (l/derived :editing-stop refs/workspace-global)) - -(def current-gradient-ref - (l/derived :current-gradient refs/workspace-global)) - ;; --- Color Picker Modal -(defn color->components [value opacity] - (let [value (if (uc/hex? value) value clr/black) - [r g b] (uc/hex->rgb value) - [h s v] (uc/hex->hsv value)] - - {:hex (or value "000000") - :alpha (or opacity 1) - :r r :g g :b b - :h h :s s :v v})) - -(defn data->state [{:keys [color opacity gradient]}] - (let [type (cond - (nil? gradient) :color - (= :linear (:type gradient)) :linear-gradient - (= :radial (:type gradient)) :radial-gradient) - - parse-stop (fn [{:keys [offset color opacity]}] - (vector offset (color->components color opacity))) - - stops (when gradient - (map parse-stop (:stops gradient))) - - current-color (if (nil? gradient) - (color->components color opacity) - (-> stops first second)) - - gradient-data (select-keys gradient [:start-x :start-y - :end-x :end-y - :width])] - - (cond-> {:type type - :current-color current-color} - gradient (assoc :gradient-data gradient-data) - stops (assoc :stops (into {} stops)) - stops (assoc :editing-stop (-> stops first first))))) - -(defn state->data [{:keys [type current-color stops gradient-data]}] - (if (= type :color) - {:color (:hex current-color) - :opacity (:alpha current-color)} - - (let [gradient-type (case type - :linear-gradient :linear - :radial-gradient :radial) - parse-stop (fn [[offset {:keys [hex alpha]}]] - (hash-map :offset offset - :color hex - :opacity alpha))] - {:gradient (-> {:type gradient-type - :stops (mapv parse-stop stops)} - (merge gradient-data))}))) - -(defn create-gradient-data [type] - {:start-x 0.5 - :start-y (if (= type :linear-gradient) 0.0 0.5) - :end-x 0.5 - :end-y 1 - :width 1.0}) - (mf/defc colorpicker [{:keys [data disable-gradient disable-opacity on-change on-accept]}] - (let [state (mf/use-state (data->state data)) - active-tab (mf/use-state :ramp #_:harmony #_:hsva) + (let [state (mf/deref refs/colorpicker) + node-ref (mf/use-ref) - ref-picker (mf/use-ref) - - dirty? (mf/use-var false) - last-color (mf/use-var data) - - picking-color? (mf/deref picking-color?) - picked-color (mf/deref picked-color) + ;; TODO: I think we need to put all this picking state under + ;; the same object for avoid creating adhoc refs for each + ;; value + picking-color? (mf/deref picking-color?) + picked-color (mf/deref picked-color) picked-color-select (mf/deref picked-color-select) - editing-spot-state (mf/deref editing-spot-state-ref) - current-gradient (mf/deref current-gradient-ref) + current-color (:current-color state) - current-color (:current-color @state) - - change-tab - (fn [tab] - #(reset! active-tab tab)) + active-tab (mf/use-state :ramp #_:harmony #_:hsva) + set-ramp-tab! (mf/use-fn #(reset! active-tab :ramp)) + set-harmony-tab! (mf/use-fn #(reset! active-tab :harmony)) + set-hsva-tab! (mf/use-fn #(reset! active-tab :hsva)) handle-change-color - (fn [changes] - (let [editing-stop (:editing-stop @state)] - (swap! state #(cond-> % - :always - (update :current-color merge changes) - - (not editing-stop) - (-> (assoc :type :color) - (dissoc :gradient-data :stops :editing-stops)) - - editing-stop - (update-in [:stops editing-stop] merge changes))) - (reset! dirty? true))) + (mf/use-fn #(st/emit! (dc/update-colorpicker-color %))) handle-click-picker - (fn [] - (if picking-color? - (do (modal/disallow-click-outside!) - (st/emit! (dc/stop-picker))) - (do (modal/allow-click-outside!) - (st/emit! (dc/start-picker))))) + (mf/use-fn + (mf/deps picking-color?) + (fn [] + (if picking-color? + (do (modal/disallow-click-outside!) + (st/emit! (dc/stop-picker))) + (do (modal/allow-click-outside!) + (st/emit! (dc/start-picker)))))) handle-change-stop - (fn [offset] - (when-let [offset-color (get-in @state [:stops offset])] - (swap! state assoc - :current-color offset-color - :editing-stop offset) - - (st/emit! (dc/select-gradient-stop offset)))) + (mf/use-fn + (fn [offset] + (st/emit! (dc/select-colorpicker-gradient-stop offset)))) on-select-library-color - (fn [color] - (let [editing-stop (:editing-stop @state) - is-gradient? (some? (:gradient color))] - - (if is-gradient? - (st/emit! (dc/start-gradient (:gradient color))) - (st/emit! (dc/stop-gradient))) - - (if (and (some? editing-stop) (not is-gradient?)) - (handle-change-color (color->components (:color color) (:opacity color))) - (do (reset! dirty? false) - (reset! state (-> (data->state color) - (assoc :editing-stop nil))) - (on-change color))))) - + (mf/use-fn + (fn [color] + (on-change color))) on-add-library-color - (fn [_] - (st/emit! (dwl/add-color (state->data @state)))) + (mf/use-fn + (mf/deps state) + (fn [_] + (st/emit! (dwl/add-color (dc/get-color-from-colorpicker-state state))))) - on-activate-gradient - (fn [type] - (fn [] - (reset! dirty? true) - (if (= type (:type @state)) - (do - (swap! state assoc :type :color) - (swap! state dissoc :editing-stop :stops :gradient-data) - (st/emit! (dc/stop-gradient))) - (let [gradient-data (create-gradient-data type)] - (swap! state assoc :type type :gradient-data gradient-data) - (when (not (:stops @state)) - (swap! state assoc - :editing-stop 0 - :stops {0 (:current-color @state) - 1 (-> (:current-color @state) - (assoc :alpha 0))}))))))] + on-activate-linear-gradient + (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :linear-gradient))) + + on-activate-radial-gradient + (mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :radial-gradient)))] + + ;; Initialize colorpicker state + (mf/with-effect [] + (st/emit! (dc/initialize-colorpicker on-change)) + (partial st/emit! (dc/finalize-colorpicker))) + + ;; Update colorpicker with external color changes + (mf/with-effect [data] + (st/emit! (dc/update-colorpicker data))) ;; Updates the CSS color variable when there is a change in the color - (mf/use-effect - (mf/deps current-color) - (fn [] (let [node (mf/ref-val ref-picker) - {:keys [r g b h v]} current-color - rgb [r g b] - hue-rgb (uc/hsv->rgb [h 1.0 255]) - hsl-from (uc/hsv->hsl [h 0.0 v]) - hsl-to (uc/hsv->hsl [h 1.0 v]) + (mf/with-effect [current-color] + (let [node (mf/ref-val node-ref) + {:keys [r g b h v]} current-color + rgb [r g b] + hue-rgb (uc/hsv->rgb [h 1.0 255]) + hsl-from (uc/hsv->hsl [h 0.0 v]) + hsl-to (uc/hsv->hsl [h 1.0 v]) - format-hsl (fn [[h s l]] - (str/fmt "hsl(%s, %s, %s)" - h - (str (* s 100) "%") - (str (* l 100) "%")))] - (dom/set-css-property! node "--color" (str/join ", " rgb)) - (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) - (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) - (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) - - ;; When closing the modal we update the recent-color list - (mf/use-effect - #(fn [] - (st/emit! (dc/stop-picker)) - (when @last-color - (st/emit! (dwl/add-recent-color @last-color))))) + format-hsl (fn [[h s l]] + (str/fmt "hsl(%s, %s, %s)" + h + (str (* s 100) "%") + (str (* l 100) "%")))] + (dom/set-css-property! node "--color" (str/join ", " rgb)) + (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb)) + (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from)) + (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))) ;; Updates color when used el pixel picker - (mf/use-effect - (mf/deps picking-color? picked-color picked-color-select) - (fn [] - (when (and picking-color? picked-color picked-color-select) - (let [[r g b alpha] picked-color - hex (uc/rgb->hex [r g b]) - [h s v] (uc/hex->hsv hex)] - (handle-change-color {:hex hex - :r r :g g :b b - :h h :s s :v v - :alpha (/ alpha 255)}))))) + (mf/with-effect [picking-color? picked-color picked-color-select] + (when (and picking-color? picked-color picked-color-select) + (let [[r g b alpha] picked-color + hex (uc/rgb->hex [r g b]) + [h s v] (uc/hex->hsv hex)] + (handle-change-color {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha (/ alpha 255)})))) - ;; Changes when another gradient handler is selected - (mf/use-effect - (mf/deps editing-spot-state) - #(when (not= editing-spot-state (:editing-stop @state)) - (handle-change-stop (or editing-spot-state 0)))) - - ;; Changes on the viewport when moving a gradient handler - (mf/use-effect - (mf/deps current-gradient) - (fn [] - (when current-gradient - (let [gradient-data (select-keys current-gradient [:start-x :start-y - :end-x :end-y - :width])] - (when (not= (:gradient-data @state) gradient-data) - (reset! dirty? true) - (swap! state assoc :gradient-data gradient-data)))))) - - ;; Check if we've opened a color with gradient - (mf/use-effect - (fn [] - (when (:gradient data) - (st/emit! (dc/start-gradient (:gradient data)))) - - ;; on-unmount we stop the handlers - #(st/emit! (dc/stop-gradient)))) - - ;; Send the properties to the store - (mf/use-effect - (mf/deps @state) - (fn [] - (when @dirty? - (let [color (state->data @state)] - (reset! dirty? false) - (reset! last-color color) - (when (:gradient color) - (st/emit! (dc/start-gradient (:gradient color)))) - (on-change color))))) - - [:div.colorpicker {:ref ref-picker} + [:div.colorpicker {:ref node-ref} [:div.colorpicker-content [:div.top-actions [:button.picker-btn @@ -292,70 +146,81 @@ (when (not disable-gradient) [:div.gradients-buttons [:button.gradient.linear-gradient - {:on-click (on-activate-gradient :linear-gradient) - :class (when (= :linear-gradient (:type @state)) "active")}] + {:on-click on-activate-linear-gradient + :class (when (= :linear-gradient (:type state)) "active")}] [:button.gradient.radial-gradient - {:on-click (on-activate-gradient :radial-gradient) - :class (when (= :radial-gradient (:type @state)) "active")}]])] + {:on-click on-activate-radial-gradient + :class (when (= :radial-gradient (:type state)) "active")}]])] - [:& gradients {:type (:type @state) - :stops (:stops @state) - :editing-stop (:editing-stop @state) - :on-select-stop handle-change-stop}] + + (when (or (= (:type state) :linear-gradient) + (= (:type state) :radial-gradient)) + [:& gradients + {:stops (:stops state) + :editing-stop (:editing-stop state) + :on-select-stop handle-change-stop}]) [:div.colorpicker-tabs [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand {:class (when (= @active-tab :ramp) "active") :alt (tr "workspace.libraries.colors.rgba") - :on-click (change-tab :ramp)} i/picker-ramp] + :on-click set-ramp-tab!} i/picker-ramp] [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand {:class (when (= @active-tab :harmony) "active") :alt (tr "workspace.libraries.colors.rgb-complementary") - :on-click (change-tab :harmony)} i/picker-harmony] + :on-click set-harmony-tab!} i/picker-harmony] [:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand {:class (when (= @active-tab :hsva) "active") :alt (tr "workspace.libraries.colors.hsv") - :on-click (change-tab :hsva)} i/picker-hsv]] + :on-click set-hsva-tab!} i/picker-hsv]] (if picking-color? [:div.picker-detail-wrapper [:div.center-circle] [:canvas#picker-detail {:width 200 :height 160}]] (case @active-tab - :ramp [:& ramp-selector {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag #(st/emit! (dwu/start-undo-transaction)) - :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] - :harmony [:& harmony-selector {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag #(st/emit! (dwu/start-undo-transaction)) - :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] - :hsva [:& hsva-selector {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag #(st/emit! (dwu/start-undo-transaction)) - :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] + :ramp + [:& ramp-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] + :harmony + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] + :hsva + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag #(st/emit! (dwu/start-undo-transaction)) + :on-finish-drag #(st/emit! (dwu/commit-undo-transaction))}] nil)) - [:& color-inputs {:type (if (= @active-tab :hsva) :hsv :rgb) - :disable-opacity disable-opacity - :color current-color - :on-change handle-change-color}] + [:& color-inputs + {:type (if (= @active-tab :hsva) :hsv :rgb) + :disable-opacity disable-opacity + :color current-color + :on-change handle-change-color}] - [:& libraries {:current-color current-color - :disable-gradient disable-gradient - :disable-opacity disable-opacity - :on-select-color on-select-library-color - :on-add-library-color on-add-library-color}] + [:& libraries + {:current-color current-color + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :on-select-color on-select-library-color + :on-add-library-color on-add-library-color}] (when on-accept [:div.actions [:button.btn-primary.btn-large {:on-click (fn [] - (on-accept (state->data @state)) + (on-accept (dc/get-color-from-colorpicker-state state)) (modal/hide!))} (tr "workspace.libraries.colors.save-color")]])]])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs index b1645d3f6..d2d58ae50 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/color_inputs.cljs @@ -11,13 +11,17 @@ [app.util.dom :as dom] [rumext.alpha :as mf])) +(defn parse-hex + [val] + (if (= (first val) \#) + val + (str \# val))) + (mf/defc color-inputs [{:keys [type color disable-opacity on-change]}] (let [{red :r green :g blue :b hue :h saturation :s value :v hex :hex alpha :alpha} color - parse-hex (fn [val] (if (= (first val) \#) val (str \# val))) - refs {:hex (mf/use-ref nil) :r (mf/use-ref nil) :g (mf/use-ref nil) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 56c3ef050..78072f564 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -6,32 +6,31 @@ (ns app.main.ui.workspace.colorpicker.gradients (:require + [app.common.data.macros :as dm] [cuerdas.core :as str] [rumext.alpha :as mf])) -(defn gradient->string [stops] - (let [format-stop - (fn [[offset {:keys [r g b alpha]}]] - (str/fmt "rgba(%s, %s, %s, %s) %s" - r g b alpha (str (* offset 100) "%"))) +(defn- format-rgba + [{:keys [r g b alpha offset]}] + (str/ffmt "rgba(%1, %2, %3, %4) %5%%" r g b alpha (* offset 100))) - gradient-css (str/join "," (map format-stop stops))] - (str/fmt "linear-gradient(90deg, %s)" gradient-css))) +(defn- gradient->string [stops] + (let [gradient-css (str/join ", " (map format-rgba stops))] + (str/ffmt "linear-gradient(90deg, %1)" gradient-css))) -(mf/defc gradients [{:keys [type stops editing-stop on-select-stop]}] - (when (#{:linear-gradient :radial-gradient} type) - [:div.gradient-stops - [:div.gradient-background-wrapper - [:div.gradient-background {:style {:background (gradient->string stops)}}]] +(mf/defc gradients + [{:keys [stops editing-stop on-select-stop]}] + [:div.gradient-stops + [:div.gradient-background-wrapper + [:div.gradient-background {:style {:background (gradient->string stops)}}]] - [:div.gradient-stop-wrapper - (for [[offset value] stops] - [:div.gradient-stop - {:class (when (= editing-stop offset) "active") - :on-click (partial on-select-stop offset) - :style {:left (str (* offset 100) "%")}} + [:div.gradient-stop-wrapper + (for [{:keys [offset hex r g b alpha] :as value} stops] + [:div.gradient-stop + {:class (when (= editing-stop offset) "active") + :on-click (partial on-select-stop offset) + :style {:left (dm/str (* offset 100) "%")} + :key (dm/str offset)} - (let [{:keys [hex r g b alpha]} value] - [:* - [:div.gradient-stop-color {:style {:background-color hex}}] - [:div.gradient-stop-alpha {:style {:background-color (str/format "rgba(%s, %s, %s, %s)" r g b alpha)}}]])])]])) + [:div.gradient-stop-color {:style {:background-color hex}}] + [:div.gradient-stop-alpha {:style {:background-color (str/ffmt "rgba(%1, %2, %3, %4)" r g b alpha)}}]])]]) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index 79b4becb6..8e3b6e392 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -6,90 +6,85 @@ (ns app.main.ui.workspace.colorpicker.libraries (:require - [app.common.uuid :refer [uuid]] + [app.common.data.macros :as dm] [app.main.data.workspace.colors :as dc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [okulary.core :as l] [rumext.alpha :as mf])) -(def selected-palette-ref - (-> (l/in [:workspace-global :selected-palette-colorpicker]) - (l/derived st/state))) - (mf/defc libraries [{:keys [on-select-color on-add-library-color disable-gradient disable-opacity]}] - (let [selected-library (or (mf/deref selected-palette-ref) :recent) - current-library-colors (mf/use-state []) + (let [selected (h/use-shared-state dc/colorpicker-selected-broadcast-key :recent) + current-colors (mf/use-state []) shared-libs (mf/deref refs/workspace-libraries) file-colors (mf/deref refs/workspace-file-colors) recent-colors (mf/deref refs/workspace-recent-colors) - parse-selected - (fn [selected-str] - (if (#{"recent" "file"} selected-str) - (keyword selected-str) - (uuid selected-str))) + on-library-change + (mf/use-fn + (fn [event] + (let [val (dom/get-target-val event)] + (reset! selected + (if (or (= val "recent") + (= val "file")) + (keyword val) + (parse-uuid val)))))) - check-valid-color? (fn [color] - (and (or (not disable-gradient) (not (:gradient color))) - (or (not disable-opacity) (= 1 (:opacity color)))))] + check-valid-color? + (fn [color] + (and (or (not disable-gradient) (not (:gradient color))) + (or (not disable-opacity) (= 1 (:opacity color)))))] ;; Load library colors when the select is changed - (mf/use-effect - (mf/deps selected-library) - (fn [] - (let [mapped-colors - (cond - (= selected-library :recent) - ;; The `map?` check is to keep backwards compatibility. We transform from string to map - (map #(if (map? %) % (hash-map :color %)) (reverse (or recent-colors []))) + (mf/with-effect [@selected recent-colors file-colors] + (let [colors (cond + (= @selected :recent) + ;; The `map?` check is to keep backwards compatibility. We transform from string to map + (map #(if (map? %) % {:color %}) (reverse (or recent-colors []))) - (= selected-library :file) - (vals file-colors) + (= @selected :file) + (vals file-colors) - :else ;; Library UUID - (->> (get-in shared-libs [selected-library :data :colors]) - (vals) - (map #(merge % {:file-id selected-library}))))] + :else ;; Library UUID + (as-> @selected file-id + (->> (get-in shared-libs [file-id :data :colors]) + (vals) + (map #(assoc % :file-id file-id)))))] - (reset! current-library-colors (into [] (filter check-valid-color?) mapped-colors))))) + (reset! current-colors (into [] (filter check-valid-color?) colors)))) ;; If the file colors change and the file option is selected updates the state - (mf/use-effect - (mf/deps file-colors) - (fn [] (when (= selected-library :file) - (let [colors (vals file-colors)] - (reset! current-library-colors (into [] (filter check-valid-color?) colors)))))) + (mf/with-effect [file-colors] + (when (= @selected :file) + (let [colors (vals file-colors)] + (reset! current-colors (into [] (filter check-valid-color?) colors))))) [:div.libraries - [:select {:on-change (fn [e] - (when-let [val (parse-selected (dom/get-target-val e))] - (st/emit! (dc/change-palette-selected-colorpicker val)))) - :value (name selected-library)} + [:select {:on-change on-library-change :value (name @selected)} [:option {:value "recent"} (tr "workspace.libraries.colors.recent-colors")] [:option {:value "file"} (tr "workspace.libraries.colors.file-library")] (for [[_ {:keys [name id]}] shared-libs] - [:option {:key id - :value id} name])] + [:option {:key id :value id} name])] [:div.selected-colors - (when (= selected-library :file) + (when (= @selected :file) [:div.color-bullet.button.plus-button {:style {:background-color "var(--color-white)"} :on-click on-add-library-color} i/plus]) [:div.color-bullet.button {:style {:background-color "var(--color-white)"} - :on-click #(st/emit! (dc/show-palette selected-library))} + :on-click #(st/emit! (dc/show-palette @selected))} i/palette] - (for [[idx color] (map-indexed vector @current-library-colors)] - [:& color-bullet {:key (str "color-" idx) - :color color - :on-click #(on-select-color color)}])]])) + (for [[idx color] (map-indexed vector @current-colors)] + [:& color-bullet + {:key (dm/str "color-" idx) + :color color + :on-click on-select-color}])]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index 9a3f0d078..28c72adf2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -79,7 +79,7 @@ shape-bb-ref (hooks/use-update-var shape-bb) - updates-str (mf/use-memo #(rx/subject)) + updates-str (mf/use-memo #(rx/subject)) thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id)) thumbnail-data (mf/deref thumbnail-data-ref) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index fd608efd1..3f2200139 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.rows.color-row (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages :as cp] [app.main.data.modal :as modal] [app.main.refs :as refs] @@ -22,34 +23,8 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) -(defn color-picker-callback - [color disable-gradient disable-opacity handle-change-color handle-open handle-close] - (fn [event] - (let [color - (cond - (uc/multiple? color) - {:color cp/default-color - :opacity 1} - - (= :multiple (:opacity color)) - (assoc color :opacity 1) - - :else - color) - - x (.-clientX event) - y (.-clientY event) - props {:x x - :y y - :disable-gradient disable-gradient - :disable-opacity disable-opacity - :on-change handle-change-color - :on-close handle-close - :data color}] - (handle-open color) - (modal/show! :colorpicker props)))) - -(defn opacity->string [opacity] +(defn opacity->string + [opacity] (if (= opacity :multiple) "" (str (-> opacity @@ -57,7 +32,8 @@ (* 100) (fmt/format-number))))) -(defn remove-multiple [v] +(defn remove-multiple + [v] (if (= v :multiple) nil v)) (mf/defc color-row @@ -68,64 +44,88 @@ file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) hover-detach (mf/use-state false) + on-change (h/use-ref-callback on-change) + src-colors (if (= (:file-id color) current-file-id) + file-colors + (dm/get-in shared-libs [(:file-id color) :data :colors])) - on-change-var (h/use-update-var {:fn on-change}) + color-name (dm/get-in src-colors [(:id color) :name]) - src-colors (if (= (:file-id color) current-file-id) - file-colors - (get-in shared-libs [(:file-id color) :data :colors])) + parse-color + (mf/use-fn + (fn [color] + (update color :color #(or % (:value color))))) - color-name (get-in src-colors [(:id color) :name]) + detach-value + (mf/use-fn + (mf/deps on-detach color) + (fn [] + (when on-detach + (on-detach color)))) - parse-color (fn [color] - (-> color - (update :color #(or % (:value color))))) + handle-select + (mf/use-fn + (mf/deps select-only color) + (fn [] + (select-only color))) - detach-value (fn [] - (when on-detach (on-detach color))) + handle-value-change + (mf/use-fn + (mf/deps color on-change) + (fn [new-value] + (on-change (-> color + (assoc :color new-value) + (dissoc :gradient))))) - change-value (fn [new-value] - (when (:fn @on-change-var) ((:fn @on-change-var) (-> color - (assoc :color new-value) - (dissoc :gradient))))) + handle-opacity-change + (mf/use-fn + (mf/deps color on-change) + (fn [value] + (on-change (assoc color + :opacity (/ value 100) + :id nil + :file-id nil)))) - change-opacity (fn [new-opacity] - (when (:fn @on-change-var) ((:fn @on-change-var) (assoc color - :opacity new-opacity - :id nil - :file-id nil)))) + handle-click-color + (mf/use-fn + (mf/deps disable-gradient disable-opacity on-change on-close on-open) + (fn [color event] + (let [color (cond + (uc/multiple? color) + {:color cp/default-color + :opacity 1} - handle-pick-color (fn [color] - (when (:fn @on-change-var) ((:fn @on-change-var) (merge uc/empty-color color)))) + (= :multiple (:opacity color)) + (assoc color :opacity 1) - handle-select (fn [] - (select-only color)) + :else + color) - handle-open (fn [color] - (when on-open (on-open (merge uc/empty-color color)))) + {:keys [x y]} (dom/get-client-position event) - handle-close (fn [value opacity id file-id] - (when on-close (on-close value opacity id file-id))) + props {:x x + :y y + :disable-gradient disable-gradient + :disable-opacity disable-opacity + :on-change #(on-change (merge uc/empty-color %)) + :on-close (fn [value opacity id file-id] + (when on-close + (on-close value opacity id file-id))) + :data color}] - handle-value-change (fn [new-value] - (-> new-value - change-value)) + (when on-open + (on-open (merge uc/empty-color color))) - handle-opacity-change (fn [value] - (change-opacity (/ value 100))) + (modal/show! :colorpicker props)))) - handle-click-color (color-picker-callback color - disable-gradient - disable-opacity - handle-pick-color - handle-open - handle-close) prev-color (h/use-previous color) on-drop - (fn [_ data] - (on-reorder (:index data))) + (mf/use-fn + (mf/deps on-reorder) + (fn [_ data] + (on-reorder (:index data)))) [dprops dref] (if (some? on-reorder) (h/use-sortable @@ -138,11 +138,9 @@ :name (str "Color row" index)}) [nil nil])] - (mf/use-effect - (mf/deps color prev-color) - (fn [] - (when (not= prev-color color) - (modal/update-props! :colorpicker {:data (parse-color color)})))) + (mf/with-effect [color prev-color] + (when (not= prev-color color) + (modal/update-props! :colorpicker {:data (parse-color color)}))) [:div.row-flex.color-data {:title title :class (dom/classnames @@ -182,7 +180,7 @@ (when select-only [:div.element-set-actions-button {:on-click handle-select} i/pointer-inner])] - + ;; Rendering a plain color/opacity :else diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 07f60122d..3dfbe8cb8 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -144,7 +144,7 @@ on-pointer-move (actions/on-pointer-move viewport-ref zoom move-stream) on-pointer-up (actions/on-pointer-up) on-move-selected (actions/on-move-selected hover hover-ids selected space?) - on-menu-selected (actions/on-menu-selected hover hover-ids selected) + on-menu-selected (actions/on-menu-selected hover hover-ids selected) on-frame-enter (actions/on-frame-enter frame-hover) on-frame-leave (actions/on-frame-leave frame-hover) diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 08b524f0b..464883647 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -19,7 +19,6 @@ [app.util.dom :as dom] [beicon.core :as rx] [cuerdas.core :as str] - [okulary.core :as l] [rumext.alpha :as mf])) (def gradient-line-stroke-width 2) @@ -32,12 +31,6 @@ (def gradient-square-stroke-color "var(--color-white)") (def gradient-square-stroke-color-selected "var(--color-select)") -(def editing-spot-ref - (l/derived (l/in [:workspace-global :editing-stop]) st/state)) - -(def current-gradient-ref - (l/derived (l/in [:workspace-global :current-gradient]) st/state =)) - (mf/defc shadow [{:keys [id x y width height offset]}] [:filter {:id id :x x @@ -130,27 +123,32 @@ (let [moving-point (mf/use-var nil) angle (+ 90 (gpt/angle from-p to-p)) - on-click (fn [position event] - (dom/stop-propagation event) - (dom/prevent-default event) - (when (#{:from-p :to-p} position) - (st/emit! (dc/select-gradient-stop (case position - :from-p 0 - :to-p 1))))) + on-click + (fn [position event] + (dom/stop-propagation event) + (dom/prevent-default event) + (when (#{:from-p :to-p} position) + (st/emit! (dc/select-colorpicker-gradient-stop + (case position + :from-p 0 + :to-p 1))))) - on-mouse-down (fn [position event] - (dom/stop-propagation event) - (dom/prevent-default event) - (reset! moving-point position) - (when (#{:from-p :to-p} position) - (st/emit! (dc/select-gradient-stop (case position - :from-p 0 - :to-p 1))))) + on-mouse-down + (fn [position event] + (dom/stop-propagation event) + (dom/prevent-default event) + (reset! moving-point position) + (when (#{:from-p :to-p} position) + (st/emit! (dc/select-colorpicker-gradient-stop + (case position + :from-p 0 + :to-p 1))))) - on-mouse-up (fn [_position event] - (dom/stop-propagation event) - (dom/prevent-default event) - (reset! moving-point nil))] + on-mouse-up + (fn [_position event] + (dom/stop-propagation event) + (dom/prevent-default event) + (reset! moving-point nil))] (mf/use-effect (mf/deps @moving-point from-p to-p width-p) @@ -230,37 +228,24 @@ :on-mouse-down (partial on-mouse-down :to-p) :on-mouse-up (partial on-mouse-up :to-p)}]])) - -(mf/defc gradient-handlers - {::mf/wrap [mf/memo]} - [{:keys [id zoom]}] - (let [current-change (mf/use-state {}) - shape-ref (mf/use-memo (mf/deps id) #(refs/object-by-id id)) - shape (mf/deref shape-ref) - - gradient (mf/deref current-gradient-ref) - gradient (merge gradient @current-change) - - editing-spot (mf/deref editing-spot-ref) - - transform (gsh/transform-matrix shape) +(mf/defc gradient-handlers* + [{:keys [zoom stops gradient editing-stop shape]}] + (let [transform (gsh/transform-matrix shape) transform-inverse (gsh/inverse-transform-matrix shape) {:keys [x y width height] :as sr} (:selrect shape) [{start-color :color start-opacity :opacity} - {end-color :color end-opacity :opacity}] (:stops gradient) + {end-color :color end-opacity :opacity}] stops from-p (-> (gpt/point (+ x (* width (:start-x gradient))) (+ y (* height (:start-y gradient)))) - (gpt/transform transform)) - to-p (-> (gpt/point (+ x (* width (:end-x gradient))) (+ y (* height (:end-y gradient)))) (gpt/transform transform)) - gradient-vec (gpt/to-vec from-p to-p) + gradient-vec (gpt/to-vec from-p to-p) gradient-length (gpt/length gradient-vec) width-v (-> gradient-vec @@ -271,13 +256,12 @@ width-p (gpt/add from-p width-v) change! - (mf/use-callback + (mf/use-fn (fn [changes] - (swap! current-change merge changes) - (st/emit! (dc/update-gradient changes)))) + (st/emit! (dc/update-colorpicker-gradient changes)))) on-change-start - (mf/use-callback + (mf/use-fn (mf/deps transform-inverse width height) (fn [point] (let [point (gpt/transform point transform-inverse) @@ -286,7 +270,7 @@ (change! {:start-x start-x :start-y start-y})))) on-change-finish - (mf/use-callback + (mf/use-fn (mf/deps transform-inverse width height) (fn [point] (let [point (gpt/transform point transform-inverse) @@ -295,7 +279,7 @@ (change! {:end-x end-x :end-y end-y})))) on-change-width - (mf/use-callback + (mf/use-fn (mf/deps gradient-length width height) (fn [point] (let [scale-factor-y (/ gradient-length (/ height 2)) @@ -304,17 +288,35 @@ (when (and norm-dist (d/num? norm-dist)) (change! {:width norm-dist})))))] - (when (and gradient + [:& gradient-handler-transformed + {:editing editing-stop + :from-p from-p + :to-p to-p + :width-p (when (= :radial (:type gradient)) width-p) + :from-color {:value start-color :opacity start-opacity} + :to-color {:value end-color :opacity end-opacity} + :zoom zoom + :on-change-start on-change-start + :on-change-finish on-change-finish + :on-change-width on-change-width}])) + +(mf/defc gradient-handlers + {::mf/wrap [mf/memo]} + [{:keys [id zoom]}] + (let [shape-ref (mf/use-memo (mf/deps id) #(refs/object-by-id id)) + shape (mf/deref shape-ref) + + state (mf/deref refs/colorpicker) + gradient (:gradient state) + stops (:stops state) + editing-stop (:editing-stop state)] + + (when (and (some? gradient) (= id (:shape-id gradient)) (not= (:type shape) :text)) - [:& gradient-handler-transformed - {:editing editing-spot - :from-p from-p - :to-p to-p - :width-p (when (= :radial (:type gradient)) width-p) - :from-color {:value start-color :opacity start-opacity} - :to-color {:value end-color :opacity end-opacity} - :zoom zoom - :on-change-start on-change-start - :on-change-finish on-change-finish - :on-change-width on-change-width}]))) + [:& gradient-handlers* + {:zoom zoom + :gradient gradient + :stops stops + :editing-stop editing-stop + :shape shape}]))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index ef31ad797..89f669623 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -123,7 +123,7 @@ (defn get-current-target "Extract the current target from event instance (different from target - when event triggered in a child of the subscribing element)." + when event triggered in a child of the subscribing element)." [^js event] (when (some? event) (.-currentTarget event)))