From 60679bbbcd26b35aae9f28558dd0cc565cd9e66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 1 Oct 2026 18:29:07 +0200 Subject: [PATCH] :bug: Revert RTL auto-width text growing from its right edge (#12051) Revert #11775 (commit 142f3d9de8). We are putting this fix on hold until we settle the UX for RTL auto-width text. Relates to #11523 --- common/src/app/common/types/text.cljc | 8 - .../app/main/data/workspace/wasm_text.cljs | 9 +- .../shapes/text/text_edition_outline.cljs | 13 +- .../ui/workspace/shapes/text/v3_editor.cljs | 11 +- .../frontend_tests/data/wasm_text_test.cljs | 131 ----------- .../data/workspace_texts_test.cljs | 50 ----- frontend/test/frontend_tests/runner.cljs | 2 - render-wasm/src/math.rs | 58 +---- render-wasm/src/render/text_editor.rs | 149 ++----------- render-wasm/src/shapes/modifiers.rs | 9 +- render-wasm/src/shapes/text.rs | 207 +----------------- 11 files changed, 29 insertions(+), 618 deletions(-) delete mode 100644 frontend/test/frontend_tests/data/wasm_text_test.cljs diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index 82c4a20f82..4e53c9b753 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -209,14 +209,6 @@ [node] (= "root" (:type node))) -(defn rtl-content? - "True when the content has paragraphs and all of them are `\"rtl\"`; picks the - growth anchor of auto-width text. Mixed, \"none\" and empty content are ltr." - [content] - (boolean - (when-let [paragraphs (node-seq is-paragraph-node? content)] - (every? #(= "rtl" (:text-direction %)) paragraphs)))) - (defn is-node? [node] (or ^boolean (is-text-node? node) diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index ddd4f81400..435c8082c4 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -15,7 +15,6 @@ [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.types.modifiers :as ctm] - [app.common.types.text :as ctt] [app.main.data.helpers :as dsh] [app.main.data.workspace :as-alias dw] [app.main.data.workspace.modifiers :as dwm] @@ -64,16 +63,12 @@ ([shape] (resize-wasm-text-modifiers shape (:content shape))) - ([{:keys [id points selrect grow-type] :as shape} content] + ([{:keys [id points selrect] :as shape} content] (when-let [new-size (get-wasm-text-new-size shape content)] (let [width-scale (/ (:width new-size) (:width selrect)) height-scale (/ (:height new-size) (:height selrect)) resize-v (gpt/point width-scale height-scale) - ;; Rtl text grows leftward, so anchor it on the top-right. - origin (if (and (= :auto-width grow-type) - (ctt/rtl-content? content)) - (second points) - (first points))] + origin (first points)] {id {:modifiers (ctm/resize-modifiers diff --git a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs index ca4c74632e..93e7562438 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs @@ -25,18 +25,11 @@ ;; finalize-only), so measure the live WASM text for the growing axes: ;; width grows on auto-width, height on auto-width/auto-height. grow-type (:grow-type shape) - {live-x :x live-width :width live-height :height} (wasm.api/get-text-dimensions (:id shape)) + {live-width :width live-height :height} (wasm.api/get-text-dimensions (:id shape)) sr-width (if (= grow-type :auto-width) live-width (:width selrect)) - sr-height (if (= grow-type :fixed) (:height selrect) live-height) - ;; Rtl auto-width text is anchored on its right edge, so use the origin - ;; wasm reports. A zero measurement means it has no layout yet. - sr-x (if (and (= grow-type :auto-width) - (some? live-x) - (pos? live-width)) - live-x - (:x selrect))] + sr-height (if (= grow-type :fixed) (:height selrect) live-height)] [:rect.main.viewport-selrect - {:x sr-x + {:x (:x selrect) :y (:y selrect) :width sr-width :height sr-height diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index 832f1f1b07..1c95406ef5 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -214,7 +214,7 @@ (font-family-from-font-id (:font-id font))) fallback-fonts) [{:keys [x y width height]} transform] - (let [{text-x :x :keys [width height]} (wasm.api/get-text-dimensions shape-id) + (let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id) selrect-transform (mf/deref refs/workspace-selrect) vbox (mf/deref refs/vbox) [selrect transform] (dsh/get-selrect selrect-transform shape) @@ -231,20 +231,13 @@ overlay-width (if (= (:grow-type shape) :auto-width) (+ max-width viewport-width) max-width) - ;; `on-pointer-down` feeds offsets within this element to wasm as - ;; paragraph-local coords, so this edge must sit on the text's. - x (if (and (= (:grow-type shape) :auto-width) - (some? text-x) - (pos? width)) - text-x - (:x selrect)) valign (-> shape :content :vertical-align) y (:y selrect) y (case valign "bottom" (+ y (- selrect-height height)) "center" (+ y (/ (- selrect-height height) 2)) y)] - [(assoc selrect :x x :y y :width overlay-width :height max-height) transform]) + [(assoc selrect :y y :width overlay-width :height max-height) transform]) on-composition-start (mf/use-fn diff --git a/frontend/test/frontend_tests/data/wasm_text_test.cljs b/frontend/test/frontend_tests/data/wasm_text_test.cljs deleted file mode 100644 index 5624e40c04..0000000000 --- a/frontend/test/frontend_tests/data/wasm_text_test.cljs +++ /dev/null @@ -1,131 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS SUBSIDIARY SL - -(ns frontend-tests.data.wasm-text-test - "Growth anchor of auto-grow wasm text shapes (see `resize-wasm-text-modifiers`). - Tests stub the wasm bridge and assert on the shape the modifiers produce." - (:require - [app.common.geom.shapes :as gsh] - [app.common.types.modifiers :as ctm] - [app.common.types.shape :as cts] - [app.main.data.workspace.wasm-text :as dwwt] - [cljs.test :as t :include-macros true])) - -;; --------------------------------------------------------------------------- -;; Helpers -;; --------------------------------------------------------------------------- - -(defn- make-content - "Text content whose paragraphs carry the given `:text-direction` values." - [& directions] - {:type "root" - :children [{:type "paragraph-set" - :children (vec (for [direction directions] - {:type "paragraph" - :text-direction direction - :children [{:text "hello"}]}))}]}) - -(defn- make-text-shape - [& {:keys [x y width height grow-type rotation] - :or {x 100 y 50 width 60 height 20 grow-type :auto-width}}] - (let [shape (-> (cts/setup-shape {:type :text - :x x - :y y - :width width - :height height}) - (assoc :grow-type grow-type))] - (if (some? rotation) - (gsh/transform-shape - shape - (ctm/rotation-modifiers shape (gsh/shape->center shape) rotation)) - shape))) - -(defn- resized - "Shape that results from `resize-wasm-text-modifiers` when the renderer - measures `new-size`." - [shape content new-size] - ;; The stub needs the real fn's arities: a variadic `fn` has no `arity$2` - ;; dispatch, so the 2-arity call site blows up. - (with-redefs [dwwt/get-wasm-text-new-size (fn ([_] new-size) ([_ _] new-size))] - (let [modifiers (dwwt/resize-wasm-text-modifiers shape content)] - (gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))) - -(defn- close? [a b] - (< (abs (- a b)) 0.01)) - -;; --------------------------------------------------------------------------- -;; Growth anchor -;; --------------------------------------------------------------------------- - -(t/deftest rtl-auto-width-keeps-its-right-edge-when-growing - (t/testing "an rtl auto-width shape extends leftward: the right and top edges - stay put and x moves left" - (let [shape (make-text-shape) - content (make-content "rtl") - before (:selrect shape) - after (:selrect (resized shape content {:width 120 :height 20}))] - (t/is (close? (+ (:x after) (:width after)) - (+ (:x before) (:width before))) - "right edge preserved") - (t/is (close? (:y after) (:y before)) "top edge preserved") - (t/is (close? (:x after) 40) "x moved left by the growth") - (t/is (close? (:width after) 120))))) - -(t/deftest rtl-auto-width-keeps-its-right-edge-when-shrinking - (t/testing "deleting text shrinks the box from the left, right edge preserved" - (let [shape (make-text-shape) - content (make-content "rtl") - before (:selrect shape) - after (:selrect (resized shape content {:width 30 :height 20}))] - (t/is (close? (+ (:x after) (:width after)) - (+ (:x before) (:width before))) - "right edge preserved") - (t/is (close? (:x after) 130) "x moved right as the box narrowed")))) - -(t/deftest ltr-auto-width-keeps-its-left-edge - (t/testing "ltr content is untouched: the left edge stays anchored" - (let [shape (make-text-shape) - content (make-content "ltr") - after (:selrect (resized shape content {:width 120 :height 20}))] - (t/is (close? (:x after) 100) "left edge preserved") - (t/is (close? (:width after) 120))))) - -(t/deftest mixed-direction-auto-width-keeps-its-left-edge - (t/testing "a box with one rtl and one ltr paragraph keeps the previous - left-anchored growth" - (let [shape (make-text-shape) - content (make-content "rtl" "ltr") - after (:selrect (resized shape content {:width 120 :height 20}))] - (t/is (close? (:x after) 100) "left edge preserved")))) - -(t/deftest rtl-auto-height-keeps-its-left-edge - (t/testing "auto-height only ever changes height, so the anchor is irrelevant - and x must not move" - (let [shape (make-text-shape :grow-type :auto-height) - content (make-content "rtl") - after (:selrect (resized shape content {:width 60 :height 80}))] - (t/is (close? (:x after) 100) "x preserved") - (t/is (close? (:width after) 60) "width preserved") - (t/is (close? (:height after) 80) "height grew")))) - -(t/deftest rotated-rtl-auto-width-grows-along-its-own-axis - (t/testing "a rotated rtl shape keeps its own top-right corner, not the - axis-aligned one" - (let [shape (make-text-shape :rotation 30) - content (make-content "rtl") - before (:points shape) - after (:points (resized shape content {:width 120 :height 20}))] - ;; points are [top-left top-right bottom-right bottom-left] - (t/is (close? (:x (second after)) (:x (second before))) - "top-right x preserved") - (t/is (close? (:y (second after)) (:y (second before))) - "top-right y preserved")))) - -(t/deftest no-modifiers-when-the-renderer-has-no-size - (t/testing "a nil measurement (shape absent from wasm state) skips the resize" - (let [shape (make-text-shape)] - (with-redefs [dwwt/get-wasm-text-new-size (fn ([_] nil) ([_ _] nil))] - (t/is (nil? (dwwt/resize-wasm-text-modifiers shape (make-content "rtl")))))))) diff --git a/frontend/test/frontend_tests/data/workspace_texts_test.cljs b/frontend/test/frontend_tests/data/workspace_texts_test.cljs index 6994f1a219..14eef12d50 100644 --- a/frontend/test/frontend_tests/data/workspace_texts_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_texts_test.cljs @@ -581,53 +581,3 @@ (t/testing "a non-root content (e.g. paragraph) is left alone" (let [node {:type "paragraph" :children []}] (t/is (= node (dwt/ensure-valid-text-content node)))))) - -;; --------------------------------------------------------------------------- -;; txt/rtl-content? -;; --------------------------------------------------------------------------- - -(defn- make-content - "Text content whose paragraphs carry the given `:text-direction` values; a - `nil` entry leaves the attribute out entirely." - [& directions] - {:type "root" - :children [{:type "paragraph-set" - :children (vec (for [direction directions] - (cond-> {:type "paragraph" - :children [{:text "hello"}]} - (some? direction) - (assoc :text-direction direction))))}]}) - -(t/deftest rtl-content-single-rtl-paragraph - (t/testing "a lone rtl paragraph makes the content rtl" - (t/is (true? (txt/rtl-content? (make-content "rtl")))))) - -(t/deftest rtl-content-every-paragraph-rtl - (t/testing "several paragraphs, all rtl" - (t/is (true? (txt/rtl-content? (make-content "rtl" "rtl" "rtl")))))) - -(t/deftest rtl-content-mixed-directions - (t/testing "a mix of rtl and ltr is not rtl" - (t/is (false? (txt/rtl-content? (make-content "rtl" "ltr")))))) - -(t/deftest rtl-content-missing-direction-on-one-paragraph - (t/testing "a paragraph without :text-direction defaults to ltr, so the - content is not rtl" - (t/is (false? (txt/rtl-content? (make-content "rtl" nil)))))) - -(t/deftest rtl-content-all-ltr - (t/testing "all-ltr content is not rtl" - (t/is (false? (txt/rtl-content? (make-content "ltr" "ltr")))))) - -(t/deftest rtl-content-none-direction-is-ltr - (t/testing "\"none\" (the sidebar's un-toggled value) is ltr, matching what - translate-text-direction sends to the renderer" - (t/is (false? (txt/rtl-content? (make-content "none")))))) - -(t/deftest rtl-content-without-paragraphs - (t/testing "a root with no paragraph nodes is not rtl" - (t/is (false? (txt/rtl-content? {:type "root" :children []}))))) - -(t/deftest rtl-content-nil - (t/testing "nil content is not rtl and does not throw" - (t/is (false? (txt/rtl-content? nil))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 0937f7b732..1f78325a70 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -21,7 +21,6 @@ [frontend-tests.data.svg-upload-test] [frontend-tests.data.uploads-test] [frontend-tests.data.viewer-test] - [frontend-tests.data.wasm-text-test] [frontend-tests.data.workspace-colors-test] [frontend-tests.data.workspace-comments-test] [frontend-tests.data.workspace-context-menu-test] @@ -157,7 +156,6 @@ 'frontend-tests.data.svg-upload-test 'frontend-tests.data.uploads-test 'frontend-tests.data.viewer-test - 'frontend-tests.data.wasm-text-test 'frontend-tests.data.workspace-colors-test 'frontend-tests.data.workspace-comments-test 'frontend-tests.data.workspace-context-menu-test diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index de5d425fb5..0b8feeed04 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -434,26 +434,6 @@ pub fn resize_matrix( child_bounds: &Bounds, new_width: f32, new_height: f32, -) -> Matrix { - resize_matrix_from( - parent_bounds, - child_bounds, - new_width, - new_height, - child_bounds.nw, - ) -} - -/* - * Like `resize_matrix`, but scaling about an explicit corner instead of `nw`. - * Rtl auto-width text anchors on `ne` so it extends leftward as it grows. - */ -pub fn resize_matrix_from( - parent_bounds: &Bounds, - child_bounds: &Bounds, - new_width: f32, - new_height: f32, - anchor: Point, ) -> Matrix { let mut result = Matrix::default(); @@ -475,7 +455,7 @@ pub fn resize_matrix_from( parent_transform.pre_translate(-center); let parent_transform_inv = &parent_transform.invert().unwrap_or_default(); - let origin = parent_transform_inv.map_point(anchor); + let origin = parent_transform_inv.map_point(child_bounds.nw); let mut scale = Matrix::scale((scale_width, scale_height)); scale.post_translate(origin); @@ -617,40 +597,4 @@ mod tests { assert!((m.translate_x() - 0.0).abs() <= 0.1); assert!((m.translate_y() - 0.0).abs() <= 0.1); } - - fn axis_aligned_bounds(x: f32, y: f32, w: f32, h: f32) -> Bounds { - Bounds::new( - Point::new(x, y), - Point::new(x + w, y), - Point::new(x + w, y + h), - Point::new(x, y + h), - ) - } - - #[test] - fn resize_matrix_keeps_the_north_west_corner() { - let bounds = axis_aligned_bounds(100.0, 50.0, 60.0, 20.0); - let matrix = resize_matrix(&bounds, &bounds, 120.0, 20.0); - let resized = bounds.transform(&matrix); - assert!(is_close_to(resized.nw.x, 100.0)); - assert!(is_close_to(resized.ne.x, 220.0)); - } - - #[test] - fn resize_matrix_from_north_east_keeps_the_right_edge() { - let bounds = axis_aligned_bounds(100.0, 50.0, 60.0, 20.0); - let matrix = resize_matrix_from(&bounds, &bounds, 120.0, 20.0, bounds.ne); - let resized = bounds.transform(&matrix); - assert!(is_close_to(resized.ne.x, 160.0), "right edge preserved"); - assert!(is_close_to(resized.nw.x, 40.0), "grew leftward"); - assert!(is_close_to(resized.nw.y, 50.0), "top edge preserved"); - } - - #[test] - fn resize_matrix_from_north_west_matches_the_default() { - let bounds = axis_aligned_bounds(100.0, 50.0, 60.0, 20.0); - let default = resize_matrix(&bounds, &bounds, 120.0, 40.0); - let explicit = resize_matrix_from(&bounds, &bounds, 120.0, 40.0, bounds.nw); - assert_eq!(default, explicit); - } } diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index b894d9f233..d1825aeca8 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -2,25 +2,9 @@ use crate::render::options::RenderOptions; use crate::shapes::{vertical_align_offset, Shape, TextContent, Type}; use crate::state::{TextEditorState, TextSelection}; use crate::view::Viewbox; -use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle, TextBox, TextDirection}; +use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; use skia_safe::{BlendMode, Canvas, Color, Paint, Rect}; -/// Caret x where a character typed *before* this glyph would land. -fn leading_edge(text_box: &TextBox) -> f32 { - match text_box.direct { - TextDirection::RTL => text_box.rect.right(), - _ => text_box.rect.left(), - } -} - -/// Caret x where a character typed *after* this glyph would land. -fn trailing_edge(text_box: &TextBox) -> f32 { - match text_box.direct { - TextDirection::RTL => text_box.rect.left(), - _ => text_box.rect.right(), - } -} - pub fn render_overlay( canvas: &Canvas, viewbox: &Viewbox, @@ -128,14 +112,6 @@ fn render_selection( canvas.restore(); } -/// Caret and selection rects are drawn through `shape.get_matrix()`, which -/// translates by the *selrect's* `left_top()`; this shifts them onto the text. -fn paragraphs_horizontal_offset(shape: &Shape, text_content: &TextContent) -> f32 { - let selrect = shape.selrect(); - let width = text_content.get_width(selrect.width()); - text_content.layout_origin_x(&selrect, width) - selrect.x() -} - fn paragraphs_vertical_offset( shape: &Shape, layout_paragraphs: &[&skia_safe::textlayout::Paragraph], @@ -165,7 +141,6 @@ fn calculate_cursor_rect( return None; } - let x_offset = paragraphs_horizontal_offset(shape, text_content); let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs); for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() { if idx == cursor.paragraph { @@ -183,13 +158,8 @@ fn calculate_cursor_rect( // Skia ranges are UTF-16 code units, not characters. let (cursor_x, cursor_y, cursor_width, cursor_height) = if para_char_count == 0 { - // No glyph to anchor to: sit where the first character will appear. - let empty_x = if para.text_direction() == TextDirection::RTL { - laid_out_para.max_width() - } else { - 0.0 - }; - (empty_x, 0.0, 1.0, laid_out_para.height()) + // Empty paragraph - use default height + (0.0, 0.0, 1.0, laid_out_para.height()) } else if char_pos == 0 { let rects = laid_out_para.get_rects_for_range( 0..para.char_utf16_len_at(0), @@ -198,7 +168,7 @@ fn calculate_cursor_rect( ); if !rects.is_empty() { let r = &rects[0].rect; - (leading_edge(&rects[0]), r.top(), r.width(), r.height()) + (r.left(), r.top(), r.width(), r.height()) } else { (0.0, 0.0, 1.0, laid_out_para.height()) } @@ -212,15 +182,14 @@ fn calculate_cursor_rect( ); if !rects.is_empty() { let r = &rects[0].rect; - (trailing_edge(&rects[0]), r.top(), r.width(), r.height()) + (r.right(), r.top(), r.width(), r.height()) } else if let Some(line) = laid_out_para.get_line_metrics().last() { - // No glyph box to measure: use the end of the line. - let line_end = if para.text_direction() == TextDirection::RTL { - line.left as f32 - } else { - line.left as f32 + line.width as f32 - }; - (line_end, 0.0, 1.0, laid_out_para.height()) + ( + line.left as f32 + line.width as f32, + 0.0, + 1.0, + laid_out_para.height(), + ) } else { (0.0, 0.0, 1.0, laid_out_para.height()) } @@ -233,7 +202,7 @@ fn calculate_cursor_rect( ); if !rects.is_empty() { let r = &rects[0].rect; - (leading_edge(&rects[0]), r.top(), r.width(), r.height()) + (r.left(), r.top(), r.width(), r.height()) } else { // Fallback: use glyph position let pos = laid_out_para.get_glyph_position_at_coordinate((0.0, 0.0)); @@ -242,7 +211,7 @@ fn calculate_cursor_rect( }; return Some(Rect::from_xywh( - x_offset + cursor_x, + cursor_x, y_offset + cursor_y, cursor_width, // cursor_width cursor_height, @@ -267,7 +236,6 @@ fn calculate_selection_rects( let paragraphs = text_content.paragraphs(); let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let x_offset = paragraphs_horizontal_offset(shape, text_content); let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs); for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() { @@ -310,7 +278,7 @@ fn calculate_selection_rects( for text_box in text_boxes { let r = text_box.rect; rects.push(Rect::from_xywh( - x_offset + r.left(), + r.left(), y_offset + r.top(), r.width(), r.height(), @@ -323,92 +291,3 @@ fn calculate_selection_rects( rects } - -#[cfg(test)] -mod tests { - use super::*; - use crate::shapes::{FontFamily, FontStyle, GrowType, Paragraph, TextAlign, TextSpan}; - use crate::uuid::Uuid; - - fn rtl_span() -> TextSpan { - TextSpan::new( - "نام".to_string(), - FontFamily::new(Uuid::nil(), 400, FontStyle::Normal), - 14.0, - 1.2, - 0.0, - None, - None, - TextDirection::RTL, - 400, - Uuid::nil(), - vec![], - ) - } - - /// Auto-width rtl content measured wider than its stale selrect: mid-edit, - /// where the overlay origin and the selrect diverge. - fn grown_rtl_shape(measured_width: f32) -> Shape { - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let mut content = TextContent::new(selrect, GrowType::AutoWidth); - content.add_paragraph(Paragraph::new( - TextAlign::Right, - TextDirection::RTL, - None, - None, - 1.2, - 0.0, - vec![rtl_span()], - )); - content.size.width = measured_width; - content.size.height = 20.0; - - let mut shape = Shape::new(Uuid::nil()); - shape.set_selrect(selrect.left, selrect.top, selrect.right, selrect.bottom); - shape.set_shape_type(Type::Text(content)); - shape - } - - fn text_content_of(shape: &Shape) -> &TextContent { - match &shape.shape_type { - Type::Text(content) => content, - _ => unreachable!(), - } - } - - #[test] - fn horizontal_offset_shifts_the_overlay_onto_grown_rtl_text() { - let shape = grown_rtl_shape(120.0); - // right edge 160 - measured 120 = 40, i.e. 60 left of selrect.x - assert_eq!( - paragraphs_horizontal_offset(&shape, text_content_of(&shape)), - -60.0 - ); - } - - #[test] - fn horizontal_offset_is_zero_once_the_selrect_is_committed() { - let shape = grown_rtl_shape(60.0); - assert_eq!( - paragraphs_horizontal_offset(&shape, text_content_of(&shape)), - 0.0 - ); - } - - #[test] - fn leading_and_trailing_edges_follow_the_run_direction() { - let rect = Rect::from_ltrb(10.0, 0.0, 30.0, 12.0); - let ltr = TextBox { - rect, - direct: TextDirection::LTR, - }; - let rtl = TextBox { - rect, - direct: TextDirection::RTL, - }; - assert_eq!(leading_edge(<r), 10.0); - assert_eq!(trailing_edge(<r), 30.0); - assert_eq!(leading_edge(&rtl), 30.0); - assert_eq!(trailing_edge(&rtl), 10.0); - } -} diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 1bb6b9d3cb..1898ed7276 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -320,18 +320,11 @@ fn propagate_transform( } } } - // Rtl text keeps its right edge, so scale about the ne corner. - let anchor = if text_content.is_rtl() { - shape_bounds_after.ne - } else { - shape_bounds_after.nw - }; - let resize_transform = math::resize_matrix_from( + let resize_transform = math::resize_matrix( &shape_bounds_after, &shape_bounds_after, new_width, new_height, - anchor, ); shape_bounds_after = shape_bounds_after.transform(&resize_transform); transform.post_concat(&resize_transform); diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index a13b4c02e7..8122ee21be 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -476,26 +476,18 @@ impl TextContent { /// [`cached_layout_paint_offset`] on the canvas so both move together. pub fn cached_layout_paint_anchor(&self, selrect: &Rect) -> Point { self.layout_paint_origin - .unwrap_or_else(|| Point::new(self.selrect_origin_x(selrect), selrect.y())) + .unwrap_or_else(|| Point::new(selrect.x(), selrect.y())) } /// Canvas translation from the baked paint origin to the current selrect. /// Zero when there is no recorded origin (fall back to painting at selrect). pub fn cached_layout_paint_offset(&self, selrect: &Rect) -> Point { match self.layout_paint_origin { - Some(origin) => Point::new( - self.selrect_origin_x(selrect) - origin.x, - selrect.y() - origin.y, - ), + Some(origin) => Point::new(selrect.x() - origin.x, selrect.y() - origin.y), None => Point::new(0.0, 0.0), } } - /// [`layout_origin_x`] for this content's own measured width. - fn selrect_origin_x(&self, selrect: &Rect) -> f32 { - self.layout_origin_x(selrect, self.size.width) - } - /// Text content for paint when [`Rect`] size may differ from stored bounds /// (e.g. modifier transform). Reuses `self` when width/height match; otherwise /// clones paragraphs into a rebound copy with an empty layout cache. @@ -576,26 +568,6 @@ impl TextContent { } } - /// Non-empty and every paragraph RTL. Mixed content counts as LTR, mirroring - /// `app.common.types.text/rtl-content?` on the ClojureScript side. - pub fn is_rtl(&self) -> bool { - !self.paragraphs.is_empty() - && self - .paragraphs - .iter() - .all(|paragraph| paragraph.text_direction() == TextDirection::RTL) - } - - /// Left edge of the laid-out text of `width` inside `selrect`. RTL auto-width - /// text anchors right so the box extends leftward as it grows. - pub fn layout_origin_x(&self, selrect: &Rect, width: f32) -> f32 { - if self.grow_type() == GrowType::AutoWidth && self.is_rtl() { - selrect.right() - width - } else { - selrect.x() - } - } - /// Compute a tight text rect from laid-out Skia paragraphs using glyph /// metrics (fm.top for overshoot, line descent for bottom, line left/width /// for horizontal extent). @@ -748,6 +720,7 @@ impl TextContent { } pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect { + let x = selrect.x(); let mut y = selrect.y(); let width = if self.grow_type() == GrowType::AutoWidth { @@ -756,8 +729,6 @@ impl TextContent { selrect.width() }; - let x = self.layout_origin_x(selrect, width); - let height = if self.size.width.round() != width.round() { self.get_height(width) } else { @@ -1182,8 +1153,8 @@ impl TextContent { self.layout.set(result.0, result.1); self.size .copy_finite_size(result.2, default_width, default_height); - // Absolute image/gradient shaders are built against `self.bounds()`, so the - // origin matches them and `cached_layout_paint_offset` carries any rtl shift. + // Paragraph paints (incl. absolute image/gradient shaders) were built + // against `self.bounds()` in `paragraph_builder_group_from_text`. self.layout_paint_origin = Some(Point::new(self.bounds.x(), self.bounds.y())); } @@ -1853,7 +1824,7 @@ pub fn calculate_text_layout_data( let selrect_width = shape.selrect().width(); let text_width = text_content.get_width(selrect_width); let selrect_height = shape.selrect().height(); - let x = text_content.layout_origin_x(&shape.selrect, text_width); + let x = shape.selrect.x(); let base_y = shape.selrect.y(); let mut position_data: Vec = Vec::new(); let mut previous_line_height = text_content.normalized_line_height(); @@ -2412,170 +2383,4 @@ mod tests { layout.clear(); assert!(layout.needs_update()); } - - // ----------------------------------------------------------------------- - // RTL auto-width growth anchor - // ----------------------------------------------------------------------- - - fn directed_paragraph(direction: TextDirection) -> Paragraph { - let span = TextSpan::new( - "hello".to_string(), - FontFamily::new(Uuid::nil(), 400, crate::shapes::FontStyle::Normal), - 14.0, - 1.2, - 0.0, - None, - None, - direction, - 400, - Uuid::nil(), - vec![], - ); - Paragraph::new(TextAlign::Left, direction, None, None, 1.2, 0.0, vec![span]) - } - - /// Auto-width content measured wider than its stale selrect: mid-edit, where - /// the anchor is observable. - fn grown_content(directions: &[TextDirection], measured_width: f32) -> TextContent { - let bounds = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let mut content = TextContent::new(bounds, GrowType::AutoWidth); - for direction in directions { - content.add_paragraph(directed_paragraph(*direction)); - } - content.size.width = measured_width; - content.size.height = 20.0; - content - } - - #[test] - fn is_rtl_false_when_no_paragraphs() { - let content = TextContent::new(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), GrowType::AutoWidth); - assert!(!content.is_rtl()); - } - - #[test] - fn is_rtl_true_when_every_paragraph_is_rtl() { - let content = grown_content(&[TextDirection::RTL, TextDirection::RTL], 120.0); - assert!(content.is_rtl()); - } - - #[test] - fn is_rtl_false_when_directions_are_mixed() { - let content = grown_content(&[TextDirection::RTL, TextDirection::LTR], 120.0); - assert!(!content.is_rtl()); - } - - #[test] - fn is_rtl_false_when_every_paragraph_is_ltr() { - let content = grown_content(&[TextDirection::LTR], 120.0); - assert!(!content.is_rtl()); - } - - #[test] - fn layout_origin_x_right_anchors_rtl_auto_width() { - let content = grown_content(&[TextDirection::RTL], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - // right edge (160) minus the measured width (120) - assert_eq!(content.layout_origin_x(&selrect, 120.0), 40.0); - } - - #[test] - fn layout_origin_x_left_anchors_ltr_auto_width() { - let content = grown_content(&[TextDirection::LTR], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - assert_eq!(content.layout_origin_x(&selrect, 120.0), 100.0); - } - - #[test] - fn layout_origin_x_left_anchors_mixed_direction_auto_width() { - let content = grown_content(&[TextDirection::RTL, TextDirection::LTR], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - assert_eq!(content.layout_origin_x(&selrect, 120.0), 100.0); - } - - #[test] - fn layout_origin_x_ignores_direction_for_fixed_and_auto_height() { - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - for grow_type in [GrowType::Fixed, GrowType::AutoHeight] { - let mut content = grown_content(&[TextDirection::RTL], 120.0); - content.set_grow_type(grow_type); - assert_eq!(content.layout_origin_x(&selrect, 120.0), 100.0); - } - } - - #[test] - fn layout_origin_x_is_selrect_x_once_the_selrect_is_committed() { - let content = grown_content(&[TextDirection::RTL], 60.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - assert_eq!(content.layout_origin_x(&selrect, 60.0), selrect.x()); - } - - #[test] - fn content_rect_right_anchors_grown_rtl_auto_width() { - let content = grown_content(&[TextDirection::RTL], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let rect = content.content_rect(&selrect, VerticalAlign::Top); - assert_eq!(rect.x(), 40.0); - assert_eq!(rect.right(), selrect.right()); - assert_eq!(rect.width(), 120.0); - assert_eq!(rect.y(), selrect.y()); - } - - #[test] - fn content_rect_left_anchors_grown_ltr_auto_width() { - let content = grown_content(&[TextDirection::LTR], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let rect = content.content_rect(&selrect, VerticalAlign::Top); - assert_eq!(rect.x(), selrect.x()); - assert_eq!(rect.width(), 120.0); - } - - #[test] - fn content_rect_unchanged_for_fixed_rtl_content() { - // A mismatch sends `content_rect` into `get_height`, which needs global - // font state tests do not have. - let mut content = grown_content(&[TextDirection::RTL], 60.0); - content.set_grow_type(GrowType::Fixed); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let rect = content.content_rect(&selrect, VerticalAlign::Top); - assert_eq!(rect.x(), selrect.x()); - assert_eq!(rect.width(), selrect.width()); - } - - #[test] - fn cached_paint_anchor_plus_offset_lands_on_the_layout_origin() { - let mut content = grown_content(&[TextDirection::RTL], 120.0); - content.layout_paint_origin = Some(Point::new(100.0, 50.0)); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let anchor = content.cached_layout_paint_anchor(&selrect); - let offset = content.cached_layout_paint_offset(&selrect); - assert_eq!( - anchor.x + offset.x, - content.layout_origin_x(&selrect, 120.0) - ); - } - - #[test] - fn cached_paint_offset_stays_a_pure_translation_when_the_shape_moves() { - let mut content = grown_content(&[TextDirection::RTL], 120.0); - content.layout_paint_origin = Some(Point::new(100.0, 50.0)); - let before = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - let after = Rect::from_xywh(130.0, 70.0, 60.0, 20.0); - let moved_by = Point::new( - content.cached_layout_paint_offset(&after).x - - content.cached_layout_paint_offset(&before).x, - content.cached_layout_paint_offset(&after).y - - content.cached_layout_paint_offset(&before).y, - ); - assert_eq!(moved_by, Point::new(30.0, 20.0)); - } - - #[test] - fn cached_paint_anchor_right_anchors_when_no_origin_was_baked() { - let content = grown_content(&[TextDirection::RTL], 120.0); - let selrect = Rect::from_xywh(100.0, 50.0, 60.0, 20.0); - assert_eq!(content.layout_paint_origin, None); - assert_eq!(content.cached_layout_paint_anchor(&selrect).x, 40.0); - assert_eq!(content.cached_layout_paint_offset(&selrect).x, 0.0); - } }