🔧 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();
});
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();

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"] {
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;
}

View File

@@ -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?))

View File

@@ -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))

View File

@@ -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
[]

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]
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 }

View File

@@ -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<ParagraphBuilder>;
@@ -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::<Vec<_>>()
.join(" "),
None => text,
}
};
transformed_text.replace("/", "/\u{200B}")
}
pub fn scale_content(&mut self, value: f32) {

View File

@@ -22,6 +22,7 @@ pub(crate) struct State<'a> {
pub render_state: RenderState,
pub text_editor_state: TextEditorState,
pub current_id: Option<Uuid>,
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

View File

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