mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Fix issues with auto-width/height
This commit is contained in:
parent
9c8f8df253
commit
5f2d7a1ff1
@ -18,6 +18,7 @@
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.worker :as mw]
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@ -46,6 +47,15 @@
|
||||
(assoc :points (grc/rect->points selrect))
|
||||
(assoc :grow-type :fixed))))
|
||||
|
||||
(defn click-draw-text
|
||||
"Place the start of an auto-width text at the clicked point."
|
||||
[shape]
|
||||
(-> shape
|
||||
(assoc :height 17 :width 4 :grow-type :auto-width :selrect nil :points nil)
|
||||
(cts/setup-shape)
|
||||
(cond-> (wm/vertical-text-content? (:content shape))
|
||||
(gsh/transform-shape (ctm/move-modifiers -4 0)))))
|
||||
|
||||
(defn clear-drawing
|
||||
([] (clear-drawing nil))
|
||||
([{:keys [preserve-tool?]}]
|
||||
@ -103,8 +113,7 @@
|
||||
(click-draw-path)
|
||||
|
||||
(and click-draw? text?)
|
||||
(-> (assoc :height 17 :width 4 :grow-type :auto-width)
|
||||
(cts/setup-shape))
|
||||
(click-draw-text)
|
||||
|
||||
:always
|
||||
(dissoc :initialized? :click-draw?))]
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.render-wasm.api.select :as wselect]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.text.cjk-layout :as cjk]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
@ -25,6 +24,7 @@
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.api.fonts :as wasm.fonts]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@ -35,25 +35,7 @@
|
||||
auto-height keeps the physical height as its wrap budget and grows width."
|
||||
[selrect grow-type content dimension]
|
||||
(when (or (= :fixed grow-type) (some? dimension))
|
||||
(let [vertical? (cjk/vertical-text-content? content)]
|
||||
{:width (cond
|
||||
(= :fixed grow-type)
|
||||
(:width selrect)
|
||||
|
||||
(and (= :auto-height grow-type) (not vertical?))
|
||||
(:width selrect)
|
||||
|
||||
:else
|
||||
(:width dimension))
|
||||
:height (cond
|
||||
(= :fixed grow-type)
|
||||
(:height selrect)
|
||||
|
||||
(and (= :auto-height grow-type) vertical?)
|
||||
(:height selrect)
|
||||
|
||||
:else
|
||||
(:height dimension))})))
|
||||
(text.geom/resolve-size selrect grow-type content dimension)))
|
||||
|
||||
(defn get-wasm-text-new-size
|
||||
"Computes the new {width, height} for a text shape from WASM text layout.
|
||||
@ -83,12 +65,12 @@
|
||||
([shape]
|
||||
(resize-wasm-text-modifiers shape (:content shape)))
|
||||
|
||||
([{:keys [id points selrect] :as shape} content]
|
||||
([{:keys [id 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)
|
||||
origin (first points)]
|
||||
origin (text.geom/resize-origin (assoc shape :content content))]
|
||||
{id
|
||||
{:modifiers
|
||||
(ctm/resize-modifiers
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc text-edition-outline*
|
||||
@ -21,14 +22,23 @@
|
||||
(let [selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
|
||||
;; While editing, the committed selrect lags the text (geometry is
|
||||
;; finalize-only), so measure the live WASM text for the growing axes.
|
||||
live (wasm.api/get-text-dimensions (:id shape))
|
||||
{:keys [width height]} (dwt/merge-live-text-dimensions
|
||||
(:grow-type shape) selrect live)]
|
||||
;; IME previews never enter the store. Subscribe here so only the
|
||||
;; outline updates; re-rendering the capture surface aborts real IMEs.
|
||||
dimension* (mf/use-state nil)
|
||||
dimension (or @dimension* (wasm.api/get-text-dimensions (:id shape)))
|
||||
{:keys [x y width height]}
|
||||
(text.geom/live-rect selrect (:grow-type shape) (:content shape) dimension)]
|
||||
(mf/use-effect
|
||||
(mf/deps (:id shape))
|
||||
(fn []
|
||||
(let [update-bounds! (fn [_]
|
||||
(reset! dimension* (wasm.api/get-text-dimensions (:id shape))))]
|
||||
(.addEventListener js/document "penpot:wasm:render" update-bounds!)
|
||||
(update-bounds! nil)
|
||||
#(.removeEventListener js/document "penpot:wasm:render" update-bounds!))))
|
||||
[:rect.main.viewport-selrect
|
||||
{:x (:x selrect)
|
||||
:y (:y selrect)
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform transform
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
[app.render-wasm.text-editor :as text-editor]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
@ -380,34 +381,33 @@
|
||||
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||
|
||||
[{:keys [x y width height ime-width ime-left]} transform]
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
(let [dimension (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)
|
||||
selrect-height (:height selrect)
|
||||
selrect-width (:width selrect)
|
||||
max-width (max width selrect-width)
|
||||
max-height (max height selrect-height)
|
||||
;; During auto-width editing the shape width is trimmed to the content, so an
|
||||
;; empty text box ends up only a few pixels wide. That is not enough room for
|
||||
;; the caret and the contenteditable overlay may fail to receive input when it
|
||||
;; is that small. Expand the overlay by one viewport width for auto-width texts
|
||||
;; (mirroring the v2 editor) so typing works and the caret is not clipped.
|
||||
live-rect (text.geom/live-rect selrect (:grow-type shape) (:content shape) dimension)
|
||||
;; Reserve room for composition without changing the capture DOM
|
||||
;; mid-IME. Vertical columns grow leftward; their inline axis is y.
|
||||
auto-width? (= (:grow-type shape) :auto-width)
|
||||
auto-grow? (not= (:grow-type shape) :fixed)
|
||||
viewport-width (or (:width vbox) 0)
|
||||
overlay-width (if (= (:grow-type shape) :auto-width)
|
||||
(+ max-width viewport-width)
|
||||
max-width)
|
||||
viewport-height (or (:height vbox) 0)
|
||||
left (min (:x selrect) (:x live-rect))
|
||||
top (min (:y selrect) (:y live-rect))
|
||||
right (max (+ (:x selrect) (:width selrect))
|
||||
(+ (:x live-rect) (:width live-rect)))
|
||||
bottom (max (+ (:y selrect) (:height selrect))
|
||||
(+ (:y live-rect) (:height live-rect)))
|
||||
x (if (and vertical? auto-grow?) (- left viewport-width) left)
|
||||
overlay-width (+ (- right x) (if (and (not vertical?) auto-width?) viewport-width 0))
|
||||
overlay-height (+ (- bottom top)
|
||||
(if (if vertical? auto-width? auto-grow?) viewport-height 0))
|
||||
ime-padding (ime-overlay-padding vertical? viewport-width)
|
||||
valign (-> shape :content :vertical-align)
|
||||
y (:y selrect)
|
||||
y (case valign
|
||||
"bottom" (+ y (- selrect-height height))
|
||||
"center" (+ y (/ (- selrect-height height) 2))
|
||||
y)]
|
||||
y top]
|
||||
;; Widen the foreignObject (not the clip) toward the IME anchor, or
|
||||
;; the browser scrolls to reveal the caret and cancels the offset.
|
||||
[(assoc selrect
|
||||
:y y :width overlay-width :height max-height
|
||||
:x x :y y :width overlay-width :height overlay-height
|
||||
:ime-width (+ overlay-width (:right ime-padding))
|
||||
:ime-left (:left ime-padding))
|
||||
transform])
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.modules :as mod]
|
||||
[app.util.text.content :as tc]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@ -1705,13 +1706,11 @@
|
||||
(defn- text-selrect-stale?
|
||||
"Check if the WASM-measured dimensions of an auto-grow text shape differ
|
||||
from its stored selrect (same 0.1px tolerance as the classic renderer)."
|
||||
[{:keys [id selrect grow-type]}]
|
||||
(when-let [{:keys [width height]} (get-text-dimensions id)]
|
||||
(case grow-type
|
||||
:auto-width (or (not (mth/close? width (:width selrect) 0.1))
|
||||
(not (mth/close? height (:height selrect) 0.1)))
|
||||
:auto-height (not (mth/close? height (:height selrect) 0.1))
|
||||
false)))
|
||||
[{:keys [id selrect grow-type content]}]
|
||||
(when-let [dimension (get-text-dimensions id)]
|
||||
(let [{:keys [width height]} (text.geom/resolve-size selrect grow-type content dimension)]
|
||||
(or (not (mth/close? width (:width selrect) 0.1))
|
||||
(not (mth/close? height (:height selrect) 0.1))))))
|
||||
|
||||
(defonce ^:private pending-stale-selrect-ids (atom #{}))
|
||||
(defonce ^:private stale-selrect-sync-token (atom 0))
|
||||
|
||||
@ -720,6 +720,7 @@
|
||||
(when (and before after)
|
||||
(normalize-selection {:anchor-para (:para before) :anchor-offset (:offset before)
|
||||
:focus-para (:para after) :focus-offset (:offset after)})))
|
||||
|
||||
(defn- para-char-count
|
||||
[para]
|
||||
(apply + (map (fn [span] (count (:text span))) (:children para))))
|
||||
|
||||
56
frontend/src/app/util/text/geometry.cljs
Normal file
56
frontend/src/app/util/text/geometry.cljs
Normal file
@ -0,0 +1,56 @@
|
||||
;; 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 app.util.text.geometry
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.text.cjk-layout :as cjk]))
|
||||
|
||||
(defn- live-axis
|
||||
"Measured axis, or the committed one when the measure is missing or not
|
||||
positive, so a failed WASM measure never shows as `0`."
|
||||
[measured committed]
|
||||
(if (and (number? measured) (pos? measured)) measured committed))
|
||||
|
||||
(defn resolve-size
|
||||
"Auto-height grows along the block axis: height horizontally, width vertically."
|
||||
[selrect grow-type content dimension]
|
||||
(when (or (= :fixed grow-type) (some? dimension))
|
||||
(let [vertical? (cjk/vertical-text-content? content)]
|
||||
{:width (if (or (= :fixed grow-type)
|
||||
(and (= :auto-height grow-type) (not vertical?)))
|
||||
(:width selrect)
|
||||
(live-axis (:width dimension) (:width selrect)))
|
||||
:height (if (or (= :fixed grow-type)
|
||||
(and (= :auto-height grow-type) vertical?))
|
||||
(:height selrect)
|
||||
(live-axis (:height dimension) (:height selrect)))})))
|
||||
|
||||
(defn- block-anchor
|
||||
[content]
|
||||
(if (cjk/vertical-text-content? content)
|
||||
(case (:vertical-align content) "center" 0.5 "bottom" 0 1)
|
||||
(case (:vertical-align content) "center" 0.5 "bottom" 1 0)))
|
||||
|
||||
(defn resize-origin
|
||||
"Keep the text's block alignment anchored when its measured size changes."
|
||||
[{:keys [points content]}]
|
||||
(gpt/lerp (nth points 0)
|
||||
(nth points (if (cjk/vertical-text-content? content) 1 3))
|
||||
(block-anchor content)))
|
||||
|
||||
(defn live-rect
|
||||
"Bounds of live editor content, before its geometry is committed."
|
||||
[selrect grow-type content dimension]
|
||||
(if-let [{:keys [width height]} (resolve-size selrect grow-type content dimension)]
|
||||
(let [vertical? (cjk/vertical-text-content? content)
|
||||
anchor (block-anchor content)]
|
||||
(assoc selrect
|
||||
:x (+ (:x selrect) (if vertical? (* anchor (- (:width selrect) width)) 0))
|
||||
:y (+ (:y selrect) (if vertical? 0 (* anchor (- (:height selrect) height))))
|
||||
:width width
|
||||
:height height))
|
||||
selrect))
|
||||
@ -8,12 +8,14 @@
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.drawing.common :as drawing]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
@ -21,6 +23,7 @@
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.ui.shapes.text.styles :as text.styles]
|
||||
[app.main.ui.workspace.shapes.text.viewport-texts-html :as vth]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
@ -44,6 +47,89 @@
|
||||
(t/is (= {:width 100 :height 360}
|
||||
(dwwt/resolve-text-size selrect :auto-height content dimension)))))
|
||||
|
||||
(t/deftest vertical-auto-grow-keeps-the-block-anchor
|
||||
(doseq [[align expected-x] [["top" -90] ["center" -40] ["bottom" 10]]]
|
||||
(let [shape (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height
|
||||
:content (assoc (text-content "vertical-rl") :vertical-align align)})
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (= expected-x (:x resized)) align)
|
||||
(t/is (= 200 (:height resized)) align))))
|
||||
|
||||
(t/deftest live-vertical-bounds-grow-left-and-keep-the-wrap-height
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
content (text-content "vertical-rl")]
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 200}
|
||||
(text.geom/live-rect selrect :auto-height content {:width 240 :height 360})))
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 360}
|
||||
(text.geom/live-rect selrect :auto-width content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :fixed content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :auto-width content nil)))))
|
||||
|
||||
(t/deftest live-horizontal-bounds-grow-down-and-keep-the-wrap-width
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
content (text-content "horizontal-tb")]
|
||||
(t/is (= {:x 10 :y 20 :width 100 :height 360}
|
||||
(text.geom/live-rect selrect :auto-height content {:width 240 :height 360})))
|
||||
(t/is (= {:x 10 :y 20 :width 240 :height 360}
|
||||
(text.geom/live-rect selrect :auto-width content {:width 240 :height 360})))))
|
||||
|
||||
(t/deftest live-bounds-keep-committed-axes-when-the-measure-is-missing
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
content (text-content "horizontal-tb")]
|
||||
(t/is (= selrect (text.geom/live-rect selrect :auto-width content {:width 0 :height 0})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :auto-width content {:width nil :height nil})))
|
||||
(t/is (= {:x 10 :y 20 :width 100 :height 360}
|
||||
(text.geom/live-rect selrect :auto-width content {:width 0 :height 360})))))
|
||||
|
||||
(t/deftest vertical-auto-grow-keeps-the-anchor-after-rotation
|
||||
(let [base (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height :content (text-content "vertical-rl")})
|
||||
shape (gsh/transform-shape base (ctm/rotation-modifiers base (gpt/point 60 120) 45))
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (gpt/almost-zero? (gpt/subtract (nth (:points shape) 1) (nth (:points resized) 1))))))
|
||||
|
||||
(t/deftest clicking-vertical-text-anchors-its-right-edge-at-the-click
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(let [shape (drawing/click-draw-text
|
||||
(cts/setup-shape {:type :text :x 100 :y 50 :width 1 :height 1
|
||||
:content (text-content "vertical-rl")}))]
|
||||
(t/is (= 100 (+ (:x shape) (:width shape))))
|
||||
(t/is (= 50 (:y shape)))
|
||||
(t/is (= [4 17] [(:width shape) (:height shape)]))
|
||||
(t/is (= :auto-width (:grow-type shape))))))
|
||||
|
||||
(t/deftest clicking-horizontal-text-keeps-its-left-edge-at-the-click
|
||||
(let [shape (drawing/click-draw-text
|
||||
(cts/setup-shape {:type :text :x 100 :y 50 :width 1 :height 1
|
||||
:content (text-content "horizontal-tb")}))]
|
||||
(t/is (= 100 (:x shape)))
|
||||
(t/is (= 50 (:y shape)))
|
||||
(t/is (= [4 17] [(:width shape) (:height shape)]))))
|
||||
|
||||
(t/deftest horizontal-auto-grow-keeps-the-block-anchor
|
||||
(doseq [[align expected-y] [["top" 20] ["center" -60] ["bottom" -140]]]
|
||||
(let [shape (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height
|
||||
:content (assoc (text-content "horizontal-tb") :vertical-align align)})
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 100 :height 360})
|
||||
([_ _] {:width 100 :height 360}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (= expected-y (:y resized)) align))))
|
||||
|
||||
(t/deftest vertical-grow-type-resize-axes-are-remapped
|
||||
(t/is (= :auto-height
|
||||
(dwm/next-grow-type :auto-width (gpt/point 1 2) true)))
|
||||
|
||||
@ -94,8 +94,8 @@
|
||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||
[frontend-tests.render-wasm.text-editor-content-range-test]
|
||||
[frontend-tests.render-wasm.text-paste-test]
|
||||
[frontend-tests.render-wasm.webgl-test]
|
||||
[frontend-tests.render-wasm.texts-test]
|
||||
[frontend-tests.render-wasm.webgl-test]
|
||||
[frontend-tests.router-test]
|
||||
[frontend-tests.svg-fills-test]
|
||||
[frontend-tests.svg-filters-test]
|
||||
|
||||
@ -4919,10 +4919,6 @@ impl RenderState {
|
||||
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
|
||||
}
|
||||
|
||||
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||
self.mark_touched_with_prev(uuid, None);
|
||||
}
|
||||
|
||||
pub fn mark_touched_with_prev(&mut self, uuid: Uuid, prev_extrect: Option<Rect>) {
|
||||
if self.touched_ids.insert(uuid) {
|
||||
if let Some(rect) = prev_extrect.filter(|r| !r.is_empty()) {
|
||||
|
||||
@ -13,7 +13,7 @@ use crate::error::Result;
|
||||
use crate::shapes;
|
||||
use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, PixelPrecision, Shape,
|
||||
TransformEntry, TransformEntrySource, Type,
|
||||
TransformEntry, TransformEntrySource, Type, VerticalAlign,
|
||||
};
|
||||
use crate::state::{ShapesPoolRef, State};
|
||||
use crate::uuid::Uuid;
|
||||
@ -306,12 +306,25 @@ fn propagate_transform(
|
||||
}
|
||||
}
|
||||
}
|
||||
let resize_transform = math::resize_matrix(
|
||||
let mut resize_transform = math::resize_matrix(
|
||||
&shape_bounds_after,
|
||||
&shape_bounds_after,
|
||||
new_width,
|
||||
new_height,
|
||||
);
|
||||
if text_content.is_vertical() {
|
||||
// Reflow adds columns toward the block end. Keep the
|
||||
// right/center/left anchor set by vertical alignment.
|
||||
let anchor = match shape.vertical_align() {
|
||||
VerticalAlign::Top => shape_bounds_after.ne,
|
||||
VerticalAlign::Center => {
|
||||
(shape_bounds_after.nw + shape_bounds_after.ne) * 0.5
|
||||
}
|
||||
VerticalAlign::Bottom => shape_bounds_after.nw,
|
||||
};
|
||||
resize_transform
|
||||
.post_translate(anchor - resize_transform.map_point(anchor));
|
||||
}
|
||||
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
|
||||
transform.post_concat(&resize_transform);
|
||||
}
|
||||
@ -599,6 +612,50 @@ mod tests {
|
||||
use crate::shapes::*;
|
||||
use crate::state::ShapesPool;
|
||||
|
||||
#[test]
|
||||
fn vertical_auto_height_resize_keeps_the_right_edge() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let rect = math::Rect::from_xywh(10.0, 20.0, 100.0, 200.0);
|
||||
let mut content = TextContent::new(rect, GrowType::AutoHeight);
|
||||
let mut paragraph = Paragraph::new(
|
||||
TextAlign::Left,
|
||||
TextDirection::LTR,
|
||||
None,
|
||||
None,
|
||||
1.2,
|
||||
0.0,
|
||||
vec![TextSpan {
|
||||
text: "あいうえおかきくけこ".to_string(),
|
||||
font_size: 20.0,
|
||||
..TextSpan::default()
|
||||
}],
|
||||
);
|
||||
paragraph.set_writing_mode(WritingMode::VerticalRl);
|
||||
content.add_paragraph(paragraph);
|
||||
let size = content.update_layout(rect);
|
||||
let id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(1);
|
||||
let shape = state.shapes.add_shape(id);
|
||||
shape.set_selrect(10.0, 20.0, 10.0 + size.width, 220.0);
|
||||
shape.set_shape_type(Type::Text(content));
|
||||
let before = shape.bounds();
|
||||
let transform = math::resize_matrix(&before, &before, size.width, 60.0);
|
||||
let result = propagate_modifiers(
|
||||
&state,
|
||||
&[TransformEntry::from_input(id, transform)],
|
||||
PixelPrecision::Disabled,
|
||||
)
|
||||
.unwrap();
|
||||
let transform = result.iter().find(|e| e.id == id).unwrap().transform;
|
||||
let after = before.transform(&transform);
|
||||
assert!(after.width() > before.width());
|
||||
assert!(is_close_to(after.height(), 60.0));
|
||||
assert!(is_close_to(after.ne.x, before.ne.x));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_shape() {
|
||||
let parent_id = Uuid::new_v4();
|
||||
|
||||
@ -47,9 +47,15 @@ pub fn modifier_changes_text_layout(base: &Shape, modifier: &Matrix) -> bool {
|
||||
let after = before.transform(modifier);
|
||||
match text_content.grow_type() {
|
||||
GrowType::AutoWidth => !crate::math::is_close_to(before.height(), after.height()),
|
||||
GrowType::AutoHeight | GrowType::Fixed => {
|
||||
!crate::math::is_close_to(before.width(), after.width())
|
||||
GrowType::AutoHeight if text_content.is_vertical() => {
|
||||
!crate::math::is_close_to(before.height(), after.height())
|
||||
}
|
||||
GrowType::AutoHeight => !crate::math::is_close_to(before.width(), after.width()),
|
||||
GrowType::Fixed if text_content.is_vertical() => {
|
||||
!crate::math::is_close_to(before.width(), after.width())
|
||||
|| !crate::math::is_close_to(before.height(), after.height())
|
||||
}
|
||||
GrowType::Fixed => !crate::math::is_close_to(before.width(), after.width()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -487,6 +493,7 @@ pub struct TextContent {
|
||||
content_version: u64,
|
||||
layout_version: u64,
|
||||
layout_width: Option<f32>,
|
||||
layout_height: Option<f32>,
|
||||
/// Canvas origin used when absolute fill shaders (image/gradient) were baked
|
||||
/// into cached Skia paragraphs. Kept across move clones so paint can
|
||||
/// translate glyphs + shaders together. See `cached_layout_paint_offset`.
|
||||
@ -518,6 +525,7 @@ impl TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_height: None,
|
||||
layout_paint_origin: None,
|
||||
layout_paint_size: None,
|
||||
}
|
||||
@ -536,6 +544,7 @@ impl TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_height: None,
|
||||
layout_paint_origin: None,
|
||||
layout_paint_size: None,
|
||||
}
|
||||
@ -1009,15 +1018,16 @@ impl TextContent {
|
||||
}
|
||||
|
||||
pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect {
|
||||
// Vertical content anchors to the shape's right edge and always
|
||||
// aligns to the top; vertical-align does not apply.
|
||||
// Auto-grow bounds follow the same block anchor as the vertical paint.
|
||||
if self.is_vertical() {
|
||||
let (width, height) = if self.grow_type() == GrowType::AutoWidth {
|
||||
(self.size.width, self.size.height)
|
||||
} else {
|
||||
(selrect.width(), selrect.height())
|
||||
let (width, height) = match self.grow_type() {
|
||||
GrowType::AutoWidth => (self.size.width, self.size.height),
|
||||
GrowType::AutoHeight => (self.size.width, selrect.height()),
|
||||
GrowType::Fixed => (selrect.width(), selrect.height()),
|
||||
};
|
||||
return Rect::from_xywh(selrect.right() - width, selrect.y(), width, height);
|
||||
let x = selrect.x()
|
||||
+ super::text_vertical::block_axis_offset(selrect.width(), width, valign);
|
||||
return Rect::from_xywh(x, selrect.y(), width, height);
|
||||
}
|
||||
|
||||
let x = selrect.x();
|
||||
@ -1563,6 +1573,7 @@ impl TextContent {
|
||||
|
||||
pub fn force_next_layout_update(&mut self) {
|
||||
self.layout_width = None;
|
||||
self.layout_height = None;
|
||||
self.layout_paint_origin = None;
|
||||
self.layout_paint_size = None;
|
||||
self.layout.cached_extrect.set(None);
|
||||
@ -1577,11 +1588,20 @@ impl TextContent {
|
||||
// match the container we are laying out for.
|
||||
self.set_xywh(selrect.x(), selrect.y(), selrect.width(), selrect.height());
|
||||
|
||||
// Auto-width ignores selrect width so get-text-dimensions can reuse the cached layout.
|
||||
let layout_matches_container = self.grow_type() == GrowType::AutoWidth
|
||||
|| self
|
||||
.layout_width
|
||||
.is_some_and(|w| (w - selrect.width()).abs() < f32::EPSILON);
|
||||
// Vertical columns wrap by height, horizontal lines by width.
|
||||
let width_matches = self
|
||||
.layout_width
|
||||
.is_some_and(|w| (w - selrect.width()).abs() < f32::EPSILON);
|
||||
let height_matches = self
|
||||
.layout_height
|
||||
.is_some_and(|h| (h - selrect.height()).abs() < f32::EPSILON);
|
||||
let layout_matches_container = match self.grow_type() {
|
||||
GrowType::AutoWidth => true,
|
||||
GrowType::AutoHeight if self.is_vertical() => height_matches,
|
||||
GrowType::AutoHeight => width_matches,
|
||||
GrowType::Fixed if self.is_vertical() => width_matches && height_matches,
|
||||
GrowType::Fixed => width_matches,
|
||||
};
|
||||
|
||||
if !self.layout.needs_update()
|
||||
&& self.layout_version == self.content_version
|
||||
@ -1592,6 +1612,7 @@ impl TextContent {
|
||||
}
|
||||
|
||||
self.size.set_size(selrect.width(), selrect.height());
|
||||
self.layout_height = Some(selrect.height());
|
||||
|
||||
if self.is_vertical() {
|
||||
// Vertical writing takes sizes from the vertical pass, so the
|
||||
@ -1641,11 +1662,15 @@ impl TextContent {
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_empty() {
|
||||
if self.is_empty() && self.grow_type() != GrowType::Fixed {
|
||||
let (placeholder_width, placeholder_height) = self.placeholder_dimensions(selrect);
|
||||
self.size.width = placeholder_width;
|
||||
self.size.height = placeholder_height;
|
||||
self.size.max_width = placeholder_width;
|
||||
if self.grow_type() == GrowType::AutoWidth || self.is_vertical() {
|
||||
self.size.width = placeholder_width;
|
||||
self.size.max_width = placeholder_width;
|
||||
}
|
||||
if self.grow_type() == GrowType::AutoWidth || !self.is_vertical() {
|
||||
self.size.height = placeholder_height;
|
||||
}
|
||||
}
|
||||
|
||||
self.layout_version = self.content_version;
|
||||
@ -1683,6 +1708,18 @@ impl TextContent {
|
||||
/// If that fails we fall back to the previous WASM size or the incoming
|
||||
/// selrect dimensions.
|
||||
fn placeholder_dimensions(&self, selrect: Rect) -> (f32, f32) {
|
||||
if self.is_vertical() {
|
||||
let layout = self.vertical_layout(&selrect);
|
||||
let font_size = self
|
||||
.paragraphs
|
||||
.first()
|
||||
.and_then(|p| p.children().first())
|
||||
.map_or(14.0, |s| s.font_size);
|
||||
return (
|
||||
layout.width.ceil().max(DEFAULT_TEXT_CONTENT_SIZE),
|
||||
font_size,
|
||||
);
|
||||
}
|
||||
if let Some(paragraph) = self.paragraphs.first() {
|
||||
if let Some(span) = paragraph.children().first() {
|
||||
let fonts = get_font_collection();
|
||||
@ -1799,6 +1836,7 @@ impl Default for TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_height: None,
|
||||
layout_paint_origin: None,
|
||||
layout_paint_size: None,
|
||||
}
|
||||
@ -2745,6 +2783,67 @@ mod tests {
|
||||
use crate::globals::TestRenderResourcesGuard;
|
||||
use crate::render::RenderResources;
|
||||
|
||||
fn vertical_content(text: &str, grow_type: GrowType) -> TextContent {
|
||||
let mut content = TextContent::new(Rect::from_xywh(10.0, 20.0, 100.0, 200.0), grow_type);
|
||||
let mut paragraph = test_paragraph(&[text]);
|
||||
paragraph.set_writing_mode(WritingMode::VerticalRl);
|
||||
paragraph.children_mut()[0].font_size = 20.0;
|
||||
content.add_paragraph(paragraph);
|
||||
content
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_auto_height_reflows_when_only_height_changes() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let mut content = vertical_content("あいうえおかきくけこ", GrowType::AutoHeight);
|
||||
let tall = content.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 240.0));
|
||||
let short = content.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 60.0));
|
||||
assert!(short.width > tall.width);
|
||||
assert_eq!(short.height, 60.0);
|
||||
let restored = content.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 240.0));
|
||||
assert_eq!(restored.width, tall.width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_height_modifier_requires_layout() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let content = vertical_content("あいうえお", GrowType::AutoHeight);
|
||||
let shape = text_shape_with_cached_layout(content);
|
||||
assert!(modifier_changes_text_layout(
|
||||
&shape,
|
||||
&Matrix::scale((1.0, 0.5))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_auto_height_preserves_the_wrap_budget() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let mut vertical = vertical_content("", GrowType::AutoHeight);
|
||||
let size = vertical.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 200.0));
|
||||
assert_eq!(size.height, 200.0);
|
||||
assert!(size.width >= 20.0);
|
||||
let mut horizontal = vertical_content("", GrowType::AutoHeight);
|
||||
horizontal.paragraphs_mut()[0].set_writing_mode(WritingMode::HorizontalTb);
|
||||
let size = horizontal.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 200.0));
|
||||
assert_eq!(size.width, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_fixed_text_preserves_its_container() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let mut content = vertical_content("", GrowType::Fixed);
|
||||
let size = content.update_layout(Rect::from_xywh(10.0, 20.0, 100.0, 200.0));
|
||||
assert_eq!((size.width, size.height), (100.0, 200.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_warichu_span_shows_no_ruby_and_reserves_no_room_for_it() {
|
||||
let span = TextSpan {
|
||||
@ -3080,6 +3179,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn has_usable_paint_layout_false_when_rotated_and_resized() {
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let mut content = sample_text_content();
|
||||
content.layout.paragraphs = Rc::new(vec![vec![]]);
|
||||
content.layout_width = Some(200.0);
|
||||
|
||||
@ -270,6 +270,17 @@ impl ShapesPoolImpl {
|
||||
})
|
||||
}
|
||||
|
||||
/// Drop both the base bounds and any transformed snapshot after a direct edit.
|
||||
pub fn invalidate_shape_extrect(&mut self, id: &Uuid) {
|
||||
let Some(idx) = self.uuid_to_idx.get(id).copied() else {
|
||||
return;
|
||||
};
|
||||
self.shapes[idx].invalidate_extrect();
|
||||
if let Some(cell) = self.modified_shape_cache.get_mut(&idx) {
|
||||
*cell = OnceCell::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops the extrect cache of every ancestor whose extrect is affected by this shape
|
||||
/// stopping at the first ancestor that clips.
|
||||
pub fn invalidate_ancestors_extrect(&mut self, id: &Uuid) {
|
||||
|
||||
@ -396,6 +396,21 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
// TEXT OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
fn text_content_for_edit(state: &mut State, shape_id: Uuid) -> Option<&mut TextContent> {
|
||||
if !matches!(state.shapes.get_raw(&shape_id)?.shape_type, Type::Text(_)) {
|
||||
return None;
|
||||
}
|
||||
// Capture old tile coverage before changing the preview, then drop the
|
||||
// shape snapshot and bounds so new glyphs and erased previews get indexed.
|
||||
state.touch_shape(shape_id);
|
||||
state.shapes.invalidate_shape_extrect(&shape_id);
|
||||
let shape = state.shapes.get_mut(&shape_id)?;
|
||||
match &mut shape.shape_type {
|
||||
Type::Text(content) => Some(content),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn text_editor_composition_start() -> Result<()> {
|
||||
@ -424,11 +439,7 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -457,8 +468,6 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> {
|
||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
get_render_state().mark_touched(shape_id);
|
||||
|
||||
get_text_editor_state().composition.end();
|
||||
});
|
||||
|
||||
@ -484,11 +493,7 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -512,8 +517,6 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
|
||||
get_text_editor_state().reset_blink();
|
||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
get_render_state().mark_touched(shape_id);
|
||||
});
|
||||
|
||||
crate::mem::free_bytes()?;
|
||||
@ -568,11 +571,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -602,8 +601,6 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
||||
get_text_editor_state().reset_blink();
|
||||
get_text_editor_state().push_event(TextEditorEvent::ContentChanged);
|
||||
get_text_editor_state().push_event(TextEditorEvent::NeedsLayout);
|
||||
|
||||
get_render_state().mark_touched(shape_id);
|
||||
});
|
||||
|
||||
crate::mem::free_bytes()?;
|
||||
@ -621,16 +618,11 @@ pub extern "C" fn text_editor_delete_backward(word_boundary: bool) {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
get_text_editor_state().delete_backward(text_content, word_boundary);
|
||||
get_render_state().mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
|
||||
@ -645,16 +637,11 @@ pub extern "C" fn text_editor_delete_forward(word_boundary: bool) {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
get_text_editor_state().delete_forward(text_content, word_boundary);
|
||||
get_render_state().mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
|
||||
@ -669,16 +656,11 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
let Some(text_content) = text_content_for_edit(state, shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
get_text_editor_state().insert_paragraph(text_content);
|
||||
get_render_state().mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1188,3 +1170,134 @@ pub extern "C" fn text_get_range_origin(
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shapes::{Group, GrowType, Paragraph, TextDirection, TextSpan, WritingMode};
|
||||
use crate::tiles;
|
||||
|
||||
fn preview_state(grow_type: GrowType, writing_mode: WritingMode) -> (State, Uuid) {
|
||||
let id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
let shape = state.shapes.add_shape(id);
|
||||
shape.set_selrect(250.0, 250.0, 274.0, 310.0);
|
||||
let mut content = TextContent::new(shape.selrect(), grow_type);
|
||||
let span = TextSpan {
|
||||
text: "あ".to_string(),
|
||||
font_size: 20.0,
|
||||
..TextSpan::default()
|
||||
};
|
||||
let mut paragraph = Paragraph::new(
|
||||
TextAlign::Left,
|
||||
TextDirection::LTR,
|
||||
None,
|
||||
None,
|
||||
1.2,
|
||||
0.0,
|
||||
vec![span],
|
||||
);
|
||||
paragraph.set_writing_mode(writing_mode);
|
||||
content.add_paragraph(paragraph);
|
||||
content.update_layout(shape.selrect());
|
||||
shape.set_shape_type(Type::Text(content));
|
||||
(state, id)
|
||||
}
|
||||
|
||||
fn replace_preview(state: &mut State, id: Uuid, editor: &mut TextEditorState, text: &str) {
|
||||
let content = text_content_for_edit(state, id).expect("text shape");
|
||||
editor.composition.update(text);
|
||||
let selection = editor.composition.get_selection(&editor.selection);
|
||||
text_helpers::delete_selection_range(content, &selection);
|
||||
let cursor = selection.start();
|
||||
editor.composition.start = Some(cursor);
|
||||
let caret =
|
||||
text_helpers::insert_text_with_newlines(content, &cursor, text).unwrap_or(cursor);
|
||||
editor.selection.set_caret(caret);
|
||||
content.layout.clear();
|
||||
update_text_layout_if_needed(state, id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_preview_reindexes_tiles_as_text_grows_and_shrinks() {
|
||||
let mut resources = crate::render::RenderResources::try_new_headless().expect("resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let long_preview = "あいうえおかきくけこさしすせそたちつてとなにぬねの".repeat(4);
|
||||
for grow_type in [GrowType::AutoWidth, GrowType::AutoHeight] {
|
||||
for writing_mode in [WritingMode::HorizontalTb, WritingMode::VerticalRl] {
|
||||
let (mut state, id) = preview_state(grow_type, writing_mode);
|
||||
let original = state.shapes.get(&id).unwrap().extrect(&state.shapes, 1.0);
|
||||
let mut editor = TextEditorState::new();
|
||||
editor.composition.start();
|
||||
for text in [long_preview.as_str(), "日本語", ""] {
|
||||
replace_preview(&mut state, id, &mut editor, text);
|
||||
let shape = state.shapes.get(&id).unwrap();
|
||||
let mut uncached = shape.clone();
|
||||
uncached.invalidate_extrect();
|
||||
let expected = uncached.extrect(&state.shapes, 1.0);
|
||||
let actual = shape.extrect(&state.shapes, 1.0);
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"{grow_type:?} {writing_mode:?} preview {text}"
|
||||
);
|
||||
let current_tiles = tiles::get_tiles_for_rect(actual, tiles::TILE_SIZE);
|
||||
let original_tiles = tiles::get_tiles_for_rect(original, tiles::TILE_SIZE);
|
||||
if text.chars().count() > 20 {
|
||||
assert_ne!(current_tiles, original_tiles, "preview reaches new tiles");
|
||||
} else if text.is_empty() {
|
||||
assert_eq!(
|
||||
current_tiles, original_tiles,
|
||||
"cancellation restores coverage"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
state.shapes.get(&id).unwrap().extrect(&state.shapes, 1.0),
|
||||
original
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_preview_updates_cached_group_bounds() {
|
||||
let mut resources = crate::render::RenderResources::try_new_headless().expect("resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let (mut state, id) = preview_state(GrowType::AutoWidth, WritingMode::HorizontalTb);
|
||||
let parent_id = Uuid::new_v4();
|
||||
let parent = state.shapes.add_shape(parent_id);
|
||||
parent.set_shape_type(Type::Group(Group::default()));
|
||||
parent.clip_content = false;
|
||||
parent.children.push(id);
|
||||
state.shapes.get_mut(&id).unwrap().set_parent(parent_id);
|
||||
let original = state
|
||||
.shapes
|
||||
.get(&parent_id)
|
||||
.unwrap()
|
||||
.extrect(&state.shapes, 1.0);
|
||||
let mut editor = TextEditorState::new();
|
||||
editor.composition.start();
|
||||
replace_preview(&mut state, id, &mut editor, "あいうえおかきくけこ");
|
||||
let parent = state.shapes.get(&parent_id).unwrap();
|
||||
assert!(parent.extrect(&state.shapes, 1.0).right() > original.right());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_preview_updates_modified_shape_snapshot() {
|
||||
let mut resources = crate::render::RenderResources::try_new_headless().expect("resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
let (mut state, id) = preview_state(GrowType::AutoWidth, WritingMode::HorizontalTb);
|
||||
state
|
||||
.shapes
|
||||
.set_modifiers(std::collections::HashMap::from([(
|
||||
id,
|
||||
Matrix::translate((20.0, 30.0)),
|
||||
)]));
|
||||
let original = state.shapes.get(&id).unwrap().extrect(&state.shapes, 1.0);
|
||||
let mut editor = TextEditorState::new();
|
||||
editor.composition.start();
|
||||
replace_preview(&mut state, id, &mut editor, "あいうえおかきくけこ");
|
||||
let shape = state.shapes.get(&id).unwrap();
|
||||
assert!(shape.extrect(&state.shapes, 1.0).right() > original.right());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user