Compare commits

...

15 Commits

Author SHA1 Message Date
David Barragán Merino
a4f2641cc9 🔧 Enable observability for plugin docs and packages 2026-02-06 18:01:11 +01:00
Andrey Antukh
989eb12139 🔥 Remove merge conflict from plugins api ns 2026-02-06 11:38:36 +01:00
Eva Marco
a5e36dbb3d 🐛 Fix broken attribute on numeric input (#8250)
* 🐛 Fix broken attribute on numeric input

* 🐛 Fix tooltip position
2026-02-06 11:32:16 +01:00
Alejandro Alonso
8acd031ab2 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-06 11:23:50 +01:00
Elena Torro
a7c1de6478 🐛 Fix lazy load intersection on dragging at the beginning 2026-02-06 10:59:05 +01:00
Elena Torro
184487f568 🐛 Fix lazy load intersection on dragging at the beginning 2026-02-06 10:53:11 +01:00
Andrey Antukh
c00d512193 Add the concept of version to plugins
And make mcp plugin version 2
2026-02-06 09:42:59 +01:00
alonso.torres
af5dbf2fbc 🐛 Set objects modified instead of modif-tree 2026-02-06 09:34:58 +01:00
Alejandro Alonso
7c7e32d85f 🐛 Fix grid lines 2026-02-06 09:34:58 +01:00
Andrey Antukh
2ccb33ba89 📎 Add missing for-update for the migration 145 2026-02-05 18:12:11 +01:00
Andrey Antukh
ee88ee63a2 Add data migration for fix plugins data on profiles 2026-02-05 18:08:28 +01:00
alonso.torres
fd3d549f9c Batch text layout updates 2026-02-05 17:29:43 +01:00
alonso.torres
53c2acb3e6 🐛 Fix several problems with layouts and texts 2026-02-05 17:29:43 +01:00
Belén Albeza
8a72eb64c3 Add integration test for 13267 2026-02-05 16:37:21 +01:00
alonso.torres
1d45ca7019 🐛 Fix problem propagating geometry changes to instances 2026-02-05 16:37:21 +01:00
50 changed files with 958 additions and 313 deletions

View File

@@ -10,6 +10,7 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.migrations.clj.migration-0023 :as mg0023] [app.migrations.clj.migration-0023 :as mg0023]
[app.migrations.clj.migration-0145 :as mg0145]
[app.util.migrations :as mg] [app.util.migrations :as mg]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -459,7 +460,11 @@
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")} :fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
{:name "0144-mod-server-error-report-table" {:name "0144-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}]) :fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile"
:fn mg0145/migrate}])
(defn apply-migrations! (defn apply-migrations!
[pool name migrations] [pool name migrations]

View File

@@ -0,0 +1,83 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.migrations.clj.migration-0145
"Migrate plugins references on profiles"
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[cuerdas.core :as str]))
(def ^:private replacements
{"https://colors-to-tokens-plugin.pages.dev"
"https://colors-to-tokens.plugins.penpot.app"
"https://contrast-penpot-plugin.pages.dev"
"https://contrast.plugins.penpot.app"
"https://create-palette-penpot-plugin.pages.dev"
"https://create-palette.plugins.penpot.app"
"https://icons-penpot-plugin.pages.dev"
"https://icons.plugins.penpot.app"
"https://lorem-ipsum-penpot-plugin.pages.dev"
"https://lorem-ipsum.plugins.penpot.app"
"https://rename-layers-penpot-plugin.pages.dev"
"https://rename-layers.plugins.penpot.app"
"https://table-penpot-plugin.pages.dev"
"https://table.plugins.penpot.app"})
(defn- fix-url
[url]
(reduce-kv (fn [url prefix replacement]
(if (str/starts-with? url prefix)
(reduced (str replacement (subs url (count prefix))))
url))
url
replacements))
(defn- fix-manifest
[manifest]
(-> manifest
(d/update-when :url fix-url)
(d/update-when :host fix-url)))
(defn- fix-plugins-data
[props]
(d/update-in-when props [:plugins :data]
(fn [data]
(reduce-kv (fn [data id manifest]
(let [manifest' (fix-manifest manifest)]
(if (= manifest manifest')
data
(assoc data id manifest'))))
data
data))))
(def ^:private sql:get-profiles
"SELECT id, props FROM profile
WHERE props ?? '~:plugins'
ORDER BY created_at
FOR UPDATE")
(defn migrate
[conn]
(->> (db/plan conn [sql:get-profiles])
(run! (fn [{:keys [id props]}]
(when-let [props (some-> props db/decode-transit-pgobject)]
(let [props' (fix-plugins-data props)]
(when (not= props props')
(l/inf :hint "fixing plugins data on profile props" :profile-id (str id))
(db/update! conn :profile
{:props (db/tjson props')}
{:id id}
{::db/return-keys false}))))))))

View File

@@ -407,17 +407,19 @@
(defn change-text (defn change-text
"Changes the content of the text shape to use the text as argument. Will use the styles of the "Changes the content of the text shape to use the text as argument. Will use the styles of the
first paragraph and text that is present in the shape (and override the rest)" first paragraph and text that is present in the shape (and override the rest)"
[content text] [content text & {:as styles}]
(let [root-styles (select-keys content root-attrs) (let [root-styles (select-keys content root-attrs)
paragraph-style paragraph-style
(merge (merge
default-text-attrs default-text-attrs
styles
(select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs)) (select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs))
text-style text-style
(merge (merge
default-text-attrs default-text-attrs
styles
(select-keys (->> content (node-seq is-text-node?) first) text-all-attrs)) (select-keys (->> content (node-seq is-text-node?) first) text-all-attrs))
paragraph-texts paragraph-texts

View File

@@ -0,0 +1,146 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "BUG 13267",
"~:revn": 3,
"~:modified-at": "~m1770302832804",
"~:vern": 0,
"~:id": "~ue9c84e12-dd29-80fc-8007-86d559dced7f",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ufc576d2f-8d02-8101-8007-70ec5793bd81",
"~:created-at": "~m1770302800755",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~ue9c84e12-dd29-80fc-8007-86d559dced80"
],
"~:pages-index": {
"~ue9c84e12-dd29-80fc-8007-86d559dced80": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~udc075bef-4a1f-8056-8007-86d562cf43b7\"]]]",
"~udc075bef-4a1f-8056-8007-86d55e028ccb": "[\"~#shape\",[\"^ \",\"~:y\",234,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",117,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",574,\"~:y\",234]],[\"^<\",[\"^ \",\"~:x\",691,\"~:y\",234]],[\"^<\",[\"^ \",\"~:x\",691,\"~:y\",316]],[\"^<\",[\"^ \",\"~:x\",574,\"~:y\",316]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:constraints-v\",\"~:scale\",\"~:constraints-h\",\"^B\",\"~:r1\",0,\"~:id\",\"~udc075bef-4a1f-8056-8007-86d55e028ccb\",\"~:parent-id\",\"~udc075bef-4a1f-8056-8007-86d562cf43b7\",\"~:frame-id\",\"~udc075bef-4a1f-8056-8007-86d562cf43b7\",\"~:strokes\",[],\"~:x\",574,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",574,\"~:y\",234,\"^8\",117,\"~:height\",82,\"~:x1\",574,\"~:y1\",234,\"~:x2\",691,\"~:y2\",316]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^M\",82,\"~:flip-y\",null]]",
"~udc075bef-4a1f-8056-8007-86d562cf43b7": "[\"~#shape\",[\"^ \",\"~:y\",234,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",true,\"~:name\",\"A Component\",\"~:width\",117,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",574,\"~:y\",234]],[\"^;\",[\"^ \",\"~:x\",691,\"~:y\",234]],[\"^;\",[\"^ \",\"~:x\",691,\"~:y\",316]],[\"^;\",[\"^ \",\"~:x\",574,\"~:y\",316]]],\"~:r2\",0,\"~:component-root\",true,\"~:show-content\",true,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~udc075bef-4a1f-8056-8007-86d562cf43b7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:component-id\",\"~udc075bef-4a1f-8056-8007-86d562d06904\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",574,\"~:main-instance\",true,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",574,\"~:y\",234,\"^7\",117,\"~:height\",82,\"~:x1\",574,\"~:y1\",234,\"~:x2\",691,\"~:y2\",316]],\"~:fills\",[],\"~:flip-x\",null,\"^M\",82,\"~:component-file\",\"~ue9c84e12-dd29-80fc-8007-86d559dced7f\",\"~:flip-y\",null,\"~:shapes\",[\"~udc075bef-4a1f-8056-8007-86d55e028ccb\"]]]"
}
},
"~:id": "~ue9c84e12-dd29-80fc-8007-86d559dced80",
"~:name": "Page 1"
}
},
"~:id": "~ue9c84e12-dd29-80fc-8007-86d559dced7f",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
},
"~:components": {
"~udc075bef-4a1f-8056-8007-86d562d06904": {
"~:id": "~udc075bef-4a1f-8056-8007-86d562d06904",
"~:name": "A Component",
"~:path": "",
"~:modified-at": "~m1770302824566",
"~:main-instance-id": "~udc075bef-4a1f-8056-8007-86d562cf43b7",
"~:main-instance-page": "~ue9c84e12-dd29-80fc-8007-86d559dced80"
}
}
}
}

View File

@@ -459,8 +459,8 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.mouse.up(); await this.page.mouse.up();
} }
async clickLeafLayer(name, clickOptions = {}) { async clickLeafLayer(name, clickOptions = {}, index = 0) {
const layer = this.layers.getByText(name).first(); const layer = this.layers.getByText(name).nth(index);
await layer.waitFor(); await layer.waitFor();
await layer.click(clickOptions); await layer.click(clickOptions);
await this.page.waitForTimeout(500); await this.page.waitForTimeout(500);
@@ -471,10 +471,11 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.clickLeafLayer(name, clickOptions); await this.clickLeafLayer(name, clickOptions);
} }
async clickToggableLayer(name, clickOptions = {}) { async clickToggableLayer(name, clickOptions = {}, index = 0) {
const layer = this.layers const layer = this.layers
.getByTestId("layer-row") .getByTestId("layer-row")
.filter({ hasText: name }); .filter({ hasText: name })
.nth(index);
const button = layer.getByTestId("toggle-content"); const button = layer.getByTestId("toggle-content");
await expect(button).toBeVisible(); await expect(button).toBeVisible();

View File

@@ -0,0 +1,33 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
});
test("BUG 13267 - Component instance is not synced with parent for geometry changes", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("components/get-file-13267.json");
await workspacePage.goToWorkspace({
fileId: "e9c84e12-dd29-80fc-8007-86d559dced7f",
pageId: "e9c84e12-dd29-80fc-8007-86d559dced80",
});
// Create a component instance
await workspacePage.clickLeafLayer("A Component");
await workspacePage.page.keyboard.press("ControlOrMeta+d");
// Select the main component
await workspacePage.clickLeafLayer("A Component", {}, 1);
const rotationInput = workspacePage.rightSidebar.getByTestId("rotation").getByRole("textbox");
await rotationInput.fill("45");
await rotationInput.press("Enter");
// Select the instance rect
await workspacePage.clickToggableLayer("A Component", {}, 0);
await workspacePage.clickLeafLayer("Rectangle");
await expect(rotationInput).toHaveValue("45");
});

View File

@@ -179,6 +179,56 @@
(map #(get objects %)) (map #(get objects %))
(reduce get-ignore-tree nil)))) (reduce get-ignore-tree nil))))
(defn calculate-ignore-tree-wasm
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
[transforms objects]
(letfn [(get-ignore-tree
([ignore-tree shape]
(let [shape-id (dm/get-prop shape :id)
transformed-shape (gsh/apply-transform shape (get transforms shape-id))
root
(if (:component-root shape)
shape
(ctn/get-component-shape objects shape {:allow-main? true}))
transformed-root
(if (:component-root shape)
transformed-shape
(gsh/apply-transform root (get transforms (:id root))))]
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
([ignore-tree shape root transformed-root]
(let [shape-id (dm/get-prop shape :id)
transformed-shape (gsh/apply-transform shape (get transforms shape-id))]
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
([ignore-tree shape transformed-shape root transformed-root]
(let [shape-id (dm/get-prop shape :id)
ignore-tree
(cond-> ignore-tree
(and (some? root) (ctk/in-component-copy? shape))
(assoc
shape-id
(check-delta shape root transformed-shape transformed-root)))
set-child
(fn [ignore-tree child]
(get-ignore-tree ignore-tree child root transformed-root))]
(->> (:shapes shape)
(map (d/getf objects))
(reduce set-child ignore-tree)))))]
;; we check twice because we want only to search parents of components but once the
;; tree is traversed we only want to process the objects in components
(->> (keys transforms)
(map #(get objects %))
(reduce get-ignore-tree nil))))
(defn assoc-position-data (defn assoc-position-data
[shape position-data old-shape] [shape position-data old-shape]
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape)) (let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
@@ -625,17 +675,6 @@
(let [objects (dsh/lookup-page-objects state) (let [objects (dsh/lookup-page-objects state)
ignore-tree
(calculate-ignore-tree modif-tree objects)
options
(-> params
(assoc :reg-objects? true)
(assoc :ignore-tree ignore-tree)
;; Attributes that can change in the transform. This
;; way we don't have to check all the attributes
(assoc :attrs transform-attrs))
geometry-entries geometry-entries
(parse-geometry-modifiers modif-tree) (parse-geometry-modifiers modif-tree)
@@ -645,6 +684,17 @@
transforms transforms
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)) (into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?))
ignore-tree
(calculate-ignore-tree-wasm transforms objects)
options
(-> params
(assoc :reg-objects? true)
(assoc :ignore-tree ignore-tree)
;; Attributes that can change in the transform. This
;; way we don't have to check all the attributes
(assoc :attrs transform-attrs))
modif-tree modif-tree
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state)) (propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))

View File

@@ -104,7 +104,7 @@
(watch [_ state _] (watch [_ state _]
(let [page-id (or page-id (:current-page-id state)) (let [page-id (or page-id (:current-page-id state))
objects (dsh/lookup-page-objects state page-id) objects (dsh/lookup-page-objects state page-id)
ids (->> ids (filter #(contains? objects %)))] ids (->> ids (remove uuid/zero?) (filter #(contains? objects %)))]
(if (d/not-empty? ids) (if (d/not-empty? ids)
(let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))] (let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
(if (features/active-feature? state "render-wasm/v1") (if (features/active-feature? state "render-wasm/v1")

View File

@@ -776,11 +776,7 @@
(rx/of (v2-update-text-editor-styles id attrs))) (rx/of (v2-update-text-editor-styles id attrs)))
(when (features/active-feature? state "render-wasm/v1") (when (features/active-feature? state "render-wasm/v1")
;; This delay is to give time for the font to be correctly rendered (rx/of (dwwt/resize-wasm-text-debounce id)))))))
;; in wasm.
(cond->> (rx/of (dwwt/resize-wasm-text id))
(contains? attrs :font-id)
(rx/delay 200)))))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]

View File

@@ -10,6 +10,7 @@
This exists to avoid circular deps: This exists to avoid circular deps:
workspace.texts -> workspace.libraries -> workspace.texts" workspace.texts -> workspace.libraries -> workspace.texts"
(:require (:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
@@ -17,6 +18,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.modifiers :as dwm]
[app.render-wasm.api :as wasm.api] [app.render-wasm.api :as wasm.api]
[app.render-wasm.api.fonts :as wasm.fonts]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@@ -62,6 +64,84 @@
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape))) (rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
(rx/empty)))))) (rx/empty))))))
(defn resize-wasm-text-debounce-commit
[]
(ptk/reify ::resize-wasm-text-debounce-commit
ptk/WatchEvent
(watch [_ state _]
(let [ids (get state ::resize-wasm-text-debounce-ids)
objects (dsh/lookup-page-objects state)
modifiers
(reduce
(fn [modifiers id]
(let [shape (get objects id)]
(cond-> modifiers
(and (some? shape)
(cfh/text-shape? shape)
(not= :fixed (:grow-type shape)))
(merge (resize-wasm-text-modifiers shape)))))
{}
ids)]
(if (not (empty? modifiers))
(rx/of (dwm/apply-wasm-modifiers modifiers))
(rx/empty))))))
;; This event will debounce the resize events so, if there are many, they
;; are processed at the same time and not one-by-one. This will improve
;; performance because it's better to make only one layout calculation instead
;; of (potentialy) hundreds.
(defn resize-wasm-text-debounce-inner
[id]
(let [cur-event (js/Symbol)]
(ptk/reify ::resize-wasm-text-debounce-inner
ptk/UpdateEvent
(update [_ state]
(-> state
(update ::resize-wasm-text-debounce-ids (fnil conj []) id)
(cond-> (nil? (::resize-wasm-text-debounce-event state))
(assoc ::resize-wasm-text-debounce-event cur-event))))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::resize-wasm-text-debounce-event state) cur-event)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/concat
(rx/merge
(->> stream
(rx/filter (ptk/type? ::resize-wasm-text-debounce-inner))
(rx/debounce 40)
(rx/take 1)
(rx/map #(resize-wasm-text-debounce-commit))
(rx/take-until stopper))
(rx/of (resize-wasm-text-debounce-inner id)))
(rx/of #(dissoc %
::resize-wasm-text-debounce-ids
::resize-wasm-text-debounce-event))))
(rx/empty))))))
(defn resize-wasm-text-debounce
[id]
(ptk/reify ::resize-wasm-text-debounce
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
content (dm/get-in objects [id :content])
fonts (wasm.fonts/get-content-fonts content)
fonts-loaded?
(->> fonts
(every?
(fn [font]
(let [font-data (wasm.fonts/make-font-data font)]
(wasm.fonts/font-stored? font-data (:emoji? font-data))))))]
(if (not fonts-loaded?)
(->> (rx/of (resize-wasm-text-debounce id))
(rx/delay 20))
(rx/of (resize-wasm-text-debounce-inner id)))))))
(defn resize-wasm-text-all (defn resize-wasm-text-all
"Resize all text shapes (auto-width/auto-height) from a collection of ids." "Resize all text shapes (auto-width/auto-height) from a collection of ids."
[ids] [ids]

View File

@@ -16,6 +16,7 @@
(def ^:private schema:icon-button (def ^:private schema:icon-button
[:map [:map
[:class {:optional true} :string] [:class {:optional true} :string]
[:tooltip-class {:optional true} [:maybe :string]]
[:icon-class {:optional true} :string] [:icon-class {:optional true} :string]
[:icon [:icon
[:and :string [:fn #(contains? icon-list %)]]] [:and :string [:fn #(contains? icon-list %)]]]
@@ -28,7 +29,7 @@
(mf/defc icon-button* (mf/defc icon-button*
{::mf/schema schema:icon-button {::mf/schema schema:icon-button
::mf/memo true} ::mf/memo true}
[{:keys [class icon icon-class variant aria-label children tooltip-placement] :rest props}] [{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class] :rest props}]
(let [variant (let [variant
(d/nilv variant "primary") (d/nilv variant "primary")
@@ -49,6 +50,7 @@
:aria-labelledby tooltip-id})] :aria-labelledby tooltip-id})]
[:> tooltip* {:content aria-label [:> tooltip* {:content aria-label
:class tooltip-class
:placement tooltip-placement :placement tooltip-placement
:id tooltip-id} :id tooltip-id}
[:> :button props [:> :button props

View File

@@ -18,7 +18,6 @@
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]] [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.token-field :refer [token-field*]] [app.main.ui.ds.controls.utilities.token-field :refer [token-field*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.main.ui.ds.tooltip :refer [tooltip*]]
[app.main.ui.formats :as fmt] [app.main.ui.formats :as fmt]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
@@ -638,27 +637,17 @@
:on-change store-raw-value :on-change store-raw-value
:variant "comfortable" :variant "comfortable"
:disabled disabled :disabled disabled
:slot-start (when (or icon text-icon) :icon icon
:aria-label property
:slot-start (when text-icon
(mf/html (mf/html
[:> tooltip* [:div {:class (stl/css :text-icon)}
{:content property text-icon]))
:id property}
(cond
icon
[:> icon*
{:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]
text-icon
[:div {:class (stl/css :text-icon)
:aria-labelledby property}
text-icon])]))
:slot-end (when-not disabled :slot-end (when-not disabled
(when (some? tokens) (when (some? tokens)
(mf/html [:> icon-button* {:variant "ghost" (mf/html [:> icon-button* {:variant "ghost"
:icon i/tokens :icon i/tokens
:tooltip-class (stl/css :button-tooltip)
:class (stl/css :invisible-button) :class (stl/css :invisible-button)
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:ref open-dropdown-ref :ref open-dropdown-ref
@@ -686,23 +675,19 @@
:disabled disabled :disabled disabled
:on-blur on-blur :on-blur on-blur
:class inner-class :class inner-class
:property property
:slot-start (when (or icon text-icon) :slot-start (when (or icon text-icon)
(mf/html (mf/html
[:> tooltip* (cond
{:content property icon
:id property} [:> icon*
(cond {:icon-id icon
icon :size "s"
[:> icon* :class (stl/css :icon)}]
{:icon-id icon
:size "s"
:aria-labelledby property
:class (stl/css :icon)}]
text-icon text-icon
[:div {:class (stl/css :text-icon) [:div {:class (stl/css :text-icon)}
:aria-labelledby property} text-icon])))
text-icon])]))
:token-wrapper-ref token-wrapper-ref :token-wrapper-ref token-wrapper-ref
:token-detach-btn-ref token-detach-btn-ref :token-detach-btn-ref token-detach-btn-ref
:detach-token detach-token})))] :detach-token detach-token})))]
@@ -737,40 +722,21 @@
(mf/with-effect [dropdown-options] (mf/with-effect [dropdown-options]
(mf/set-ref-val! options-ref dropdown-options)) (mf/set-ref-val! options-ref dropdown-options))
(if (some? icon) [:div {:class [class (stl/css :input-wrapper)]
[:div {:class [class (stl/css :input-wrapper)] :ref wrapper-ref}
:ref wrapper-ref}
(if (and (some? token-applied) (if (and (some? token-applied)
(not= :multiple token-applied)) (not= :multiple token-applied))
[:> token-field* token-props] [:> token-field* token-props]
[:> input-field* input-props]) [:> input-field* input-props])
(when ^boolean is-open (when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)] (let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click [:> options-dropdown* {:on-click on-option-click
:id listbox-id :id listbox-id
:options options :options options
:selected selected-id :selected selected-id
:focused focused-id :focused focused-id
:align align :align align
:empty-to-end empty-to-end :empty-to-end empty-to-end
:ref set-option-ref}]))] :ref set-option-ref}]))]))
[:div {:class [class (stl/css :input-wrapper)]
:ref wrapper-ref}
(if (and (some? token-applied)
(not= :multiple token-applied))
[:> token-field* token-props]
[:> input-field* input-props])
(when ^boolean is-open
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
[:> options-dropdown* {:on-click on-option-click
:id listbox-id
:options options
:selected selected-id
:focused focused-id
:align align
:empty-to-end empty-to-end
:ref set-option-ref}]))])))

View File

@@ -55,3 +55,8 @@
--opacity-button: 1; --opacity-button: 1;
} }
} }
.button-tooltip {
inline-size: var($sz-28);
block-size: 100%;
}

View File

@@ -42,7 +42,6 @@
type (d/nilv type "text") type (d/nilv type "text")
variant (d/nilv variant "dense") variant (d/nilv variant "dense")
tooltip-id (mf/use-id) tooltip-id (mf/use-id)
props (mf/spread-props props props (mf/spread-props props
{:class [class {:class [class
(stl/css-case (stl/css-case
@@ -54,15 +53,11 @@
"true") "true")
:aria-describedby (when has-hint :aria-describedby (when has-hint
(str id "-hint")) (str id "-hint"))
:aria-labelledby tooltip-id
:type (d/nilv type "text") :type (d/nilv type "text")
:id id :id id
:max-length (d/nilv max-length max-input-length)}) :max-length (d/nilv max-length max-input-length)})
props (if (and aria-label (not (some? icon)))
(mf/spread-props props
{:aria-label aria-label})
(mf/spread-props props
{:aria-labelledby tooltip-id}))
inside-class (stl/css-case :input-wrapper true inside-class (stl/css-case :input-wrapper true
:has-hint has-hint :has-hint has-hint
:hint-type-hint (= hint-type "hint") :hint-type-hint (= hint-type "hint")
@@ -83,11 +78,14 @@
(when (some? slot-start) (when (some? slot-start)
slot-start) slot-start)
(when (some? icon) (when (some? icon)
(if aria-label [:> icon* {:icon-id icon
[:> tooltip* {:content aria-label :class (stl/css :icon)
:id tooltip-id} :size "s"
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}]] :on-click on-icon-click}])
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}])) (if aria-label
[:> "input" props] [:> tooltip* {:content aria-label
:id tooltip-id}
[:> "input" props]]
[:> "input" props])
(when (some? slot-end) (when (some? slot-end)
slot-end)])) slot-end)]))

View File

@@ -118,4 +118,5 @@
.icon { .icon {
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
min-inline-size: var(--sp-l);
} }

View File

@@ -22,6 +22,7 @@
[:class {:optional true} [:maybe :string]] [:class {:optional true} [:maybe :string]]
[:id {:optional true} [:maybe :string]] [:id {:optional true} [:maybe :string]]
[:label {:optional true} [:maybe :string]] [:label {:optional true} [:maybe :string]]
[:property {:optional true} [:maybe :string]]
[:value :any] [:value :any]
[:disabled {:optional true} :boolean] [:disabled {:optional true} :boolean]
[:slot-start {:optional true} [:maybe some?]] [:slot-start {:optional true} [:maybe some?]]
@@ -35,7 +36,7 @@
{::mf/schema schema:token-field} {::mf/schema schema:token-field}
[{:keys [id label value slot-start disabled class [{:keys [id label value slot-start disabled class
on-click on-token-key-down on-blur detach-token on-click on-token-key-down on-blur detach-token
token-wrapper-ref token-detach-btn-ref on-focus]}] token-wrapper-ref token-detach-btn-ref on-focus property]}]
(let [set-active? (some? id) (let [set-active? (some? id)
content (if set-active? content (if set-active?
label label
@@ -50,37 +51,42 @@
(when-not ^boolean disabled (when-not ^boolean disabled
(dom/prevent-default event) (dom/prevent-default event)
(dom/focus! (mf/ref-val token-wrapper-ref)))))] (dom/focus! (mf/ref-val token-wrapper-ref)))))]
[:> tooltip* {:content property
:class (stl/css :token-field-wrapper)
:id (dm/str default-id "-input")}
[:div {:class [class (stl/css-case :token-field true
:with-icon (some? slot-start)
:token-field-disabled disabled)]
:on-click focus-wrapper
:disabled disabled
:on-key-down on-token-key-down
:ref token-wrapper-ref
:on-blur on-blur
:on-focus on-focus
:aria-labelledby (dm/str default-id "-input")
:tab-index (if disabled -1 0)}
[:div {:class [class (stl/css-case :token-field true (when (some? slot-start) slot-start)
:with-icon (some? slot-start)
:token-field-disabled disabled)]
:on-click focus-wrapper
:disabled disabled
:on-key-down on-token-key-down
:ref token-wrapper-ref
:on-blur on-blur
:on-focus on-focus
:tab-index (if disabled -1 0)}
(when (some? slot-start) slot-start) [:div {:class (stl/css :content-wrapper)}
[:> tooltip* {:content content
:id (dm/str id "-pill")}
[:button {:on-click on-click
:class (stl/css-case :pill true
:no-set-pill (not set-active?)
:pill-disabled disabled)
:disabled disabled
:aria-labelledby (dm/str id "-pill")
:on-key-down on-token-key-down}
value
(when-not set-active?
[:div {:class (stl/css :pill-dot)}])]]]
[:> tooltip* {:content content (when-not ^boolean disabled
:id (dm/str id "-pill")} [:> icon-button* {:variant "ghost"
[:button {:on-click on-click :class (stl/css :invisible-button)
:class (stl/css-case :pill true :tooltip-class (stl/css :button-tooltip)
:no-set-pill (not set-active?) :icon i/broken-link
:pill-disabled disabled) :ref token-detach-btn-ref
:disabled disabled :aria-label (tr "ds.inputs.token-field.detach-token")
:aria-labelledby (dm/str id "-pill") :on-click detach-token}])]]))
:on-key-down on-token-key-down}
value
(when-not set-active?
[:div {:class (stl/css :pill-dot)}])]]
(when-not ^boolean disabled
[:> icon-button* {:variant "ghost"
:class (stl/css :invisible-button)
:icon i/broken-link
:ref token-detach-btn-ref
:aria-label (tr "ds.inputs.token-field.detach-token")
:on-click detach-token}])]))

View File

@@ -9,6 +9,7 @@
@use "ds/typography.scss" as t; @use "ds/typography.scss" as t;
@use "ds/colors.scss" as *; @use "ds/colors.scss" as *;
@use "ds/mixins.scss" as *; @use "ds/mixins.scss" as *;
@use "ds/_utils.scss" as *;
.token-field { .token-field {
--token-field-bg-color: var(--color-background-tertiary); --token-field-bg-color: var(--color-background-tertiary);
@@ -37,6 +38,9 @@
--token-field-outline-color: var(--color-accent-primary); --token-field-outline-color: var(--color-accent-primary);
} }
} }
.token-field-wrapper {
inline-size: 100%;
}
.with-icon { .with-icon {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
@@ -132,3 +136,12 @@
--opacity-button: 1; --opacity-button: 1;
} }
} }
.content-wrapper {
inline-size: 100%;
}
.button-tooltip {
inline-size: px2rem(28);
block-size: 100%;
}

View File

@@ -171,7 +171,7 @@
(def ^:private schema:tooltip (def ^:private schema:tooltip
[:map [:map
[:class {:optional true} :string] [:class {:optional true} [:maybe :string]]
[:id {:optional true} :string] [:id {:optional true} :string]
[:offset {:optional true} :int] [:offset {:optional true} :int]
[:delay {:optional true} :int] [:delay {:optional true} :int]
@@ -184,6 +184,7 @@
[{:keys [class id children content placement offset delay] :rest props}] [{:keys [class id children content placement offset delay] :rest props}]
(let [internal-id (let [internal-id
(mf/use-id) (mf/use-id)
trigger-ref (mf/use-ref nil)
id id
(d/nilv id internal-id) (d/nilv id internal-id)
@@ -204,19 +205,23 @@
(mf/use-fn (mf/use-fn
(mf/deps id placement offset) (mf/deps id placement offset)
(fn [event] (fn [event]
(clear-schedule schedule-ref)
(when-let [tooltip (dom/get-element id)]
(let [origin-brect
(->> (dom/get-target event)
(dom/get-bounding-rect))
update-position (let [current (dom/get-current-target event)
(fn [] related (dom/get-related-target event)
(let [new-placement (update-tooltip-position tooltip placement origin-brect offset)] is-node? (fn [node] (and node (.-nodeType node)))]
(when (not= new-placement placement) (when-not (and related (is-node? related) (.contains current related))
(reset! placement* new-placement))))] (clear-schedule schedule-ref)
(when-let [tooltip (dom/get-element id)]
(let [origin-brect
(dom/get-bounding-rect (mf/ref-val trigger-ref))
(add-schedule schedule-ref delay update-position))))) update-position
(fn []
(let [new-placement (update-tooltip-position tooltip placement origin-brect offset)]
(when (not= new-placement placement)
(reset! placement* new-placement))))]
(add-schedule schedule-ref delay update-position)))))))
on-hide on-hide
(mf/use-fn (mf/use-fn
@@ -252,6 +257,7 @@
:on-focus on-show :on-focus on-show
:on-blur on-hide :on-blur on-hide
:on-key-down handle-key-down :on-key-down handle-key-down
:ref trigger-ref
:class [class (stl/css :tooltip-trigger)] :class [class (stl/css :tooltip-trigger)]
:aria-describedby id}) :aria-describedby id})
content content

View File

@@ -95,10 +95,10 @@
[] []
(let [plugins-state* (mf/use-state #(preg/plugins-list)) (let [plugins-state* (mf/use-state #(preg/plugins-list))
plugins-state @plugins-state* plugins-state (deref plugins-state*)
plugin-url* (mf/use-state "") plugin-url* (mf/use-state "")
plugin-url @plugin-url* plugin-url (deref plugin-url*)
fetching-manifest? (mf/use-state false) fetching-manifest? (mf/use-state false)

View File

@@ -300,7 +300,7 @@
on-drop on-drop
(mf/use-fn (mf/use-fn
(mf/deps id index objects expanded? selected) (mf/deps id objects expanded? selected)
(fn [side _data] (fn [side _data]
(let [single? (= (count selected) 1) (let [single? (= (count selected) 1)
same? (and single? (= (first selected) id))] same? (and single? (= (first selected) id))]
@@ -321,14 +321,18 @@
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files) [parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
parent (get objects parent-id) parent (get objects parent-id)
current-index (d/index-of (:shapes parent) id)
to-index (cond to-index (cond
(= side :center) 0 (= side :center) 0
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent)) (and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(= side :top) (inc index) ;; target not found in parent (while lazy loading)
:else index)] (neg? current-index) nil
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))) (= side :top) (inc current-index)
:else current-index)]
(when (some? to-index)
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
on-hold on-hold
(mf/use-fn (mf/use-fn
@@ -417,11 +421,7 @@
current @children-count* current @children-count*
new-count (min total (max current chunk-size min-count))] new-count (min total (max current chunk-size min-count))]
(reset! children-count* new-count)) (reset! children-count* new-count))
(reset! children-count* 0))) (reset! children-count* 0))))
(fn []
(when-let [obs ^js @observer-var]
(.disconnect obs)
(reset! observer-var nil))))
;; Re-observe sentinel whenever children-count changes (sentinel moves) ;; Re-observe sentinel whenever children-count changes (sentinel moves)
;; and (shapes item) to reconnect observer after shape changes ;; and (shapes item) to reconnect observer after shape changes
@@ -502,4 +502,4 @@
:component-child? component-tree?}]))) :component-child? component-tree?}])))
(when (< children-count (count (:shapes item))) (when (< children-count (count (:shapes item)))
[:div {:ref lazy-ref [:div {:ref lazy-ref
:style {:min-height 1}}])])])) :class (stl/css :lazy-load-sentinel)}])])]))

View File

@@ -298,3 +298,11 @@
.filtered { .filtered {
min-inline-size: $sz-12; min-inline-size: $sz-12;
} }
.lazy-load-sentinel {
min-height: 1px;
pointer-events: none;
}
.lazy-load-sentinel {
min-height: 1px;
pointer-events: none;
}

View File

@@ -521,8 +521,7 @@
[:& filters-tree {:objects filtered-objects [:& filters-tree {:objects filtered-objects
:key (dm/str (:id page)) :key (dm/str (:id page))
:parent-size size-parent}] :parent-size size-parent}]
[:div {:ref lazy-load-ref [:div {:ref lazy-load-ref}]]
:style {:min-height 16}}]]
[:div {:on-scroll on-scroll [:div {:on-scroll on-scroll
:class (stl/css :tool-window-content) :class (stl/css :tool-window-content)
:data-scroll-container true :data-scroll-container true

View File

@@ -541,7 +541,8 @@
:value (get values :rotation)}] :value (get values :rotation)}]
[:div {:class (stl/css :rotation) [:div {:class (stl/css :rotation)
:title (tr "workspace.options.rotation")} :title (tr "workspace.options.rotation")
:data-testid "rotation"}
[:span {:class (stl/css :icon)} deprecated-icon/rotation] [:span {:class (stl/css :icon)} deprecated-icon/rotation]
[:> deprecated-input/numeric-input* [:> deprecated-input/numeric-input*
{:no-validate true {:no-validate true

View File

@@ -75,32 +75,31 @@
[{:keys [points] :as shape} zoom grid-edition?] [{:keys [points] :as shape} zoom grid-edition?]
(let [leftmost (->> points (reduce left?)) (let [leftmost (->> points (reduce left?))
topmost (->> points (remove #{leftmost}) (reduce top?)) topmost (->> points (remove #{leftmost}) (reduce top?))
rightmost (->> points (remove #{leftmost topmost}) (reduce right?)) rightmost (->> points (remove #{leftmost topmost}) (reduce right?))]
(when (and (some? leftmost) (some? topmost) (some? rightmost))
(let [left-top (gpt/to-vec leftmost topmost)
left-top-angle (gpt/angle left-top)
left-top (gpt/to-vec leftmost topmost) top-right (gpt/to-vec topmost rightmost)
left-top-angle (gpt/angle left-top) top-right-angle (gpt/angle top-right)
top-right (gpt/to-vec topmost rightmost) ;; Choose the position that creates the less angle between left-side and top-side
top-right-angle (gpt/angle top-right) [label-pos angle h-pos v-pos]
(if (< (mth/abs left-top-angle) (mth/abs top-right-angle))
[leftmost left-top-angle left-top (gpt/perpendicular left-top)]
[topmost top-right-angle top-right (gpt/perpendicular top-right)])
;; Choose the position that creates the less angle between left-side and top-side delta-x (if grid-edition? 40 0)
[label-pos angle h-pos v-pos] delta-y (if grid-edition? 50 10)
(if (< (mth/abs left-top-angle) (mth/abs top-right-angle))
[leftmost left-top-angle left-top (gpt/perpendicular left-top)]
[topmost top-right-angle top-right (gpt/perpendicular top-right)])
delta-x (if grid-edition? 40 0) label-pos
delta-y (if grid-edition? 50 10) (-> label-pos
(gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom)))
label-pos (gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))]
(-> label-pos (dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)"
(gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom))) ;; rotate
(gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))] angle (:x label-pos) (:y label-pos)
;; scale
(dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)" (/ 1 zoom) (/ 1 zoom)
;; rotate ;; translate
angle (:x label-pos) (:y label-pos) (* zoom (:x label-pos)) (* zoom (:y label-pos)))))))
;; scale
(/ 1 zoom) (/ 1 zoom)
;; translate
(* zoom (:x label-pos)) (* zoom (:y label-pos)))))

View File

@@ -705,8 +705,8 @@
[:& grid-layout/editor [:& grid-layout/editor
{:zoom zoom {:zoom zoom
:objects objects-modified :objects objects-modified
:shape (or (get base-objects edition) :shape (or (get objects-modified edition)
(get base-objects @hover-top-frame-id)) (get objects-modified @hover-top-frame-id))
:view-only (not show-grid-editor?)}])] :view-only (not show-grid-editor?)}])]
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"} [:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}

View File

@@ -26,6 +26,7 @@
[app.main.data.workspace.groups :as dwg] [app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.media :as dwm] [app.main.data.workspace.media :as dwm]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.fonts :refer [fetch-font-css]] [app.main.fonts :refer [fetch-font-css]]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
@@ -348,9 +349,14 @@
:else :else
(let [page (dsh/lookup-page @st/state) (let [page (dsh/lookup-page @st/state)
shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width}) shape (-> (cts/setup-shape {:type :text
(update :content txt/change-text text) :x 0 :y 0
(assoc :position-data nil)) :width 1 :height 1
:grow-type :auto-width})
(update :content txt/change-text text
;; Text should be given a color by default
{:fills [{:fill-color "#000000" :fill-opacity 1}]})
(dissoc :position-data))
changes changes
(-> (cb/empty-changes) (-> (cb/empty-changes)
@@ -358,9 +364,10 @@
(cb/with-objects (:objects page)) (cb/with-objects (:objects page))
(cb/add-object shape))] (cb/add-object shape))]
(st/emit! (st/emit! (ch/commit-changes changes)
(ch/commit-changes changes) (se/event plugin-id "create-shape" :type :text)
(se/event plugin-id "create-shape" :type :text)) (dwwt/resize-wasm-text-debounce (:id shape)))
(shape/shape-proxy plugin-id (:id shape))))) (shape/shape-proxy plugin-id (:id shape)))))
:createShapeFromSvg :createShapeFromSvg

View File

@@ -38,6 +38,7 @@
desc (obj/get manifest "description") desc (obj/get manifest "description")
code (obj/get manifest "code") code (obj/get manifest "code")
icon (obj/get manifest "icon") icon (obj/get manifest "icon")
vers (d/nilv (obj/get manifest "version") 1)
permissions (into #{} (obj/get manifest "permissions" [])) permissions (into #{} (obj/get manifest "permissions" []))
permissions permissions
@@ -55,9 +56,13 @@
(u/uri plugin-url) (u/uri plugin-url)
origin origin
(-> plugin-url (if (= vers 1)
(u/join ".") (-> plugin-url
(str)) (assoc :path "/")
(str))
(-> plugin-url
(u/join ".")
(str)))
prev-plugin prev-plugin
(->> (:data @registry) (->> (:data @registry)

View File

@@ -190,11 +190,13 @@
(defn update-text-rect! (defn update-text-rect!
[id] [id]
(when wasm/context-initialized? (when wasm/context-initialized?
(mw/emit! (let [dimensions (get-text-dimensions id)
{:cmd :index/update-text-rect page-id (:current-page-id @st/state)]
:page-id (:current-page-id @st/state) (mw/emit!
:shape-id id {:cmd :index/update-text-rect
:dimensions (get-text-dimensions id)}))) :page-id page-id
:shape-id id
:dimensions dimensions}))))
(defn- ensure-text-content (defn- ensure-text-content
@@ -865,12 +867,12 @@
(set-shape-vertical-align (get content :vertical-align)) (set-shape-vertical-align (get content :vertical-align))
(let [fonts (f/get-content-fonts content) (let [fonts (f/get-content-fonts content)
fallback-fonts (fonts-from-text-content content true) fallback-fonts (fonts-from-text-content content true)
all-fonts (concat fonts fallback-fonts) all-fonts (concat fonts fallback-fonts)
result (f/store-fonts shape-id all-fonts)] result (f/store-fonts all-fonts)]
(f/load-fallback-fonts-for-editor! fallback-fonts) (f/load-fallback-fonts-for-editor! fallback-fonts)
(h/call wasm/internal-module "_update_shape_text_layout") (f/update-text-layout shape-id)
result)) result))
(defn set-shape-grow-type (defn set-shape-grow-type
@@ -1564,7 +1566,7 @@
:text-decoration (get element :text-decoration) :text-decoration (get element :text-decoration)
:letter-spacing (get element :letter-spacing) :letter-spacing (get element :letter-spacing)
:font-style (get element :font-style) :font-style (get element :font-style)
:fills (get element :fills) :fills (d/nilv (get element :fills) [{:fill-color "#000000"}])
:text text}))))))] :text text}))))))]
(mem/free) (mem/free)

View File

@@ -97,9 +97,8 @@
;; IMPORTANT: Only TTF fonts can be stored. ;; IMPORTANT: Only TTF fonts can be stored.
(defn- store-font-buffer (defn- store-font-buffer
[shape-id font-data font-array-buffer emoji? fallback?] [font-data font-array-buffer emoji? fallback?]
(let [font-id-buffer (:family-id-buffer font-data) (let [font-id-buffer (:family-id-buffer font-data)
shape-id-buffer (uuid/get-u32 shape-id)
size (.-byteLength font-array-buffer) size (.-byteLength font-array-buffer)
ptr (h/call wasm/internal-module "_alloc_bytes" size) ptr (h/call wasm/internal-module "_alloc_bytes" size)
heap (gobj/get ^js wasm/internal-module "HEAPU8") heap (gobj/get ^js wasm/internal-module "HEAPU8")
@@ -107,10 +106,6 @@
(.set mem (js/Uint8Array. font-array-buffer)) (.set mem (js/Uint8Array. font-array-buffer))
(h/call wasm/internal-module "_store_font" (h/call wasm/internal-module "_store_font"
(aget shape-id-buffer 0)
(aget shape-id-buffer 1)
(aget shape-id-buffer 2)
(aget shape-id-buffer 3)
(aget font-id-buffer 0) (aget font-id-buffer 0)
(aget font-id-buffer 1) (aget font-id-buffer 1)
(aget font-id-buffer 2) (aget font-id-buffer 2)
@@ -119,24 +114,31 @@
(:style font-data) (:style font-data)
emoji? emoji?
fallback?) fallback?)
(update-text-layout shape-id)
true)) true))
;; This variable will store the fonts that are currently being fetched
;; so we don't fetch more than once the same font
(def fetching (atom #{}))
(defn- fetch-font (defn- fetch-font
[shape-id font-data font-url emoji? fallback?] [font-data font-url emoji? fallback?]
{:key font-url (when-not (contains? @fetching font-url)
:callback #(->> (http/send! {:method :get (swap! fetching conj font-url)
:uri font-url {:key font-url
:response-type :buffer}) :callback
(rx/map (fn [{:keys [body]}] (fn []
(store-font-buffer shape-id font-data body emoji? fallback?))) (->> (http/send! {:method :get
(rx/catch (fn [cause] :uri font-url
(log/error :hint "Could not fetch font" :response-type :buffer})
:font-url font-url (rx/map (fn [{:keys [body]}]
:cause cause) (swap! fetching disj font-url)
(rx/empty))))}) (store-font-buffer font-data body emoji? fallback?)))
(rx/catch (fn [cause]
(swap! fetching disj font-url)
(log/error :hint "Could not fetch font"
:font-url font-url
:cause cause)
(rx/empty)))))}))
(defn- google-font-ttf-url (defn- google-font-ttf-url
[font-id font-variant-id font-weight font-style] [font-id font-variant-id font-weight font-style]
@@ -155,22 +157,31 @@
:builtin :builtin
(dm/str (u/join cf/public-uri "fonts/" asset-id)))) (dm/str (u/join cf/public-uri "fonts/" asset-id))))
(defn font-stored?
[font-data emoji?]
(when-let [id-buffer (uuid/get-u32 (:wasm-id font-data))]
(not= 0 (h/call wasm/internal-module "_is_font_uploaded"
(aget id-buffer 0)
(aget id-buffer 1)
(aget id-buffer 2)
(aget id-buffer 3)
(:weight font-data)
(:style font-data)
emoji?))))
(defn- store-font-id (defn- store-font-id
[shape-id font-data asset-id emoji? fallback?] [font-data asset-id emoji? fallback?]
(when asset-id (when asset-id
(let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data) (:weight font-data) (:style-name font-data)) (let [uri (font-id->ttf-url
(:font-id font-data) asset-id
(:font-variant-id font-data)
(:weight font-data)
(:style-name font-data))
id-buffer (uuid/get-u32 (:wasm-id font-data)) id-buffer (uuid/get-u32 (:wasm-id font-data))
font-data (assoc font-data :family-id-buffer id-buffer) font-data (assoc font-data :family-id-buffer id-buffer)
font-stored? (not= 0 (h/call wasm/internal-module "_is_font_uploaded" font-stored? (font-stored? font-data emoji?)]
(aget id-buffer 0)
(aget id-buffer 1)
(aget id-buffer 2)
(aget id-buffer 3)
(:weight font-data)
(:style font-data)
emoji?))]
(when-not font-stored? (when-not font-stored?
(fetch-font shape-id font-data uri emoji? fallback?))))) (fetch-font font-data uri emoji? fallback?)))))
(defn serialize-font-style (defn serialize-font-style
[font-style] [font-style]
@@ -280,8 +291,8 @@
"regular" "regular"
font-variant-id)) font-variant-id))
(defn store-font (defn make-font-data
[shape-id font] [font]
(let [font-id (get font :font-id) (let [font-id (get font :font-id)
font-variant-id (get font :font-variant-id) font-variant-id (get font :font-variant-id)
normalized-variant-id (when font-variant-id normalized-variant-id (when font-variant-id
@@ -301,14 +312,21 @@
(str/includes? raw-weight "italic") "italic" (str/includes? raw-weight "italic") "italic"
:else font-style-fallback) :else font-style-fallback)
variant-id (or (:id font-data) normalized-variant-id) variant-id (or (:id font-data) normalized-variant-id)
asset-id (font-id->asset-id font-id variant-id raw-weight style) asset-id (font-id->asset-id font-id variant-id raw-weight style)]
font-data {:wasm-id wasm-id {:wasm-id wasm-id
:font-id font-id :font-id font-id
:font-variant-id variant-id :font-variant-id variant-id
:style (serialize-font-style style) :style (serialize-font-style style)
:style-name style :style-name style
:weight weight}] :weight weight
(store-font-id shape-id font-data asset-id emoji? fallback?))) :emoji? emoji?
:fallbck? fallback?
:asset-id asset-id}))
(defn store-font
[font]
(let [{:keys [asset-id emoji? fallback?] :as font-data} (make-font-data font)]
(store-font-id font-data asset-id emoji? fallback?)))
;; FIXME: This is a temporary function to load the fallback fonts for the editor. ;; FIXME: This is a temporary function to load the fallback fonts for the editor.
;; Once we render the editor content within wasm, we can remove this function. ;; Once we render the editor content within wasm, we can remove this function.
@@ -341,8 +359,8 @@
#{})))) #{}))))
(defn store-fonts (defn store-fonts
[shape-id fonts] [fonts]
(keep (fn [font] (store-font shape-id font)) fonts)) (keep (fn [font] (store-font font)) fonts))
(defn add-emoji-font (defn add-emoji-font
[fonts] [fonts]

View File

@@ -1,6 +1,7 @@
{ {
"name": "Penpot MCP Plugin", "name": "Penpot MCP Plugin",
"code": "plugin.js", "code": "plugin.js",
"version": 2,
"description": "This plugin enables interaction with the Penpot MCP server", "description": "This plugin enables interaction with the Penpot MCP server",
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"] "permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
} }

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" } assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/contrast-plugin/browser" } assets = { directory = "../../dist/apps/contrast-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/create-palette-plugin" } assets = { directory = "../../dist/apps/create-palette-plugin" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/icons-plugin/browser" } assets = { directory = "../../dist/apps/icons-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" } assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/rename-layers-plugin/browser" } assets = { directory = "../../dist/apps/rename-layers-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/table-plugin/browser" } assets = { directory = "../../dist/apps/table-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "dist/doc" } assets = { directory = "dist/doc" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
assets = { directory = "dist/apps/example-styles" } assets = { directory = "dist/apps/example-styles" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -346,6 +346,14 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
}); });
} }
#[no_mangle]
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) {
with_state_mut!(state, {
let shape_id = uuid_from_u32_quartet(a, b, c, d);
state.touch_shape(shape_id);
});
}
#[no_mangle] #[no_mangle]
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) { pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) {
with_state_mut!(state, { with_state_mut!(state, {

View File

@@ -1,21 +1,30 @@
use skia_safe::{self as skia}; use skia_safe::{self as skia};
use crate::math::Rect;
use crate::shapes::modifiers::grid_layout::grid_cell_data; use crate::shapes::modifiers::grid_layout::grid_cell_data;
use crate::shapes::Shape; use crate::shapes::Shape;
use crate::state::ShapesPoolRef; use crate::state::ShapesPoolRef;
pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: ShapesPoolRef) { pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: ShapesPoolRef) {
let cells = grid_cell_data(shape, shapes, true); let cells: Vec<crate::shapes::grid_layout::CellData<'_>> = grid_cell_data(shape, shapes, true);
let bounds = shape.bounds();
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(skia::Color::from_rgb(255, 111, 224)); paint.set_color(skia::Color::from_rgb(255, 111, 224));
paint.set_anti_alias(shape.should_use_antialias(zoom));
paint.set_stroke_width(1.0 / zoom); paint.set_stroke_width(1.0 / zoom);
for cell in cells.iter() { for cell in cells.iter() {
let rect = Rect::from_xywh(cell.anchor.x, cell.anchor.y, cell.width, cell.height); let hv = bounds.hv(cell.width);
canvas.draw_rect(rect, &paint); let vv = bounds.vv(cell.height);
let points = [
cell.anchor,
cell.anchor + hv,
cell.anchor + hv + vv,
cell.anchor + vv,
];
let polygon = skia::Path::polygon(&points, true, None, None);
canvas.draw_path(&polygon, &paint);
} }
} }

View File

@@ -2,6 +2,7 @@ use skia_safe::{self as skia, Color4f};
use super::{RenderState, ShapesPoolRef, SurfaceId}; use super::{RenderState, ShapesPoolRef, SurfaceId};
use crate::render::grid_layout; use crate::render::grid_layout;
use crate::shapes::{Layout, Type};
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
let canvas = render_state.surfaces.canvas(SurfaceId::UI); let canvas = render_state.surfaces.canvas(SurfaceId::UI);
@@ -18,12 +19,37 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
let canvas = render_state.surfaces.canvas(SurfaceId::UI); let canvas = render_state.surfaces.canvas(SurfaceId::UI);
if let Some(id) = render_state.show_grid { let show_grid_id = render_state.show_grid;
if let Some(id) = show_grid_id {
if let Some(shape) = shapes.get(&id) { if let Some(shape) = shapes.get(&id) {
grid_layout::render_overlay(zoom, canvas, shape, shapes); grid_layout::render_overlay(zoom, canvas, shape, shapes);
} }
} }
// Render overlays for empty grid frames
for shape in shapes.iter() {
if shape.id.is_nil() || !shape.children.is_empty() {
continue;
}
if show_grid_id == Some(shape.id) {
continue;
}
let Type::Frame(frame) = &shape.shape_type else {
continue;
};
if !matches!(frame.layout, Some(Layout::GridLayout(_, _))) {
continue;
}
if let Some(shape) = shapes.get(&shape.id) {
grid_layout::render_overlay(zoom, canvas, shape, shapes);
}
}
canvas.restore(); canvas.restore();
render_state.surfaces.draw_into( render_state.surfaces.draw_into(
SurfaceId::UI, SurfaceId::UI,

View File

@@ -1074,6 +1074,10 @@ impl Shape {
self.children.first() self.children.first()
} }
pub fn children_count(&self) -> usize {
self.children_ids_iter(false).count()
}
pub fn children_ids(&self, include_hidden: bool) -> Vec<Uuid> { pub fn children_ids(&self, include_hidden: bool) -> Vec<Uuid> {
if include_hidden { if include_hidden {
return self.children.iter().rev().copied().collect(); return self.children.iter().rev().copied().collect();

View File

@@ -264,7 +264,7 @@ fn propagate_transform(
// If this is a layout and we're only moving don't need to reflow // If this is a layout and we're only moving don't need to reflow
if shape.has_layout() && is_resize { if shape.has_layout() && is_resize {
entries.push_back(Modifier::reflow(shape.id)); entries.push_back(Modifier::reflow(shape.id, false));
} }
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
@@ -272,7 +272,7 @@ fn propagate_transform(
// if the current transformation is not a move propagation. // if the current transformation is not a move propagation.
// If it's a move propagation we don't need to reflow, the parent is already changed. // If it's a move propagation we don't need to reflow, the parent is already changed.
if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) { if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) {
entries.push_back(Modifier::reflow(parent.id)); entries.push_back(Modifier::reflow(parent.id, false));
} }
} }
} }
@@ -282,7 +282,7 @@ fn propagate_reflow(
state: &State, state: &State,
entries: &mut VecDeque<Modifier>, entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
layout_reflows: &mut Vec<Uuid>, layout_reflows: &mut HashSet<Uuid>,
reflown: &mut HashSet<Uuid>, reflown: &mut HashSet<Uuid>,
modifiers: &HashMap<Uuid, Matrix>, modifiers: &HashMap<Uuid, Matrix>,
) { ) {
@@ -300,20 +300,7 @@ fn propagate_reflow(
Type::Frame(Frame { Type::Frame(Frame {
layout: Some(_), .. layout: Some(_), ..
}) => { }) => {
let mut skip_reflow = false; layout_reflows.insert(*id);
if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() {
if let Some(parent_id) = shape.parent_id {
if parent_id != Uuid::nil() && !reflown.contains(&parent_id) {
// If this is a fill layout but the parent has not been reflown yet
// we wait for the next iteration for reflow
skip_reflow = true;
}
}
}
if !skip_reflow {
layout_reflows.push(*id);
}
} }
Type::Group(Group { masked: true }) => { Type::Group(Group { masked: true }) => {
let children_ids = shape.children_ids(true); let children_ids = shape.children_ids(true);
@@ -340,7 +327,7 @@ fn propagate_reflow(
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
if parent.has_layout() || parent.is_group_like() { if parent.has_layout() || parent.is_group_like() {
entries.push_back(Modifier::reflow(parent.id)); entries.push_back(Modifier::reflow(parent.id, false));
} }
} }
} }
@@ -382,19 +369,20 @@ pub fn propagate_modifiers(
let mut entries: VecDeque<_> = modifiers let mut entries: VecDeque<_> = modifiers
.iter() .iter()
.map(|entry| { .map(|entry| {
// If we receibe a identity matrix we force a reflow // If we receive a identity matrix we force a reflow
if math::identitish(&entry.transform) { if math::identitish(&entry.transform) {
Modifier::Reflow(entry.id) Modifier::Reflow(entry.id, false)
} else { } else {
Modifier::Transform(*entry) Modifier::Transform(*entry)
} }
}) })
.collect(); .collect();
let shapes = &state.shapes;
let mut modifiers = HashMap::<Uuid, Matrix>::new(); let mut modifiers = HashMap::<Uuid, Matrix>::new();
let mut bounds = HashMap::<Uuid, Bounds>::new(); let mut bounds = HashMap::<Uuid, Bounds>::new();
let mut reflown = HashSet::<Uuid>::new(); let mut reflown = HashSet::<Uuid>::new();
let mut layout_reflows = Vec::<Uuid>::new(); let mut layout_reflows = HashSet::<Uuid>::new();
// We first propagate the transforms to the children and then after // We first propagate the transforms to the children and then after
// recalculate the layouts. The layout can create further transforms that // recalculate the layouts. The layout can create further transforms that
@@ -412,25 +400,43 @@ pub fn propagate_modifiers(
&mut bounds, &mut bounds,
&mut modifiers, &mut modifiers,
), ),
Modifier::Reflow(id) => propagate_reflow( Modifier::Reflow(id, force_reflow) => {
&id, if force_reflow {
state, reflown.remove(&id);
&mut entries, }
&mut bounds,
&mut layout_reflows, propagate_reflow(
&mut reflown, &id,
&modifiers, state,
), &mut entries,
&mut bounds,
&mut layout_reflows,
&mut reflown,
&modifiers,
)
}
} }
} }
for id in layout_reflows.iter() { let mut layout_reflows_vec: Vec<Uuid> = layout_reflows.into_iter().collect();
// We sort the reflows so they are process first the ones that are more
// deep in the tree structure. This way we can be sure that the children layouts
// are already reflowed.
layout_reflows_vec.sort_unstable_by(|id_a, id_b| {
let da = shapes.get_depth(id_a);
let db = shapes.get_depth(id_b);
db.cmp(&da)
});
let mut bounds_temp = bounds.clone();
for id in layout_reflows_vec.iter() {
if reflown.contains(id) { if reflown.contains(id) {
continue; continue;
} }
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds); reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
} }
layout_reflows = Vec::new(); layout_reflows = HashSet::new();
} }
modifiers modifiers

View File

@@ -61,6 +61,7 @@ impl LayoutAxis {
layout_data: &LayoutData, layout_data: &LayoutData,
flex_data: &FlexData, flex_data: &FlexData,
) -> Self { ) -> Self {
let num_child = shape.children_count();
if flex_data.is_row() { if flex_data.is_row() {
Self { Self {
main_size: layout_bounds.width(), main_size: layout_bounds.width(),
@@ -73,8 +74,8 @@ impl LayoutAxis {
padding_across_end: layout_data.padding_bottom, padding_across_end: layout_data.padding_bottom,
gap_main: layout_data.column_gap, gap_main: layout_data.column_gap,
gap_across: layout_data.row_gap, gap_across: layout_data.row_gap,
is_auto_main: shape.is_layout_horizontal_auto(), is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(),
is_auto_across: shape.is_layout_vertical_auto(), is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(),
} }
} else { } else {
Self { Self {
@@ -88,8 +89,8 @@ impl LayoutAxis {
padding_across_end: layout_data.padding_right, padding_across_end: layout_data.padding_right,
gap_main: layout_data.row_gap, gap_main: layout_data.row_gap,
gap_across: layout_data.column_gap, gap_across: layout_data.column_gap,
is_auto_main: shape.is_layout_vertical_auto(), is_auto_main: num_child > 0 && shape.is_layout_vertical_auto(),
is_auto_across: shape.is_layout_horizontal_auto(), is_auto_across: num_child > 0 && shape.is_layout_horizontal_auto(),
} }
} }
} }
@@ -345,7 +346,10 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat
let mut size = let mut size =
track.across_size - child.margin_across_start - child.margin_across_end; track.across_size - child.margin_across_start - child.margin_across_end;
size = size.clamp(child.min_across_size, child.max_across_size); size = size.clamp(child.min_across_size, child.max_across_size);
size = f32::min(size, layout_axis.across_space());
if !layout_axis.is_auto_across {
size = f32::min(size, layout_axis.across_space());
}
child.across_size = size; child.across_size = size;
} }
} }
@@ -620,9 +624,12 @@ pub fn reflow_flex_layout(
let mut transform = Matrix::default(); let mut transform = Matrix::default();
let mut force_reflow = false;
if (new_width - child_bounds.width()).abs() > MIN_SIZE if (new_width - child_bounds.width()).abs() > MIN_SIZE
|| (new_height - child_bounds.height()).abs() > MIN_SIZE || (new_height - child_bounds.height()).abs() > MIN_SIZE
{ {
// When the child is fill we need to force a reflow
force_reflow = true;
transform.post_concat(&math::resize_matrix( transform.post_concat(&math::resize_matrix(
layout_bounds, layout_bounds,
child_bounds, child_bounds,
@@ -637,7 +644,7 @@ pub fn reflow_flex_layout(
result.push_back(Modifier::transform_propagate(child.id, transform)); result.push_back(Modifier::transform_propagate(child.id, transform));
if child.has_layout() { if child.has_layout() {
result.push_back(Modifier::reflow(child.id)); result.push_back(Modifier::reflow(child.id, force_reflow));
} }
shape_anchor = next_anchor( shape_anchor = next_anchor(

View File

@@ -765,9 +765,12 @@ pub fn reflow_grid_layout(
let mut transform = Matrix::default(); let mut transform = Matrix::default();
let mut force_reflow = false;
if (new_width - child_bounds.width()).abs() > MIN_SIZE if (new_width - child_bounds.width()).abs() > MIN_SIZE
|| (new_height - child_bounds.height()).abs() > MIN_SIZE || (new_height - child_bounds.height()).abs() > MIN_SIZE
{ {
// When the child is a fill it needs to be reflown
force_reflow = true;
transform.post_concat(&math::resize_matrix( transform.post_concat(&math::resize_matrix(
&layout_bounds, &layout_bounds,
&child_bounds, &child_bounds,
@@ -793,7 +796,7 @@ pub fn reflow_grid_layout(
result.push_back(Modifier::transform_propagate(child.id, transform)); result.push_back(Modifier::transform_propagate(child.id, transform));
if child.has_layout() { if child.has_layout() {
result.push_back(Modifier::reflow(child.id)); result.push_back(Modifier::reflow(child.id, force_reflow));
} }
} }

View File

@@ -8,7 +8,7 @@ use skia::Matrix;
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum Modifier { pub enum Modifier {
Transform(TransformEntry), Transform(TransformEntry),
Reflow(Uuid), Reflow(Uuid, bool),
} }
impl Modifier { impl Modifier {
@@ -18,8 +18,8 @@ impl Modifier {
pub fn parent(id: Uuid, transform: Matrix) -> Self { pub fn parent(id: Uuid, transform: Matrix) -> Self {
Modifier::Transform(TransformEntry::parent(id, transform)) Modifier::Transform(TransformEntry::parent(id, transform))
} }
pub fn reflow(id: Uuid) -> Self { pub fn reflow(id: Uuid, force_reflow: bool) -> Self {
Modifier::Reflow(id) Modifier::Reflow(id, force_reflow)
} }
} }

View File

@@ -177,6 +177,26 @@ impl ShapesPoolImpl {
} }
} }
// Given an id, returns the depth in the tree-shaped structure
// of shapes.
pub fn get_depth(&self, id: &Uuid) -> usize {
if id == &Uuid::nil() {
return 0;
}
let Some(idx) = self.uuid_to_idx.get(id) else {
return 0;
};
let shape = &self.shapes[*idx];
let Some(parent_id) = shape.parent_id else {
return 0;
};
self.get_depth(&parent_id) + 1
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn iter(&self) -> std::slice::Iter<'_, Shape> { pub fn iter(&self) -> std::slice::Iter<'_, Shape> {
self.shapes.iter() self.shapes.iter()

View File

@@ -31,21 +31,17 @@ impl From<RawFontStyle> for FontStyle {
#[no_mangle] #[no_mangle]
pub extern "C" fn store_font( pub extern "C" fn store_font(
a1: u32, a: u32,
b1: u32, b: u32,
c1: u32, c: u32,
d1: u32, d: u32,
a2: u32,
b2: u32,
c2: u32,
d2: u32,
weight: u32, weight: u32,
style: u8, style: u8,
is_emoji: bool, is_emoji: bool,
is_fallback: bool, is_fallback: bool,
) { ) {
with_state_mut!(state, { with_state_mut!(state, {
let id = uuid_from_u32_quartet(a2, b2, c2, d2); let id = uuid_from_u32_quartet(a, b, c, d);
let font_bytes = mem::bytes(); let font_bytes = mem::bytes();
let font_style = RawFontStyle::from(style); let font_style = RawFontStyle::from(style);
@@ -57,9 +53,6 @@ pub extern "C" fn store_font(
.add(family, &font_bytes, is_emoji, is_fallback); .add(family, &font_bytes, is_emoji, is_fallback);
mem::free_bytes(); mem::free_bytes();
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
state.touch_shape(shape_id);
}); });
} }

View File

@@ -384,6 +384,7 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) {
if let Some(shape) = state.shapes.get_mut(&shape_id) { if let Some(shape) = state.shapes.get_mut(&shape_id) {
update_text_layout(shape); update_text_layout(shape);
} }
state.touch_shape(shape_id);
}); });
} }