diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js b/frontend/playwright/ui/render-wasm-specs/texts.spec.js index 56f593104f..26484a990e 100644 --- a/frontend/playwright/ui/render-wasm-specs/texts.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/texts.spec.js @@ -387,6 +387,26 @@ test("Renders a file with texts with empty lines", async ({ await expect(workspace.canvas).toHaveScreenshot(); }); +test("Renders a file with texts with breaking words", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-empty-lines.json"); + + await workspace.goToWorkspace({ + id: "58c5cc60-d124-81bd-8007-0ecbaf9da983", + pageId: "15222a7a-d3bc-80f1-8007-0d8e166e650f", + }); + + await workspace.waitForFirstRender({ hideUI: false }); + await workspace.clickLeafLayer("text-with-empty-lines-3"); + await workspace.hideUI(); + await workspace.page.keyboard.press("Enter"); + + await expect(workspace.canvas).toHaveScreenshot(); +}); + test.skip("Updates text alignment edition - part 1", async ({ page }) => { const workspace = new WasmWorkspacePage(page); await workspace.setupEmptyFile(); diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-breaking-words-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-breaking-words-1.png new file mode 100644 index 0000000000..e04aebd38f Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-breaking-words-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png index 635cba0e30..86c2d90867 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png differ diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss index 86856063c5..9300316961 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss @@ -35,11 +35,13 @@ } [data-itype="inline"] { + box-sizing: content-box; display: inline; - line-break: auto; line-height: inherit; caret-color: var(--text-editor-caret-color); white-space-collapse: pre; + word-break: normal; + overflow-wrap: break-word; tab-size: 2; -o-tab-size: 2; } diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 904405ea1d..1109bb14cb 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1063,13 +1063,28 @@ (set! (.-width canvas) (* dpr (.-clientWidth ^js canvas))) (set! (.-height canvas) (* dpr (.-clientHeight ^js canvas)))) +(defn- get-browser + [] + (when (exists? js/navigator) + (let [user-agent (.-userAgent js/navigator)] + (when user-agent + (cond + (re-find #"(?i)firefox" user-agent) :firefox + (re-find #"(?i)chrome" user-agent) :chrome + (re-find #"(?i)safari" user-agent) :safari + (re-find #"(?i)edge" user-agent) :edge + :else :unknown))))) + + (defn init-canvas-context [canvas] (let [gl (unchecked-get wasm/internal-module "GL") flags (debug-flags) context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2") context (.getContext ^js canvas context-id context-options) - context-init? (not (nil? context))] + context-init? (not (nil? context)) + browser (get-browser) + browser (sr/translate-browser browser)] (when-not (nil? context) (let [handle (.registerContext ^js gl context #js {"majorVersion" 2})] (.makeContextCurrent ^js gl handle) @@ -1081,6 +1096,10 @@ (h/call wasm/internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr)) (h/call wasm/internal-module "_set_render_options" flags dpr)) (set! wasm/context-initialized? true)) + + (h/call wasm/internal-module "_set_browser" browser) + + (h/call wasm/internal-module "_set_render_options" flags dpr) (set-canvas-size canvas) context-init?)) diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index 1f7d0727d5..d6e73aa3f5 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -264,3 +264,13 @@ "regular" (unchecked-get values "normal") "italic" (unchecked-get values "italic") default))) + +(defn translate-browser + [browser] + (case browser + :firefox 0 + :chrome 1 + :safari 2 + :edge 3 + :unknown 4 + 4)) diff --git a/frontend/src/app/util/text/content/to_dom.cljs b/frontend/src/app/util/text/content/to_dom.cljs index 7c7b8af6b7..93434e746e 100644 --- a/frontend/src/app/util/text/content/to_dom.cljs +++ b/frontend/src/app/util/text/content/to_dom.cljs @@ -98,12 +98,18 @@ styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)] (dissoc styles :line-height))) +(defn normalize-spaces + "Add zero-width spaces after forward slashes to enable word breaking" + [text] + (when text + (.replace text (js/RegExp "/" "g") "/\u200B"))) + (defn get-inline-children [inline paragraph] [(if (and (= "" (:text inline)) (= 1 (count (:children paragraph)))) (dom/create-element "br") - (dom/create-text (:text inline)))]) + (dom/create-text (normalize-spaces (:text inline))))]) (defn create-random-key [] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 38e447e07a..8dd0adbccd 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -92,7 +92,6 @@ macro_rules! with_state_mut_current_shape { }; } -/// This is called from JS after the WebGL context has been created. #[no_mangle] pub extern "C" fn init(width: i32, height: i32) { let state_box = Box::new(State::new(width, height)); @@ -101,6 +100,13 @@ pub extern "C" fn init(width: i32, height: i32) { } } +#[no_mangle] +pub extern "C" fn set_browser(browser: u8) { + with_state_mut!(state, { + state.set_browser(browser); + }); +} + #[no_mangle] pub extern "C" fn clean_up() { unsafe { STATE = None } diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index ada61b152d..d59fbc83c0 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -1,6 +1,7 @@ use crate::{ math::{Bounds, Matrix, Rect}, render::{default_font, DEFAULT_EMOJI_FONT}, + utils::Browser, }; use core::f32; @@ -19,6 +20,7 @@ use crate::math::Point; use crate::shapes::{self, merge_fills, Shape, VerticalAlign}; use crate::utils::{get_fallback_fonts, get_font_collection}; use crate::Uuid; +use crate::STATE; // TODO: maybe move this to the wasm module? pub type ParagraphBuilderGroup = Vec; @@ -607,6 +609,7 @@ impl Paragraph { style.set_text_align(self.text_align); style.set_text_direction(self.text_direction); style.set_replace_tab_characters(true); + style.set_apply_rounding_hack(true); style.set_text_height_behavior(skia::textlayout::TextHeightBehavior::All); style } @@ -711,7 +714,7 @@ impl TextSpan { style.set_font_families(&font_families); style.set_font_size(self.font_size); style.set_letter_spacing(self.letter_spacing); - style.set_half_leading(false); + style.set_half_leading(true); style } @@ -753,15 +756,26 @@ impl TextSpan { format!("{}", self.font_family) } - fn remove_ignored_chars(text: &str) -> String { + fn process_ignored_chars(text: &str, browser: u8) -> String { text.chars() - .filter(|&c| c >= '\u{0020}' && c != '\u{2028}' && c != '\u{2029}') + .filter_map(|c| { + if c < '\u{0020}' || c == '\u{2028}' || c == '\u{2029}' { + if browser == Browser::Firefox as u8 { + None + } else { + Some(' ') + } + } else { + Some(c) + } + }) .collect() } pub fn apply_text_transform(&self) -> String { - let text = Self::remove_ignored_chars(&self.text); - match self.text_transform { + let browser = crate::with_state!(state, { state.current_browser }); + let text = Self::process_ignored_chars(&self.text, browser); + let transformed_text = match self.text_transform { Some(TextTransform::Uppercase) => text.to_uppercase(), Some(TextTransform::Lowercase) => text.to_lowercase(), Some(TextTransform::Capitalize) => text @@ -776,7 +790,9 @@ impl TextSpan { .collect::>() .join(" "), None => text, - } + }; + + transformed_text.replace("/", "/\u{200B}") } pub fn scale_content(&mut self, value: f32) { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 7cf42d3c74..5a116d4e85 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -22,6 +22,7 @@ pub(crate) struct State<'a> { pub render_state: RenderState, pub text_editor_state: TextEditorState, pub current_id: Option, + pub current_browser: u8, pub shapes: ShapesPool<'a>, } @@ -31,6 +32,7 @@ impl<'a> State<'a> { render_state: RenderState::new(width, height), text_editor_state: TextEditorState::new(), current_id: None, + current_browser: 0, shapes: ShapesPool::new(), } } @@ -123,6 +125,10 @@ impl<'a> State<'a> { self.render_state.set_background_color(color); } + pub fn set_browser(&mut self, browser: u8) { + self.current_browser = browser; + } + /// Sets the parent for the current shape and updates the parent's extended rectangle /// /// When a shape is assigned a new parent, the parent's extended rectangle needs to be diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs index fb39a9bf6d..63a031d761 100644 --- a/render-wasm/src/utils.rs +++ b/render-wasm/src/utils.rs @@ -36,3 +36,25 @@ pub fn get_fallback_fonts() -> &'static HashSet { pub fn get_font_collection() -> &'static FontCollection { with_state_mut!(state, { state.font_collection() }) } + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum Browser { + Firefox = 0, + Chrome = 1, + Safari = 2, + Edge = 3, + Unknown = 4, +} + +impl From for Browser { + fn from(value: u8) -> Self { + match value { + 0 => Browser::Firefox, + 1 => Browser::Chrome, + 2 => Browser::Safari, + 3 => Browser::Edge, + _ => Browser::Unknown, + } + } +}