Compare commits

..

1 Commits

Author SHA1 Message Date
alonso.torres
9f49204fd0 WIP 2026-01-12 09:46:38 +01:00
39 changed files with 340 additions and 665 deletions

View File

@@ -23,12 +23,6 @@
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
## 2.12.1

View File

@@ -97,8 +97,8 @@
:jmx-remote
{:jvm-opts ["-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=9000"
"-Dcom.sun.management.jmxremote.rmi.port=9000"
"-Dcom.sun.management.jmxremote.port=9090"
"-Dcom.sun.management.jmxremote.rmi.port=9090"
"-Dcom.sun.management.jmxremote.local.only=false"
"-Dcom.sun.management.jmxremote.authenticate=false"
"-Dcom.sun.management.jmxremote.ssl=false"

View File

@@ -41,10 +41,7 @@ services:
- 6062:6062
- 6063:6063
- 6064:6064
- 9000:9000
- 9001:9001
- 9090:9090
- 9091:9091
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}

View File

@@ -145,8 +145,8 @@ http {
proxy_pass http://127.0.0.1:3000/;
}
location /wasm-playground {
alias /home/penpot/penpot/frontend/resources/public/wasm-playground/;
location /playground {
alias /home/penpot/penpot/experiments/;
add_header Cache-Control "no-cache, max-age=0";
autoindex on;
}

View File

@@ -58,7 +58,8 @@
:share-id share-id
:object-id (mapv :id objects)
:route "objects"
:skip-children skip-children}
:skip-children skip-children
:wasm "true"}
uri (-> (cf/get :public-uri)
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]

View File

@@ -33,7 +33,9 @@
:page-id page-id
:share-id share-id
:object-id object-id
:route "objects"}]
:route "objects"
;;:wasm "true"
}]
(-> base-uri
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))))

View File

@@ -667,9 +667,6 @@
}
// UI ELEMENTS
// FIXME: This is used multiple times accross the app. We should design this in
// the DS and create a proper component for it.
.asset-element {
@include bodySmallTypography;
display: flex;

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill,

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, draw_star,

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, allocBytes,

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill

View File

@@ -23,7 +23,7 @@
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import initWasmModule from '/js/render_wasm.js';
import {
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
} from './js/lib.js';
@@ -102,4 +102,4 @@
});
</script>
</body>
</html>
</html>

View File

@@ -236,7 +236,7 @@
Uses `font-size-value` to calculate the relative line-height value.
Returns an error for an invalid font-size value."
[line-height-value font-size-value font-size-errors]
(let [missing-references (seq (cto/find-token-value-references line-height-value))
(let [missing-references (seq (some cto/find-token-value-references line-height-value))
error
(cond
missing-references

View File

@@ -1122,7 +1122,7 @@
ref-id (:stroke-color-ref-id stroke)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
@@ -1167,7 +1167,7 @@
ref-file (get color :ref-file)
ref-id (get color :ref-id)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
@@ -1180,20 +1180,19 @@
:index (:index shadow)}))
(defn- text->color-att
[fill file-id libraries & {:keys [has-token-applied token-name]}]
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
base-attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))
attrs (cond-> base-attrs
has-token-applied (assoc :has-token-applied true)
token-name (assoc :token-name token-name))]
attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :content
:shape-id (:shape-id fill)
@@ -1201,18 +1200,13 @@
(defn- extract-text-colors
[text file-id libraries]
(let [applied-fill-token (get-in text [:applied-tokens :fill])
treat-node
(let [treat-node
(fn [node shape-id]
(map-indexed (fn [idx fill]
(let [args (cond-> []
(and (= idx 0) applied-fill-token)
(conj :has-token-applied true :token-name applied-fill-token))]
(apply text->color-att (assoc fill :shape-id shape-id :index idx) file-id libraries args)))
node))]
(map-indexed #(assoc %2 :shape-id shape-id :index %1) node))]
(->> (txt/node-seq txt/is-text-node? (:content text))
(map :fills)
(mapcat #(treat-node % (:id text))))))
(mapcat #(treat-node % (:id text)))
(map #(text->color-att % file-id libraries)))))
(defn- fill->color-att
"Given a fill map enriched with :shape-id, :index, and optionally
@@ -1238,7 +1232,7 @@
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)

View File

@@ -45,7 +45,9 @@
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.text :as text]
[app.main.ui.shapes.text.fontfaces :as ff]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
[app.util.globals :as g]
[app.util.http :as http]
[app.util.strings :as ust]
[app.util.thumbnails :as th]
@@ -53,6 +55,7 @@
[beicon.v2.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]))
(def ^:const viewbox-decimal-precision 3)
@@ -171,6 +174,8 @@
;; Don't wrap svg elements inside a <g> otherwise some can break
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
(set! wasm.api/shape-wrapper-factory shape-wrapper-factory)
(defn format-viewbox
"Format a viewbox given a rectangle"
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
@@ -480,6 +485,48 @@
[:& ff/fontfaces-style {:fonts fonts}]
[:& shape-wrapper {:shape object}]]]]))
(mf/defc object-wasm
{::mf/wrap [mf/memo]}
[{:keys [objects object-id embed skip-children]
:or {embed false}
:as props}]
(let [object (get objects object-id)
object (cond-> object
(:hide-fill-on-export object)
(assoc :fills [])
skip-children
(assoc :shapes []))
{:keys [width height] :as bounds}
(gsb/get-object-bounds objects object {:ignore-margin? false})
vbox (format-viewbox bounds)
zoom 1
canvas-ref (mf/use-ref nil)]
(mf/use-effect
(fn []
(let [canvas (mf/ref-val canvas-ref)]
(->> @wasm.api/module
(p/fmap
(fn [ready?]
(when ready?
(try
(when (wasm.api/init-canvas-context canvas)
(wasm.api/initialize-viewport
objects zoom vbox "transparent"
(fn []
(wasm.api/render-sync-shape object-id)
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id)))))
(catch :default e
(js/console.error "Error initializing canvas context:" e)
false)))))))))
[:canvas {:ref canvas-ref
:width width
:height height
:style {:background "red"}}]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SPRITES (DEBUG)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -4,29 +4,22 @@
//
// Copyright (c) KALEIDOS INC
// FIXME: we need this import for .asset-element
@use "refactor/basic-rules.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/spacing.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.editable-select {
@extend .asset-element;
margin: 0;
padding: 0;
border: $b-1 solid var(--input-border-color);
border: deprecated.$s-1 solid var(--input-border-color);
position: relative;
display: flex;
height: $sz-32;
height: deprecated.$s-32;
width: 100%;
padding: var(--sp-s);
border-radius: $br-8;
padding: deprecated.$s-8;
border-radius: deprecated.$br-8;
cursor: pointer;
.dropdown-button {
display: flex;
place-content: center;
@include deprecated.flexCenter;
svg {
@extend .button-icon-small;
transform: rotate(90deg);
@@ -36,11 +29,10 @@
.custom-select-dropdown {
@extend .dropdown-wrapper;
width: fit-content;
max-height: px2rem(320); // TODO: when this gets addressed in the DS, use a token
max-height: deprecated.$s-320;
.separator {
margin: 0;
height: $sz-12;
height: deprecated.$s-12;
}
.dropdown-element {
@extend .dropdown-element-base;
@@ -51,8 +43,7 @@
}
.check-icon {
display: flex;
place-content: center;
@include deprecated.flexCenter;
svg {
@extend .button-icon-small;
visibility: hidden;

View File

@@ -10,7 +10,6 @@ $z-index-200: 200;
$z-index-300: 300;
$z-index-400: 400;
$z-index-500: 500;
$z-index-600: 600;
:global(:root) {
--z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ...
@@ -19,5 +18,4 @@ $z-index-600: 600;
--z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements
--z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns
--z-index-notifications: #{$z-index-500}; // Index for notification
--z-index-loaders: #{$z-index-600}; // Index for loaders
}

View File

@@ -217,10 +217,6 @@
design-tokens? (features/use-feature "design-tokens/v1")
wasm-renderer-enabled? (features/use-feature "render-wasm/v1")
first-frame-rendered? (mf/use-state false)
background-color (:background-color wglobal)]
(mf/with-effect []
@@ -245,17 +241,6 @@
(when (and file-loaded? (not page-id))
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
(mf/with-effect [file-id page-id]
(reset! first-frame-rendered? false))
(mf/with-effect []
(let [handle-wasm-render
(fn [_]
(reset! first-frame-rendered? true))
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
(fn []
(events/unlistenByKey listener-key))))
[:> (mf/provider ctx/current-project-id) {:value project-id}
[:> (mf/provider ctx/current-file-id) {:value file-id}
[:> (mf/provider ctx/current-page-id) {:value page-id}
@@ -264,18 +249,15 @@
[:> modal-container*]
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"
:position "relative"}}
:touch-action "none"}}
[:> context-menu*]
(when (and file-loaded? page-id)
(if (and file-loaded? page-id)
[:> workspace-inner*
{:page-id page-id
:file-id file-id
:file file
:wglobal wglobal
:layout layout}])
(when (or (not (and file-loaded? page-id))
(and wasm-renderer-enabled? (not @first-frame-rendered?)))
:layout layout}]
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*

View File

@@ -20,13 +20,7 @@
}
.workspace-loader {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: var(--z-index-loaders);
background-color: var(--color-background-primary);
grid-area: viewport;
}
.workspace-content {

View File

@@ -54,7 +54,7 @@
modifiers (dm/get-in modifiers [shape-id :modifiers])
shape (gsh/transform-shape shape modifiers)
props (mf/spread-props props {:shape shape :file-id file-id :page-id page-id :libraries libraries})]
props (mf/spread-props props {:shape shape :file-id file-id :page-id page-id})]
(case shape-type
:frame [:> frame/options* props]

View File

@@ -349,7 +349,6 @@
(let [form (mf/use-ctx fc/context)
input-name name
error
(get-in @form [:errors :value value-subfield index input-name])

View File

@@ -35,8 +35,8 @@
on-change
(mf/use-fn
(mf/deps input-name)
(fn [id]
(let [is-inner? (= id "inner")]
(fn [type]
(let [is-inner? (= type "inner")]
(swap! form assoc-in [:data :value indexed-type index input-name] is-inner?))))
props (mf/spread-props props {:default-selected (if value "inner" "drop")

View File

@@ -76,7 +76,7 @@
[token index prop value-subfield]
(let [value (get-in token [:value value-subfield index prop])]
(d/without-nils
{:type (if (= prop :color) :color :dimensions)
{:type (if (= prop :color) :color :number)
:value value})))
(mf/defc shadow-formset*
@@ -114,7 +114,7 @@
:token inset-token
:tokens tokens
:index index
:indexed-type value-subfield
:value-subfield value-subfield
:name :inset}]
(when show-button
[:> icon-button* {:variant "ghost"
@@ -269,25 +269,13 @@
[:value
[:map
[:shadow {:optional true}
[:shadow {:optinal true}
[:vector
[:map
[:offset-x {:optional true} [:maybe :string]]
[:offset-y {:optional true} [:maybe :string]]
[:blur {:optional true}
[:and
[:maybe :string]
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-blur-value-error")}
(fn [blur]
(let [n (d/parse-double blur)]
(or (nil? n) (not (< n 0)))))]]]
[:spread {:optional true}
[:and
[:maybe :string]
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")}
(fn [spread]
(let [n (d/parse-double spread)]
(or (nil? n) (not (< n 0)))))]]]
[:blur {:optional true} [:maybe :string]]
[:spread {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe :string]]
[:color-result {:optional true} ::sm/any]
[:inset {:optional true} [:maybe :boolean]]]]]

View File

@@ -63,7 +63,7 @@
(mf/defc object-svg
{::mf/wrap-props false}
[{:keys [object-id embed skip-children]}]
[{:keys [object-id embed skip-children wasm]}]
(let [objects (mf/deref ref:objects)]
;; Set the globa CSS to assign the page size, needed for PDF
@@ -77,27 +77,42 @@
(mth/ceil height) "px")}))))
(when objects
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]])))
(if wasm
[:& render/object-wasm
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]
(mf/defc objects-svg
{::mf/wrap-props false}
[{:keys [object-ids embed skip-children]}]
(when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]]))))
(mf/defc objects-svg
{::mf/wrap-props false}
[{:keys [object-ids embed skip-children wasm]}]
(when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)]
(if wasm
[:& render/object-wasm
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]])))))
(defn- fetch-objects-bundle
[& {:keys [file-id page-id share-id object-id] :as options}]
(ptk/reify ::fetch-objects-bundle
@@ -136,7 +151,7 @@
(defn- render-objects
[params]
(try
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
(coerce-render-objects-params params)]
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
(if (uuid? object-id)
@@ -147,7 +162,8 @@
:share-id share-id
:object-id object-id
:embed embed
:skip-children skip-children}])
:skip-children skip-children
:wasm wasm}])
(mf/html
[:& objects-svg
@@ -156,7 +172,8 @@
:share-id share-id
:object-ids (into #{} object-id)
:embed embed
:skip-children skip-children}])))
:skip-children skip-children
:wasm wasm}])))
(catch :default cause
(when-let [explain (-> cause ex-data ::sm/explain)]
(js/console.log "Unexpected error")
@@ -307,6 +324,3 @@
(defn ^:dev/after-load after-load
[]
(reinit))

View File

@@ -23,7 +23,6 @@
[app.config :as cf]
[app.main.data.render-wasm :as drw]
[app.main.refs :as refs]
[app.main.render :as render]
[app.main.store :as st]
[app.main.ui.shapes.text]
[app.main.worker :as mw]
@@ -74,6 +73,9 @@
(def noop-fn
(constantly nil))
;;
(def shape-wrapper-factory nil)
;; Based on app.main.render/object-svg
(mf/defc object-svg
{::mf/props :obj}
@@ -81,7 +83,7 @@
(let [objects (mf/deref refs/workspace-page-objects)
shape-wrapper
(mf/with-memo [shape]
(render/shape-wrapper-factory objects))]
(shape-wrapper-factory objects))]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
@@ -915,84 +917,85 @@
(defn set-object
[shape]
(perf/begin-measure "set-object")
(let [shape (svg-filters/apply-svg-derived shape)
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
(when shape
(let [shape (svg-filters/apply-svg-derived shape)
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
parent-id (get shape :parent-id)
masked (get shape :masked-group)
selrect (get shape :selrect)
constraint-h (get shape :constraints-h)
constraint-v (get shape :constraints-v)
clip-content (if (= type :frame)
(not (get shape :show-content))
false)
rotation (get shape :rotation)
transform (get shape :transform)
parent-id (get shape :parent-id)
masked (get shape :masked-group)
selrect (get shape :selrect)
constraint-h (get shape :constraints-h)
constraint-v (get shape :constraints-v)
clip-content (if (= type :frame)
(not (get shape :show-content))
false)
rotation (get shape :rotation)
transform (get shape :transform)
fills (get shape :fills)
strokes (if (= type :group)
[] (get shape :strokes))
children (get shape :shapes)
blend-mode (get shape :blend-mode)
opacity (get shape :opacity)
hidden (get shape :hidden)
content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
content))
bool-type (get shape :bool-type)
grow-type (get shape :grow-type)
blur (get shape :blur)
svg-attrs (get shape :svg-attrs)
shadows (get shape :shadow)
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
fills (get shape :fills)
strokes (if (= type :group)
[] (get shape :strokes))
children (get shape :shapes)
blend-mode (get shape :blend-mode)
opacity (get shape :opacity)
hidden (get shape :hidden)
content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
content))
bool-type (get shape :bool-type)
grow-type (get shape :grow-type)
blur (get shape :blur)
svg-attrs (get shape :svg-attrs)
shadows (get shape :shadow)
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
(use-shape id)
(set-parent-id parent-id)
(set-shape-type type)
(set-shape-clip-content clip-content)
(set-shape-constraints constraint-h constraint-v)
(use-shape id)
(set-parent-id parent-id)
(set-shape-type type)
(set-shape-clip-content clip-content)
(set-shape-constraints constraint-h constraint-v)
(set-shape-rotation rotation)
(set-shape-transform transform)
(set-shape-blend-mode blend-mode)
(set-shape-opacity opacity)
(set-shape-hidden hidden)
(set-shape-children children)
(set-shape-corners corners)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
(or (= type :path)
(= type :bool)))
(set-shape-path-content content))
(when (some? svg-attrs)
(set-shape-svg-attrs svg-attrs))
(when (and (some? content) (= type :svg-raw))
(set-shape-svg-raw-content (get-static-markup shape)))
(set-shape-shadows shadows)
(when (= type :text)
(set-shape-grow-type grow-type))
(set-shape-rotation rotation)
(set-shape-transform transform)
(set-shape-blend-mode blend-mode)
(set-shape-opacity opacity)
(set-shape-hidden hidden)
(set-shape-children children)
(set-shape-corners corners)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
(or (= type :path)
(= type :bool)))
(set-shape-path-content content))
(when (some? svg-attrs)
(set-shape-svg-attrs svg-attrs))
(when (and (some? content) (= type :svg-raw))
(set-shape-svg-raw-content (get-static-markup shape)))
(set-shape-shadows shadows)
(when (= type :text)
(set-shape-grow-type grow-type))
(set-shape-layout shape)
(set-shape-selrect selrect)
(set-shape-layout shape)
(set-shape-selrect selrect)
(let [pending_thumbnails (into [] (concat
(set-shape-text-content id content)
(set-shape-text-images id content true)
(set-shape-fills id fills true)
(set-shape-strokes id strokes true)))
pending_full (into [] (concat
(set-shape-text-images id content false)
(set-shape-fills id fills false)
(set-shape-strokes id strokes false)))]
(perf/end-measure "set-object")
{:thumbnails pending_thumbnails
:full pending_full})))
(let [pending_thumbnails (into [] (concat
(set-shape-text-content id content)
(set-shape-text-images id content true)
(set-shape-fills id fills true)
(set-shape-strokes id strokes true)))
pending_full (into [] (concat
(set-shape-text-images id content false)
(set-shape-fills id fills false)
(set-shape-strokes id strokes false)))]
(perf/end-measure "set-object")
{:thumbnails pending_thumbnails
:full pending_full}))))
(defn update-text-layouts
[shapes]
@@ -1055,8 +1058,9 @@
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full noop-fn
(fn []
(when render-callback (render-callback))
(render-finish)
(if render-callback
(render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode

View File

@@ -199,7 +199,7 @@
:string-or-size-array (format-string-or-size-array value)
:keyword (format-keyword value)
:tracks (format-tracks value)
:shadows (format-shadow value options)
:shadow (format-shadow value options)
:blur (format-blur value)
:matrix (format-matrix value)
(if (keyword? value) (d/name value) value))))

View File

@@ -47,18 +47,17 @@
[acc {:keys [schema in value type] :as problem}]
(let [props (m/properties schema)
tprops (m/type-properties schema)
field (or (:error/field props)
in)
field (or (first in)
(:error/field props))
field (if (vector? field)
field
[field])]
(if (and (= 1 (count field))
(contains? acc (first field)))
(if (contains? acc field)
acc
(cond
(or (nil? field)
(empty? field))
(nil? field)
acc
(or (= type :malli.core/missing-key)

View File

@@ -242,6 +242,7 @@ export class SelectionController extends EventTarget {
continue;
}
let styleValue = element.style.getPropertyValue(styleName);
if (styleName === "font-family") {
styleValue = sanitizeFontFamily(styleValue);
}
@@ -276,29 +277,22 @@ export class SelectionController extends EventTarget {
this.#applyDefaultStylesToCurrentStyle();
const root = startNode.parentElement.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(root);
if (startNode === endNode) {
const paragraph = startNode.parentElement.parentElement;
// FIXME: I don't like this approximation. Having to iterate nodes twice
// is bad for performance. I think we need another way of "computing"
// the cascade.
for (const textNode of this.#textNodeIterator.iterateFrom(
startNode,
endNode,
)) {
const paragraph = textNode.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(paragraph);
const textSpan = startNode.parentElement;
this.#applyStylesFromElementToCurrentStyle(textSpan);
} else {
// FIXME: I don't like this approximation. Having to iterate nodes twice
// is bad for performance. I think we need another way of "computing"
// the cascade.
for (const textNode of this.#textNodeIterator.iterateFrom(
startNode,
endNode,
)) {
const paragraph = textNode.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(paragraph);
}
for (const textNode of this.#textNodeIterator.iterateFrom(
startNode,
endNode,
)) {
const textSpan = textNode.parentElement;
this.#mergeStylesFromElementToCurrentStyle(textSpan);
}
}
for (const textNode of this.#textNodeIterator.iterateFrom(
startNode,
endNode,
)) {
const textSpan = textNode.parentElement;
this.#mergeStylesFromElementToCurrentStyle(textSpan);
}
return this;
}

View File

@@ -8032,14 +8032,6 @@ msgstr "Name"
msgid "workspace.tokens.token-name-duplication-validation-error"
msgstr "A token already exists at the path: %s"
#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
msgid "workspace.tokens.shadow-token-blur-value-error"
msgstr "Blur value cannot be negative"
#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
msgid "workspace.tokens.shadow-token-spread-value-error"
msgstr "Spread value cannot be negative"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:42, src/app/main/ui/workspace/tokens/management/create/form.cljs:68
msgid "workspace.tokens.token-name-length-validation-error"
msgstr "Name should be at least 1 character"

View File

@@ -7908,14 +7908,6 @@ msgstr "Nombre"
msgid "workspace.tokens.token-name-duplication-validation-error"
msgstr "Ya existe un token en la ruta: %s"
#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
msgid "workspace.tokens.shadow-token-blur-value-error"
msgstr "El valor de blur no puede ser negativo"
#: src/app/main/ui/workspace/tokens/management/forms/shadow.cljs
msgid "workspace.tokens.shadow-token-spread-value-error"
msgstr "El valor de spread no puede ser negativo"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:42, src/app/main/ui/workspace/tokens/management/create/form.cljs:68
msgid "workspace.tokens.token-name-length-validation-error"
msgstr "El nombre debería ser de al menos 1 caracter"

View File

@@ -10,6 +10,7 @@ mod shadows;
mod strokes;
mod surfaces;
pub mod text;
mod ui;
use skia_safe::{self as skia, Matrix, RRect, Rect};
@@ -52,25 +53,6 @@ pub struct NodeRenderState {
mask: bool,
}
/// Get simplified children of a container, flattening nested flattened containers
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec<Uuid> {
let mut result = Vec::new();
for child_id in shape.children_ids_iter(false) {
if let Some(child) = tree.get(child_id) {
if child.can_flatten() {
// Child is flattened: recursively get its simplified children
result.extend(get_simplified_children(tree, child));
} else {
// Child is not flattened: add it directly
result.push(*child_id);
}
}
}
result
}
impl NodeRenderState {
pub fn is_root(&self) -> bool {
self.id.is_nil()
@@ -294,10 +276,6 @@ pub(crate) struct RenderState {
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
/// `with_nested_blurs_suppressed` to ensure it's always restored.
pub ignore_nested_blurs: bool,
/// Cached root_ids and root_ids_map to avoid recalculating them every frame
/// These are invalidated when the tree structure changes
cached_root_ids: Option<Vec<Uuid>>,
cached_root_ids_map: Option<std::collections::HashMap<Uuid, usize>>,
}
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
@@ -370,8 +348,6 @@ impl RenderState {
focus_mode: FocusMode::new(),
touched_ids: HashSet::default(),
ignore_nested_blurs: false,
cached_root_ids: None,
cached_root_ids_map: None,
}
}
@@ -422,7 +398,12 @@ impl RenderState {
}
fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
shape.frame_clip_layer_blur()
match shape.shape_type {
Type::Frame(_) if shape.clip() => shape.blur.filter(|blur| {
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.
}),
_ => None,
}
}
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
@@ -540,59 +521,38 @@ impl RenderState {
let paint = skia::Paint::default();
// Only draw surfaces that have content (dirty flag optimization)
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
self.surfaces
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
}
self.surfaces
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
if self.surfaces.is_dirty(SurfaceId::Fills) {
self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
}
self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
let mut render_overlay_below_strokes = false;
if let Some(shape) = shape {
render_overlay_below_strokes = shape.has_fills();
}
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
if render_overlay_below_strokes {
self.surfaces
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::Strokes) {
self.surfaces
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
}
self.surfaces
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
if !render_overlay_below_strokes {
self.surfaces
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
// Build mask of dirty surfaces that need clearing
let mut dirty_surfaces_to_clear = 0u32;
if self.surfaces.is_dirty(SurfaceId::Strokes) {
dirty_surfaces_to_clear |= SurfaceId::Strokes as u32;
}
if self.surfaces.is_dirty(SurfaceId::Fills) {
dirty_surfaces_to_clear |= SurfaceId::Fills as u32;
}
if self.surfaces.is_dirty(SurfaceId::InnerShadows) {
dirty_surfaces_to_clear |= SurfaceId::InnerShadows as u32;
}
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32;
}
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
if dirty_surfaces_to_clear != 0 {
self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
});
// Clear dirty flags for surfaces we just cleared
self.surfaces.clear_dirty(dirty_surfaces_to_clear);
}
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
});
}
pub fn clear_focus_mode(&mut self) {
@@ -645,17 +605,9 @@ impl RenderState {
| strokes_surface_id as u32
| innershadows_surface_id as u32
| text_drop_shadows_surface_id as u32;
// Only save canvas state if we have clipping or transforms
// For simple shapes without clipping, skip expensive save/restore
let needs_save =
clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
if needs_save {
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save();
});
}
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save();
});
let antialias = shape.should_use_antialias(self.get_scale());
@@ -731,18 +683,16 @@ impl RenderState {
matrix.pre_concat(&svg_transform);
}
self.surfaces
.canvas_and_mark_dirty(fills_surface_id)
.concat(&matrix);
self.surfaces.canvas(fills_surface_id).concat(&matrix);
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
svg.render(self.surfaces.canvas(fills_surface_id))
} else {
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager);
match dom_result {
Ok(dom) => {
dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id));
dom.render(self.surfaces.canvas(fills_surface_id));
shape.to_mut().set_svg(dom);
}
Err(e) => {
@@ -958,13 +908,9 @@ impl RenderState {
if apply_to_current_surface {
self.apply_drawing_to_render_canvas(Some(&shape));
}
// Only restore if we saved (optimization for simple shapes)
if needs_save {
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().restore();
});
}
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().restore();
});
}
pub fn update_render_context(&mut self, tile: tiles::Tile) {
@@ -1085,7 +1031,6 @@ impl RenderState {
// reorder by distance to the center.
self.current_tile = None;
self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None);
if sync_render {
@@ -1172,6 +1117,18 @@ impl RenderState {
self.nested_fills.push(Vec::new());
}
let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
let scale = self.get_scale();
let sigma = frame_blur.value * scale;
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
paint.set_image_filter(filter);
}
}
// When we're rendering the mask shape we need to set a special blend mode
// called 'destination-in' that keeps the drawn content within the mask.
// @see https://skia.org/docs/user/api/skblendmode_overview/
@@ -1184,29 +1141,10 @@ impl RenderState {
.save_layer(&mask_rec);
}
// Only create save_layer if actually needed
// For simple shapes with default opacity and blend mode, skip expensive save_layer
// Groups with masks need a layer to properly handle the mask rendering
let needs_layer = element.needs_layer();
if needs_layer {
let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
let scale = self.get_scale();
let sigma = frame_blur.value * scale;
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
paint.set_image_filter(filter);
}
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
self.focus_mode.enter(&element.id);
}
@@ -1279,14 +1217,7 @@ impl RenderState {
);
}
// Only restore if we created a layer (optimization for simple shapes)
// Groups with masks need restore to properly handle the mask rendering
let needs_layer = element.needs_layer();
if needs_layer {
self.surfaces.canvas(SurfaceId::Current).restore();
}
self.surfaces.canvas(SurfaceId::Current).restore();
self.focus_mode.exit(&element.id);
}
@@ -1526,48 +1457,18 @@ impl RenderState {
}
if visited_children {
// Skip render_shape_exit for flattened containers
if !element.can_flatten() {
self.render_shape_exit(element, visited_mask);
}
self.render_shape_exit(element, visited_mask);
continue;
}
if !node_render_state.is_root() {
let transformed_element: Cow<Shape> = Cow::Borrowed(element);
// Aggressive early exit: check hidden first (fastest check)
if transformed_element.hidden {
continue;
}
// For frames and groups, we must use extrect because they can have nested content
// that extends beyond their selrect. Using selrect for early exit would incorrectly
// skip frames/groups that have nested content in the current tile.
let is_container = matches!(
transformed_element.shape_type,
crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_)
);
let scale = self.get_scale();
let has_effects = transformed_element.has_effects_that_extend_bounds();
let extrect = transformed_element.extrect(tree, scale);
let is_visible = if is_container {
// Containers (frames/groups) must always use extrect to include nested content
let extrect = transformed_element.extrect(tree, scale);
extrect.intersects(self.render_area)
&& !transformed_element.visually_insignificant(scale, tree)
} else if !has_effects {
// Simple shape: selrect check is sufficient, skip expensive extrect
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
&& !transformed_element.visually_insignificant(scale, tree)
} else {
// Shape with effects: need extrect for accurate bounds
let extrect = transformed_element.extrect(tree, scale);
extrect.intersects(self.render_area)
&& !transformed_element.visually_insignificant(scale, tree)
};
let is_visible = extrect.intersects(self.render_area)
&& !transformed_element.hidden
&& !transformed_element.visually_insignificant(scale, tree);
if self.options.is_debug_visible() {
let shape_extrect_bounds =
@@ -1580,12 +1481,7 @@ impl RenderState {
}
}
// Skip render_shape_enter/exit for flattened containers
// If a container was flattened, it doesn't affect children visually, so we skip
// the expensive enter/exit operations and process children directly
if !element.can_flatten() {
self.render_shape_enter(element, mask);
}
self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() {
let scale: f32 = self.get_scale();
@@ -1619,8 +1515,6 @@ impl RenderState {
_ => None,
};
let element_extrect = element.extrect(tree, scale);
for shadow in element.drop_shadows_visible() {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
@@ -1632,7 +1526,7 @@ impl RenderState {
// First pass: Render shadow in black to establish alpha mask
self.render_drop_black_shadow(
element,
&element_extrect,
&element.extrect(tree, scale),
shadow,
clip_bounds.clone(),
scale,
@@ -1652,10 +1546,9 @@ impl RenderState {
.get_nested_shadow_clip_bounds(element, shadow);
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
let shadow_extrect = shadow_shape.extrect(tree, scale);
self.render_drop_black_shadow(
shadow_shape,
&shadow_extrect,
&shadow_shape.extrect(tree, scale),
shadow,
clip_bounds,
scale,
@@ -1789,18 +1682,14 @@ impl RenderState {
self.apply_drawing_to_render_canvas(Some(element));
}
// Skip nested state updates for flattened containers
// Flattened containers don't affect children, so we don't need to track their state
if !element.can_flatten() {
match element.shape_type {
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
self.nested_blurs.push(None);
}
Type::Frame(_) | Type::Group(_) => {
self.nested_blurs.push(element.blur);
}
_ => {}
match element.shape_type {
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
self.nested_blurs.push(None);
}
Type::Frame(_) | Type::Group(_) => {
self.nested_blurs.push(element.blur);
}
_ => {}
}
// Set the node as visited_children before processing children
@@ -1815,35 +1704,24 @@ impl RenderState {
if element.is_recursive() {
let children_clip_bounds =
node_render_state.get_children_clip_bounds(element, None);
let children_ids: Vec<_> = if element.can_flatten() {
// Container was flattened: get simplified children (which skip this level)
get_simplified_children(tree, element)
} else {
// Container not flattened: use original children
element.children_ids_iter(false).copied().collect()
};
let mut children_ids: Vec<_> = element.children_ids_iter(false).collect();
// Z-index ordering on Layouts
let children_ids = if element.has_layout() {
let mut ids = children_ids;
if element.has_layout() {
if element.is_flex() && !element.is_flex_reverse() {
ids.reverse();
children_ids.reverse();
}
ids.sort_by(|id1, id2| {
children_ids.sort_by(|id1, id2| {
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
z2.cmp(&z1)
});
ids
} else {
children_ids
};
}
for child_id in children_ids.iter() {
self.pending_nodes.push(NodeRenderState {
id: *child_id,
id: **child_id,
visited_children: false,
clip_bounds: children_clip_bounds.clone(),
visited_mask: false,
@@ -1925,39 +1803,14 @@ impl RenderState {
.canvas(SurfaceId::Current)
.clear(self.background_color);
// Get or compute root_ids and root_ids_map (cached to avoid recalculation every frame)
let root_ids_map = {
let root_ids = if let Some(shape_id) = base_object {
let root_ids = {
if let Some(shape_id) = base_object {
vec![*shape_id]
} else {
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
};
root.children_ids(false)
};
// Check if cache is valid (same root_ids)
let cache_valid = self
.cached_root_ids
.as_ref()
.map(|cached| cached.as_slice() == root_ids.as_slice())
.unwrap_or(false);
if cache_valid {
// Use cached map
self.cached_root_ids_map.as_ref().unwrap().clone()
} else {
// Recompute and cache
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
self.cached_root_ids = Some(root_ids.clone());
self.cached_root_ids_map = Some(root_ids_map.clone());
root_ids_map
}
};
@@ -1968,6 +1821,12 @@ impl RenderState {
if !self.surfaces.has_cached_tile_surface(next_tile) {
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
// We only need first level shapes
let mut valid_ids: Vec<Uuid> = ids
.iter()

View File

@@ -18,7 +18,7 @@ fn draw_image_fill(
}
let size = image.unwrap().dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let canvas = render_state.surfaces.canvas(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();

View File

@@ -41,7 +41,7 @@ where
F: FnOnce(&mut RenderState, SurfaceId),
{
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
let canvas = render_state.surfaces.canvas(target_surface);
// If we scaled down, we need to scale the source rect and adjust the destination
if scale < 1.0 {

View File

@@ -135,7 +135,7 @@ pub fn render_text_shadows(
let canvas = render_state
.surfaces
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::TextDropShadows));
.canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows));
for shadow in shadows {
let shadow_layer = SaveLayerRec::default().paint(shadow);

View File

@@ -387,7 +387,7 @@ fn draw_image_stroke_in_container(
}
let size = image.unwrap().dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let canvas = render_state.surfaces.canvas(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
let svg_attrs = shape.svg_attrs.as_ref();
@@ -606,7 +606,7 @@ fn render_internal(
let scale = render_state.get_scale();
let target_surface = surface_id.unwrap_or(SurfaceId::Strokes);
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
let canvas = render_state.surfaces.canvas(target_surface);
let selrect = shape.selrect;
let path_transform = shape.to_path_transform();
let svg_attrs = shape.svg_attrs.as_ref();
@@ -688,7 +688,7 @@ pub fn render_text_paths(
let scale = render_state.get_scale();
let canvas = render_state
.surfaces
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
let selrect = &shape.selrect;
let svg_attrs = shape.svg_attrs.as_ref();
let mut paint: skia_safe::Handle<_> =

View File

@@ -55,8 +55,6 @@ pub struct Surfaces {
tiles: TileTextureCache,
sampling_options: skia::SamplingOptions,
margins: skia::ISize,
// Tracks which surfaces have content (dirty flag bitmask)
dirty_surfaces: u32,
}
#[allow(dead_code)]
@@ -107,7 +105,6 @@ impl Surfaces {
tiles,
sampling_options,
margins,
dirty_surfaces: 0,
}
}
@@ -150,51 +147,10 @@ impl Surfaces {
None
}
/// Returns a mutable reference to the canvas and automatically marks
/// render surfaces as dirty when accessed. This tracks which surfaces
/// have content for optimization purposes.
pub fn canvas_and_mark_dirty(&mut self, id: SurfaceId) -> &skia::Canvas {
// Automatically mark render surfaces as dirty when accessed
// This tracks which surfaces have content for optimization
match id {
SurfaceId::Fills
| SurfaceId::Strokes
| SurfaceId::InnerShadows
| SurfaceId::TextDropShadows => {
self.mark_dirty(id);
}
_ => {}
}
self.canvas(id)
}
/// Returns a mutable reference to the canvas without any side effects.
/// Use this when you only need to read or manipulate the canvas state
/// without marking the surface as dirty.
pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas {
self.get_mut(id).canvas()
}
/// Marks a surface as having content (dirty)
pub fn mark_dirty(&mut self, id: SurfaceId) {
self.dirty_surfaces |= id as u32;
}
/// Checks if a surface has content
pub fn is_dirty(&self, id: SurfaceId) -> bool {
(self.dirty_surfaces & id as u32) != 0
}
/// Clears the dirty flag for a surface or set of surfaces
pub fn clear_dirty(&mut self, ids: u32) {
self.dirty_surfaces &= !ids;
}
/// Clears all dirty flags
pub fn clear_all_dirty(&mut self) {
self.dirty_surfaces = 0;
}
pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) {
let surface = self.get_mut(id);
gpu_state.context.flush_and_submit_surface(surface, None);
@@ -203,12 +159,9 @@ impl Surfaces {
pub fn draw_into(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) {
let sampling_options = self.sampling_options;
self.get_mut(from).clone().draw(
self.canvas_and_mark_dirty(to),
(0.0, 0.0),
sampling_options,
paint,
);
self.get_mut(from)
.clone()
.draw(self.canvas(to), (0.0, 0.0), sampling_options, paint);
}
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
@@ -259,33 +212,18 @@ impl Surfaces {
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
let translation = self.get_render_context_translation(render_area, scale);
// When context changes (zoom/pan/tile), clear all render surfaces first
// to remove any residual content from previous tiles, then mark as dirty
// so they get redrawn with new transformations
let surface_ids = SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
// Clear surfaces before updating transformations to remove residual content
self.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
});
// Mark all render surfaces as dirty so they get redrawn
self.mark_dirty(SurfaceId::Fills);
self.mark_dirty(SurfaceId::Strokes);
self.mark_dirty(SurfaceId::InnerShadows);
self.mark_dirty(SurfaceId::TextDropShadows);
// Update transformations
self.apply_mut(surface_ids, |s| {
let canvas = s.canvas();
canvas.reset_matrix();
canvas.scale((scale, scale));
canvas.translate(translation);
});
self.apply_mut(
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32,
|s| {
let canvas = s.canvas();
canvas.reset_matrix();
canvas.scale((scale, scale));
canvas.translate(translation);
},
);
}
#[inline]
@@ -326,21 +264,19 @@ impl Surfaces {
pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
if let Some(corners) = shape.shape_type.corners() {
let rrect = RRect::new_rect_radii(shape.selrect, &corners);
self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint);
self.canvas(id).draw_rrect(rrect, paint);
} else {
self.canvas_and_mark_dirty(id)
.draw_rect(shape.selrect, paint);
self.canvas(id).draw_rect(shape.selrect, paint);
}
}
pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
self.canvas_and_mark_dirty(id)
.draw_oval(shape.selrect, paint);
self.canvas(id).draw_oval(shape.selrect, paint);
}
pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) {
if let Some(path) = shape.get_skia_path() {
self.canvas_and_mark_dirty(id).draw_path(&path, paint);
self.canvas(id).draw_path(&path, paint);
}
}
@@ -368,9 +304,6 @@ impl Surfaces {
self.canvas(SurfaceId::UI)
.clear(skia::Color::TRANSPARENT)
.reset_matrix();
// Clear all dirty flags after reset
self.clear_all_dirty();
}
pub fn cache_current_tile_texture(

View File

@@ -192,7 +192,7 @@ pub fn render(
}
}
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
let canvas = render_state.surfaces.canvas(target_surface);
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur);
return;
}
@@ -371,7 +371,7 @@ pub fn render_as_path(
) {
let canvas = render_state
.surfaces
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills));
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
for (path, paint) in paths {
// Note: path can be empty
@@ -397,7 +397,7 @@ pub fn render_position_data(
let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height);
render_state
.surfaces
.canvas_and_mark_dirty(surface_id)
.canvas(surface_id)
.draw_rect(rect, &paint);
}
}

View File

@@ -920,13 +920,8 @@ impl Shape {
}
Type::Group(_) | Type::Frame(_) if !self.clip_content => {
// For frames and groups, we must always calculate extrect for all children
// to ensure accurate bounds that include nested content across all tiles.
// Using selrect for children can cause frames to be incorrectly omitted from
// tiles where they have nested content.
for child_id in self.children_ids_iter(false) {
if let Some(child_shape) = shapes_pool.get(child_id) {
// Always calculate full extrect for children to ensure accurate bounds
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
rect.join(child_extrect);
}
@@ -1424,97 +1419,6 @@ impl Shape {
!self.fills.is_empty()
}
/// Determines if this frame or group can be flattened (doesn't affect children visually)
/// A container can be flattened if it has no visual effects that affect its children
/// and doesn't render its own content (no fills/strokes)
pub fn can_flatten(&self) -> bool {
// Only frames and groups can be flattened
if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) {
return false;
}
// Cannot flatten if it has visual effects that affect children:
if self.clip_content {
return false;
}
if !self.transform.is_identity() {
return false;
}
if self.opacity != 1.0 {
return false;
}
if self.blend_mode() != BlendMode::default() {
return false;
}
if self.blur.is_some() {
return false;
}
if !self.shadows.is_empty() {
return false;
}
if let Type::Group(group) = &self.shape_type {
if group.masked {
return false;
}
}
if self.hidden {
return false;
}
// If the container itself has fills/strokes, it renders something visible
// We cannot flatten containers that render their own background/border
// because they need to be rendered even if they don't affect children
if self.has_fills() || self.has_visible_strokes() {
return false;
}
true
}
/// Checks if this shape needs a layer for rendering due to visual effects
/// (opacity < 1.0, non-default blend mode, or frame clip layer blur)
pub fn needs_layer(&self) -> bool {
self.opacity() < 1.0
|| self.blend_mode().0 != skia::BlendMode::SrcOver
|| self.has_frame_clip_layer_blur()
|| (matches!(self.shape_type, Type::Group(g) if g.masked))
}
/// Checks if this frame has clip layer blur (affects children)
/// A frame has clip layer blur if it clips content and has layer blur
pub fn has_frame_clip_layer_blur(&self) -> bool {
self.frame_clip_layer_blur().is_some()
}
/// Returns the frame clip layer blur if this frame has one
/// A frame has clip layer blur if it clips content and has layer blur
pub fn frame_clip_layer_blur(&self) -> Option<Blur> {
use crate::shapes::BlurType;
match self.shape_type {
Type::Frame(_) if self.clip_content => self.blur.filter(|blur| {
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
}),
_ => None,
}
}
/// Checks if this shape has visual effects that might extend its bounds beyond selrect
/// Shapes with these effects require expensive extrect calculation for accurate visibility checks
pub fn has_effects_that_extend_bounds(&self) -> bool {
!self.shadows.is_empty()
|| self.blur.is_some()
|| !self.strokes.is_empty()
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
}
pub fn count_visible_inner_strokes(&self) -> usize {
self.visible_strokes()
.filter(|s| s.kind == StrokeKind::Inner)