mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🔧 Fix cross-browser text issues
This commit is contained in:
@@ -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.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?))
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user