mirror of
https://github.com/penpot/penpot.git
synced 2026-09-24 04:46:14 +00:00
🐛 Fix RTL auto-width text growing away from its right edge (#11775)
This makes RTL texts in auto-width to grow towards their left side in the text editor v3. AI-assisted-by: claude-opus-5
This commit is contained in:
parent
89e91ba372
commit
142f3d9de8
@ -175,6 +175,14 @@
|
|||||||
[node]
|
[node]
|
||||||
(= "root" (:type 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?
|
(defn is-node?
|
||||||
[node]
|
[node]
|
||||||
(or ^boolean (is-text-node? node)
|
(or ^boolean (is-text-node? node)
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.types.modifiers :as ctm]
|
[app.common.types.modifiers :as ctm]
|
||||||
|
[app.common.types.text :as ctt]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
[app.main.data.workspace :as-alias dw]
|
[app.main.data.workspace :as-alias dw]
|
||||||
[app.main.data.workspace.modifiers :as dwm]
|
[app.main.data.workspace.modifiers :as dwm]
|
||||||
@ -63,12 +64,16 @@
|
|||||||
([shape]
|
([shape]
|
||||||
(resize-wasm-text-modifiers shape (:content shape)))
|
(resize-wasm-text-modifiers shape (:content shape)))
|
||||||
|
|
||||||
([{:keys [id points selrect] :as shape} content]
|
([{:keys [id points selrect grow-type] :as shape} content]
|
||||||
(when-let [new-size (get-wasm-text-new-size shape content)]
|
(when-let [new-size (get-wasm-text-new-size shape content)]
|
||||||
(let [width-scale (/ (:width new-size) (:width selrect))
|
(let [width-scale (/ (:width new-size) (:width selrect))
|
||||||
height-scale (/ (:height new-size) (:height selrect))
|
height-scale (/ (:height new-size) (:height selrect))
|
||||||
resize-v (gpt/point width-scale height-scale)
|
resize-v (gpt/point width-scale height-scale)
|
||||||
origin (first points)]
|
;; 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))]
|
||||||
{id
|
{id
|
||||||
{:modifiers
|
{:modifiers
|
||||||
(ctm/resize-modifiers
|
(ctm/resize-modifiers
|
||||||
|
|||||||
@ -25,11 +25,18 @@
|
|||||||
;; finalize-only), so measure the live WASM text for the growing axes:
|
;; finalize-only), so measure the live WASM text for the growing axes:
|
||||||
;; width grows on auto-width, height on auto-width/auto-height.
|
;; width grows on auto-width, height on auto-width/auto-height.
|
||||||
grow-type (:grow-type shape)
|
grow-type (:grow-type shape)
|
||||||
{live-width :width live-height :height} (wasm.api/get-text-dimensions (:id shape))
|
{live-x :x 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-width (if (= grow-type :auto-width) live-width (:width selrect))
|
||||||
sr-height (if (= grow-type :fixed) (:height selrect) live-height)]
|
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))]
|
||||||
[:rect.main.viewport-selrect
|
[:rect.main.viewport-selrect
|
||||||
{:x (:x selrect)
|
{:x sr-x
|
||||||
:y (:y selrect)
|
:y (:y selrect)
|
||||||
:width sr-width
|
:width sr-width
|
||||||
:height sr-height
|
:height sr-height
|
||||||
|
|||||||
@ -214,7 +214,7 @@
|
|||||||
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||||
|
|
||||||
[{:keys [x y width height]} transform]
|
[{:keys [x y width height]} transform]
|
||||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
(let [{text-x :x :keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||||
selrect-transform (mf/deref refs/workspace-selrect)
|
selrect-transform (mf/deref refs/workspace-selrect)
|
||||||
vbox (mf/deref refs/vbox)
|
vbox (mf/deref refs/vbox)
|
||||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||||
@ -231,13 +231,20 @@
|
|||||||
overlay-width (if (= (:grow-type shape) :auto-width)
|
overlay-width (if (= (:grow-type shape) :auto-width)
|
||||||
(+ max-width viewport-width)
|
(+ max-width viewport-width)
|
||||||
max-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)
|
valign (-> shape :content :vertical-align)
|
||||||
y (:y selrect)
|
y (:y selrect)
|
||||||
y (case valign
|
y (case valign
|
||||||
"bottom" (+ y (- selrect-height height))
|
"bottom" (+ y (- selrect-height height))
|
||||||
"center" (+ y (/ (- selrect-height height) 2))
|
"center" (+ y (/ (- selrect-height height) 2))
|
||||||
y)]
|
y)]
|
||||||
[(assoc selrect :y y :width overlay-width :height max-height) transform])
|
[(assoc selrect :x x :y y :width overlay-width :height max-height) transform])
|
||||||
|
|
||||||
on-composition-start
|
on-composition-start
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|||||||
131
frontend/test/frontend_tests/data/wasm_text_test.cljs
Normal file
131
frontend/test/frontend_tests/data/wasm_text_test.cljs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
;; 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"))))))))
|
||||||
@ -513,3 +513,53 @@
|
|||||||
(t/testing "a non-root content (e.g. paragraph) is left alone"
|
(t/testing "a non-root content (e.g. paragraph) is left alone"
|
||||||
(let [node {:type "paragraph" :children []}]
|
(let [node {:type "paragraph" :children []}]
|
||||||
(t/is (= node (dwt/ensure-valid-text-content node))))))
|
(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)))))
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
[frontend-tests.data.svg-upload-test]
|
[frontend-tests.data.svg-upload-test]
|
||||||
[frontend-tests.data.uploads-test]
|
[frontend-tests.data.uploads-test]
|
||||||
[frontend-tests.data.viewer-test]
|
[frontend-tests.data.viewer-test]
|
||||||
|
[frontend-tests.data.wasm-text-test]
|
||||||
[frontend-tests.data.workspace-colors-test]
|
[frontend-tests.data.workspace-colors-test]
|
||||||
[frontend-tests.data.workspace-comments-test]
|
[frontend-tests.data.workspace-comments-test]
|
||||||
[frontend-tests.data.workspace-interactions-test]
|
[frontend-tests.data.workspace-interactions-test]
|
||||||
@ -141,6 +142,7 @@
|
|||||||
'frontend-tests.data.svg-upload-test
|
'frontend-tests.data.svg-upload-test
|
||||||
'frontend-tests.data.uploads-test
|
'frontend-tests.data.uploads-test
|
||||||
'frontend-tests.data.viewer-test
|
'frontend-tests.data.viewer-test
|
||||||
|
'frontend-tests.data.wasm-text-test
|
||||||
'frontend-tests.data.workspace-colors-test
|
'frontend-tests.data.workspace-colors-test
|
||||||
'frontend-tests.data.workspace-comments-test
|
'frontend-tests.data.workspace-comments-test
|
||||||
'frontend-tests.data.workspace-interactions-test
|
'frontend-tests.data.workspace-interactions-test
|
||||||
|
|||||||
@ -434,6 +434,26 @@ pub fn resize_matrix(
|
|||||||
child_bounds: &Bounds,
|
child_bounds: &Bounds,
|
||||||
new_width: f32,
|
new_width: f32,
|
||||||
new_height: 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 {
|
) -> Matrix {
|
||||||
let mut result = Matrix::default();
|
let mut result = Matrix::default();
|
||||||
|
|
||||||
@ -455,7 +475,7 @@ pub fn resize_matrix(
|
|||||||
parent_transform.pre_translate(-center);
|
parent_transform.pre_translate(-center);
|
||||||
|
|
||||||
let parent_transform_inv = &parent_transform.invert().unwrap_or_default();
|
let parent_transform_inv = &parent_transform.invert().unwrap_or_default();
|
||||||
let origin = parent_transform_inv.map_point(child_bounds.nw);
|
let origin = parent_transform_inv.map_point(anchor);
|
||||||
|
|
||||||
let mut scale = Matrix::scale((scale_width, scale_height));
|
let mut scale = Matrix::scale((scale_width, scale_height));
|
||||||
scale.post_translate(origin);
|
scale.post_translate(origin);
|
||||||
@ -597,4 +617,40 @@ mod tests {
|
|||||||
assert!((m.translate_x() - 0.0).abs() <= 0.1);
|
assert!((m.translate_x() - 0.0).abs() <= 0.1);
|
||||||
assert!((m.translate_y() - 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,25 @@ use crate::render::options::RenderOptions;
|
|||||||
use crate::shapes::{vertical_align_offset, Shape, TextContent, Type};
|
use crate::shapes::{vertical_align_offset, Shape, TextContent, Type};
|
||||||
use crate::state::{TextEditorState, TextSelection};
|
use crate::state::{TextEditorState, TextSelection};
|
||||||
use crate::view::Viewbox;
|
use crate::view::Viewbox;
|
||||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle, TextBox, TextDirection};
|
||||||
use skia_safe::{BlendMode, Canvas, Color, Paint, Rect};
|
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(
|
pub fn render_overlay(
|
||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
viewbox: &Viewbox,
|
viewbox: &Viewbox,
|
||||||
@ -112,6 +128,14 @@ fn render_selection(
|
|||||||
canvas.restore();
|
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(
|
fn paragraphs_vertical_offset(
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
layout_paragraphs: &[&skia_safe::textlayout::Paragraph],
|
layout_paragraphs: &[&skia_safe::textlayout::Paragraph],
|
||||||
@ -141,6 +165,7 @@ fn calculate_cursor_rect(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let x_offset = paragraphs_horizontal_offset(shape, text_content);
|
||||||
let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs);
|
let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs);
|
||||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
if idx == cursor.paragraph {
|
if idx == cursor.paragraph {
|
||||||
@ -158,8 +183,13 @@ fn calculate_cursor_rect(
|
|||||||
|
|
||||||
// Skia ranges are UTF-16 code units, not characters.
|
// Skia ranges are UTF-16 code units, not characters.
|
||||||
let (cursor_x, cursor_y, cursor_width, cursor_height) = if para_char_count == 0 {
|
let (cursor_x, cursor_y, cursor_width, cursor_height) = if para_char_count == 0 {
|
||||||
// Empty paragraph - use default height
|
// No glyph to anchor to: sit where the first character will appear.
|
||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
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())
|
||||||
} else if char_pos == 0 {
|
} else if char_pos == 0 {
|
||||||
let rects = laid_out_para.get_rects_for_range(
|
let rects = laid_out_para.get_rects_for_range(
|
||||||
0..para.char_utf16_len_at(0),
|
0..para.char_utf16_len_at(0),
|
||||||
@ -168,7 +198,7 @@ fn calculate_cursor_rect(
|
|||||||
);
|
);
|
||||||
if !rects.is_empty() {
|
if !rects.is_empty() {
|
||||||
let r = &rects[0].rect;
|
let r = &rects[0].rect;
|
||||||
(r.left(), r.top(), r.width(), r.height())
|
(leading_edge(&rects[0]), r.top(), r.width(), r.height())
|
||||||
} else {
|
} else {
|
||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
(0.0, 0.0, 1.0, laid_out_para.height())
|
||||||
}
|
}
|
||||||
@ -182,14 +212,15 @@ fn calculate_cursor_rect(
|
|||||||
);
|
);
|
||||||
if !rects.is_empty() {
|
if !rects.is_empty() {
|
||||||
let r = &rects[0].rect;
|
let r = &rects[0].rect;
|
||||||
(r.right(), r.top(), r.width(), r.height())
|
(trailing_edge(&rects[0]), r.top(), r.width(), r.height())
|
||||||
} else if let Some(line) = laid_out_para.get_line_metrics().last() {
|
} else if let Some(line) = laid_out_para.get_line_metrics().last() {
|
||||||
(
|
// No glyph box to measure: use the end of the line.
|
||||||
line.left as f32 + line.width as f32,
|
let line_end = if para.text_direction() == TextDirection::RTL {
|
||||||
0.0,
|
line.left as f32
|
||||||
1.0,
|
} else {
|
||||||
laid_out_para.height(),
|
line.left as f32 + line.width as f32
|
||||||
)
|
};
|
||||||
|
(line_end, 0.0, 1.0, laid_out_para.height())
|
||||||
} else {
|
} else {
|
||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
(0.0, 0.0, 1.0, laid_out_para.height())
|
||||||
}
|
}
|
||||||
@ -202,7 +233,7 @@ fn calculate_cursor_rect(
|
|||||||
);
|
);
|
||||||
if !rects.is_empty() {
|
if !rects.is_empty() {
|
||||||
let r = &rects[0].rect;
|
let r = &rects[0].rect;
|
||||||
(r.left(), r.top(), r.width(), r.height())
|
(leading_edge(&rects[0]), r.top(), r.width(), r.height())
|
||||||
} else {
|
} else {
|
||||||
// Fallback: use glyph position
|
// Fallback: use glyph position
|
||||||
let pos = laid_out_para.get_glyph_position_at_coordinate((0.0, 0.0));
|
let pos = laid_out_para.get_glyph_position_at_coordinate((0.0, 0.0));
|
||||||
@ -211,7 +242,7 @@ fn calculate_cursor_rect(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Some(Rect::from_xywh(
|
return Some(Rect::from_xywh(
|
||||||
cursor_x,
|
x_offset + cursor_x,
|
||||||
y_offset + cursor_y,
|
y_offset + cursor_y,
|
||||||
cursor_width, // cursor_width
|
cursor_width, // cursor_width
|
||||||
cursor_height,
|
cursor_height,
|
||||||
@ -236,6 +267,7 @@ fn calculate_selection_rects(
|
|||||||
let paragraphs = text_content.paragraphs();
|
let paragraphs = text_content.paragraphs();
|
||||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
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);
|
let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs);
|
||||||
|
|
||||||
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
@ -278,7 +310,7 @@ fn calculate_selection_rects(
|
|||||||
for text_box in text_boxes {
|
for text_box in text_boxes {
|
||||||
let r = text_box.rect;
|
let r = text_box.rect;
|
||||||
rects.push(Rect::from_xywh(
|
rects.push(Rect::from_xywh(
|
||||||
r.left(),
|
x_offset + r.left(),
|
||||||
y_offset + r.top(),
|
y_offset + r.top(),
|
||||||
r.width(),
|
r.width(),
|
||||||
r.height(),
|
r.height(),
|
||||||
@ -291,3 +323,92 @@ fn calculate_selection_rects(
|
|||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -320,11 +320,18 @@ fn propagate_transform(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let resize_transform = math::resize_matrix(
|
// 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(
|
||||||
&shape_bounds_after,
|
&shape_bounds_after,
|
||||||
&shape_bounds_after,
|
&shape_bounds_after,
|
||||||
new_width,
|
new_width,
|
||||||
new_height,
|
new_height,
|
||||||
|
anchor,
|
||||||
);
|
);
|
||||||
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
|
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
|
||||||
transform.post_concat(&resize_transform);
|
transform.post_concat(&resize_transform);
|
||||||
|
|||||||
@ -476,18 +476,26 @@ impl TextContent {
|
|||||||
/// [`cached_layout_paint_offset`] on the canvas so both move together.
|
/// [`cached_layout_paint_offset`] on the canvas so both move together.
|
||||||
pub fn cached_layout_paint_anchor(&self, selrect: &Rect) -> Point {
|
pub fn cached_layout_paint_anchor(&self, selrect: &Rect) -> Point {
|
||||||
self.layout_paint_origin
|
self.layout_paint_origin
|
||||||
.unwrap_or_else(|| Point::new(selrect.x(), selrect.y()))
|
.unwrap_or_else(|| Point::new(self.selrect_origin_x(selrect), selrect.y()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canvas translation from the baked paint origin to the current selrect.
|
/// Canvas translation from the baked paint origin to the current selrect.
|
||||||
/// Zero when there is no recorded origin (fall back to painting at selrect).
|
/// Zero when there is no recorded origin (fall back to painting at selrect).
|
||||||
pub fn cached_layout_paint_offset(&self, selrect: &Rect) -> Point {
|
pub fn cached_layout_paint_offset(&self, selrect: &Rect) -> Point {
|
||||||
match self.layout_paint_origin {
|
match self.layout_paint_origin {
|
||||||
Some(origin) => Point::new(selrect.x() - origin.x, selrect.y() - origin.y),
|
Some(origin) => Point::new(
|
||||||
|
self.selrect_origin_x(selrect) - origin.x,
|
||||||
|
selrect.y() - origin.y,
|
||||||
|
),
|
||||||
None => Point::new(0.0, 0.0),
|
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
|
/// Text content for paint when [`Rect`] size may differ from stored bounds
|
||||||
/// (e.g. modifier transform). Reuses `self` when width/height match; otherwise
|
/// (e.g. modifier transform). Reuses `self` when width/height match; otherwise
|
||||||
/// clones paragraphs into a rebound copy with an empty layout cache.
|
/// clones paragraphs into a rebound copy with an empty layout cache.
|
||||||
@ -568,6 +576,26 @@ 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
|
/// Compute a tight text rect from laid-out Skia paragraphs using glyph
|
||||||
/// metrics (fm.top for overshoot, line descent for bottom, line left/width
|
/// metrics (fm.top for overshoot, line descent for bottom, line left/width
|
||||||
/// for horizontal extent).
|
/// for horizontal extent).
|
||||||
@ -720,7 +748,6 @@ impl TextContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect {
|
pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect {
|
||||||
let x = selrect.x();
|
|
||||||
let mut y = selrect.y();
|
let mut y = selrect.y();
|
||||||
|
|
||||||
let width = if self.grow_type() == GrowType::AutoWidth {
|
let width = if self.grow_type() == GrowType::AutoWidth {
|
||||||
@ -729,6 +756,8 @@ impl TextContent {
|
|||||||
selrect.width()
|
selrect.width()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let x = self.layout_origin_x(selrect, width);
|
||||||
|
|
||||||
let height = if self.size.width.round() != width.round() {
|
let height = if self.size.width.round() != width.round() {
|
||||||
self.get_height(width)
|
self.get_height(width)
|
||||||
} else {
|
} else {
|
||||||
@ -1153,8 +1182,8 @@ impl TextContent {
|
|||||||
self.layout.set(result.0, result.1);
|
self.layout.set(result.0, result.1);
|
||||||
self.size
|
self.size
|
||||||
.copy_finite_size(result.2, default_width, default_height);
|
.copy_finite_size(result.2, default_width, default_height);
|
||||||
// Paragraph paints (incl. absolute image/gradient shaders) were built
|
// Absolute image/gradient shaders are built against `self.bounds()`, so the
|
||||||
// against `self.bounds()` in `paragraph_builder_group_from_text`.
|
// origin matches them and `cached_layout_paint_offset` carries any rtl shift.
|
||||||
self.layout_paint_origin = Some(Point::new(self.bounds.x(), self.bounds.y()));
|
self.layout_paint_origin = Some(Point::new(self.bounds.x(), self.bounds.y()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1824,7 +1853,7 @@ pub fn calculate_text_layout_data(
|
|||||||
let selrect_width = shape.selrect().width();
|
let selrect_width = shape.selrect().width();
|
||||||
let text_width = text_content.get_width(selrect_width);
|
let text_width = text_content.get_width(selrect_width);
|
||||||
let selrect_height = shape.selrect().height();
|
let selrect_height = shape.selrect().height();
|
||||||
let x = shape.selrect.x();
|
let x = text_content.layout_origin_x(&shape.selrect, text_width);
|
||||||
let base_y = shape.selrect.y();
|
let base_y = shape.selrect.y();
|
||||||
let mut position_data: Vec<PositionData> = Vec::new();
|
let mut position_data: Vec<PositionData> = Vec::new();
|
||||||
let mut previous_line_height = text_content.normalized_line_height();
|
let mut previous_line_height = text_content.normalized_line_height();
|
||||||
@ -2383,4 +2412,170 @@ mod tests {
|
|||||||
layout.clear();
|
layout.clear();
|
||||||
assert!(layout.needs_update());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user