0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-16 01:31:22 -05:00

Merge pull request #2152 from penpot/niwinz-colorpicker-state-management-refactor

♻️ Refactor state management on colorpicker and gradients
This commit is contained in:
Alejandro 2022-08-17 11:12:22 +02:00 committed by GitHub
commit 5c5ec8ef56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 752 additions and 657 deletions

View file

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

View file

@ -82,6 +82,7 @@
.color-palette-actions-button {
cursor: pointer;
display: flex;
& svg {
width: 1rem;
height: 1rem;

View file

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

View file

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

View file

@ -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 _ _]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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