🔧 Fix cross-browser text issues

This commit is contained in:
Elena Torro
2025-11-05 14:41:27 +01:00
parent bb65782d08
commit f496ba78f3
11 changed files with 117 additions and 10 deletions

View File

@@ -387,6 +387,26 @@ test("Renders a file with texts with empty lines", async ({
await expect(workspace.canvas).toHaveScreenshot(); 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 }) => { test.skip("Updates text alignment edition - part 1", async ({ page }) => {
const workspace = new WasmWorkspacePage(page); const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -35,11 +35,13 @@
} }
[data-itype="inline"] { [data-itype="inline"] {
box-sizing: content-box;
display: inline; display: inline;
line-break: auto;
line-height: inherit; line-height: inherit;
caret-color: var(--text-editor-caret-color); caret-color: var(--text-editor-caret-color);
white-space-collapse: pre; white-space-collapse: pre;
word-break: normal;
overflow-wrap: break-word;
tab-size: 2; tab-size: 2;
-o-tab-size: 2; -o-tab-size: 2;
} }

View File

@@ -1063,13 +1063,28 @@
(set! (.-width canvas) (* dpr (.-clientWidth ^js canvas))) (set! (.-width canvas) (* dpr (.-clientWidth ^js canvas)))
(set! (.-height canvas) (* dpr (.-clientHeight ^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 (defn init-canvas-context
[canvas] [canvas]
(let [gl (unchecked-get wasm/internal-module "GL") (let [gl (unchecked-get wasm/internal-module "GL")
flags (debug-flags) flags (debug-flags)
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2") context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
context (.getContext ^js canvas context-id context-options) 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) (when-not (nil? context)
(let [handle (.registerContext ^js gl context #js {"majorVersion" 2})] (let [handle (.registerContext ^js gl context #js {"majorVersion" 2})]
(.makeContextCurrent ^js gl handle) (.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 "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr))
(h/call wasm/internal-module "_set_render_options" flags dpr)) (h/call wasm/internal-module "_set_render_options" flags dpr))
(set! wasm/context-initialized? true)) (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) (set-canvas-size canvas)
context-init?)) context-init?))

View File

@@ -264,3 +264,13 @@
"regular" (unchecked-get values "normal") "regular" (unchecked-get values "normal")
"italic" (unchecked-get values "italic") "italic" (unchecked-get values "italic")
default))) default)))
(defn translate-browser
[browser]
(case browser
:firefox 0
:chrome 1
:safari 2
:edge 3
:unknown 4
4))

View File

@@ -98,12 +98,18 @@
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)] styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
(dissoc styles :line-height))) (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 (defn get-inline-children
[inline paragraph] [inline paragraph]
[(if (and (= "" (:text inline)) [(if (and (= "" (:text inline))
(= 1 (count (:children paragraph)))) (= 1 (count (:children paragraph))))
(dom/create-element "br") (dom/create-element "br")
(dom/create-text (:text inline)))]) (dom/create-text (normalize-spaces (:text inline))))])
(defn create-random-key (defn create-random-key
[] []

View File

@@ -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] #[no_mangle]
pub extern "C" fn init(width: i32, height: i32) { pub extern "C" fn init(width: i32, height: i32) {
let state_box = Box::new(State::new(width, height)); 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] #[no_mangle]
pub extern "C" fn clean_up() { pub extern "C" fn clean_up() {
unsafe { STATE = None } unsafe { STATE = None }

View File

@@ -1,6 +1,7 @@
use crate::{ use crate::{
math::{Bounds, Matrix, Rect}, math::{Bounds, Matrix, Rect},
render::{default_font, DEFAULT_EMOJI_FONT}, render::{default_font, DEFAULT_EMOJI_FONT},
utils::Browser,
}; };
use core::f32; use core::f32;
@@ -19,6 +20,7 @@ use crate::math::Point;
use crate::shapes::{self, merge_fills, Shape, VerticalAlign}; use crate::shapes::{self, merge_fills, Shape, VerticalAlign};
use crate::utils::{get_fallback_fonts, get_font_collection}; use crate::utils::{get_fallback_fonts, get_font_collection};
use crate::Uuid; use crate::Uuid;
use crate::STATE;
// TODO: maybe move this to the wasm module? // TODO: maybe move this to the wasm module?
pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>; pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
@@ -607,6 +609,7 @@ impl Paragraph {
style.set_text_align(self.text_align); style.set_text_align(self.text_align);
style.set_text_direction(self.text_direction); style.set_text_direction(self.text_direction);
style.set_replace_tab_characters(true); style.set_replace_tab_characters(true);
style.set_apply_rounding_hack(true);
style.set_text_height_behavior(skia::textlayout::TextHeightBehavior::All); style.set_text_height_behavior(skia::textlayout::TextHeightBehavior::All);
style style
} }
@@ -711,7 +714,7 @@ impl TextSpan {
style.set_font_families(&font_families); style.set_font_families(&font_families);
style.set_font_size(self.font_size); style.set_font_size(self.font_size);
style.set_letter_spacing(self.letter_spacing); style.set_letter_spacing(self.letter_spacing);
style.set_half_leading(false); style.set_half_leading(true);
style style
} }
@@ -753,15 +756,26 @@ impl TextSpan {
format!("{}", self.font_family) format!("{}", self.font_family)
} }
fn remove_ignored_chars(text: &str) -> String { fn process_ignored_chars(text: &str, browser: u8) -> String {
text.chars() 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() .collect()
} }
pub fn apply_text_transform(&self) -> String { pub fn apply_text_transform(&self) -> String {
let text = Self::remove_ignored_chars(&self.text); let browser = crate::with_state!(state, { state.current_browser });
match self.text_transform { let text = Self::process_ignored_chars(&self.text, browser);
let transformed_text = match self.text_transform {
Some(TextTransform::Uppercase) => text.to_uppercase(), Some(TextTransform::Uppercase) => text.to_uppercase(),
Some(TextTransform::Lowercase) => text.to_lowercase(), Some(TextTransform::Lowercase) => text.to_lowercase(),
Some(TextTransform::Capitalize) => text Some(TextTransform::Capitalize) => text
@@ -776,7 +790,9 @@ impl TextSpan {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "), .join(" "),
None => text, None => text,
} };
transformed_text.replace("/", "/\u{200B}")
} }
pub fn scale_content(&mut self, value: f32) { pub fn scale_content(&mut self, value: f32) {

View File

@@ -22,6 +22,7 @@ pub(crate) struct State<'a> {
pub render_state: RenderState, pub render_state: RenderState,
pub text_editor_state: TextEditorState, pub text_editor_state: TextEditorState,
pub current_id: Option<Uuid>, pub current_id: Option<Uuid>,
pub current_browser: u8,
pub shapes: ShapesPool<'a>, pub shapes: ShapesPool<'a>,
} }
@@ -31,6 +32,7 @@ impl<'a> State<'a> {
render_state: RenderState::new(width, height), render_state: RenderState::new(width, height),
text_editor_state: TextEditorState::new(), text_editor_state: TextEditorState::new(),
current_id: None, current_id: None,
current_browser: 0,
shapes: ShapesPool::new(), shapes: ShapesPool::new(),
} }
} }
@@ -123,6 +125,10 @@ impl<'a> State<'a> {
self.render_state.set_background_color(color); 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 /// 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 /// When a shape is assigned a new parent, the parent's extended rectangle needs to be

View File

@@ -36,3 +36,25 @@ pub fn get_fallback_fonts() -> &'static HashSet<String> {
pub fn get_font_collection() -> &'static FontCollection { pub fn get_font_collection() -> &'static FontCollection {
with_state_mut!(state, { state.font_collection() }) 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<u8> for Browser {
fn from(value: u8) -> Self {
match value {
0 => Browser::Firefox,
1 => Browser::Chrome,
2 => Browser::Safari,
3 => Browser::Edge,
_ => Browser::Unknown,
}
}
}