mirror of
https://github.com/penpot/penpot.git
synced 2026-05-11 11:03:52 +00:00
440 lines
15 KiB
Clojure
440 lines
15 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) UXBOX Labs SL
|
|
|
|
(ns app.main.data.workspace.texts
|
|
(:require
|
|
[app.common.attrs :as attrs]
|
|
[app.common.data :as d]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.math :as mth]
|
|
[app.common.pages.helpers :as cph]
|
|
[app.common.text :as txt]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.workspace.changes :as dch]
|
|
[app.main.data.workspace.common :as dwc]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.state-helpers :as wsh]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.util.router :as rt]
|
|
[app.util.text-editor :as ted]
|
|
[app.util.timers :as ts]
|
|
[beicon.core :as rx]
|
|
[potok.core :as ptk]))
|
|
|
|
(defn update-editor
|
|
[editor]
|
|
(ptk/reify ::update-editor
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (some? editor)
|
|
(assoc state :workspace-editor editor)
|
|
(dissoc state :workspace-editor)))))
|
|
|
|
(defn focus-editor
|
|
[]
|
|
(ptk/reify ::focus-editor
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(when-let [editor (:workspace-editor state)]
|
|
(ts/schedule #(.focus ^js editor))))))
|
|
|
|
(defn update-editor-state
|
|
[{:keys [id] :as shape} editor-state]
|
|
(ptk/reify ::update-editor-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (some? editor-state)
|
|
(update state :workspace-editor-state assoc id editor-state)
|
|
(update state :workspace-editor-state dissoc id)))))
|
|
|
|
(defn finalize-editor-state
|
|
[id]
|
|
(ptk/reify ::finalize-editor-state
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (dwc/initialized? state)
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
content (-> (get-in state [:workspace-editor-state id])
|
|
(ted/get-editor-current-content))]
|
|
(if (ted/content-has-text? content)
|
|
(let [content (d/merge (ted/export-content content)
|
|
(dissoc (:content shape) :children))]
|
|
(rx/merge
|
|
(rx/of (update-editor-state shape nil))
|
|
(when (and (not= content (:content shape))
|
|
(some? (:current-page-id state)))
|
|
(rx/of
|
|
(dch/update-shapes [id] #(assoc % :content content))
|
|
(dwu/commit-undo-transaction)))))
|
|
|
|
(when (some? id)
|
|
(rx/of (dws/deselect-shape id)
|
|
(dwc/delete-shapes #{id})))))))))
|
|
|
|
(defn initialize-editor-state
|
|
[{:keys [id content] :as shape} decorator]
|
|
(ptk/reify ::initialize-editor-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [text-state (some->> content ted/import-content)
|
|
attrs (d/merge txt/default-text-attrs
|
|
(get-in state [:workspace-global :default-font]))
|
|
editor (cond-> (ted/create-editor-state text-state decorator)
|
|
(and (nil? content) (some? attrs))
|
|
(ted/update-editor-current-block-data attrs))]
|
|
(-> state
|
|
(assoc-in [:workspace-editor-state id] editor))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
;; We need to finalize editor on two main events: (1) when user
|
|
;; explicitly navigates to other section or page; (2) when user
|
|
;; leaves the editor.
|
|
(->> (rx/merge
|
|
(rx/filter (ptk/type? ::rt/navigate) stream)
|
|
(rx/filter #(= ::finalize-editor-state %) stream))
|
|
(rx/take 1)
|
|
(rx/map #(finalize-editor-state id))))))
|
|
|
|
(defn select-all
|
|
"Select all content of the current editor. When not editor found this
|
|
event is noop."
|
|
[{:keys [id] :as shape}]
|
|
(ptk/reify ::editor-select-all
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/editor-select-all))))
|
|
|
|
(defn cursor-to-end
|
|
[{:keys [id] :as shape}]
|
|
(ptk/reify ::cursor-to-end
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/cursor-to-end))))
|
|
|
|
;; --- Helpers
|
|
|
|
(defn to-new-fills
|
|
[data]
|
|
[(d/without-nils (select-keys data [:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file]))])
|
|
|
|
(defn- shape-current-values
|
|
[shape pred attrs]
|
|
(let [root (:content shape)
|
|
nodes (->> (txt/node-seq pred root)
|
|
(map (fn [node]
|
|
(if (txt/is-text-node? node)
|
|
(let [fills
|
|
(cond
|
|
(or (some? (:fill-color node))
|
|
(some? (:fill-opacity node))
|
|
(some? (:fill-color-gradient node)))
|
|
(to-new-fills node)
|
|
|
|
(some? (:fills node))
|
|
(:fills node)
|
|
|
|
:else
|
|
(:fills txt/default-text-attrs))]
|
|
(-> (merge txt/default-text-attrs node)
|
|
(assoc :fills fills)))
|
|
node))))]
|
|
(attrs/get-attrs-multi nodes attrs)))
|
|
|
|
(defn current-root-values
|
|
[{:keys [attrs shape]}]
|
|
(shape-current-values shape txt/is-root-node? attrs))
|
|
|
|
(defn current-paragraph-values
|
|
[{:keys [editor-state attrs shape]}]
|
|
(if editor-state
|
|
(-> (ted/get-editor-current-block-data editor-state)
|
|
(select-keys attrs))
|
|
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
|
|
|
(defn current-text-values
|
|
[{:keys [editor-state attrs shape]}]
|
|
(if editor-state
|
|
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
|
(select-keys attrs))
|
|
result (if (empty? result) txt/default-text-attrs result)]
|
|
result)
|
|
(shape-current-values shape txt/is-text-node? attrs)))
|
|
|
|
|
|
;; --- TEXT EDITION IMPL
|
|
|
|
(defn- update-text-content
|
|
[shape pred-fn update-fn attrs]
|
|
(let [update-attrs #(update-fn % attrs)
|
|
transform #(txt/transform-nodes pred-fn update-attrs %)]
|
|
(-> shape
|
|
(update :content transform))))
|
|
|
|
(defn update-root-attrs
|
|
[{:keys [id attrs]}]
|
|
(ptk/reify ::update-root-attrs
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
update-fn
|
|
(fn [shape]
|
|
(if (some? (:content shape))
|
|
(update-text-content shape txt/is-root-node? d/txt-merge attrs)
|
|
(assoc shape :content (d/txt-merge {:type "root"} attrs))))
|
|
|
|
shape-ids (cond (cph/text-shape? shape) [id]
|
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
|
|
|
(rx/of (dch/update-shapes shape-ids update-fn))))))
|
|
|
|
(defn update-paragraph-attrs
|
|
[{:keys [id attrs]}]
|
|
(let [attrs (d/without-nils attrs)]
|
|
(ptk/reify ::update-paragraph-attrs
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-block-data attrs))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
merge-fn (fn [node attrs]
|
|
(reduce-kv
|
|
(fn [node k v] (assoc node k v))
|
|
node
|
|
attrs))
|
|
|
|
update-fn #(update-text-content % txt/is-paragraph-node? merge-fn attrs)
|
|
shape-ids (cond
|
|
(cph/text-shape? shape) [id]
|
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
|
|
|
(rx/of (dch/update-shapes shape-ids update-fn))))))))
|
|
|
|
(defn update-text-attrs
|
|
[{:keys [id attrs]}]
|
|
(ptk/reify ::update-text-attrs
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles attrs))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
update-node? (fn [node]
|
|
(or (txt/is-text-node? node)
|
|
(txt/is-paragraph-node? node)))
|
|
shape-ids (cond
|
|
(cph/text-shape? shape) [id]
|
|
(cph/group-shape? shape) (cph/get-children-ids objects id))]
|
|
(rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs))))))))
|
|
|
|
(defn migrate-node
|
|
[node]
|
|
(let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])]
|
|
(cond-> node
|
|
(nil? (:fills node))
|
|
(assoc :fills (:fills txt/default-text-attrs))
|
|
|
|
(and (d/not-empty? color-attrs) (nil? (:fills node)))
|
|
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
|
|
(assoc :fills [color-attrs])))
|
|
))
|
|
|
|
(defn migrate-content
|
|
[content]
|
|
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content))
|
|
|
|
(defn update-text-with-function
|
|
[id update-node-fn]
|
|
(ptk/reify ::update-text-with-function
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (nil? (get-in state [:workspace-editor-state id]))
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?)
|
|
|
|
shape-ids
|
|
(cond
|
|
(cph/text-shape? shape) [id]
|
|
(cph/group-shape? shape) (cph/get-children-ids objects id))
|
|
|
|
update-content
|
|
(fn [content]
|
|
(->> content
|
|
(migrate-content)
|
|
(txt/transform-nodes update-node? update-node-fn)))
|
|
|
|
update-shape
|
|
(fn [shape]
|
|
(d/update-when shape :content update-content))]
|
|
|
|
(rx/of (dch/update-shapes shape-ids update-shape)))))))
|
|
|
|
;; --- RESIZE UTILS
|
|
|
|
(def start-edit-if-selected
|
|
(ptk/reify ::start-edit-if-selected
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
selected (->> state wsh/lookup-selected (mapv #(get objects %)))]
|
|
(cond-> state
|
|
(and (= 1 (count selected))
|
|
(= (-> selected first :type) :text))
|
|
(assoc-in [:workspace-local :edition] (-> selected first :id)))))))
|
|
|
|
(defn not-changed? [old-dim new-dim]
|
|
(> (mth/abs (- old-dim new-dim)) 0.1))
|
|
|
|
(defn resize-text
|
|
[id new-width new-height]
|
|
(ptk/reify ::resize-text
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(letfn [(update-fn [shape]
|
|
(let [{:keys [selrect grow-type]} shape
|
|
{shape-width :width shape-height :height} selrect
|
|
modifier-width (gsh/resize-modifiers shape :width new-width)
|
|
modifier-height (gsh/resize-modifiers shape :height new-height)]
|
|
(cond-> shape
|
|
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
|
|
(-> (assoc :modifiers modifier-width)
|
|
(gsh/transform-shape))
|
|
|
|
(and (not-changed? shape-height new-height)
|
|
(or (= grow-type :auto-height) (= grow-type :auto-width)))
|
|
(-> (assoc :modifiers modifier-height)
|
|
(gsh/transform-shape)))))]
|
|
|
|
(rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false}))))))
|
|
|
|
(defn save-font
|
|
[data]
|
|
(ptk/reify ::save-font
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [multiple? (->> data vals (d/seek #(= % :multiple)))]
|
|
(cond-> state
|
|
(not multiple?)
|
|
(assoc-in [:workspace-global :default-font] data))))))
|
|
|
|
(defn apply-text-modifier
|
|
[shape {:keys [width height position-data]}]
|
|
|
|
(let [modifier-width (when width (gsh/resize-modifiers shape :width width))
|
|
modifier-height (when height (gsh/resize-modifiers shape :height height))
|
|
|
|
new-shape
|
|
(cond-> shape
|
|
(some? modifier-width)
|
|
(-> (assoc :modifiers modifier-width)
|
|
(gsh/transform-shape))
|
|
|
|
(some? modifier-height)
|
|
(-> (assoc :modifiers modifier-height)
|
|
(gsh/transform-shape))
|
|
|
|
(some? position-data)
|
|
(assoc :position-data position-data))
|
|
|
|
delta-move
|
|
(gpt/subtract (gpt/point (:selrect new-shape))
|
|
(gpt/point (:selrect shape)))
|
|
|
|
|
|
new-shape
|
|
(update new-shape :position-data gsh/move-position-data (:x delta-move) (:y delta-move))]
|
|
|
|
|
|
new-shape))
|
|
|
|
(defn update-text-modifier
|
|
[id props]
|
|
(ptk/reify ::update-text-modifier
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:workspace-text-modifier id] (fnil merge {}) props))))
|
|
|
|
(defn clean-text-modifier
|
|
[id]
|
|
(ptk/reify ::clean-text-modifier
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(->> (rx/of #(update % :workspace-text-modifier dissoc id))
|
|
;; We delay a bit the change so there is no weird transition to the user
|
|
(rx/delay 50)))))
|
|
|
|
(defn remove-text-modifier
|
|
[id]
|
|
(ptk/reify ::remove-text-modifier
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/dissoc-in state [:workspace-text-modifier id]))))
|
|
|
|
(defn commit-position-data
|
|
[]
|
|
(ptk/reify ::commit-position-data
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [ids (keys (::update-position-data state))]
|
|
(update state :workspace-text-modifiers #(apply dissoc % ids))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [position-data (::update-position-data state)]
|
|
(rx/concat
|
|
(rx/of (dch/update-shapes
|
|
(keys position-data)
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :position-data (get position-data (:id shape)))))
|
|
{:save-undo? false :reg-objects? false}))
|
|
(rx/of (fn [state]
|
|
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
|
|
|
(defn update-position-data
|
|
[id position-data]
|
|
|
|
(let [start (uuid/next)]
|
|
(ptk/reify ::update-position-data
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [state (assoc-in state [:workspace-text-modifier id :position-data] position-data)]
|
|
(if (nil? (::update-position-data-debounce state))
|
|
(assoc state ::update-position-data-debounce start)
|
|
(assoc-in state [::update-position-data id] position-data))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(if (= (::update-position-data-debounce state) start)
|
|
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::update-position-data))
|
|
(rx/debounce 50)
|
|
(rx/take 1)
|
|
(rx/map #(commit-position-data))
|
|
(rx/take-until stopper))
|
|
(rx/of (update-position-data id position-data))))
|
|
(rx/empty))))))
|