🐛 Fix deselect and delete events for empty texts

This commit is contained in:
Alejandro Alonso
2025-11-11 08:51:22 +01:00
parent 7594f1883b
commit 718f42aa94
4 changed files with 140 additions and 19 deletions

View File

@@ -96,6 +96,16 @@
(->> (rx/from ids) (->> (rx/from ids)
(rx/map resize-wasm-text))))) (rx/map resize-wasm-text)))))
;; -- Content helpers
(defn- v2-content-has-text?
[content]
(boolean
(when content
(some (fn [node]
(not (str/blank? (:text node ""))))
(txt/node-seq txt/is-text-node? content)))))
;; -- Editor ;; -- Editor
(defn update-editor (defn update-editor
@@ -948,28 +958,34 @@
(let [objects (dsh/lookup-page-objects state) (let [objects (dsh/lookup-page-objects state)
shape (get objects id) shape (get objects id)
new-shape? (nil? (:content shape))] new-shape? (nil? (:content shape))]
(rx/of (rx/concat
(dwsh/update-shapes (rx/of
[id] (dwsh/update-shapes
(fn [shape] [id]
(let [new-shape (-> shape (fn [shape]
(assoc :content content) (let [new-shape (-> shape
(cond-> (and update-name? (some? name)) (assoc :content content)
(assoc :name name)))] (cond-> (and update-name? (some? name))
new-shape)) (assoc :name name)))]
{:undo-group (when new-shape? id)}) new-shape))
{:undo-group (when new-shape? id)})
(if (and (not= :fixed (:grow-type shape)) finalize?) (if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers (dwm/apply-wasm-modifiers
(resize-wasm-text-modifiers shape content) (resize-wasm-text-modifiers shape content)
{:undo-group (when new-shape? id)}) {:undo-group (when new-shape? id)})
(dwm/set-wasm-modifiers (dwm/set-wasm-modifiers
(resize-wasm-text-modifiers shape content) (resize-wasm-text-modifiers shape content)
{:undo-group (when new-shape? id)})) {:undo-group (when new-shape? id)})))
(when finalize? (when finalize?
(dwt/finish-transform)))) (rx/concat
(when (and (not (v2-content-has-text? content)) (some? id))
(rx/of
(dws/deselect-shape id)
(dwsh/delete-shapes #{id})))
(rx/of (dwt/finish-transform))))))
(let [objects (dsh/lookup-page-objects state) (let [objects (dsh/lookup-page-objects state)
shape (get objects id) shape (get objects id)

View File

@@ -35,6 +35,7 @@
[app.render-wasm.wasm :as wasm] [app.render-wasm.wasm :as wasm]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.functions :as fns] [app.util.functions :as fns]
[app.util.text.content :as tc]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[promesa.core :as p] [promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@@ -106,6 +107,14 @@
(reset! pending-render false) (reset! pending-render false)
(render ts))))) (render ts)))))
(defn- ensure-text-content
"Guarantee that the shape always sends a valid text tree to WASM. When the
content is nil (freshly created text) we fall back to
tc/default-text-content so the renderer receives typography information."
[content]
(or content (tc/v2-default-text-content)))
(defn use-shape (defn use-shape
[id] [id]
(when wasm/context-initialized? (when wasm/context-initialized?
@@ -850,7 +859,10 @@
blend-mode (get shape :blend-mode) blend-mode (get shape :blend-mode)
opacity (get shape :opacity) opacity (get shape :opacity)
hidden (get shape :hidden) hidden (get shape :hidden)
content (get shape :content) content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
content))
bool-type (get shape :bool-type) bool-type (get shape :bool-type)
grow-type (get shape :grow-type) grow-type (get shape :grow-type)
blur (get shape :blur) blur (get shape :blur)

View File

@@ -6,6 +6,8 @@
(ns app.util.text.content (ns app.util.text.content
(:require (:require
[app.common.types.text :as txt]
[app.main.refs :as refs]
[app.util.text.content.from-dom :as fd] [app.util.text.content.from-dom :as fd]
[app.util.text.content.to-dom :as td])) [app.util.text.content.to-dom :as td]))
@@ -18,3 +20,22 @@
"Sets the editor content from a CLJS structure" "Sets the editor content from a CLJS structure"
[root] [root]
(td/create-root root)) (td/create-root root))
(defn v2-default-text-content
"Build the base text tree (root -> paragraph-set -> paragraph -> span) with the
current default typography. Used by the V2 editor/WASM path when a shape is
created with no content yet."
[]
(let [default-font (deref refs/default-font)
text-defaults (merge (txt/get-default-text-attrs) default-font)
default-span (merge {:text ""}
(select-keys text-defaults txt/text-node-attrs))
default-paragraph (merge {:type "paragraph"
:children [default-span]}
(select-keys text-defaults txt/paragraph-attrs))
default-paragraph-set {:type "paragraph-set"
:children [default-paragraph]}]
(merge {:type "root"
:children [default-paragraph-set]}
txt/default-root-attrs
(select-keys text-defaults txt/root-attrs))))

View File

@@ -508,8 +508,80 @@ impl TextContent {
self.set_layout_from_result(result, selrect.width(), selrect.height()); self.set_layout_from_result(result, selrect.width(), selrect.height());
} }
} }
if self.is_empty() {
let (placeholder_width, placeholder_height) = self.placeholder_dimensions(selrect);
self.size.width = placeholder_width;
self.size.height = placeholder_height;
self.size.max_width = placeholder_width;
}
self.size self.size
} }
/// Return true when the content represents a freshly created empty text.
/// We consider it empty only if there is exactly one paragraph with a single
/// span whose text buffer is empty. Any additional paragraphs or characters
/// mean the user has already entered content.
fn is_empty(&self) -> bool {
if self.paragraphs.len() != 1 {
return false;
}
let paragraph = match self.paragraphs.first() {
Some(paragraph) => paragraph,
None => return true,
};
if paragraph.children().len() != 1 {
return false;
}
let span = match paragraph.children().first() {
Some(span) => span,
None => return true,
};
span.text.is_empty()
}
/// Compute the placeholder size used while the text is still empty. We ask
/// Skia to measure a single glyph using the span's typography so the editor
/// shows a caret-sized box that reflects the selected font, size and spacing.
/// If that fails we fall back to the previous WASM size or the incoming
/// selrect dimensions.
fn placeholder_dimensions(&self, selrect: Rect) -> (f32, f32) {
if let Some(paragraph) = self.paragraphs.first() {
if let Some(span) = paragraph.children().first() {
let fonts = get_font_collection();
let fallback_fonts = get_fallback_fonts();
let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
let text_style = span.to_style(
&self.bounds(),
fallback_fonts,
false,
paragraph.line_height(),
);
builder.push_style(&text_style);
builder.add_text("0");
let mut paragraph_layout = builder.build();
paragraph_layout.layout(f32::MAX);
let width = paragraph_layout.max_intrinsic_width();
let height = paragraph_layout.height();
return (width, height);
}
}
let fallback_width = selrect.width().max(self.size.width);
let fallback_height = selrect.height().max(self.size.height);
(fallback_width, fallback_height)
}
} }
impl Default for TextContent { impl Default for TextContent {