diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index be12360bd9..a82f9ed167 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -96,6 +96,16 @@ (->> (rx/from ids) (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 (defn update-editor @@ -948,28 +958,34 @@ (let [objects (dsh/lookup-page-objects state) shape (get objects id) new-shape? (nil? (:content shape))] - (rx/of - (dwsh/update-shapes - [id] - (fn [shape] - (let [new-shape (-> shape - (assoc :content content) - (cond-> (and update-name? (some? name)) - (assoc :name name)))] - new-shape)) - {:undo-group (when new-shape? id)}) + (rx/concat + (rx/of + (dwsh/update-shapes + [id] + (fn [shape] + (let [new-shape (-> shape + (assoc :content content) + (cond-> (and update-name? (some? name)) + (assoc :name name)))] + new-shape)) + {:undo-group (when new-shape? id)}) - (if (and (not= :fixed (:grow-type shape)) finalize?) - (dwm/apply-wasm-modifiers - (resize-wasm-text-modifiers shape content) - {:undo-group (when new-shape? id)}) + (if (and (not= :fixed (:grow-type shape)) finalize?) + (dwm/apply-wasm-modifiers + (resize-wasm-text-modifiers shape content) + {:undo-group (when new-shape? id)}) - (dwm/set-wasm-modifiers - (resize-wasm-text-modifiers shape content) - {:undo-group (when new-shape? id)})) + (dwm/set-wasm-modifiers + (resize-wasm-text-modifiers shape content) + {:undo-group (when new-shape? id)}))) (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) shape (get objects id) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index b8120054c7..6dd63cd75c 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -35,6 +35,7 @@ [app.render-wasm.wasm :as wasm] [app.util.debug :as dbg] [app.util.functions :as fns] + [app.util.text.content :as tc] [beicon.v2.core :as rx] [promesa.core :as p] [rumext.v2 :as mf])) @@ -106,6 +107,14 @@ (reset! pending-render false) (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 [id] (when wasm/context-initialized? @@ -850,7 +859,10 @@ blend-mode (get shape :blend-mode) opacity (get shape :opacity) 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) grow-type (get shape :grow-type) blur (get shape :blur) diff --git a/frontend/src/app/util/text/content.cljs b/frontend/src/app/util/text/content.cljs index 279b8f07cc..492aa4477b 100644 --- a/frontend/src/app/util/text/content.cljs +++ b/frontend/src/app/util/text/content.cljs @@ -6,6 +6,8 @@ (ns app.util.text.content (:require + [app.common.types.text :as txt] + [app.main.refs :as refs] [app.util.text.content.from-dom :as fd] [app.util.text.content.to-dom :as td])) @@ -18,3 +20,22 @@ "Sets the editor content from a CLJS structure" [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)))) diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index d59fbc83c0..b12ee09935 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -508,8 +508,80 @@ impl TextContent { 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 } + + /// 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(¶graph_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 {