mirror of
https://github.com/penpot/penpot.git
synced 2026-02-06 12:42:21 -05:00
Compare commits
15 Commits
fix-ipv6-f
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4f2641cc9 | ||
|
|
989eb12139 | ||
|
|
a5e36dbb3d | ||
|
|
8acd031ab2 | ||
|
|
a7c1de6478 | ||
|
|
184487f568 | ||
|
|
c00d512193 | ||
|
|
af5dbf2fbc | ||
|
|
7c7e32d85f | ||
|
|
2ccb33ba89 | ||
|
|
ee88ee63a2 | ||
|
|
fd3d549f9c | ||
|
|
53c2acb3e6 | ||
|
|
8a72eb64c3 | ||
|
|
1d45ca7019 |
@@ -10,6 +10,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.migrations.clj.migration-0023 :as mg0023]
|
||||
[app.migrations.clj.migration-0145 :as mg0145]
|
||||
[app.util.migrations :as mg]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -459,7 +460,11 @@
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
|
||||
|
||||
{: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!
|
||||
[pool name migrations]
|
||||
|
||||
83
backend/src/app/migrations/clj/migration_0145.clj
Normal file
83
backend/src/app/migrations/clj/migration_0145.clj
Normal 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}))))))))
|
||||
|
||||
@@ -407,17 +407,19 @@
|
||||
(defn change-text
|
||||
"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)"
|
||||
[content text]
|
||||
[content text & {:as styles}]
|
||||
(let [root-styles (select-keys content root-attrs)
|
||||
|
||||
paragraph-style
|
||||
(merge
|
||||
default-text-attrs
|
||||
styles
|
||||
(select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs))
|
||||
|
||||
text-style
|
||||
(merge
|
||||
default-text-attrs
|
||||
styles
|
||||
(select-keys (->> content (node-seq is-text-node?) first) text-all-attrs))
|
||||
|
||||
paragraph-texts
|
||||
|
||||
@@ -1 +1 @@
|
||||
resolver $PENPOT_INTERNAL_RESOLVER valid=10s;
|
||||
resolver $PENPOT_INTERNAL_RESOLVER ipv6=off valid=10s;
|
||||
|
||||
@@ -73,7 +73,6 @@ http {
|
||||
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
listen [::]:8080 default_server;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
|
||||
|
||||
146
frontend/playwright/data/components/get-file-13267.json
Normal file
146
frontend/playwright/data/components/get-file-13267.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,8 +459,8 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
async clickLeafLayer(name, clickOptions = {}) {
|
||||
const layer = this.layers.getByText(name).first();
|
||||
async clickLeafLayer(name, clickOptions = {}, index = 0) {
|
||||
const layer = this.layers.getByText(name).nth(index);
|
||||
await layer.waitFor();
|
||||
await layer.click(clickOptions);
|
||||
await this.page.waitForTimeout(500);
|
||||
@@ -471,10 +471,11 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
}
|
||||
|
||||
async clickToggableLayer(name, clickOptions = {}) {
|
||||
async clickToggableLayer(name, clickOptions = {}, index = 0) {
|
||||
const layer = this.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
.filter({ hasText: name })
|
||||
.nth(index);
|
||||
const button = layer.getByTestId("toggle-content");
|
||||
|
||||
await expect(button).toBeVisible();
|
||||
|
||||
33
frontend/playwright/ui/specs/components.spec.js
Normal file
33
frontend/playwright/ui/specs/components.spec.js
Normal 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");
|
||||
});
|
||||
@@ -179,6 +179,56 @@
|
||||
(map #(get objects %))
|
||||
(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
|
||||
[shape position-data old-shape]
|
||||
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
|
||||
@@ -625,17 +675,6 @@
|
||||
|
||||
(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
|
||||
(parse-geometry-modifiers modif-tree)
|
||||
|
||||
@@ -645,6 +684,17 @@
|
||||
transforms
|
||||
(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
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
(watch [_ state _]
|
||||
(let [page-id (or page-id (:current-page-id state))
|
||||
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)
|
||||
(let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
|
||||
@@ -776,11 +776,7 @@
|
||||
(rx/of (v2-update-text-editor-styles id attrs)))
|
||||
|
||||
(when (features/active-feature? state "render-wasm/v1")
|
||||
;; This delay is to give time for the font to be correctly rendered
|
||||
;; in wasm.
|
||||
(cond->> (rx/of (dwwt/resize-wasm-text id))
|
||||
(contains? attrs :font-id)
|
||||
(rx/delay 200)))))))
|
||||
(rx/of (dwwt/resize-wasm-text-debounce id)))))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
This exists to avoid circular deps:
|
||||
workspace.texts -> workspace.libraries -> workspace.texts"
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
@@ -17,6 +18,7 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.api.fonts :as wasm.fonts]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@@ -62,6 +64,84 @@
|
||||
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
|
||||
(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
|
||||
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
||||
[ids]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
(def ^:private schema:icon-button
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:tooltip-class {:optional true} [:maybe :string]]
|
||||
[:icon-class {:optional true} :string]
|
||||
[:icon
|
||||
[:and :string [:fn #(contains? icon-list %)]]]
|
||||
@@ -28,7 +29,7 @@
|
||||
(mf/defc icon-button*
|
||||
{::mf/schema schema:icon-button
|
||||
::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
|
||||
(d/nilv variant "primary")
|
||||
|
||||
@@ -49,6 +50,7 @@
|
||||
:aria-labelledby tooltip-id})]
|
||||
|
||||
[:> tooltip* {:content aria-label
|
||||
:class tooltip-class
|
||||
:placement tooltip-placement
|
||||
:id tooltip-id}
|
||||
[:> :button props
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
[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.foundations.assets.icon :refer [icon* icon-list] :as i]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -638,27 +637,17 @@
|
||||
:on-change store-raw-value
|
||||
:variant "comfortable"
|
||||
:disabled disabled
|
||||
:slot-start (when (or icon text-icon)
|
||||
:icon icon
|
||||
:aria-label property
|
||||
:slot-start (when text-icon
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
: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])]))
|
||||
[:div {:class (stl/css :text-icon)}
|
||||
text-icon]))
|
||||
:slot-end (when-not disabled
|
||||
(when (some? tokens)
|
||||
(mf/html [:> icon-button* {:variant "ghost"
|
||||
:icon i/tokens
|
||||
:tooltip-class (stl/css :button-tooltip)
|
||||
:class (stl/css :invisible-button)
|
||||
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
|
||||
:ref open-dropdown-ref
|
||||
@@ -686,23 +675,19 @@
|
||||
:disabled disabled
|
||||
:on-blur on-blur
|
||||
:class inner-class
|
||||
:property property
|
||||
:slot-start (when (or icon text-icon)
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:class (stl/css :icon)}]
|
||||
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)
|
||||
:aria-labelledby property}
|
||||
text-icon])]))
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)}
|
||||
text-icon])))
|
||||
:token-wrapper-ref token-wrapper-ref
|
||||
:token-detach-btn-ref token-detach-btn-ref
|
||||
:detach-token detach-token})))]
|
||||
@@ -737,40 +722,21 @@
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
(if (some? icon)
|
||||
[:div {:class [class (stl/css :input-wrapper)]
|
||||
:ref wrapper-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])
|
||||
(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}]))]
|
||||
[: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}]))])))
|
||||
(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}]))]))
|
||||
|
||||
@@ -55,3 +55,8 @@
|
||||
--opacity-button: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button-tooltip {
|
||||
inline-size: var($sz-28);
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
type (d/nilv type "text")
|
||||
variant (d/nilv variant "dense")
|
||||
tooltip-id (mf/use-id)
|
||||
|
||||
props (mf/spread-props props
|
||||
{:class [class
|
||||
(stl/css-case
|
||||
@@ -54,15 +53,11 @@
|
||||
"true")
|
||||
:aria-describedby (when has-hint
|
||||
(str id "-hint"))
|
||||
:aria-labelledby tooltip-id
|
||||
:type (d/nilv type "text")
|
||||
:id id
|
||||
: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
|
||||
:has-hint has-hint
|
||||
:hint-type-hint (= hint-type "hint")
|
||||
@@ -83,11 +78,14 @@
|
||||
(when (some? slot-start)
|
||||
slot-start)
|
||||
(when (some? icon)
|
||||
(if aria-label
|
||||
[:> tooltip* {:content aria-label
|
||||
:id tooltip-id}
|
||||
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}]]
|
||||
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}]))
|
||||
[:> "input" props]
|
||||
[:> icon* {:icon-id icon
|
||||
:class (stl/css :icon)
|
||||
:size "s"
|
||||
:on-click on-icon-click}])
|
||||
(if aria-label
|
||||
[:> tooltip* {:content aria-label
|
||||
:id tooltip-id}
|
||||
[:> "input" props]]
|
||||
[:> "input" props])
|
||||
(when (some? slot-end)
|
||||
slot-end)]))
|
||||
|
||||
@@ -118,4 +118,5 @@
|
||||
|
||||
.icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
min-inline-size: var(--sp-l);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:id {:optional true} [:maybe :string]]
|
||||
[:label {:optional true} [:maybe :string]]
|
||||
[:property {:optional true} [:maybe :string]]
|
||||
[:value :any]
|
||||
[:disabled {:optional true} :boolean]
|
||||
[:slot-start {:optional true} [:maybe some?]]
|
||||
@@ -35,7 +36,7 @@
|
||||
{::mf/schema schema:token-field}
|
||||
[{:keys [id label value slot-start disabled class
|
||||
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)
|
||||
content (if set-active?
|
||||
label
|
||||
@@ -50,37 +51,42 @@
|
||||
(when-not ^boolean disabled
|
||||
(dom/prevent-default event)
|
||||
(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
|
||||
: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)
|
||||
|
||||
(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
|
||||
: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)}])]]
|
||||
|
||||
(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}])]))
|
||||
(when-not ^boolean disabled
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:class (stl/css :invisible-button)
|
||||
:tooltip-class (stl/css :button-tooltip)
|
||||
:icon i/broken-link
|
||||
:ref token-detach-btn-ref
|
||||
:aria-label (tr "ds.inputs.token-field.detach-token")
|
||||
:on-click detach-token}])]]))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/colors.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.token-field {
|
||||
--token-field-bg-color: var(--color-background-tertiary);
|
||||
@@ -37,6 +38,9 @@
|
||||
--token-field-outline-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
.token-field-wrapper {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.with-icon {
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -132,3 +136,12 @@
|
||||
--opacity-button: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.button-tooltip {
|
||||
inline-size: px2rem(28);
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
|
||||
(def ^:private schema:tooltip
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:class {:optional true} [:maybe :string]]
|
||||
[:id {:optional true} :string]
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
@@ -184,6 +184,7 @@
|
||||
[{:keys [class id children content placement offset delay] :rest props}]
|
||||
(let [internal-id
|
||||
(mf/use-id)
|
||||
trigger-ref (mf/use-ref nil)
|
||||
|
||||
id
|
||||
(d/nilv id internal-id)
|
||||
@@ -204,19 +205,23 @@
|
||||
(mf/use-fn
|
||||
(mf/deps id placement offset)
|
||||
(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
|
||||
(fn []
|
||||
(let [new-placement (update-tooltip-position tooltip placement origin-brect offset)]
|
||||
(when (not= new-placement placement)
|
||||
(reset! placement* new-placement))))]
|
||||
(let [current (dom/get-current-target event)
|
||||
related (dom/get-related-target event)
|
||||
is-node? (fn [node] (and node (.-nodeType node)))]
|
||||
(when-not (and related (is-node? related) (.contains current related))
|
||||
(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
|
||||
(mf/use-fn
|
||||
@@ -252,6 +257,7 @@
|
||||
:on-focus on-show
|
||||
:on-blur on-hide
|
||||
:on-key-down handle-key-down
|
||||
:ref trigger-ref
|
||||
:class [class (stl/css :tooltip-trigger)]
|
||||
:aria-describedby id})
|
||||
content
|
||||
|
||||
@@ -95,10 +95,10 @@
|
||||
[]
|
||||
|
||||
(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 @plugin-url*
|
||||
plugin-url* (mf/use-state "")
|
||||
plugin-url (deref plugin-url*)
|
||||
|
||||
fetching-manifest? (mf/use-state false)
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@
|
||||
|
||||
on-drop
|
||||
(mf/use-fn
|
||||
(mf/deps id index objects expanded? selected)
|
||||
(mf/deps id objects expanded? selected)
|
||||
(fn [side _data]
|
||||
(let [single? (= (count selected) 1)
|
||||
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 (get objects parent-id)
|
||||
parent (get objects parent-id)
|
||||
current-index (d/index-of (:shapes parent) id)
|
||||
|
||||
to-index (cond
|
||||
(= side :center) 0
|
||||
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
|
||||
(= side :top) (inc index)
|
||||
:else index)]
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))))
|
||||
;; target not found in parent (while lazy loading)
|
||||
(neg? current-index) nil
|
||||
(= side :top) (inc current-index)
|
||||
:else current-index)]
|
||||
(when (some? to-index)
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
|
||||
|
||||
on-hold
|
||||
(mf/use-fn
|
||||
@@ -417,11 +421,7 @@
|
||||
current @children-count*
|
||||
new-count (min total (max current chunk-size min-count))]
|
||||
(reset! children-count* new-count))
|
||||
(reset! children-count* 0)))
|
||||
(fn []
|
||||
(when-let [obs ^js @observer-var]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))))
|
||||
(reset! children-count* 0))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
;; and (shapes item) to reconnect observer after shape changes
|
||||
@@ -502,4 +502,4 @@
|
||||
:component-child? component-tree?}])))
|
||||
(when (< children-count (count (:shapes item)))
|
||||
[:div {:ref lazy-ref
|
||||
:style {:min-height 1}}])])]))
|
||||
:class (stl/css :lazy-load-sentinel)}])])]))
|
||||
|
||||
@@ -298,3 +298,11 @@
|
||||
.filtered {
|
||||
min-inline-size: $sz-12;
|
||||
}
|
||||
.lazy-load-sentinel {
|
||||
min-height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.lazy-load-sentinel {
|
||||
min-height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -521,8 +521,7 @@
|
||||
[:& filters-tree {:objects filtered-objects
|
||||
:key (dm/str (:id page))
|
||||
:parent-size size-parent}]
|
||||
[:div {:ref lazy-load-ref
|
||||
:style {:min-height 16}}]]
|
||||
[:div {:ref lazy-load-ref}]]
|
||||
[:div {:on-scroll on-scroll
|
||||
:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
|
||||
@@ -541,7 +541,8 @@
|
||||
:value (get values :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]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:no-validate true
|
||||
|
||||
@@ -75,32 +75,31 @@
|
||||
[{:keys [points] :as shape} zoom grid-edition?]
|
||||
(let [leftmost (->> points (reduce left?))
|
||||
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)
|
||||
left-top-angle (gpt/angle left-top)
|
||||
top-right (gpt/to-vec topmost rightmost)
|
||||
top-right-angle (gpt/angle top-right)
|
||||
|
||||
top-right (gpt/to-vec topmost rightmost)
|
||||
top-right-angle (gpt/angle top-right)
|
||||
;; Choose the position that creates the less angle between left-side and top-side
|
||||
[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
|
||||
[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)])
|
||||
delta-x (if grid-edition? 40 0)
|
||||
delta-y (if grid-edition? 50 10)
|
||||
|
||||
delta-x (if grid-edition? 40 0)
|
||||
delta-y (if grid-edition? 50 10)
|
||||
|
||||
label-pos
|
||||
(-> label-pos
|
||||
(gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom)))
|
||||
(gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))]
|
||||
|
||||
(dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)"
|
||||
;; rotate
|
||||
angle (:x label-pos) (:y label-pos)
|
||||
;; scale
|
||||
(/ 1 zoom) (/ 1 zoom)
|
||||
;; translate
|
||||
(* zoom (:x label-pos)) (* zoom (:y label-pos)))))
|
||||
label-pos
|
||||
(-> label-pos
|
||||
(gpt/subtract (gpt/scale (gpt/unit v-pos) (/ delta-y zoom)))
|
||||
(gpt/subtract (gpt/scale (gpt/unit h-pos) (/ delta-x zoom))))]
|
||||
(dm/fmt "rotate(% %,%) scale(%, %) translate(%, %)"
|
||||
;; rotate
|
||||
angle (:x label-pos) (:y label-pos)
|
||||
;; scale
|
||||
(/ 1 zoom) (/ 1 zoom)
|
||||
;; translate
|
||||
(* zoom (:x label-pos)) (* zoom (:y label-pos)))))))
|
||||
|
||||
@@ -705,8 +705,8 @@
|
||||
[:& grid-layout/editor
|
||||
{:zoom zoom
|
||||
:objects objects-modified
|
||||
:shape (or (get base-objects edition)
|
||||
(get base-objects @hover-top-frame-id))
|
||||
:shape (or (get objects-modified edition)
|
||||
(get objects-modified @hover-top-frame-id))
|
||||
:view-only (not show-grid-editor?)}])]
|
||||
|
||||
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[app.main.data.workspace.groups :as dwg]
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.fonts :refer [fetch-font-css]]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
@@ -348,9 +349,14 @@
|
||||
|
||||
:else
|
||||
(let [page (dsh/lookup-page @st/state)
|
||||
shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
|
||||
(update :content txt/change-text text)
|
||||
(assoc :position-data nil))
|
||||
shape (-> (cts/setup-shape {:type :text
|
||||
:x 0 :y 0
|
||||
: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
|
||||
(-> (cb/empty-changes)
|
||||
@@ -358,9 +364,10 @@
|
||||
(cb/with-objects (:objects page))
|
||||
(cb/add-object shape))]
|
||||
|
||||
(st/emit!
|
||||
(ch/commit-changes changes)
|
||||
(se/event plugin-id "create-shape" :type :text))
|
||||
(st/emit! (ch/commit-changes changes)
|
||||
(se/event plugin-id "create-shape" :type :text)
|
||||
(dwwt/resize-wasm-text-debounce (:id shape)))
|
||||
|
||||
(shape/shape-proxy plugin-id (:id shape)))))
|
||||
|
||||
:createShapeFromSvg
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
desc (obj/get manifest "description")
|
||||
code (obj/get manifest "code")
|
||||
icon (obj/get manifest "icon")
|
||||
vers (d/nilv (obj/get manifest "version") 1)
|
||||
|
||||
permissions (into #{} (obj/get manifest "permissions" []))
|
||||
permissions
|
||||
@@ -55,9 +56,13 @@
|
||||
(u/uri plugin-url)
|
||||
|
||||
origin
|
||||
(-> plugin-url
|
||||
(u/join ".")
|
||||
(str))
|
||||
(if (= vers 1)
|
||||
(-> plugin-url
|
||||
(assoc :path "/")
|
||||
(str))
|
||||
(-> plugin-url
|
||||
(u/join ".")
|
||||
(str)))
|
||||
|
||||
prev-plugin
|
||||
(->> (:data @registry)
|
||||
|
||||
@@ -190,11 +190,13 @@
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
(let [dimensions (get-text-dimensions id)
|
||||
page-id (:current-page-id @st/state)]
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id page-id
|
||||
:shape-id id
|
||||
:dimensions dimensions}))))
|
||||
|
||||
|
||||
(defn- ensure-text-content
|
||||
@@ -865,12 +867,12 @@
|
||||
|
||||
(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)
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts shape-id all-fonts)]
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts all-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))
|
||||
|
||||
(defn set-shape-grow-type
|
||||
@@ -1564,7 +1566,7 @@
|
||||
:text-decoration (get element :text-decoration)
|
||||
:letter-spacing (get element :letter-spacing)
|
||||
:font-style (get element :font-style)
|
||||
:fills (get element :fills)
|
||||
:fills (d/nilv (get element :fills) [{:fill-color "#000000"}])
|
||||
:text text}))))))]
|
||||
(mem/free)
|
||||
|
||||
|
||||
@@ -97,9 +97,8 @@
|
||||
|
||||
;; IMPORTANT: Only TTF fonts can be stored.
|
||||
(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)
|
||||
shape-id-buffer (uuid/get-u32 shape-id)
|
||||
size (.-byteLength font-array-buffer)
|
||||
ptr (h/call wasm/internal-module "_alloc_bytes" size)
|
||||
heap (gobj/get ^js wasm/internal-module "HEAPU8")
|
||||
@@ -107,10 +106,6 @@
|
||||
|
||||
(.set mem (js/Uint8Array. font-array-buffer))
|
||||
(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 1)
|
||||
(aget font-id-buffer 2)
|
||||
@@ -119,24 +114,31 @@
|
||||
(:style font-data)
|
||||
emoji?
|
||||
fallback?)
|
||||
|
||||
(update-text-layout shape-id)
|
||||
|
||||
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
|
||||
[shape-id font-data font-url emoji? fallback?]
|
||||
{:key font-url
|
||||
:callback #(->> (http/send! {:method :get
|
||||
:uri font-url
|
||||
:response-type :buffer})
|
||||
(rx/map (fn [{:keys [body]}]
|
||||
(store-font-buffer shape-id font-data body emoji? fallback?)))
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "Could not fetch font"
|
||||
:font-url font-url
|
||||
:cause cause)
|
||||
(rx/empty))))})
|
||||
[font-data font-url emoji? fallback?]
|
||||
(when-not (contains? @fetching font-url)
|
||||
(swap! fetching conj font-url)
|
||||
{:key font-url
|
||||
:callback
|
||||
(fn []
|
||||
(->> (http/send! {:method :get
|
||||
:uri font-url
|
||||
:response-type :buffer})
|
||||
(rx/map (fn [{:keys [body]}]
|
||||
(swap! fetching disj font-url)
|
||||
(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
|
||||
[font-id font-variant-id font-weight font-style]
|
||||
@@ -155,22 +157,31 @@
|
||||
:builtin
|
||||
(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
|
||||
[shape-id font-data asset-id emoji? fallback?]
|
||||
[font-data asset-id emoji? fallback?]
|
||||
(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))
|
||||
font-data (assoc font-data :family-id-buffer id-buffer)
|
||||
font-stored? (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?))]
|
||||
font-stored? (font-stored? font-data emoji?)]
|
||||
(when-not font-stored?
|
||||
(fetch-font shape-id font-data uri emoji? fallback?)))))
|
||||
(fetch-font font-data uri emoji? fallback?)))))
|
||||
|
||||
(defn serialize-font-style
|
||||
[font-style]
|
||||
@@ -280,8 +291,8 @@
|
||||
"regular"
|
||||
font-variant-id))
|
||||
|
||||
(defn store-font
|
||||
[shape-id font]
|
||||
(defn make-font-data
|
||||
[font]
|
||||
(let [font-id (get font :font-id)
|
||||
font-variant-id (get font :font-variant-id)
|
||||
normalized-variant-id (when font-variant-id
|
||||
@@ -301,14 +312,21 @@
|
||||
(str/includes? raw-weight "italic") "italic"
|
||||
:else font-style-fallback)
|
||||
variant-id (or (:id font-data) normalized-variant-id)
|
||||
asset-id (font-id->asset-id font-id variant-id raw-weight style)
|
||||
font-data {:wasm-id wasm-id
|
||||
:font-id font-id
|
||||
:font-variant-id variant-id
|
||||
:style (serialize-font-style style)
|
||||
:style-name style
|
||||
:weight weight}]
|
||||
(store-font-id shape-id font-data asset-id emoji? fallback?)))
|
||||
asset-id (font-id->asset-id font-id variant-id raw-weight style)]
|
||||
{:wasm-id wasm-id
|
||||
:font-id font-id
|
||||
:font-variant-id variant-id
|
||||
:style (serialize-font-style style)
|
||||
:style-name style
|
||||
:weight weight
|
||||
: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.
|
||||
;; Once we render the editor content within wasm, we can remove this function.
|
||||
@@ -341,8 +359,8 @@
|
||||
#{}))))
|
||||
|
||||
(defn store-fonts
|
||||
[shape-id fonts]
|
||||
(keep (fn [font] (store-font shape-id font)) fonts))
|
||||
[fonts]
|
||||
(keep (fn [font] (store-font font)) fonts))
|
||||
|
||||
(defn add-emoji-font
|
||||
[fonts]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Penpot MCP Plugin",
|
||||
"code": "plugin.js",
|
||||
"version": 2,
|
||||
"description": "This plugin enables interaction with the Penpot MCP server",
|
||||
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -3,6 +3,21 @@ compatibility_date = "2025-01-01"
|
||||
|
||||
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]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
|
||||
@@ -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]
|
||||
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) {
|
||||
with_state_mut!(state, {
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
use skia_safe::{self as skia};
|
||||
|
||||
use crate::math::Rect;
|
||||
use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
use crate::shapes::Shape;
|
||||
use crate::state::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();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
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);
|
||||
|
||||
for cell in cells.iter() {
|
||||
let rect = Rect::from_xywh(cell.anchor.x, cell.anchor.y, cell.width, cell.height);
|
||||
canvas.draw_rect(rect, &paint);
|
||||
let hv = bounds.hv(cell.width);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use skia_safe::{self as skia, Color4f};
|
||||
|
||||
use super::{RenderState, ShapesPoolRef, SurfaceId};
|
||||
use crate::render::grid_layout;
|
||||
use crate::shapes::{Layout, Type};
|
||||
|
||||
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
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();
|
||||
render_state.surfaces.draw_into(
|
||||
SurfaceId::UI,
|
||||
|
||||
@@ -1074,6 +1074,10 @@ impl Shape {
|
||||
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> {
|
||||
if include_hidden {
|
||||
return self.children.iter().rev().copied().collect();
|
||||
|
||||
@@ -264,7 +264,7 @@ fn propagate_transform(
|
||||
|
||||
// If this is a layout and we're only moving don't need to reflow
|
||||
if shape.has_layout() && is_resize {
|
||||
entries.push_back(Modifier::reflow(shape.id));
|
||||
entries.push_back(Modifier::reflow(shape.id, false));
|
||||
}
|
||||
|
||||
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 it's a move propagation we don't need to reflow, the parent is already changed.
|
||||
if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) {
|
||||
entries.push_back(Modifier::reflow(parent.id));
|
||||
entries.push_back(Modifier::reflow(parent.id, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,7 +282,7 @@ fn propagate_reflow(
|
||||
state: &State,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
layout_reflows: &mut Vec<Uuid>,
|
||||
layout_reflows: &mut HashSet<Uuid>,
|
||||
reflown: &mut HashSet<Uuid>,
|
||||
modifiers: &HashMap<Uuid, Matrix>,
|
||||
) {
|
||||
@@ -300,20 +300,7 @@ fn propagate_reflow(
|
||||
Type::Frame(Frame {
|
||||
layout: Some(_), ..
|
||||
}) => {
|
||||
let mut skip_reflow = false;
|
||||
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);
|
||||
}
|
||||
layout_reflows.insert(*id);
|
||||
}
|
||||
Type::Group(Group { masked: 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 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
|
||||
.iter()
|
||||
.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) {
|
||||
Modifier::Reflow(entry.id)
|
||||
Modifier::Reflow(entry.id, false)
|
||||
} else {
|
||||
Modifier::Transform(*entry)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let shapes = &state.shapes;
|
||||
let mut modifiers = HashMap::<Uuid, Matrix>::new();
|
||||
let mut bounds = HashMap::<Uuid, Bounds>::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
|
||||
// recalculate the layouts. The layout can create further transforms that
|
||||
@@ -412,25 +400,43 @@ pub fn propagate_modifiers(
|
||||
&mut bounds,
|
||||
&mut modifiers,
|
||||
),
|
||||
Modifier::Reflow(id) => propagate_reflow(
|
||||
&id,
|
||||
state,
|
||||
&mut entries,
|
||||
&mut bounds,
|
||||
&mut layout_reflows,
|
||||
&mut reflown,
|
||||
&modifiers,
|
||||
),
|
||||
Modifier::Reflow(id, force_reflow) => {
|
||||
if force_reflow {
|
||||
reflown.remove(&id);
|
||||
}
|
||||
|
||||
propagate_reflow(
|
||||
&id,
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -61,6 +61,7 @@ impl LayoutAxis {
|
||||
layout_data: &LayoutData,
|
||||
flex_data: &FlexData,
|
||||
) -> Self {
|
||||
let num_child = shape.children_count();
|
||||
if flex_data.is_row() {
|
||||
Self {
|
||||
main_size: layout_bounds.width(),
|
||||
@@ -73,8 +74,8 @@ impl LayoutAxis {
|
||||
padding_across_end: layout_data.padding_bottom,
|
||||
gap_main: layout_data.column_gap,
|
||||
gap_across: layout_data.row_gap,
|
||||
is_auto_main: shape.is_layout_horizontal_auto(),
|
||||
is_auto_across: shape.is_layout_vertical_auto(),
|
||||
is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(),
|
||||
is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
@@ -88,8 +89,8 @@ impl LayoutAxis {
|
||||
padding_across_end: layout_data.padding_right,
|
||||
gap_main: layout_data.row_gap,
|
||||
gap_across: layout_data.column_gap,
|
||||
is_auto_main: shape.is_layout_vertical_auto(),
|
||||
is_auto_across: shape.is_layout_horizontal_auto(),
|
||||
is_auto_main: num_child > 0 && shape.is_layout_vertical_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 =
|
||||
track.across_size - child.margin_across_start - child.margin_across_end;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -620,9 +624,12 @@ pub fn reflow_flex_layout(
|
||||
|
||||
let mut transform = Matrix::default();
|
||||
|
||||
let mut force_reflow = false;
|
||||
if (new_width - child_bounds.width()).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(
|
||||
layout_bounds,
|
||||
child_bounds,
|
||||
@@ -637,7 +644,7 @@ pub fn reflow_flex_layout(
|
||||
|
||||
result.push_back(Modifier::transform_propagate(child.id, transform));
|
||||
if child.has_layout() {
|
||||
result.push_back(Modifier::reflow(child.id));
|
||||
result.push_back(Modifier::reflow(child.id, force_reflow));
|
||||
}
|
||||
|
||||
shape_anchor = next_anchor(
|
||||
|
||||
@@ -765,9 +765,12 @@ pub fn reflow_grid_layout(
|
||||
|
||||
let mut transform = Matrix::default();
|
||||
|
||||
let mut force_reflow = false;
|
||||
if (new_width - child_bounds.width()).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(
|
||||
&layout_bounds,
|
||||
&child_bounds,
|
||||
@@ -793,7 +796,7 @@ pub fn reflow_grid_layout(
|
||||
|
||||
result.push_back(Modifier::transform_propagate(child.id, transform));
|
||||
if child.has_layout() {
|
||||
result.push_back(Modifier::reflow(child.id));
|
||||
result.push_back(Modifier::reflow(child.id, force_reflow));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use skia::Matrix;
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Modifier {
|
||||
Transform(TransformEntry),
|
||||
Reflow(Uuid),
|
||||
Reflow(Uuid, bool),
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
@@ -18,8 +18,8 @@ impl Modifier {
|
||||
pub fn parent(id: Uuid, transform: Matrix) -> Self {
|
||||
Modifier::Transform(TransformEntry::parent(id, transform))
|
||||
}
|
||||
pub fn reflow(id: Uuid) -> Self {
|
||||
Modifier::Reflow(id)
|
||||
pub fn reflow(id: Uuid, force_reflow: bool) -> Self {
|
||||
Modifier::Reflow(id, force_reflow)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, Shape> {
|
||||
self.shapes.iter()
|
||||
|
||||
@@ -31,21 +31,17 @@ impl From<RawFontStyle> for FontStyle {
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn store_font(
|
||||
a1: u32,
|
||||
b1: u32,
|
||||
c1: u32,
|
||||
d1: u32,
|
||||
a2: u32,
|
||||
b2: u32,
|
||||
c2: u32,
|
||||
d2: u32,
|
||||
a: u32,
|
||||
b: u32,
|
||||
c: u32,
|
||||
d: u32,
|
||||
weight: u32,
|
||||
style: u8,
|
||||
is_emoji: bool,
|
||||
is_fallback: bool,
|
||||
) {
|
||||
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_style = RawFontStyle::from(style);
|
||||
|
||||
@@ -57,9 +53,6 @@ pub extern "C" fn store_font(
|
||||
.add(family, &font_bytes, is_emoji, is_fallback);
|
||||
|
||||
mem::free_bytes();
|
||||
|
||||
let shape_id = uuid_from_u32_quartet(a1, b1, c1, d1);
|
||||
state.touch_shape(shape_id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
update_text_layout(shape);
|
||||
}
|
||||
state.touch_shape(shape_id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user