Merge pull request #7730 from penpot/alotor-fixes-layouts

 Fix new render problems with layout
This commit is contained in:
Elena Torró 2025-11-13 16:38:20 +01:00 committed by GitHub
commit 8aaa953604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 270 additions and 162 deletions

View File

@ -732,89 +732,89 @@
[shape scale-text-content value]
(update shape :content scale-text-content value))
(defn scale-text-content
[content value]
(->> content
(txt/transform-nodes txt/is-text-node? (partial transform-text-node value))
(txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value))))
(defn apply-scale-content
[shape value]
;; Scale can only be positive
(let [value (mth/abs value)]
(cond-> shape
(cfh/text-shape? shape)
(update-text-content scale-text-content value)
:always
(gsc/update-corners-scale value)
(d/not-empty? (:strokes shape))
(gss/update-strokes-width value)
(d/not-empty? (:shadow shape))
(gse/update-shadows-scale value)
(some? (:blur shape))
(gse/update-blur-scale value)
(ctl/flex-layout? shape)
(ctl/update-flex-scale value)
(ctl/grid-layout? shape)
(ctl/update-grid-scale value)
:always
(ctl/update-flex-child value))))
(defn remove-children-set
[shapes children-to-remove]
(let [remove? (set children-to-remove)]
(d/removev remove? shapes)))
(defn apply-modifier
[shape operation]
(let [type (dm/get-prop operation :type)]
(case type
:rotation
(let [rotation (dm/get-prop operation :value)]
(update shape :rotation #(mod (+ (or % 0) rotation) 360)))
:add-children
(let [value (dm/get-prop operation :value)
index (dm/get-prop operation :index)
shape
(if (some? index)
(update shape :shapes
(fn [shapes]
(if (vector? shapes)
(d/insert-at-index shapes index value)
(d/concat-vec shapes value))))
(update shape :shapes d/concat-vec value))]
;; Remove duplication
(update shape :shapes #(into [] (apply d/ordered-set %))))
:remove-children
(let [value (dm/get-prop operation :value)]
(update shape :shapes remove-children-set value))
:scale-content
(let [value (dm/get-prop operation :value)]
(apply-scale-content shape value))
:change-property
(let [property (dm/get-prop operation :property)
value (dm/get-prop operation :value)]
(assoc shape property value))
;; :default => no change to shape
shape)))
(defn apply-structure-modifiers
"Apply structure changes to a shape"
[shape modifiers]
(letfn [(scale-text-content
[content value]
(->> content
(txt/transform-nodes txt/is-text-node? (partial transform-text-node value))
(txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value))))
(apply-scale-content
[shape value]
;; Scale can only be positive
(let [value (mth/abs value)]
(cond-> shape
(cfh/text-shape? shape)
(update-text-content scale-text-content value)
:always
(gsc/update-corners-scale value)
(d/not-empty? (:strokes shape))
(gss/update-strokes-width value)
(d/not-empty? (:shadow shape))
(gse/update-shadows-scale value)
(some? (:blur shape))
(gse/update-blur-scale value)
(ctl/flex-layout? shape)
(ctl/update-flex-scale value)
(ctl/grid-layout? shape)
(ctl/update-grid-scale value)
:always
(ctl/update-flex-child value))))]
(let [remove-children
(fn [shapes children-to-remove]
(let [remove? (set children-to-remove)]
(d/removev remove? shapes)))
apply-modifier
(fn [shape operation]
(let [type (dm/get-prop operation :type)]
(case type
:rotation
(let [rotation (dm/get-prop operation :value)]
(update shape :rotation #(mod (+ (or % 0) rotation) 360)))
:add-children
(let [value (dm/get-prop operation :value)
index (dm/get-prop operation :index)
shape
(if (some? index)
(update shape :shapes
(fn [shapes]
(if (vector? shapes)
(d/insert-at-index shapes index value)
(d/concat-vec shapes value))))
(update shape :shapes d/concat-vec value))]
;; Remove duplication
(update shape :shapes #(into [] (apply d/ordered-set %))))
:remove-children
(let [value (dm/get-prop operation :value)]
(update shape :shapes remove-children value))
:scale-content
(let [value (dm/get-prop operation :value)]
(apply-scale-content shape value))
:change-property
(let [property (dm/get-prop operation :property)
value (dm/get-prop operation :value)]
(assoc shape property value))
;; :default => no change to shape
shape)))]
(as-> shape $
(reduce apply-modifier $ (dm/get-prop modifiers :structure-parent))
(reduce apply-modifier $ (dm/get-prop modifiers :structure-child))))))
(as-> shape $
(reduce apply-modifier $ (dm/get-prop modifiers :structure-parent))
(reduce apply-modifier $ (dm/get-prop modifiers :structure-child))))

View File

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.types.path :as path]))
@ -207,3 +208,12 @@
:projects
(filter #(= team-id (:team-id (val %))))
(into {}))))
(defn get-selrect
[selrect-transform shape]
(if (some? selrect-transform)
(let [{:keys [center width height transform]} selrect-transform]
[(gsh/center->rect center width height)
(gmt/transform-in center transform)])
[(dm/get-prop shape :selrect)
(gsh/transform-matrix shape)]))

View File

@ -212,13 +212,14 @@
;; Create a new objects only with the temporary modifications
objects-changed
(->> wasm-props
(group-by first)
(reduce
(fn [objects [id properties]]
(let [shape
(->> properties
(reduce
(fn [shape {:keys [property value]}]
(assoc shape property value))
(fn [shape [_ operation]]
(ctm/apply-modifier shape operation))
(get objects id)))]
(assoc objects id shape)))
objects))]

View File

@ -154,6 +154,9 @@
"All tokens related ephimeral state"
(l/derived :workspace-tokens st/state))
(def workspace-selrect
(l/derived :workspace-selrect st/state))
;; WARNING: Don't use directly from components, this is a proxy to
;; improve performance of selected-shapes and
(def ^:private selected-shapes-data

View File

@ -7,19 +7,18 @@
(ns app.main.ui.workspace.shapes.text.text-edition-outline
(:require
[app.common.geom.shapes :as gsh]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.texts :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.render-wasm.api :as wasm.api]
[rumext.v2 :as mf]))
(mf/defc text-edition-outline
[{:keys [shape zoom modifiers]}]
(if (features/active-feature? @st/state "render-wasm/v1")
(let [transform (gsh/transform-str shape)
{:keys [id x y grow-type]} shape
{:keys [width height]} (if (= :fixed grow-type) shape (wasm.api/get-text-dimensions id))]
(let [selrect-transform (mf/deref refs/workspace-selrect)
[{:keys [x y width height]} transform] (dsh/get-selrect selrect-transform shape)]
[:rect.main.viewport-selrect
{:x x
:y y

View File

@ -10,12 +10,14 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.text :as gst]
[app.common.math :as mth]
[app.common.types.color :as color]
[app.common.types.text :as txt]
[app.config :as cf]
[app.main.data.helpers :as dsh]
[app.main.data.workspace :as dw]
[app.main.data.workspace.texts :as dwt]
[app.main.features :as features]
@ -226,8 +228,8 @@
(stl/css :text-editor-container))
:ref container-ref
:data-testid "text-editor-container"
:style {:width (:width shape)
:height (:height shape)}
:style {:width "var(--editor-container-width)"
:height "var(--editor-container-height)"}
;; We hide the editor when is blurred because otherwise the
;; selection won't let us see the underlying text. Use opacity
;; because display or visibility won't allow to recover focus
@ -303,12 +305,22 @@
(some? modifiers)
(gsh/transform-shape modifiers))
[x y width height]
(if (features/active-feature? @st/state "render-wasm/v1")
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
{:keys [x y]} (:selrect shape)]
render-wasm? (mf/use-memo #(features/active-feature? @st/state "render-wasm/v1"))
[x y width height])
[{:keys [x y width height]} transform]
(if render-wasm?
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
selrect-transform (mf/deref refs/workspace-selrect)
[selrect transform] (dsh/get-selrect selrect-transform shape)
valign (-> shape :content :vertical-align)
y (:y selrect)
y (case valign
"bottom" (- y (- height (:height selrect)))
"center" (- y (/ (- height (:height selrect)) 2))
y)]
[(assoc selrect :y y :width width :height height) transform])
(let [bounds (gst/shape->rect shape)
x (mth/min (dm/get-prop bounds :x)
@ -319,12 +331,24 @@
(dm/get-prop shape :width))
height (mth/max (dm/get-prop bounds :height)
(dm/get-prop shape :height))]
[x y width height]))
[(grc/make-rect x y width height) (gsh/transform-matrix shape)]))
style
(cond-> #js {:pointerEvents "all"}
render-wasm?
(obj/merge!
#js {"--editor-container-width" (dm/str width "px")
"--editor-container-height" (dm/str height "px")})
(not (cf/check-browser? :safari))
(not render-wasm?)
(obj/merge!
#js {"--editor-container-width" (dm/str (:width shape) "px")
"--editor-container-height" (dm/str (:height shape) "px")})
;; Transform is necessary when there is a text overflow and the vertical
;; aligment is center or bottom.
(and (not render-wasm?)
(not (cf/check-browser? :safari)))
(obj/merge!
#js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))})
@ -345,7 +369,7 @@
(dm/fmt "scale(%)" maybe-zoom))}))]
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
:transform (dm/str (gsh/transform-matrix shape))}
:transform (dm/str transform)}
[:defs
[:clipPath {:id clip-id}
[:rect {:x x :y y :width width :height height}]]]

View File

@ -15,6 +15,7 @@
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.main.data.helpers :as dsh]
[app.main.data.workspace :as dw]
[app.main.data.workspace.shapes :as dwsh]
[app.main.refs :as refs]
@ -25,7 +26,6 @@
[app.util.debug :as dbg]
[app.util.dom :as dom]
[app.util.object :as obj]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def rotation-handler-size 20)
@ -327,23 +327,11 @@
:style {:fill (if (dbg/enabled? :handlers) "yellow" "none")
:stroke-width 0}}]]))
(def workspace-selrect-transform
(l/derived :workspace-selrect st/state))
(defn get-selrect
[selrect-transform shape]
(if (some? selrect-transform)
(let [{:keys [center width height transform]} selrect-transform]
[(gsh/center->rect center width height)
(gmt/transform-in center transform)])
[(dm/get-prop shape :selrect)
(gsh/transform-matrix shape)]))
(mf/defc controls-selection*
[{:keys [shape zoom color on-move-selected on-context-menu disabled]}]
(let [selrect-transform (mf/deref workspace-selrect-transform)
(let [selrect-transform (mf/deref refs/workspace-selrect)
transform-type (mf/deref refs/current-transform)
[selrect transform] (get-selrect selrect-transform shape)]
[selrect transform] (dsh/get-selrect selrect-transform shape)]
(when (and (some? selrect)
(not (or (= transform-type :move)
@ -360,7 +348,7 @@
(mf/defc controls-handlers*
{::mf/private true}
[{:keys [shape zoom color on-resize on-rotate disabled]}]
(let [selrect-transform (mf/deref workspace-selrect-transform)
(let [selrect-transform (mf/deref refs/workspace-selrect)
transform-type (mf/deref refs/current-transform)
read-only? (mf/use-ctx ctx/workspace-read-only?)
@ -368,7 +356,7 @@
layout (mf/deref refs/workspace-layout)
scale-text? (contains? layout :scale-text)
[selrect transform] (get-selrect selrect-transform shape)
[selrect transform] (dsh/get-selrect selrect-transform shape)
rotation (-> (gpt/point 1 0)
(gpt/transform (:transform shape))

View File

@ -66,9 +66,9 @@
(defn apply-modifiers-to-selected
[selected objects modifiers]
(->> modifiers
(filter #(contains? selected (:id %)))
(filter #(contains? selected (first %)))
(reduce
(fn [objects {:keys [id transform]}]
(fn [objects [id transform]]
(update objects id gsh/apply-transform transform))
objects)))

View File

@ -51,6 +51,13 @@ pub fn identitish(m: &Matrix) -> bool {
&& is_close_to(m.skew_y(), 0.0)
}
pub fn is_move_only_matrix(m: &Matrix) -> bool {
is_close_to(m.scale_x(), 1.0)
&& is_close_to(m.scale_y(), 1.0)
&& is_close_to(m.skew_x(), 0.0)
&& is_close_to(m.skew_y(), 0.0)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Bounds {
pub nw: Point,

View File

@ -1472,6 +1472,10 @@ impl RenderState {
// Z-index ordering on Layouts
if element.has_layout() {
if element.is_flex() && !element.is_flex_reverse() {
children_ids.reverse();
}
children_ids.sort_by(|id1, id2| {
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());

View File

@ -1,5 +1,7 @@
use skia_safe::{self as skia};
use indexmap::IndexSet;
use crate::uuid::Uuid;
use std::borrow::Cow;
use std::cell::{OnceCell, RefCell};
@ -328,6 +330,33 @@ impl Shape {
)
}
pub fn is_flex(&self) -> bool {
matches!(
self.shape_type,
Type::Frame(Frame {
layout: Some(layouts::Layout::FlexLayout(_, _)),
..
})
)
}
pub fn is_flex_reverse(&self) -> bool {
matches!(
self.shape_type,
Type::Frame(Frame {
layout: Some(layouts::Layout::FlexLayout(
_,
FlexData {
direction: layouts::FlexDirection::RowReverse
| layouts::FlexDirection::ColumnReverse,
..
}
)),
..
})
)
}
pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
self.invalidate_bounds();
self.invalidate_extrect();
@ -1257,13 +1286,14 @@ impl Shape {
}
pub fn apply_structure(&mut self, structure: &Vec<StructureEntry>) {
let mut result: Vec<Uuid> = Vec::from_iter(self.children.iter().copied());
let mut result = IndexSet::<Uuid>::from_iter(self.children.iter().copied());
let mut to_remove = HashSet::<&Uuid>::new();
for st in structure {
match st.entry_type {
StructureEntryType::AddChild => {
result.insert(st.index as usize, st.id);
let index = usize::min(result.len() - 1, st.index as usize);
result.shift_insert(index, st.id);
}
StructureEntryType::RemoveChild => {
to_remove.insert(&st.id);

View File

@ -6,11 +6,12 @@ mod flex_layout;
pub mod common;
pub mod grid_layout;
use crate::math::{self as math, bools, identitish, Bounds, Matrix, Point};
use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point};
use common::GetBounds;
use crate::shapes::{
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, Type,
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
TransformEntrySource, Type,
};
use crate::state::{ShapesPoolRef, State};
use crate::uuid::Uuid;
@ -75,7 +76,7 @@ fn propagate_children(
child.ignore_constraints,
);
result.push_back(Modifier::transform(*child_id, transform));
result.push_back(Modifier::transform_propagate(*child_id, transform));
}
result
@ -182,33 +183,43 @@ fn propagate_transform(
let mut transform = entry.transform;
// NOTA: No puedo utilizar un clone porque entonces estaríamos
// perdiendo la referencia al contenido del layout...
if let Type::Text(text_content) = &mut shape.shape_type.clone() {
if text_content.needs_update_layout() {
text_content.update_layout(shape.selrect);
}
match text_content.grow_type() {
GrowType::AutoHeight => {
let height = text_content.size.height;
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
shape_bounds_after.width(),
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
// Only check the text layout when the width/height changes
if !is_close_to(shape_bounds_before.width(), shape_bounds_after.width())
|| !is_close_to(shape_bounds_before.height(), shape_bounds_after.height())
{
if let Type::Text(text_content) = &mut shape.shape_type.clone() {
match text_content.grow_type() {
GrowType::AutoHeight => {
if text_content.needs_update_layout() {
text_content.update_layout(shape.selrect);
}
let height = text_content.size.height;
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
shape_bounds_after.width(),
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
}
GrowType::AutoWidth => {
if text_content.needs_update_layout() {
text_content.update_layout(shape.selrect);
}
let width = text_content.width();
let height = text_content.size.height;
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
width,
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
}
GrowType::Fixed => {}
}
GrowType::AutoWidth => {
let width = text_content.width();
let height = text_content.size.height;
let resize_transform =
math::resize_matrix(&shape_bounds_after, &shape_bounds_after, width, height);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
}
GrowType::Fixed => {}
}
}
@ -234,12 +245,19 @@ fn propagate_transform(
shape_modif.post_concat(&transform);
modifiers.insert(shape.id, shape_modif);
if shape.has_layout() {
let is_resize = !math::is_move_only_matrix(&transform);
let is_propagate = entry.source == TransformEntrySource::Propagate;
// If this is a layout and we're only moving don't need to reflow
if shape.has_layout() && is_resize {
entries.push_back(Modifier::reflow(shape.id));
}
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
if parent.has_layout() || parent.is_group_like() {
// When the parent is either a group or a layout we only mark for reflow
// if the current transformation is not a move propagation.
// If it's a move propagation we don't need to reflow, the parent is already changed.
if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) {
entries.push_back(Modifier::reflow(parent.id));
}
}
@ -360,7 +378,14 @@ pub fn propagate_modifiers(
) -> Vec<TransformEntry> {
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| Modifier::Transform(entry.clone()))
.map(|entry| {
// If we receibe a identity matrix we force a reflow
if math::identitish(&entry.transform) {
Modifier::Reflow(entry.id)
} else {
Modifier::Transform(entry.clone())
}
})
.collect();
let mut modifiers = HashMap::<Uuid, Matrix>::new();
@ -407,7 +432,7 @@ pub fn propagate_modifiers(
modifiers
.iter()
.map(|(key, val)| TransformEntry::new(*key, *val))
.map(|(key, val)| TransformEntry::from_input(*key, *val))
.collect()
}

View File

@ -1,4 +1,4 @@
use crate::math::{Bounds, Matrix};
use crate::math::{is_move_only_matrix, Bounds, Matrix};
use crate::shapes::{ConstraintH, ConstraintV};
pub fn calculate_resize(
@ -110,7 +110,7 @@ pub fn propagate_shape_constraints(
// can propagate as is
if (ignore_constrainst
|| constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|| transform.is_translate()
|| is_move_only_matrix(&transform)
{
return transform;
}

View File

@ -623,7 +623,7 @@ pub fn reflow_flex_layout(
transform.post_concat(&Matrix::translate(delta_v));
}
result.push_back(Modifier::transform(child.id, transform));
result.push_back(Modifier::transform_propagate(child.id, transform));
shape_anchor = next_anchor(
layout_data,

View File

@ -791,7 +791,7 @@ pub fn reflow_grid_layout(
transform.post_concat(&Matrix::translate(delta_v));
}
result.push_back(Modifier::transform(child.id, transform));
result.push_back(Modifier::transform_propagate(child.id, transform));
}
if shape.is_layout_horizontal_auto() || shape.is_layout_vertical_auto() {

View File

@ -12,8 +12,8 @@ pub enum Modifier {
}
impl Modifier {
pub fn transform(id: Uuid, transform: Matrix) -> Self {
Modifier::Transform(TransformEntry::new(id, transform))
pub fn transform_propagate(id: Uuid, transform: Matrix) -> Self {
Modifier::Transform(TransformEntry::from_propagate(id, transform))
}
pub fn parent(id: Uuid, transform: Matrix) -> Self {
Modifier::Transform(TransformEntry::parent(id, transform))
@ -23,19 +23,35 @@ impl Modifier {
}
}
#[derive(PartialEq, Debug, Clone)]
pub enum TransformEntrySource {
Input,
Propagate,
}
#[derive(PartialEq, Debug, Clone)]
#[repr(C)]
pub struct TransformEntry {
pub id: Uuid,
pub transform: Matrix,
pub source: TransformEntrySource,
pub propagate: bool,
}
impl TransformEntry {
pub fn new(id: Uuid, transform: Matrix) -> Self {
pub fn from_input(id: Uuid, transform: Matrix) -> Self {
TransformEntry {
id,
transform,
source: TransformEntrySource::Input,
propagate: true,
}
}
pub fn from_propagate(id: Uuid, transform: Matrix) -> Self {
TransformEntry {
id,
transform,
source: TransformEntrySource::Propagate,
propagate: true,
}
}
@ -43,6 +59,7 @@ impl TransformEntry {
TransformEntry {
id,
transform,
source: TransformEntrySource::Propagate,
propagate: false,
}
}
@ -70,7 +87,7 @@ impl SerializableResult for TransformEntry {
0.0,
1.0,
);
TransformEntry::new(id, transform)
TransformEntry::from_input(id, transform)
}
fn as_bytes(&self) -> Self::BytesType {
@ -176,7 +193,7 @@ mod tests {
#[test]
fn test_serialization() {
let entry = TransformEntry::new(
let entry = TransformEntry::from_input(
Uuid::new_v4(),
Matrix::new_all(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 1.0),
);