mirror of
https://github.com/penpot/penpot.git
synced 2026-02-05 04:02:03 -05:00
Compare commits
14 Commits
eva-create
...
alotor-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea023c244e | ||
|
|
b7c7c1c4fe | ||
|
|
bc16b8ddc3 | ||
|
|
b07c98faa5 | ||
|
|
25aff100cf | ||
|
|
5be887f10b | ||
|
|
f7403935c8 | ||
|
|
79be3ab7df | ||
|
|
629649aca6 | ||
|
|
cc326f23cf | ||
|
|
2c4efc6b53 | ||
|
|
4d5c874b91 | ||
|
|
e3b97638b4 | ||
|
|
daedc660b9 |
@@ -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,4 +1,5 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import { platform } from "os";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
@@ -6,6 +7,10 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
const userAgent = platform === 'darwin' ?
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" :
|
||||
undefined;
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
@@ -43,12 +48,20 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: "default",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
testDir: "./playwright/ui/specs",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
|
||||
video: 'retain-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
}
|
||||
userAgent,
|
||||
},
|
||||
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.001,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ds",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"~:file-id": "~u8d38942d-b01f-800e-8007-79ee6a9bac45",
|
||||
"~:tag": "component",
|
||||
"~:object-id": "8d38942d-b01f-800e-8007-79ee6a9bac45/8d38942d-b01f-800e-8007-79ee6a9bac46/6b68aedd-4c5b-80b9-8007-7b38c1d34ce4/component",
|
||||
"~:media-id": "~ube2dc82e-615b-486b-a193-8768bdb51d7a",
|
||||
"~:created-at": "~m1769523563389"
|
||||
}
|
||||
@@ -253,7 +253,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
|
||||
async #waitForWebSocketReadiness() {
|
||||
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
|
||||
await expect(this.pageName).toHaveText("Page 1");
|
||||
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 })
|
||||
}
|
||||
|
||||
async sendPresenceMessage(fixture) {
|
||||
@@ -383,19 +383,46 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
await this.clickAndMove(x1, y1, x2, y2);
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
await expect(this.page.getByTestId("text-editor")).toBeVisible();
|
||||
|
||||
if (initialText) {
|
||||
await this.page.keyboard.type(initialText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the selected element into the clipboard.
|
||||
* Copies the selected element into the clipboard, or copy the
|
||||
* content of the locator into the clipboard.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async copy() {
|
||||
return this.page.keyboard.press("Control+C");
|
||||
async copy(kind = "keyboard", locator = undefined) {
|
||||
if (kind === "context-menu" && locator) {
|
||||
await locator.click({ button: "right" });
|
||||
await this.page.getByText("Copy", { exact: true }).click();
|
||||
} else {
|
||||
await this.page.keyboard.press("ControlOrMeta+C");
|
||||
}
|
||||
// wait for the clipboard to be updated
|
||||
await this.page.waitForFunction(async () => {
|
||||
const content = await navigator.clipboard.readText()
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
}
|
||||
|
||||
async cut(kind = "keyboard", locator = undefined) {
|
||||
if (kind === "context-menu" && locator) {
|
||||
await locator.click({ button: "right" });
|
||||
await this.page.getByText("Cut", { exact: true }).click();
|
||||
} else {
|
||||
await this.page.keyboard.press("ControlOrMeta+X");
|
||||
}
|
||||
// wait for the clipboard to be updated
|
||||
await this.page.waitForFunction(async () => {
|
||||
const content = await navigator.clipboard.readText()
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,9 +434,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
async paste(kind = "keyboard") {
|
||||
if (kind === "context-menu") {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("PasteCtrlV").click();
|
||||
return this.page.getByText("Paste", { exact: true }).click();
|
||||
}
|
||||
return this.page.keyboard.press("Control+V");
|
||||
return this.page.keyboard.press("ControlOrMeta+V");
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
@@ -448,11 +475,11 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
const layer = this.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
const button = layer.getByRole("button");
|
||||
const button = layer.getByTestId("toggle-content");
|
||||
|
||||
await button.waitFor();
|
||||
await expect(button).toBeVisible();
|
||||
await button.click(clickOptions);
|
||||
await this.page.waitForTimeout(500);
|
||||
await button.waitFor({ ariaExpanded: true });
|
||||
}
|
||||
|
||||
async expectSelectedLayer(name) {
|
||||
@@ -495,13 +522,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
|
||||
async clickColorPalette(clickOptions = {}) {
|
||||
await this.palette
|
||||
.getByRole("button", { name: "Color Palette (Alt+P)" })
|
||||
.click(clickOptions);
|
||||
}
|
||||
|
||||
async clickColorPalette(clickOptions = {}) {
|
||||
await this.palette
|
||||
.getByRole("button", { name: "Color Palette (Alt+P)" })
|
||||
.getByRole("button", { name: /Color Palette/ })
|
||||
.click(clickOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ test("User can complete the onboarding", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
const onboardingPage = new OnboardingPage(page);
|
||||
|
||||
await dashboardPage.mockConfigFlags(["enable-onboarding"]);
|
||||
|
||||
await dashboardPage.goToDashboard();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Help us get to know you" }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage, WASM_FLAGS } from "../pages/WasmWorkspacePage";
|
||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
const timeToWait = 100;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
||||
|
||||
await WorkspacePage.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
|
||||
import { Clipboard } from "../../helpers/Clipboard";
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ALL);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
|
||||
});
|
||||
|
||||
test.afterEach(async ({ context }) => {
|
||||
context.clearPermissions();
|
||||
});
|
||||
|
||||
const setupVariantsFile = async (workspacePage) => {
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
@@ -34,9 +41,9 @@ const setupVariantsFileWithVariant = async (workspacePage) => {
|
||||
await setupVariantsFile(workspacePage);
|
||||
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
// We wait until layer-row starts looking like it an component
|
||||
@@ -156,7 +163,7 @@ test("User duplicates a variant container", async ({ page }) => {
|
||||
await variant.container.click();
|
||||
|
||||
//Duplicate the variant container
|
||||
await workspacePage.page.keyboard.press("Control+d");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+d");
|
||||
|
||||
const variant_original = await findVariant(workspacePage, 1); // On duplicate, the new item is the first
|
||||
const variant_duplicate = await findVariant(workspacePage, 0);
|
||||
@@ -169,25 +176,27 @@ test("User duplicates a variant container", async ({ page }) => {
|
||||
await validateVariant(variant_duplicate);
|
||||
});
|
||||
|
||||
test("User copy paste a variant container", async ({ page }) => {
|
||||
test("User copy paste a variant container", async ({ page, context }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
// Access to the read/write clipboard necesary for this functionality
|
||||
await setupVariantsFileWithVariant(workspacePage);
|
||||
await workspacePage.mockRPC(
|
||||
/create-file-object-thumbnail.*/,
|
||||
"workspace/create-file-object-thumbnail.json",
|
||||
);
|
||||
|
||||
const variant = findVariantNoWait(workspacePage, 0);
|
||||
|
||||
// await variant.container.waitFor();
|
||||
|
||||
// Select the variant container
|
||||
await variant.container.click();
|
||||
|
||||
await workspacePage.page.waitForTimeout(1000);
|
||||
|
||||
// Copy the variant container
|
||||
await workspacePage.page.keyboard.press("Control+c");
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
await workspacePage.copy("keyboard");
|
||||
|
||||
// Paste the variant container
|
||||
await workspacePage.clickAt(400, 400);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const variants = workspacePage.layers.getByText("Rectangle");
|
||||
await expect(variants).toHaveCount(2);
|
||||
|
||||
const variantDuplicate = findVariantNoWait(workspacePage, 0);
|
||||
const variantOriginal = findVariantNoWait(workspacePage, 1);
|
||||
@@ -212,18 +221,17 @@ test("User cut paste a variant container", async ({ page }) => {
|
||||
await variant.container.click();
|
||||
|
||||
//Cut the variant container
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the variant container
|
||||
await workspacePage.clickAt(500, 500);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
const variantPasted = await findVariant(workspacePage, 0);
|
||||
|
||||
// Expand the layers
|
||||
await variantPasted.container.locator("button").first().click();
|
||||
await workspacePage.clickToggableLayer("Rectangle");
|
||||
|
||||
// The variants are valid
|
||||
await validateVariant(variantPasted);
|
||||
@@ -239,27 +247,34 @@ test("User cut paste a variant container into a board, and undo twice", async ({
|
||||
|
||||
//Create a board
|
||||
await workspacePage.boardButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 100, 100);
|
||||
// NOTE: this board should not intersect the existing variants, otherwise
|
||||
// this test is flaky
|
||||
await workspacePage.clickWithDragViewportAt(200, 200, 100, 100);
|
||||
await workspacePage.clickAt(495, 495);
|
||||
const board = await workspacePage.rootShape.locator("Board");
|
||||
|
||||
// Select the variant container
|
||||
await variant.container.click();
|
||||
// await variant.container.click();
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
|
||||
//Cut the variant container
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
await workspacePage.cut("keyboard");
|
||||
await expect(variant.container).not.toBeVisible();
|
||||
|
||||
//Select the board
|
||||
await workspacePage.clickLeafLayer("Board");
|
||||
|
||||
//Paste the variant container inside the board
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
await expect(variant.container).toBeVisible();
|
||||
|
||||
//Undo twice
|
||||
await workspacePage.page.keyboard.press("Control+z");
|
||||
await workspacePage.page.keyboard.press("Control+z");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+z");
|
||||
|
||||
await expect(variant.container).not.toBeVisible();
|
||||
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+z");
|
||||
await expect(variant.container).toBeVisible();
|
||||
|
||||
const variantAfterUndo = await findVariant(workspacePage, 0);
|
||||
|
||||
@@ -276,12 +291,12 @@ test("User copy paste a variant", async ({ page }) => {
|
||||
// Select the variant1
|
||||
await variant.variant1.click();
|
||||
|
||||
//Cut the variant
|
||||
await workspacePage.page.keyboard.press("Control+c");
|
||||
// Copy the variant
|
||||
await workspacePage.copy("keyboard");
|
||||
|
||||
//Paste the variant
|
||||
// Paste the variant
|
||||
await workspacePage.clickAt(500, 500);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const copy = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
@@ -302,11 +317,11 @@ test("User cut paste a variant outside the container", async ({ page }) => {
|
||||
await variant.variant1.click();
|
||||
|
||||
//Cut the variant
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the variant
|
||||
await workspacePage.clickAt(500, 500);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const component = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
@@ -324,15 +339,11 @@ test("User drag and drop a variant outside the container", async ({ page }) => {
|
||||
const variant = await findVariant(workspacePage, 0);
|
||||
|
||||
// Drag and drop the variant
|
||||
await workspacePage.clickWithDragViewportAt(350, 400, 0, 200);
|
||||
// FIXME: to make this test more resilient, we should get the bounding box of the Value 1 variant
|
||||
// and use it to calculate the target position
|
||||
await workspacePage.clickWithDragViewportAt(600, 500, 0, 300);
|
||||
|
||||
const component = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ has: workspacePage.page.getByText("Rectangle / Value 1") })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-component") });
|
||||
|
||||
//The component exists and is visible
|
||||
await expect(component).toBeVisible();
|
||||
await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible();
|
||||
});
|
||||
|
||||
test("User cut paste a component inside a variant", async ({ page }) => {
|
||||
@@ -345,14 +356,14 @@ test("User cut paste a component inside a variant", async ({ page }) => {
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
|
||||
//Cut the component
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the component inside the variant
|
||||
await variant.container.click();
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const variant3 = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
@@ -376,7 +387,7 @@ test("User cut paste a component with path inside a variant", async ({
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
|
||||
//Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
@@ -387,11 +398,11 @@ test("User cut paste a component with path inside a variant", async ({
|
||||
await workspacePage.page.keyboard.press("Enter");
|
||||
|
||||
//Cut the component
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the component inside the variant
|
||||
await variant.container.click();
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const variant3 = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
@@ -415,7 +426,7 @@ test("User drag and drop a component with path inside a variant", async ({
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
|
||||
//Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
@@ -426,7 +437,7 @@ test("User drag and drop a component with path inside a variant", async ({
|
||||
await workspacePage.page.keyboard.press("Enter");
|
||||
|
||||
//Drag and drop the component the component
|
||||
await workspacePage.clickWithDragViewportAt(510, 510, 0, -200);
|
||||
await workspacePage.clickWithDragViewportAt(510, 510, 200, 0);
|
||||
|
||||
const variant3 = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
@@ -446,8 +457,8 @@ test("User cut paste a variant into another container", async ({ page }) => {
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
|
||||
const variantOrigin = await findVariantNoWait(workspacePage, 1);
|
||||
|
||||
@@ -457,11 +468,11 @@ test("User cut paste a variant into another container", async ({ page }) => {
|
||||
await variantOrigin.variant1.click();
|
||||
|
||||
//Cut the variant
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the variant
|
||||
await workspacePage.layers.getByText("Ellipse").first().click();
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
const variant3 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||
import { presenceFixture } from "../../data/workspace/ws-notifications";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await WasmWorkspacePage.init(page);
|
||||
});
|
||||
|
||||
test("User loads worskpace with empty file", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
@@ -16,7 +16,7 @@ test("User loads worskpace with empty file", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("User opens a file with a bad page id", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace({
|
||||
@@ -29,7 +29,7 @@ test("User opens a file with a bad page id", async ({ page }) => {
|
||||
test("User receives presence notifications updates in the workspace", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
@@ -41,7 +41,7 @@ test("User receives presence notifications updates in the workspace", async ({
|
||||
});
|
||||
|
||||
test("User draws a rect", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
@@ -52,13 +52,12 @@ test("User draws a rect", async ({ page }) => {
|
||||
await workspacePage.rectShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
|
||||
|
||||
const shape = await workspacePage.rootShape.locator("rect");
|
||||
await expect(shape).toHaveAttribute("width", "200");
|
||||
await expect(shape).toHaveAttribute("height", "100");
|
||||
await workspacePage.hideUI();
|
||||
await expect(workspacePage.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("User makes a group", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
@@ -74,14 +73,14 @@ test("User makes a group", async ({ page }) => {
|
||||
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
|
||||
});
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
await workspacePage.page.keyboard.press("Control+g");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+g");
|
||||
await workspacePage.expectSelectedLayer("Group");
|
||||
});
|
||||
|
||||
test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
@@ -91,10 +90,10 @@ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
|
||||
await workspacePage.expectHiddenToolbarOptions();
|
||||
});
|
||||
|
||||
test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({
|
||||
test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
@@ -110,8 +109,8 @@ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", asyn
|
||||
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
|
||||
});
|
||||
|
||||
// Move created rect to a corner, in orther to get scrollbars
|
||||
await workspacePage.panOnViewportAt(128, 128, 300, 300);
|
||||
// Move created rect to a corner, in order to get scrollbars
|
||||
await workspacePage.panOnViewportAt(128, 128, 600, 600);
|
||||
|
||||
// Check scrollbars appear
|
||||
const horizontalScrollbar = workspacePage.horizontalScrollbar;
|
||||
@@ -130,7 +129,7 @@ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", asyn
|
||||
test("User adds a library and its automatically selected in the color palette", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.mockRPC(
|
||||
"link-file-to-library",
|
||||
@@ -175,7 +174,7 @@ test("User adds a library and its automatically selected in the color palette",
|
||||
test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.moveButton.click();
|
||||
@@ -218,7 +217,7 @@ test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette",
|
||||
test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
@@ -235,7 +234,7 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-
|
||||
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
await workspacePage.goToWorkspace();
|
||||
await workspacePage.pageName.click();
|
||||
@@ -245,7 +244,7 @@ test("Bug 8784 - Use keyboard arrow to move inside a text input does not change
|
||||
});
|
||||
|
||||
test("Bug 9066 - Problem with grid layout", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
|
||||
|
||||
@@ -273,7 +272,7 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("User have toolbar", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
@@ -282,7 +281,7 @@ test("User have toolbar", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("User have edition menu entries", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
@@ -298,7 +297,7 @@ test("User have edition menu entries", async ({ page }) => {
|
||||
});
|
||||
|
||||
test("Copy/paste properties", async ({ page, context }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockRPC(
|
||||
/get\-file\?/,
|
||||
@@ -355,23 +354,23 @@ test("Copy/paste properties", async ({ page, context }) => {
|
||||
});
|
||||
|
||||
test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.goToWorkspace();
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor"));
|
||||
await workspacePage.viewport.click({ button: "right" });
|
||||
await page.getByText("PasteCtrlV").click();
|
||||
await page.getByText(/^Paste/i).click();
|
||||
await workspacePage.viewport
|
||||
.getByRole("textbox")
|
||||
.getByText("Lorem ipsum dolor");
|
||||
});
|
||||
|
||||
test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
|
||||
test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json");
|
||||
await workspacePage.goToWorkspace({
|
||||
@@ -379,16 +378,18 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
|
||||
pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c",
|
||||
});
|
||||
|
||||
const zoom = await page.getByTitle("Zoom");
|
||||
const zoom = page.getByTitle("Zoom");
|
||||
await zoom.click();
|
||||
|
||||
const zoomIn = await page.getByRole("button", { name: "Zoom in" });
|
||||
const zoomIn = page.getByRole("button", { name: "Zoom in" });
|
||||
await zoomIn.click();
|
||||
await zoomIn.click();
|
||||
await zoomIn.click();
|
||||
|
||||
// Zoom fit all
|
||||
await page.keyboard.press("Shift+1");
|
||||
// Select all shapes to display selrect
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+a");
|
||||
|
||||
const ids = [
|
||||
"shape-165d1e5a-5873-8010-8005-9ffdbeaeec59",
|
||||
@@ -410,7 +411,7 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
|
||||
|
||||
const viewportBoundingBox = await workspacePage.viewport.boundingBox();
|
||||
for (const id of ids) {
|
||||
const shape = await page.locator(`.ws-shape-wrapper > g#${id}`);
|
||||
const shape = page.locator(`.viewport-selrect`);
|
||||
const shapeBoundingBox = await shape.boundingBox();
|
||||
expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy();
|
||||
}
|
||||
@@ -419,7 +420,7 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
|
||||
test("Bug 9877, user navigation to dashboard from header goes to blank page", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
|
||||
await workspacePage.goToWorkspace();
|
||||
@@ -436,7 +437,7 @@ test("Bug 9877, user navigation to dashboard from header goes to blank page", as
|
||||
test("Bug 8371 - Flatten option is not visible in context menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockGetFile("workspace/get-file-8371.json");
|
||||
await workspacePage.goToWorkspace({
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
90
frontend/resources/wasm-playground/shadows.html
Normal file
90
frontend/resources/wasm-playground/shadows.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WASM + WebGL2 Canvas</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
|
||||
} from './js/lib.js';
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
const shapes = params.get("shapes") || 1000;
|
||||
|
||||
initWasmModule().then(Module => {
|
||||
init(Module);
|
||||
assignCanvas(canvas);
|
||||
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
|
||||
Module._init_shapes_pool(shapes + 1);
|
||||
setupInteraction(canvas);
|
||||
|
||||
const children = [];
|
||||
for (let i = 0; i < shapes; i++) {
|
||||
const uuid = crypto.randomUUID();
|
||||
children.push(uuid);
|
||||
|
||||
useShape(uuid);
|
||||
Module._set_parent(0, 0, 0, 0);
|
||||
Module._set_shape_type(3);
|
||||
const x1 = getRandomInt(0, canvas.width);
|
||||
const y1 = getRandomInt(0, canvas.height);
|
||||
const width = getRandomInt(20, 100);
|
||||
const height = getRandomInt(20, 100);
|
||||
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
|
||||
|
||||
const color = getRandomColor();
|
||||
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
|
||||
addShapeSolidFill(argb)
|
||||
|
||||
Module._add_shape_center_stroke(10, 0, 0, 0);
|
||||
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
|
||||
addShapeSolidStrokeFill(argb2);
|
||||
|
||||
// Add shadows
|
||||
// Shadow 1: drop-shadow, #dedede opacity 0.33, blur 4, spread -2, offsetX 0, offsetY 2
|
||||
Module._add_shape_shadow(hexToU32ARGB("#dedede", 0.33), 4, -2, 0, 2, 0, false);
|
||||
// Shadow 2: drop-shadow, #dedede opacity 1, blur 12, spread -8, offsetX 0, offsetY 12
|
||||
Module._add_shape_shadow(hexToU32ARGB("#dedede", 1), 12, -8, 0, 12, 0, false);
|
||||
// Shadow 3: inner-shadow, #002046 opacity 0.12, blur 12, spread -8, offsetX 0, offsetY -4
|
||||
Module._add_shape_shadow(hexToU32ARGB("#002046", 0.12), 12, -8, 0, -4, 1, false);
|
||||
}
|
||||
|
||||
useShape("00000000-0000-0000-0000-000000000000");
|
||||
setShapeChildren(children);
|
||||
|
||||
performance.mark('render:begin');
|
||||
Module._set_view(1, 0, 0);
|
||||
Module._render(Date.now());
|
||||
performance.mark('render:end');
|
||||
const { duration } = performance.measure('render', 'render:begin', 'render:end');
|
||||
// alert(`render time: ${duration.toFixed(2)}ms`);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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,81 @@
|
||||
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
|
||||
(rx/empty))))))
|
||||
|
||||
(defn resize-wasm-text-debounce-commit
|
||||
[]
|
||||
(ptk/reify ::resize-wasm-text
|
||||
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
|
||||
[id]
|
||||
(let [cur-event (js/Symbol)]
|
||||
(ptk/reify ::resize-wasm-text-debounce
|
||||
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]
|
||||
(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))))))]
|
||||
(cond
|
||||
(not fonts-loaded?)
|
||||
(->> (rx/of (resize-wasm-text-debounce id))
|
||||
(rx/delay 20))
|
||||
|
||||
(= (::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))
|
||||
(rx/debounce 20)
|
||||
(rx/take 1)
|
||||
;; (rx/delay 200)
|
||||
(rx/map #(resize-wasm-text-debounce-commit))
|
||||
(rx/take-until stopper))
|
||||
|
||||
(rx/of (resize-wasm-text-debounce id)))
|
||||
|
||||
(rx/of #(dissoc %
|
||||
::resize-wasm-text-debounce-ids
|
||||
::resize-wasm-text-debounce-event))))
|
||||
:else
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn resize-wasm-text-all
|
||||
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
||||
[ids]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
(def ^:private schema:properties-row
|
||||
[:map
|
||||
[:term :string]
|
||||
[:detail :string]
|
||||
[:detail {:optional true} [:maybe :string]]
|
||||
[:property {:optional true} :string] ;; CSS valid property
|
||||
[:token {:optional true} :any] ;; resolved token object
|
||||
[:copiable {:optional true} :boolean]])
|
||||
|
||||
@@ -401,7 +401,8 @@
|
||||
(dm/fmt "scale(%)" maybe-zoom))}))]
|
||||
|
||||
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
|
||||
:transform (dm/str transform)}
|
||||
:transform (dm/str transform)
|
||||
:data-testid "text-editor"}
|
||||
[:defs
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x x :y y :width width :height height}]]]
|
||||
|
||||
@@ -119,6 +119,9 @@
|
||||
[:button {:class (stl/css-case
|
||||
:toggle-content true
|
||||
:inverse expanded?)
|
||||
:data-testid "toggle-content"
|
||||
:aria-expanded expanded?
|
||||
:aria-labelledby (dm/str "layer-name-" id)
|
||||
:on-click on-toggle-collapse}
|
||||
deprecated-icon/arrow])
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
:on-blur accept-edit
|
||||
:on-key-down on-key-down
|
||||
:auto-focus true
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:default-value (d/nilv default-value "")}]
|
||||
[:*
|
||||
[:span
|
||||
@@ -118,6 +119,7 @@
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
:ref ref
|
||||
:on-double-click start-edit}
|
||||
|
||||
@@ -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)))))))
|
||||
|
||||
@@ -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]
|
||||
@@ -338,9 +339,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)
|
||||
@@ -348,7 +354,9 @@
|
||||
(cb/with-objects (:objects page))
|
||||
(cb/add-object shape))]
|
||||
|
||||
(st/emit! (ch/commit-changes changes))
|
||||
(st/emit!
|
||||
(ch/commit-changes changes)
|
||||
(dwwt/resize-wasm-text-debounce (:id shape)))
|
||||
(shape/shape-proxy plugin-id (:id shape)))))
|
||||
|
||||
:createShapeFromSvg
|
||||
|
||||
@@ -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,10 +867,10 @@
|
||||
|
||||
(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 shape-id all-fonts)]
|
||||
(f/load-fallback-fonts-for-editor! fallback-fonts)
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result))
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[goog.object :as gobj]
|
||||
[potok.v2.core :as ptk]
|
||||
[lambdaisland.uri :as u]
|
||||
[okulary.core :as l]))
|
||||
|
||||
@@ -97,9 +98,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 +107,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 +115,35 @@
|
||||
(: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))))})
|
||||
(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?)
|
||||
(update-text-layout shape-id)
|
||||
;; TODO: Emit font fetched event
|
||||
(prn ">>Fetched" font-data)
|
||||
(ptk/data-event :wasm-text/font-fetched {:font font-data})))
|
||||
(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,20 +162,29 @@
|
||||
:builtin
|
||||
(dm/str (u/join cf/public-uri "fonts/" asset-id))))
|
||||
|
||||
(defn font-stored?
|
||||
[font-data emoji?]
|
||||
(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?]
|
||||
(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?)))))
|
||||
|
||||
@@ -280,8 +296,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
|
||||
@@ -307,7 +323,14 @@
|
||||
:font-variant-id variant-id
|
||||
:style (serialize-font-style style)
|
||||
:style-name style
|
||||
:weight weight}]
|
||||
:weight weight
|
||||
:emoji? emoji?
|
||||
:fallbck? fallback?
|
||||
:asset-id asset-id}]))
|
||||
|
||||
(defn store-font
|
||||
[shape-id font]
|
||||
(let [{:keys [asset-id emoji? fallback?] :as font-data} (make-font-data font)]
|
||||
(store-font-id shape-id font-data asset-id emoji? fallback?)))
|
||||
|
||||
;; FIXME: This is a temporary function to load the fallback fonts for the editor.
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -22,7 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
use crate::performance;
|
||||
use crate::shapes::{
|
||||
all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type,
|
||||
all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Type,
|
||||
};
|
||||
use crate::state::{ShapesPoolMutRef, ShapesPoolRef};
|
||||
use crate::tiles::{self, PendingTiles, TileRect};
|
||||
@@ -1512,6 +1512,16 @@ impl RenderState {
|
||||
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
||||
let blur_filter = combined_blur
|
||||
.and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None));
|
||||
// Legacy path is only stable up to 1.0 zoom: the canvas is scaled and the shadow
|
||||
// filter is evaluated in that scaled space, so for scale > 1 it over-inflates blur/spread.
|
||||
// We also disable it when combined layer blur is present to avoid incorrect composition.
|
||||
let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none();
|
||||
|
||||
if use_low_zoom_path {
|
||||
// Match pre-commit behavior: scale blur/spread with zoom for low zoom levels.
|
||||
transformed_shadow.to_mut().blur = shadow.blur * scale;
|
||||
transformed_shadow.to_mut().spread = shadow.spread * scale;
|
||||
}
|
||||
|
||||
let mut transform_matrix = shape.transform;
|
||||
let center = shape.center();
|
||||
@@ -1526,28 +1536,20 @@ impl RenderState {
|
||||
let world_offset = (mapped.x, mapped.y);
|
||||
|
||||
// The opacity of fills and strokes shouldn't affect the shadow,
|
||||
// so we paint everything black with the same opacity
|
||||
plain_shape.to_mut().clear_fills();
|
||||
// so we paint everything black with the same opacity.
|
||||
let plain_shape_mut = plain_shape.to_mut();
|
||||
plain_shape_mut.clear_fills();
|
||||
if shape.has_fills() {
|
||||
plain_shape
|
||||
.to_mut()
|
||||
.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
|
||||
plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
|
||||
}
|
||||
|
||||
plain_shape.to_mut().clear_strokes();
|
||||
for stroke in shape.strokes.iter() {
|
||||
plain_shape.to_mut().add_stroke(Stroke {
|
||||
fill: Fill::Solid(SolidColor(skia::Color::BLACK)),
|
||||
width: stroke.width,
|
||||
style: stroke.style,
|
||||
cap_end: stroke.cap_end,
|
||||
cap_start: stroke.cap_start,
|
||||
kind: stroke.kind,
|
||||
});
|
||||
// Reuse existing strokes and only override their fill color.
|
||||
for stroke in plain_shape_mut.strokes.iter_mut() {
|
||||
stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK));
|
||||
}
|
||||
|
||||
plain_shape.to_mut().clear_shadows();
|
||||
plain_shape.to_mut().blur = None;
|
||||
plain_shape_mut.clear_shadows();
|
||||
plain_shape_mut.blur = None;
|
||||
|
||||
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
|
||||
return;
|
||||
@@ -1556,6 +1558,39 @@ impl RenderState {
|
||||
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
|
||||
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
|
||||
bounds.offset(world_offset);
|
||||
// Early cull if the shadow bounds are outside the render area.
|
||||
if !bounds.intersects(self.render_area) {
|
||||
return;
|
||||
}
|
||||
|
||||
if use_low_zoom_path {
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
shadow_paint.set_image_filter(drop_filter);
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save_layer(&layer_rec);
|
||||
drop_canvas.scale((scale, scale));
|
||||
drop_canvas.translate(translation);
|
||||
|
||||
self.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
);
|
||||
});
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
return;
|
||||
}
|
||||
|
||||
let filter_result =
|
||||
filters::render_into_filter_surface(self, bounds, |state, temp_surface| {
|
||||
|
||||
@@ -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