mirror of
https://github.com/penpot/penpot.git
synced 2026-01-15 01:40:10 -05:00
Compare commits
1 Commits
staging-re
...
andy-docs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b049af8229 |
@@ -29,7 +29,7 @@
|
||||
- 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)
|
||||
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
|
||||
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
BIN
docs/img/design-tokens/37-tokens-shadow-individual.webp
Normal file
BIN
docs/img/design-tokens/37-tokens-shadow-individual.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/img/design-tokens/38-tokens-shadow-reference.webp
Normal file
BIN
docs/img/design-tokens/38-tokens-shadow-reference.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -455,6 +455,43 @@ ExtraBold Italic
|
||||
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
||||
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
||||
|
||||
<h3 id="design-tokens-shadow">Shadow</h3>
|
||||
<p>Shadow tokens are composite entities that encapsulate the properties of one or more shadows into a single token definition. This token can contain a single shadow or an array of multiple shadows that can be reordered.</p>
|
||||
<p>Shadow tokens support both <strong>Drop Shadow</strong> and <strong>Inner Shadow</strong> types. When creating or editing a shadow token, you can select the type of shadow you want to use. The default selection is Drop Shadow.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/37-tokens-shadow-individual.webp" alt="Shadow token creation with individual values" />
|
||||
</figure>
|
||||
|
||||
<h4 id="design-tokens-shadow-properties">Shadow properties</h4>
|
||||
<p>Each shadow within a shadow token contains a set of properties that define how the shadow appears:</p>
|
||||
<ul>
|
||||
<li><strong>Color:</strong> The color of the shadow. Accepts the same values as <a href="#design-tokens-color">color tokens</a> (Hex, RGB, RGBA, ARGB, HSL, HSLA), and you can reference existing color tokens. The color picker is available when defining the value.</li>
|
||||
<li><strong>X offset:</strong> The horizontal offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||
<li><strong>Y offset:</strong> The vertical offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||
<li><strong>Blur:</strong> The blur radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||
<li><strong>Spread:</strong> The spread radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
||||
<li><strong>Type:</strong> Whether the shadow is a drop shadow or an inner shadow. Selected via a dropdown menu, with Drop Shadow as the default.</li>
|
||||
</ul>
|
||||
<p>Each property within a shadow token can reference existing tokens or be assigned hardcoded values. Shadows can also reference other shadow tokens (the type of shadow must match when using references).</p>
|
||||
<p class="advice">Not all properties are mandatory to save a shadow token. Some can be empty (and will be computed as 0). Only the color property is mandatory. In an array of shadows, if any shadow does not have the color set, the form cannot be saved.</p>
|
||||
|
||||
<h4 id="design-tokens-shadow-create">Creating shadow tokens</h4>
|
||||
<p>To create a shadow token, click on the <strong>+</strong> next to <strong>Shadow</strong> in the Tokens panel. Shadow tokens can be created in two ways:</p>
|
||||
<ul>
|
||||
<li><strong>Individual values:</strong> You can create one shadow or multiple shadows with individual property values. Click the <strong>+</strong> button to add more shadows to the array. New shadows are added at the top of the list.</li>
|
||||
<li><strong>Single reference:</strong> You can reference another existing shadow token. When using a single reference, you cannot add more than one shadow. The resolved value will display the shadow or list of shadows that the referenced token contains.</li>
|
||||
</ul>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/38-tokens-shadow-reference.webp" alt="Shadow token creation with reference" />
|
||||
</figure>
|
||||
<p>When creating a shadow with individual values, the color value starts empty, but the other inputs have default values (X: 4, Y: 4, Blur: 4, Spread: 0). You can reorder shadows by hovering over a shadow form and using the reorder button to drag it to a different position.</p>
|
||||
<p>You can also reference another existing shadow token instead of defining each property manually. When doing so, Penpot resolves all shadow properties from the referenced token.</p>
|
||||
|
||||
<h4 id="design-tokens-shadow-apply">Applying shadow tokens</h4>
|
||||
<p>Shadow tokens can be applied to any layer type. Clicking on a shadow token will apply it to the selected layer. Right-clicking on a shadow token shows the context menu with the <strong>Shadow</strong> option to apply it.</p>
|
||||
<p class="advice">Text elements in CSS do not support inner shadows, but Penpot does, since it uses the filter property internally instead of the box-shadow property.</p>
|
||||
<p>When applying a shadow token, any existing shadow on the layer will be overridden (whether it's a raw shadow or an applied token shadow). If the token contains an array of shadows, each shadow will be added in the same order as in the creation form.</p>
|
||||
<p class="advice">In Penpot, an element can have multiple shadows, but only one token of the same type can be applied. This means that applying a second shadow token would override the first one, regardless of how many shadows the shape currently has.</p>
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -308,16 +308,6 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-click} (tr "labels.retry")]]]))
|
||||
|
||||
(mf/defc webgl-context-lost*
|
||||
[]
|
||||
(let [on-reload (mf/use-fn #(js/location.reload))]
|
||||
[:> error-container* {}
|
||||
[:div {:class (stl/css :main-message)} (tr "labels.webgl-context-lost.main-message")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "labels.webgl-context-lost.desc-message")]
|
||||
[:div {:class (stl/css :buttons-container)}
|
||||
[:> button* {:variant "primary" :on-click on-reload}
|
||||
(tr "labels.reload-page")]]]))
|
||||
|
||||
(defn- generate-report
|
||||
[data]
|
||||
(try
|
||||
@@ -447,7 +437,6 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
@@ -480,9 +469,6 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
@@ -22,6 +21,7 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[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]
|
||||
@@ -1236,8 +1236,7 @@
|
||||
(dom/prevent-default event)
|
||||
(reset! wasm/context-lost? true)
|
||||
(log/warn :hint "WebGL context lost")
|
||||
(ex/raise :type :webgl-context-lost
|
||||
:hint "WebGL context lost"))
|
||||
(st/emit! (drw/context-lost)))
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2521,9 +2521,6 @@ msgstr "Release notes"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Reload file"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Reload page"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
@@ -8478,12 +8475,6 @@ msgstr "Recent"
|
||||
msgid "labels.deleted"
|
||||
msgstr "Deleted"
|
||||
|
||||
msgid "labels.webgl-context-lost.main-message"
|
||||
msgstr "Oops! The canvas context was lost"
|
||||
|
||||
msgid "labels.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL has stopped working. Please reload the page to reset it"
|
||||
|
||||
msgid "dashboard.restore-all-deleted-button"
|
||||
msgstr "Restore All"
|
||||
|
||||
|
||||
@@ -2502,9 +2502,6 @@ msgstr "Notas de versión"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Recargar archivo"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Recargar página"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
@@ -8331,12 +8328,6 @@ msgstr "Recientes"
|
||||
msgid "labels.deleted"
|
||||
msgstr "Eliminados"
|
||||
|
||||
msgid "labels.webgl-context-lost.main-message"
|
||||
msgstr "Ups! Se ha perdido el contexto del canvas"
|
||||
|
||||
msgid "labels.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"
|
||||
|
||||
msgid "dashboard.restore-all-deleted-button"
|
||||
msgstr "Restaurar todo"
|
||||
|
||||
|
||||
@@ -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,40 +1141,16 @@ 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);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn render_shape_exit(
|
||||
&mut self,
|
||||
element: &Shape,
|
||||
visited_mask: bool,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
) {
|
||||
pub fn render_shape_exit(&mut self, element: &Shape, visited_mask: bool) {
|
||||
if visited_mask {
|
||||
// Because masked groups needs two rendering passes (first drawing
|
||||
// the content and then drawing the mask), we need to do an
|
||||
@@ -1273,7 +1206,7 @@ impl RenderState {
|
||||
element_strokes.to_mut().clip_content = false;
|
||||
self.render_shape(
|
||||
&element_strokes,
|
||||
clip_bounds,
|
||||
None,
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
@@ -1284,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);
|
||||
}
|
||||
|
||||
@@ -1531,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, clip_bounds);
|
||||
}
|
||||
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 =
|
||||
@@ -1585,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();
|
||||
@@ -1624,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);
|
||||
@@ -1637,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,
|
||||
@@ -1657,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,
|
||||
@@ -1794,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
|
||||
@@ -1820,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,
|
||||
@@ -1930,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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1973,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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<_> =
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user