Merge branch 'staging-render' into develop

This commit is contained in:
Andrey Antukh
2026-02-09 14:14:02 +01:00
13 changed files with 200 additions and 86 deletions

View File

@@ -46,6 +46,7 @@
(def ^function create-editor editor.v2/create)
(def ^function set-editor-root! editor.v2/setRoot)
(def ^function get-editor-root editor.v2/getRoot)
(def ^function is-empty? editor.v2/isEmpty)
(def ^function dispose! editor.v2/dispose)
(declare v2-update-text-shape-content)
@@ -901,15 +902,22 @@
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
(defn v2-update-text-shape-content
[id content & {:keys [update-name? name finalize?]
:or {update-name? false name nil finalize? false}}]
[id content & {:keys [update-name? name finalize? save-undo?]
:or {update-name? false name nil finalize? false save-undo? true}}]
(ptk/reify ::v2-update-text-shape-content
ptk/WatchEvent
(watch [_ state _]
(if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)
new-shape? (nil? (:content shape))]
new-shape? (nil? (:content shape))
prev-content (:content shape)
has-prev-content? (not (nil? (:prev-content shape)))
has-content? (when-not new-shape?
(v2-content-has-text? content))
did-has-content? (when-not new-shape?
(v2-content-has-text? prev-content))]
(rx/concat
(rx/of
(dwsh/update-shapes
@@ -917,10 +925,16 @@
(fn [shape]
(let [new-shape (-> shape
(assoc :content content)
(cond-> (and has-content?
has-prev-content?)
(dissoc :prev-content))
(cond-> (and did-has-content?
(not has-content?))
(assoc :prev-content prev-content))
(cond-> (and update-name? (some? name))
(assoc :name name)))]
new-shape))
{:undo-group (when new-shape? id)})
{:save-undo? save-undo? :undo-group (when new-shape? id)})
(if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers
@@ -933,8 +947,16 @@
(when finalize?
(rx/concat
(when (and (not (v2-content-has-text? content)) (some? id))
(when (and (not has-content?) (some? id))
(rx/of
(when has-prev-content?
(dwsh/update-shapes
[id]
(fn [shape]
(let [new-shape (-> shape
(assoc :content (:prev-content shape)))]
new-shape))
{:save-undo? false}))
(dws/deselect-shape id)
(dwsh/delete-shapes #{id})))
(rx/of (dwt/finish-transform))))))

View File

@@ -621,7 +621,7 @@
(->> stream
(rx/filter (ptk/type? ::dws/duplicate-selected))
(rx/take 1)
(rx/map #(start-move from-position))))))
(rx/map #(start-move from-position nil true))))))
(defn get-drop-cell
[target-frame objects position]
@@ -641,8 +641,9 @@
(dom/set-property! node "transform" (gmt/translate-matrix move-vector))))))
(defn start-move
([from-position] (start-move from-position nil))
([from-position ids]
([from-position] (start-move from-position nil false))
([from-position ids] (start-move from-position ids false))
([from-position ids from-duplicate?]
(ptk/reify ::start-move
ptk/UpdateEvent
(update [_ state]
@@ -750,38 +751,47 @@
(rx/share))]
(if (features/active-feature? state "render-wasm/v1")
(rx/merge
(->> modifiers-stream
(rx/map
(fn [[modifiers snap-ignore-axis]]
(dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis))))
(let [duplicate-stopper
(->> ms/mouse-position-alt
(rx/mapcat
(fn [alt?]
(if (and alt? (not from-duplicate?))
(rx/of true)
(rx/empty)))))]
(rx/merge
(->> modifiers-stream
(rx/take-until duplicate-stopper)
(rx/map
(fn [[modifiers snap-ignore-axis]]
(dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis))))
(->> move-stream
(rx/with-latest-from ms/mouse-position-alt)
(rx/filter (fn [[_ alt?]] alt?))
(rx/take 1)
(rx/mapcat
(fn [[_ alt?]]
(if (and (not duplicate-move-started?) alt?)
(rx/of (start-move-duplicate from-position)
(dws/duplicate-selected false true))
(rx/empty)))))
(->> move-stream
(rx/with-latest-from ms/mouse-position-alt)
(rx/filter (fn [[_ alt?]] alt?))
(rx/take 1)
(rx/mapcat
(fn [[_ alt?]]
(if (and (not from-duplicate?) alt?)
(rx/of (start-move-duplicate from-position)
(dws/duplicate-selected false true))
(rx/empty)))))
;; Last event will write the modifiers creating the changes
(->> move-stream
(rx/last)
(rx/with-latest-from modifiers-stream)
(rx/mapcat
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]]
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwm/apply-wasm-modifiers modifiers
:snap-ignore-axis snap-ignore-axis
:undo-transation? false)
(move-shapes-to-frame ids target-frame drop-index drop-cell)
(finish-transform)
(dwu/commit-undo-transaction undo-id)))))))
;; Last event will write the modifiers creating the changes
(->> move-stream
(rx/last)
(rx/take-until duplicate-stopper)
(rx/with-latest-from modifiers-stream)
(rx/mapcat
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]]
(let [undo-id (js/Symbol)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwm/apply-wasm-modifiers modifiers
:snap-ignore-axis snap-ignore-axis
:undo-transation? false)
(move-shapes-to-frame ids target-frame drop-index drop-cell)
(finish-transform)
(dwu/commit-undo-transaction undo-id))))))))
(rx/merge
(->> modifiers-stream

View File

@@ -117,7 +117,8 @@
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? update-name?
:name generated-name
:finalize? true))))
:finalize? true
:save-undo? false))))
(let [container-node (mf/ref-val container-ref)]
(dom/set-style! container-node "opacity" 0)))
@@ -135,15 +136,21 @@
on-needs-layout
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true)))
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? false)))
;; FIXME: We need to find a better way to trigger layout changes.
#_(st/emit!
(dwt/v2-update-text-shape-position-data shape-id [])))
on-change
(fn []
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true))))
(let [is-empty? (dwt/is-empty? instance)
save-undo? (not is-empty?)]
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
(st/emit! (dwt/v2-update-text-shape-content shape-id content
:update-name? true
:save-undo? save-undo?)))))
on-clipboard-change
(fn [event]
@@ -247,7 +254,7 @@
:ref container-ref
:data-testid "text-editor-container"
:style {:width "var(--editor-container-width)"
:height "var(--editor-container-height)"}
:height "var(--editor-container-height)"}}
;; We hide the editor when is blurred because otherwise the
;; selection won't let us see the underlying text. Use opacity
;; because display or visibility won't allow to recover focus
@@ -256,7 +263,7 @@
;; IMPORTANT! This is now done through DOM mutations (see
;; on-blur and on-focus) but I keep this for future references.
;; :opacity (when @blurred 0)}}
}
[:div
{:class (dm/str
"mousetrap "

View File

@@ -911,17 +911,23 @@
(def render-finish
(letfn [(do-render [ts]
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(render ts)
(perf/end-measure "render-finish"))]
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when wasm/context-initialized?
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(render ts)
(perf/end-measure "render-finish")))]
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
(def render-pan
(letfn [(do-render-pan [ts]
(perf/begin-measure "render-pan")
(render ts)
(perf/end-measure "render-pan"))]
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when wasm/context-initialized?
(perf/begin-measure "render-pan")
(render ts)
(perf/end-measure "render-pan")))]
(fns/throttle do-render-pan THROTTLE_DELAY_MS)))
(defn set-view-box
@@ -1399,6 +1405,16 @@
[]
(when wasm/context-initialized?
(try
;; Cancel any pending animation frame to prevent race conditions
(when wasm/internal-frame-id
(js/cancelAnimationFrame wasm/internal-frame-id)
(set! wasm/internal-frame-id nil))
;; Reset render flags to prevent new renders from being scheduled
(reset! pending-render false)
(reset! shapes-loading? false)
(reset! deferred-render? false)
;; TODO: perform corresponding cleaning
(set! wasm/context-initialized? false)
(h/call wasm/internal-module "_clean_up")

View File

@@ -32,7 +32,8 @@
"This function adds units to style values"
[k v]
(cond
(and (or (= k :font-size)
(and (keyword? k)
(or (= k :font-size)
(= k :letter-spacing))
(not= (str/slice v -2) "px"))
(str v "px")

View File

@@ -326,7 +326,9 @@ export class TextEditor extends EventTarget {
* @param {FocusEvent} e
*/
#onBlur = (e) => {
this.#changeController.notifyImmediately();
if (!this.isEmpty) {
this.#changeController.notifyImmediately();
}
this.#selectionController.saveSelection();
this.dispatchEvent(new FocusEvent(e.type, e));
};
@@ -683,13 +685,26 @@ export function createRootFromString(string) {
* Returns true if the passed object is a TextEditor
* instance.
*
* @param {*} instance
* @param {TextEditor} instance
* @returns {boolean}
*/
export function isTextEditor(instance) {
return instance instanceof TextEditor;
}
/**
* Returns true if the TextEditor is empty.
*
* @param {TextEditor} instance
* @returns {boolean}
*/
export function isEmpty(instance) {
if (isTextEditor(instance)) {
return instance.isEmpty;
}
throw new TypeError('Instance is not a TextEditor');
}
/**
* Returns the root element of a TextEditor
* instance.
@@ -701,7 +716,7 @@ export function getRoot(instance) {
if (isTextEditor(instance)) {
return instance.root;
}
return null;
throw new TypeError("Instance is not a TextEditor");
}
/**
@@ -714,9 +729,9 @@ export function getRoot(instance) {
export function setRoot(instance, root) {
if (isTextEditor(instance)) {
instance.root = root;
return instance;
}
return instance;
throw new TypeError("Instance is not a TextEditor");
}
/**
@@ -741,7 +756,7 @@ export function getCurrentStyle(instance) {
if (isTextEditor(instance)) {
return instance.currentStyle;
}
return null;
throw new TypeError("Instance is not a TextEditor");
}
/**
@@ -756,7 +771,7 @@ export function applyStylesToSelection(instance, styles) {
if (isTextEditor(instance)) {
return instance.applyStylesToSelection(styles);
}
return null;
throw new TypeError("Instance is not a TextEditor");
}
/**
@@ -770,7 +785,7 @@ export function dispose(instance) {
if (isTextEditor(instance)) {
return instance.dispose();
}
return null;
throw new TypeError("Instance is not a TextEditor");
}
export default TextEditor;

View File

@@ -54,8 +54,12 @@ export class ChangeController extends EventTarget {
return this.#hasPendingChanges;
}
/**
* Handles timeout.
*/
#onTimeout = () => {
this.dispatchEvent(new Event("change"));
this.#hasPendingChanges = false;
};
/**