mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging-render' into develop
This commit is contained in:
commit
2de3ead14f
21
.github/workflows/tests.yml
vendored
21
.github/workflows/tests.yml
vendored
@ -34,6 +34,8 @@ jobs:
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
|
pnpm run check-fmt:clj
|
||||||
|
pnpm run check-fmt:js
|
||||||
pnpm run lint:clj
|
pnpm run lint:clj
|
||||||
|
|
||||||
- name: Lint Frontend
|
- name: Lint Frontend
|
||||||
@ -42,6 +44,9 @@ jobs:
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
|
pnpm run check-fmt:js
|
||||||
|
pnpm run check-fmt:clj
|
||||||
|
pnpm run check-fmt:scss
|
||||||
pnpm run lint:clj
|
pnpm run lint:clj
|
||||||
pnpm run lint:js
|
pnpm run lint:js
|
||||||
pnpm run lint:scss
|
pnpm run lint:scss
|
||||||
@ -52,7 +57,8 @@ jobs:
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
pnpm run lint:clj
|
pnpm run check-fmt
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
- name: Lint Exporter
|
- name: Lint Exporter
|
||||||
working-directory: ./exporter
|
working-directory: ./exporter
|
||||||
@ -60,7 +66,8 @@ jobs:
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
pnpm run lint:clj
|
pnpm run check-fmt
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
- name: Lint Library
|
- name: Lint Library
|
||||||
working-directory: ./library
|
working-directory: ./library
|
||||||
@ -68,7 +75,8 @@ jobs:
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
pnpm run lint:clj
|
pnpm run check-fmt
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
test-common:
|
test-common:
|
||||||
name: "Common Tests"
|
name: "Common Tests"
|
||||||
@ -79,12 +87,7 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run tests on JVM
|
- name: Run tests
|
||||||
working-directory: ./common
|
|
||||||
run: |
|
|
||||||
clojure -M:dev:test
|
|
||||||
|
|
||||||
- name: Run tests on NODE
|
|
||||||
working-directory: ./common
|
working-directory: ./common
|
||||||
run: |
|
run: |
|
||||||
./scripts/test
|
./scripts/test
|
||||||
|
|||||||
22
.gitignore
vendored
22
.gitignore
vendored
@ -1,11 +1,4 @@
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
.pnpm-store
|
|
||||||
*-init.clj
|
*-init.clj
|
||||||
*.css.json
|
*.css.json
|
||||||
*.jar
|
*.jar
|
||||||
@ -20,8 +13,6 @@
|
|||||||
.nyc_output
|
.nyc_output
|
||||||
.rebel_readline_history
|
.rebel_readline_history
|
||||||
.repl
|
.repl
|
||||||
.shadow-cljs
|
|
||||||
.pnpm-store/
|
|
||||||
/*.jpg
|
/*.jpg
|
||||||
/*.md
|
/*.md
|
||||||
/*.png
|
/*.png
|
||||||
@ -36,6 +27,7 @@
|
|||||||
/playground/
|
/playground/
|
||||||
/backend/*.md
|
/backend/*.md
|
||||||
!/backend/AGENTS.md
|
!/backend/AGENTS.md
|
||||||
|
/backend/.shadow-cljs
|
||||||
/backend/*.sql
|
/backend/*.sql
|
||||||
/backend/*.txt
|
/backend/*.txt
|
||||||
/backend/assets/
|
/backend/assets/
|
||||||
@ -48,13 +40,13 @@
|
|||||||
/backend/experiments
|
/backend/experiments
|
||||||
/backend/scripts/_env.local
|
/backend/scripts/_env.local
|
||||||
/bundle*
|
/bundle*
|
||||||
/cd.md
|
|
||||||
/clj-profiler/
|
/clj-profiler/
|
||||||
/common/coverage
|
/common/coverage
|
||||||
/common/target
|
/common/target
|
||||||
/deploy
|
/common/.shadow-cljs
|
||||||
/docker/images/bundle*
|
/docker/images/bundle*
|
||||||
/exporter/target
|
/exporter/target
|
||||||
|
/exporter/.shadow-cljs
|
||||||
/frontend/.storybook/preview-body.html
|
/frontend/.storybook/preview-body.html
|
||||||
/frontend/.storybook/preview-head.html
|
/frontend/.storybook/preview-head.html
|
||||||
/frontend/playwright-report/
|
/frontend/playwright-report/
|
||||||
@ -68,9 +60,9 @@
|
|||||||
/frontend/storybook-static/
|
/frontend/storybook-static/
|
||||||
/frontend/target/
|
/frontend/target/
|
||||||
/frontend/test-results/
|
/frontend/test-results/
|
||||||
|
/frontend/.shadow-cljs
|
||||||
/other/
|
/other/
|
||||||
/scripts/
|
/nexus/
|
||||||
/telemetry/
|
|
||||||
/tmp/
|
/tmp/
|
||||||
/vendor/**/target
|
/vendor/**/target
|
||||||
/vendor/svgclean/bundle*.js
|
/vendor/svgclean/bundle*.js
|
||||||
@ -79,13 +71,11 @@
|
|||||||
/library/*.zip
|
/library/*.zip
|
||||||
/external
|
/external
|
||||||
/penpot-nitrate
|
/penpot-nitrate
|
||||||
|
|
||||||
clj-profiler/
|
|
||||||
node_modules
|
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/render-wasm/target/
|
/render-wasm/target/
|
||||||
|
/**/node_modules
|
||||||
/**/.yarn/*
|
/**/.yarn/*
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
|
|||||||
352
AGENTS.md
352
AGENTS.md
@ -1,4 +1,4 @@
|
|||||||
# Penpot – Copilot Instructions
|
# Penpot – Instructions
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
@ -18,7 +18,13 @@ The monorepo is managed with `pnpm` workspaces. The `manage.sh`
|
|||||||
orchestrates cross-component builds. `run-ci.sh` defines the CI
|
orchestrates cross-component builds. `run-ci.sh` defines the CI
|
||||||
pipeline.
|
pipeline.
|
||||||
|
|
||||||
---
|
## Search Standards
|
||||||
|
|
||||||
|
When searching code, always use `ripgrep` (rg) instead of grep if
|
||||||
|
available, as it respects `.gitignore` by default.
|
||||||
|
|
||||||
|
If using grep, try to exclude node_modules and .shadow-cljs directories
|
||||||
|
|
||||||
|
|
||||||
## Build, Test & Lint Commands
|
## Build, Test & Lint Commands
|
||||||
|
|
||||||
@ -28,27 +34,26 @@ Run `./scripts/setup` for setup all dependencies.
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Dev
|
# Build (Producution)
|
||||||
pnpm run watch:app # Full dev build (WASM + CLJS + assets)
|
|
||||||
|
|
||||||
# Production Build
|
|
||||||
./scripts/build
|
./scripts/build
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
|
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
|
||||||
pnpm run watch:test # Watch + auto-rerun on change
|
|
||||||
pnpm run test:e2e # Playwright e2e tests
|
|
||||||
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
|
|
||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
pnpm run lint:js # format and linter check for JS
|
pnpm run lint:js # Linter for JS/TS
|
||||||
pnpm run lint:clj # format and linter check for CLJ
|
pnpm run lint:clj # Linter for CLJ/CLJS/CLJC
|
||||||
pnpm run lint:scss # prettier check for SCSS
|
pnpm run lint:scss # Linter for SCSS
|
||||||
|
|
||||||
# Code formatting
|
# Check Code Formart
|
||||||
pnpm run fmt:clj # Format CLJ
|
pnpm run check-fmt:clj # Format CLJ/CLJS/CLJC
|
||||||
pnpm run fmt:js # prettier for JS
|
pnpm run check-fmt:js # Format JS/TS
|
||||||
pnpm run fmt:scss # prettier for SCSS
|
pnpm run check-fmt:scss # Format SCSS
|
||||||
|
|
||||||
|
# Code Format (Automatic Formating)
|
||||||
|
pnpm run fmt:clj # Format CLJ/CLJS/CLJC
|
||||||
|
pnpm run fmt:js # Format JS/TS
|
||||||
|
pnpm run fmt:scss # Format SCSS
|
||||||
```
|
```
|
||||||
|
|
||||||
To run a focused ClojureScript unit test: edit
|
To run a focused ClojureScript unit test: edit
|
||||||
@ -58,28 +63,63 @@ run build:test && node target/tests/test.js`.
|
|||||||
|
|
||||||
### Backend (`cd backend`)
|
### Backend (`cd backend`)
|
||||||
|
|
||||||
```bash
|
Run `pnpm install` for install all dependencies.
|
||||||
# Tests (Kaocha)
|
|
||||||
clojure -M:dev:test # Full suite
|
|
||||||
clojure -M:dev:test --focus backend-tests.my-ns-test # Single namespace
|
|
||||||
|
|
||||||
# Lint / Format
|
```bash
|
||||||
pnpm run lint:clj
|
# Run full test suite
|
||||||
pnpm run fmt:clj
|
pnpm run test
|
||||||
|
|
||||||
|
# Run single namespace
|
||||||
|
pnpm run test --focus backend-tests.rpc-doc-test
|
||||||
|
|
||||||
|
# Check Code Format
|
||||||
|
pnpm run check-fmt
|
||||||
|
|
||||||
|
# Code Format (Automatic Formatting)
|
||||||
|
pnpm run fmt
|
||||||
|
|
||||||
|
# Code Linter
|
||||||
|
pnpm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
Test config is in `backend/tests.edn`; test namespaces match `.*-test$` under `test/`.
|
Test config is in `backend/tests.edn`; test namespaces match
|
||||||
|
`.*-test$` under `test/` directory. You should not touch this file,
|
||||||
|
just use it for reference.
|
||||||
|
|
||||||
|
|
||||||
### Common (`cd common`)
|
### Common (`cd common`)
|
||||||
|
|
||||||
|
This contains code that should compile and run under different runtimes: JVM & JS so the commands are
|
||||||
|
separarated for each runtime.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run test # Build + run node target/tests/test.js
|
clojure -M:dev:test # Run full test suite under JVM
|
||||||
pnpm run watch:test # Watch mode
|
clojure -M:dev:test --focus backend-tests.my-ns-test # Run single namespace under JVM
|
||||||
pnpm run lint:clj
|
|
||||||
pnpm run fmt:clj
|
# Run full test suite under JS or JVM runtimes
|
||||||
|
pnpm run test:js
|
||||||
|
pnpm run test:jvm
|
||||||
|
|
||||||
|
# Run single namespace (only on JVM)
|
||||||
|
pnpm run test:jvm --focus common-tests.my-ns-test
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
pnpm run lint:clj # Lint CLJ/CLJS/CLJC code
|
||||||
|
|
||||||
|
# Check Format
|
||||||
|
pnpm run check-fmt:clj # Check CLJ/CLJS/CLJS code
|
||||||
|
pnpm run check-fmt:js # Check JS/TS code
|
||||||
|
|
||||||
|
# Code Format (Automatic Formatting)
|
||||||
|
pnpm run fmt:clj # Check CLJ/CLJS/CLJS code
|
||||||
|
pnpm run fmt:js # Check JS/TS code
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To run a focused ClojureScript unit test: edit
|
||||||
|
`test/common_tests/runner.cljs` to narrow the test suite, then `pnpm
|
||||||
|
run build:test && node target/tests/test.js`.
|
||||||
|
|
||||||
|
|
||||||
### Render-WASM (`cd render-wasm`)
|
### Render-WASM (`cd render-wasm`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -93,6 +133,10 @@ cargo fmt --check
|
|||||||
|
|
||||||
### Namespace Structure
|
### Namespace Structure
|
||||||
|
|
||||||
|
The backend, frontend and exporter are developed using clojure and
|
||||||
|
clojurescript and code is organized in namespaces. This is a general
|
||||||
|
overview of the available namespaces.
|
||||||
|
|
||||||
**Backend:**
|
**Backend:**
|
||||||
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
||||||
- `app.http.*` – HTTP routes and middleware
|
- `app.http.*` – HTTP routes and middleware
|
||||||
@ -109,14 +153,26 @@ cargo fmt --check
|
|||||||
- `app.util.*` – Utilities (DOM, HTTP, i18n, keyboard shortcuts)
|
- `app.util.*` – Utilities (DOM, HTTP, i18n, keyboard shortcuts)
|
||||||
|
|
||||||
**Common:**
|
**Common:**
|
||||||
- `app.common.types.*` – Shared data types for shapes, files, pages
|
- `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas
|
||||||
- `app.common.schema` – Malli validation schemas
|
- `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli
|
||||||
- `app.common.geom.*` – Geometry utilities
|
- `app.common.geom.*` – Geometry and shape transformation helpers
|
||||||
|
- `app.common.data` – Generic helpers used around all application
|
||||||
|
- `app.common.math` – Generic math helpers used around all aplication
|
||||||
|
- `app.common.json` – Generic JSON encoding/decoding helpers
|
||||||
- `app.common.data.macros` – Performance macros used everywhere
|
- `app.common.data.macros` – Performance macros used everywhere
|
||||||
|
|
||||||
|
|
||||||
### Backend RPC Commands
|
### Backend RPC Commands
|
||||||
|
|
||||||
All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name>`.
|
The PRC methods are implement in a some kind of multimethod structure using
|
||||||
|
`app.util.serivices` namespace. All RPC methods are collected under `app.rpc`
|
||||||
|
namespace and exposed under `/api/rpc/command/<cmd-name>`. The RPC method
|
||||||
|
accepts POST and GET requests indistinctly and uses `Accept` header for
|
||||||
|
negotiate the response encoding (which can be transit, the defaut or plain
|
||||||
|
json). It also accepts transit (defaut) or json as input, which should be
|
||||||
|
indicated using `Content-Type` header.
|
||||||
|
|
||||||
|
This is an example:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(sv/defmethod ::my-command
|
(sv/defmethod ::my-command
|
||||||
@ -129,12 +185,18 @@ All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name
|
|||||||
{:id (uuid/next)})
|
{:id (uuid/next)})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Look under `src/app/rpc/commands/*.clj` to see more examples.
|
||||||
|
|
||||||
|
|
||||||
### Frontend State Management (Potok)
|
### Frontend State Management (Potok)
|
||||||
|
|
||||||
State is a single atom managed by a Potok store. Events implement protocols:
|
State is a single atom managed by a Potok store. Events implement protocols
|
||||||
|
(funcool/potok library):
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(defn my-event [data]
|
(defn my-event
|
||||||
|
"doc string"
|
||||||
|
[data]
|
||||||
(ptk/reify ::my-event
|
(ptk/reify ::my-event
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state] ;; synchronous state transition
|
(update [_ state] ;; synchronous state transition
|
||||||
@ -148,19 +210,40 @@ State is a single atom managed by a Potok store. Events implement protocols:
|
|||||||
|
|
||||||
ptk/EffectEvent
|
ptk/EffectEvent
|
||||||
(effect [_ state _] ;; pure side effects (DOM, logging)
|
(effect [_ state _] ;; pure side effects (DOM, logging)
|
||||||
(.focus (dom/get-element "id")))))
|
(dom/focus (dom/get-element "id")))))
|
||||||
```
|
```
|
||||||
|
|
||||||
Dispatch with `(st/emit! (my-event data))`. Read state via reactive
|
The state is located under `app.main.store` namespace where we have
|
||||||
refs: `(deref refs/selected-shapes)`. Prefer helpers from
|
the `emit!` function responsible of emiting events.
|
||||||
`app.util.dom` instead of using direct dom calls, if no helper is
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```cljs
|
||||||
|
(ns some.ns
|
||||||
|
(:require
|
||||||
|
[app.main.data.my-events :refer [my-event]]
|
||||||
|
[app.main.store :as st]))
|
||||||
|
|
||||||
|
(defn on-click
|
||||||
|
[event]
|
||||||
|
(st/emit! (my-event)))
|
||||||
|
```
|
||||||
|
|
||||||
|
On `app.main.refs` we have reactive references which lookup into the main state
|
||||||
|
for just inner data or precalculated data. That references are very usefull but
|
||||||
|
should be used with care because, per example if we have complex operation, this
|
||||||
|
operation will be executed on each state change, and sometimes is better to have
|
||||||
|
simple references and use react `use-memo` for more granular memoization.
|
||||||
|
|
||||||
|
Prefer helpers from `app.util.dom` instead of using direct dom calls, if no helper is
|
||||||
available, prefer adding a new helper for handling it and the use the
|
available, prefer adding a new helper for handling it and the use the
|
||||||
new helper.
|
new helper.
|
||||||
|
|
||||||
|
|
||||||
### CSS Modules Pattern
|
### CSS (Modules Pattern)
|
||||||
|
|
||||||
Styles are co-located with components. Each `.cljs` file has a corresponding `.scss` file:
|
Styles are co-located with components. Each `.cljs` file has a corresponding
|
||||||
|
`.scss` file:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
;; In the component namespace:
|
;; In the component namespace:
|
||||||
@ -174,8 +257,24 @@ Styles are co-located with components. Each `.cljs` file has a corresponding `.s
|
|||||||
|
|
||||||
;; When you need concat an existing class:
|
;; When you need concat an existing class:
|
||||||
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
|
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration tests (Playwright)
|
||||||
|
|
||||||
|
Integration tests are developed under `frontend/playwright` directory, we use
|
||||||
|
mocks for remove communication with backend.
|
||||||
|
|
||||||
|
The tests should be executed under `./frontend` directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
cd frontend/
|
||||||
|
|
||||||
|
pnpm run test:e2e # Playwright e2e tests
|
||||||
|
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure everything installed with `./scripts/setup` script.
|
||||||
|
|
||||||
|
|
||||||
### Performance Macros (`app.common.data.macros`)
|
### Performance Macros (`app.common.data.macros`)
|
||||||
|
|
||||||
@ -187,7 +286,7 @@ Always prefer these macros over their `clojure.core` equivalents — they compil
|
|||||||
(dm/str "a" "b" "c") ;; string concatenation
|
(dm/str "a" "b" "c") ;; string concatenation
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shared Code (cljc)
|
### Shared Code under Common (CLJC)
|
||||||
|
|
||||||
Files in `common/src/app/common/` use reader conditionals to target both runtimes:
|
Files in `common/src/app/common/` use reader conditionals to target both runtimes:
|
||||||
|
|
||||||
@ -196,37 +295,129 @@ Files in `common/src/app/common/` use reader conditionals to target both runtime
|
|||||||
:cljs (:require [cljs.core :as core]))
|
:cljs (:require [cljs.core :as core]))
|
||||||
```
|
```
|
||||||
|
|
||||||
Both frontend and backend depend on `common` as a local library (`penpot/common {:local/root "../common"}`).
|
Both frontend and backend depend on `common` as a local library (`penpot/common
|
||||||
|
{:local/root "../common"}`).
|
||||||
|
|
||||||
|
|
||||||
### Component Definition (Rumext / React)
|
|
||||||
|
|
||||||
The codebase has several kind of components, some of them use legacy
|
### Component Standards & Syntax (React & Rumext: mf/defc)
|
||||||
syntax. The current and the most recent syntax uses `*` suffix on the
|
|
||||||
name. This indicates to the `mf/defc` macro apply concrete rules on
|
|
||||||
how props should be treated.
|
|
||||||
|
|
||||||
```clojure
|
The codebase contains various component patterns. When creating or refactoring
|
||||||
|
components, follow the Modern Syntax rules outlined below.
|
||||||
|
|
||||||
|
1. The * Suffix Convention
|
||||||
|
|
||||||
|
The most recent syntax uses a * suffix in the component name (e.g.,
|
||||||
|
my-component*). This suffix signals the mf/defc macro to apply specific rules
|
||||||
|
for props handling and destructuring and optimization.
|
||||||
|
|
||||||
|
2. Component Definition
|
||||||
|
|
||||||
|
Modern components should use the following structure:
|
||||||
|
|
||||||
|
```clj
|
||||||
(mf/defc my-component*
|
(mf/defc my-component*
|
||||||
{::mf/wrap [mf/memo]} ;; React.memo
|
{::mf/wrap [mf/memo]} ;; Equivalent to React.memo
|
||||||
[{:keys [name on-click]}]
|
[{:keys [name on-click]}] ;; Destructured props
|
||||||
[:div {:class (stl/css :root)
|
[:div {:class (stl/css :root)
|
||||||
:on-click on-click}
|
:on-click on-click}
|
||||||
name])
|
name])
|
||||||
```
|
```
|
||||||
|
|
||||||
Hooks: `(mf/use-state)`, `(mf/use-effect)`, `(mf/use-memo)` – analgous to react hooks.
|
3. Hooks
|
||||||
|
|
||||||
|
Use the mf namespace for hooks to maintain consistency with the macro's
|
||||||
|
lifecycle management. These are analogous to standard React hooks:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
(mf/use-state) ;; analogous to React.useState adapted to cljs semantics
|
||||||
|
(mf/use-effect) ;; analogous to React.useEffect
|
||||||
|
(mf/use-memo) ;; analogous to React.useMemo
|
||||||
|
(mf/use-fn) ;; analogous to React.useCallback
|
||||||
|
```
|
||||||
|
|
||||||
|
The `mf/use-state` in difference with React.useState, returns an atom-like
|
||||||
|
object, where you can use `swap!` or `reset!` for to perform an update and
|
||||||
|
`deref` for get the current value.
|
||||||
|
|
||||||
|
You also has `mf/deref` hook (which does not follow the `use-` naming pattern)
|
||||||
|
and it's purpose is watch (subscribe to changes) on atom or derived atom (from
|
||||||
|
okulary) and get the current value. Is mainly used for subscribe to lenses
|
||||||
|
defined in `app.main.refs` or (private lenses defined in namespaces).
|
||||||
|
|
||||||
|
Rumext also comes with improved syntax macros as alternative to `mf/use-effect`
|
||||||
|
and `mf/use-memo` functions. Examples:
|
||||||
|
|
||||||
|
|
||||||
The component usage should always follow the `[:> my-component*
|
Example for `mf/with-memo` macro:
|
||||||
props]`, where props should be a map literal or symbol pointing to
|
|
||||||
javascript props objects. The javascript props object can be created
|
|
||||||
manually `#js {:data-foo "bar"}` or using `mf/spread-object` helper
|
|
||||||
macro.
|
|
||||||
|
|
||||||
---
|
```clj
|
||||||
|
;; Using functions
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps team-id)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dd/initialize team-id))
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dd/finalize team-id)))))
|
||||||
|
|
||||||
## Commit Guidelines
|
;; The same effect but using mf/with-effect
|
||||||
|
(mf/with-effect [team-id]
|
||||||
|
(st/emit! (dd/initialize team-id))
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dd/finalize team-id))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Example for `mf/with-memo` macro:
|
||||||
|
|
||||||
|
```
|
||||||
|
;; Using functions
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps projects team-id)
|
||||||
|
(fn []
|
||||||
|
(->> (vals projects)
|
||||||
|
(filterv #(= team-id (:team-id %))))))
|
||||||
|
|
||||||
|
;; Using the macro
|
||||||
|
(mf/with-memo [projects team-id]
|
||||||
|
(->> (vals projects)
|
||||||
|
(filterv #(= team-id (:team-id %)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer using the macros for it syntax simplicity.
|
||||||
|
|
||||||
|
|
||||||
|
4. Component Usage (Hiccup Syntax)
|
||||||
|
|
||||||
|
When invoking a component within Hiccup, always use the [:> component* props]
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
Requirements for props:
|
||||||
|
|
||||||
|
- Must be a map literal or a symbol pointing to a JavaScript props object.
|
||||||
|
- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```clj
|
||||||
|
;; Using object literal (no need of #js because macro already interprets it)
|
||||||
|
[:> my-component* {:data-foo "bar"}]
|
||||||
|
|
||||||
|
;; Using object literal (no need of #js because macro already interprets it)
|
||||||
|
(let [props #js {:data-foo "bar"
|
||||||
|
:className "myclass"}]
|
||||||
|
[:> my-component* props])
|
||||||
|
|
||||||
|
;; Using the spread helper
|
||||||
|
(let [props (mf/spread-object base-props {:extra "data"})]
|
||||||
|
[:> my-component* props])
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Checklist
|
||||||
|
|
||||||
|
- [ ] Does the component name end with *?
|
||||||
|
|
||||||
|
|
||||||
|
## Commit Format Guidelines
|
||||||
|
|
||||||
Format: `<emoji-code> <subject>`
|
Format: `<emoji-code> <subject>`
|
||||||
|
|
||||||
@ -263,3 +454,46 @@ applicable.
|
|||||||
| ⬇️ | `:arrow_down:` | Dependency downgrade |
|
| ⬇️ | `:arrow_down:` | Dependency downgrade |
|
||||||
| 🔥 | `:fire:` | Remove files or code |
|
| 🔥 | `:fire:` | Remove files or code |
|
||||||
| 🌐 | `:globe_with_meridians:` | Translations |
|
| 🌐 | `:globe_with_meridians:` | Translations |
|
||||||
|
|
||||||
|
|
||||||
|
## SCSS Rules & Migration
|
||||||
|
|
||||||
|
### General rules
|
||||||
|
|
||||||
|
- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss
|
||||||
|
variables and get the already defined properties from `_sizes.scss`. The SCSS
|
||||||
|
variables are allowed and still used, just prefer properties if they are
|
||||||
|
already defined.
|
||||||
|
- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss"
|
||||||
|
as *; padding: px2rem(23);`.
|
||||||
|
- Do **not** create new SCSS variables for one-off values.
|
||||||
|
- Use physical directions with logical ones to support RTL/LTR naturally.
|
||||||
|
- ❌ `margin-left`, `padding-right`, `left`, `right`.
|
||||||
|
- ✅ `margin-inline-start`, `padding-inline-end`, `inset-inline-start`.
|
||||||
|
- Always use the `use-typography` mixin from `ds/typography.scss`.
|
||||||
|
- ✅ `@include t.use-typography("title-small");`
|
||||||
|
- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`.
|
||||||
|
- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or
|
||||||
|
legacy color variables.
|
||||||
|
- Use mixins only those defined in`ds/mixins.scss`. Avoid legacy mixins like
|
||||||
|
`@include flexCenter;`. Write standard CSS (flex/grid) instead.
|
||||||
|
|
||||||
|
### Syntax & Structure
|
||||||
|
|
||||||
|
- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file,
|
||||||
|
try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as
|
||||||
|
*;` (Use `as *` to expose variables directly).
|
||||||
|
- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors:
|
||||||
|
- ❌ `.card { .title { ... } }`
|
||||||
|
- ✅ `.card-title { ... }`
|
||||||
|
- Leverage component-level CSS variables for state changes (hover/focus) instead
|
||||||
|
of rewriting properties.
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
- [ ] No references to `common/refactor/`
|
||||||
|
- [ ] All `@import` converted to `@use` (only if refactoring)
|
||||||
|
- [ ] Physical properties (left/right) using logical properties (inline-start/end).
|
||||||
|
- [ ] Typography implemented via `use-typography()` mixin.
|
||||||
|
- [ ] Hardcoded pixel values wrapped in `px2rem()`.
|
||||||
|
- [ ] Selectors are flat (no deep nesting).
|
||||||
|
|||||||
@ -61,6 +61,7 @@
|
|||||||
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
|
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
|
||||||
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
|
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
|
||||||
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
||||||
|
- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463)
|
||||||
|
|
||||||
## 2.13.3
|
## 2.13.3
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,9 @@
|
|||||||
"ws": "^8.17.0"
|
"ws": "^8.17.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/"
|
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
||||||
|
"fmt": "cljfmt fix --parallel=true src/ test/",
|
||||||
|
"test": "clojure -M:dev:test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,6 +138,7 @@
|
|||||||
c.deleted_at
|
c.deleted_at
|
||||||
FROM snapshots1 AS c
|
FROM snapshots1 AS c
|
||||||
WHERE c.file_id = ?
|
WHERE c.file_id = ?
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
), snapshots3 AS (
|
), snapshots3 AS (
|
||||||
(SELECT * FROM snapshots2
|
(SELECT * FROM snapshots2
|
||||||
WHERE created_by = 'system'
|
WHERE created_by = 'system'
|
||||||
@ -150,8 +151,7 @@
|
|||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
LIMIT 500)
|
LIMIT 500)
|
||||||
)
|
)
|
||||||
SELECT * FROM snapshots3
|
SELECT * FROM snapshots3;"))
|
||||||
ORDER BY created_at DESC"))
|
|
||||||
|
|
||||||
(defn get-visible-snapshots
|
(defn get-visible-snapshots
|
||||||
"Return a list of snapshots fecheable from the API, it has a limited
|
"Return a list of snapshots fecheable from the API, it has a limited
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
"prettier": "3.5.3",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
@ -20,11 +21,15 @@
|
|||||||
"date-fns": "^4.1.0"
|
"date-fns": "^4.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel=true --lint src/",
|
"lint:clj": "clj-kondo --parallel=true --lint src/",
|
||||||
|
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||||
|
"check-fmt:js": "prettier -c src/**/*.js",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
|
"fmt:js": "prettier -c src/**/*.js -w",
|
||||||
"lint": "pnpm run lint:clj",
|
"lint": "pnpm run lint:clj",
|
||||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
|
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
|
||||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||||
"test": "pnpm run build:test && node target/tests/test.js"
|
"test:js": "pnpm run build:test && node target/tests/test.js",
|
||||||
|
"test:jvm": "clojure -M:dev:test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
common/pnpm-lock.yaml
generated
10
common/pnpm-lock.yaml
generated
@ -18,6 +18,9 @@ importers:
|
|||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.10
|
specifier: ^3.1.10
|
||||||
version: 3.1.11
|
version: 3.1.11
|
||||||
|
prettier:
|
||||||
|
specifier: 3.5.3
|
||||||
|
version: 3.5.3
|
||||||
source-map-support:
|
source-map-support:
|
||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
@ -169,6 +172,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
prettier@3.5.3:
|
||||||
|
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pstree.remy@1.1.8:
|
pstree.remy@1.1.8:
|
||||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
@ -405,6 +413,8 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
prettier@3.5.3: {}
|
||||||
|
|
||||||
pstree.remy@1.1.8: {}
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
|
|||||||
@ -4,4 +4,5 @@ set -ex
|
|||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
pnpm run test;
|
pnpm run test:js;
|
||||||
|
pnpm run test:jvm;
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
goog.require("cljs.core");
|
goog.require("cljs.core");
|
||||||
goog.provide("app.common.encoding_impl");
|
goog.provide("app.common.encoding_impl");
|
||||||
|
|
||||||
goog.scope(function() {
|
goog.scope(function () {
|
||||||
const core = cljs.core;
|
const core = cljs.core;
|
||||||
const global = goog.global;
|
const global = goog.global;
|
||||||
const self = app.common.encoding_impl;
|
const self = app.common.encoding_impl;
|
||||||
@ -28,8 +28,10 @@ goog.scope(function() {
|
|||||||
// Accept UUID hex format
|
// Accept UUID hex format
|
||||||
input = input.replace(/-/g, "");
|
input = input.replace(/-/g, "");
|
||||||
|
|
||||||
if ((input.length % 2) !== 0) {
|
if (input.length % 2 !== 0) {
|
||||||
throw new RangeError("Expected string to be an even number of characters")
|
throw new RangeError(
|
||||||
|
"Expected string to be an even number of characters",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = new Uint8Array(input.length / 2);
|
const view = new Uint8Array(input.length / 2);
|
||||||
@ -44,7 +46,11 @@ goog.scope(function() {
|
|||||||
function bufferToHex(source, isUuid) {
|
function bufferToHex(source, isUuid) {
|
||||||
if (source instanceof Uint8Array) {
|
if (source instanceof Uint8Array) {
|
||||||
} else if (ArrayBuffer.isView(source)) {
|
} else if (ArrayBuffer.isView(source)) {
|
||||||
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
source = new Uint8Array(
|
||||||
|
source.buffer,
|
||||||
|
source.byteOffset,
|
||||||
|
source.byteLength,
|
||||||
|
);
|
||||||
} else if (Array.isArray(source)) {
|
} else if (Array.isArray(source)) {
|
||||||
source = Uint8Array.from(source);
|
source = Uint8Array.from(source);
|
||||||
}
|
}
|
||||||
@ -56,22 +62,28 @@ goog.scope(function() {
|
|||||||
const spacer = isUuid ? "-" : "";
|
const spacer = isUuid ? "-" : "";
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
return (hexMap[source[i++]] +
|
return (
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] + spacer +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] + spacer +
|
spacer +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] + spacer +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
spacer +
|
||||||
hexMap[source[i++]] + spacer +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
spacer +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
hexMap[source[i++]] +
|
||||||
hexMap[source[i++]] +
|
spacer +
|
||||||
hexMap[source[i++]]);
|
hexMap[source[i++]] +
|
||||||
|
hexMap[source[i++]] +
|
||||||
|
hexMap[source[i++]] +
|
||||||
|
hexMap[source[i++]] +
|
||||||
|
hexMap[source[i++]] +
|
||||||
|
hexMap[source[i++]]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hexToBuffer = hexToBuffer;
|
self.hexToBuffer = hexToBuffer;
|
||||||
@ -87,8 +99,10 @@ goog.scope(function() {
|
|||||||
// for base16 (hex), base32, or base64 encoding in a standards
|
// for base16 (hex), base32, or base64 encoding in a standards
|
||||||
// compliant manner.
|
// compliant manner.
|
||||||
|
|
||||||
function getBaseCodec (ALPHABET) {
|
function getBaseCodec(ALPHABET) {
|
||||||
if (ALPHABET.length >= 255) { throw new TypeError("Alphabet too long"); }
|
if (ALPHABET.length >= 255) {
|
||||||
|
throw new TypeError("Alphabet too long");
|
||||||
|
}
|
||||||
let BASE_MAP = new Uint8Array(256);
|
let BASE_MAP = new Uint8Array(256);
|
||||||
for (let j = 0; j < BASE_MAP.length; j++) {
|
for (let j = 0; j < BASE_MAP.length; j++) {
|
||||||
BASE_MAP[j] = 255;
|
BASE_MAP[j] = 255;
|
||||||
@ -96,22 +110,32 @@ goog.scope(function() {
|
|||||||
for (let i = 0; i < ALPHABET.length; i++) {
|
for (let i = 0; i < ALPHABET.length; i++) {
|
||||||
let x = ALPHABET.charAt(i);
|
let x = ALPHABET.charAt(i);
|
||||||
let xc = x.charCodeAt(0);
|
let xc = x.charCodeAt(0);
|
||||||
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + " is ambiguous"); }
|
if (BASE_MAP[xc] !== 255) {
|
||||||
|
throw new TypeError(x + " is ambiguous");
|
||||||
|
}
|
||||||
BASE_MAP[xc] = i;
|
BASE_MAP[xc] = i;
|
||||||
}
|
}
|
||||||
let BASE = ALPHABET.length;
|
let BASE = ALPHABET.length;
|
||||||
let LEADER = ALPHABET.charAt(0);
|
let LEADER = ALPHABET.charAt(0);
|
||||||
let FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
|
let FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
|
||||||
let iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
|
let iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
|
||||||
function encode (source) {
|
function encode(source) {
|
||||||
if (source instanceof Uint8Array) {
|
if (source instanceof Uint8Array) {
|
||||||
} else if (ArrayBuffer.isView(source)) {
|
} else if (ArrayBuffer.isView(source)) {
|
||||||
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
source = new Uint8Array(
|
||||||
|
source.buffer,
|
||||||
|
source.byteOffset,
|
||||||
|
source.byteLength,
|
||||||
|
);
|
||||||
} else if (Array.isArray(source)) {
|
} else if (Array.isArray(source)) {
|
||||||
source = Uint8Array.from(source);
|
source = Uint8Array.from(source);
|
||||||
}
|
}
|
||||||
if (!(source instanceof Uint8Array)) { throw new TypeError("Expected Uint8Array"); }
|
if (!(source instanceof Uint8Array)) {
|
||||||
if (source.length === 0) { return ""; }
|
throw new TypeError("Expected Uint8Array");
|
||||||
|
}
|
||||||
|
if (source.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
// Skip & count leading zeroes.
|
// Skip & count leading zeroes.
|
||||||
let zeroes = 0;
|
let zeroes = 0;
|
||||||
let length = 0;
|
let length = 0;
|
||||||
@ -129,12 +153,18 @@ goog.scope(function() {
|
|||||||
let carry = source[pbegin];
|
let carry = source[pbegin];
|
||||||
// Apply "b58 = b58 * 256 + ch".
|
// Apply "b58 = b58 * 256 + ch".
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
|
for (
|
||||||
|
let it1 = size - 1;
|
||||||
|
(carry !== 0 || i < length) && it1 !== -1;
|
||||||
|
it1--, i++
|
||||||
|
) {
|
||||||
carry += (256 * b58[it1]) >>> 0;
|
carry += (256 * b58[it1]) >>> 0;
|
||||||
b58[it1] = (carry % BASE) >>> 0;
|
b58[it1] = carry % BASE >>> 0;
|
||||||
carry = (carry / BASE) >>> 0;
|
carry = (carry / BASE) >>> 0;
|
||||||
}
|
}
|
||||||
if (carry !== 0) { throw new Error("Non-zero carry"); }
|
if (carry !== 0) {
|
||||||
|
throw new Error("Non-zero carry");
|
||||||
|
}
|
||||||
length = i;
|
length = i;
|
||||||
pbegin++;
|
pbegin++;
|
||||||
}
|
}
|
||||||
@ -145,13 +175,19 @@ goog.scope(function() {
|
|||||||
}
|
}
|
||||||
// Translate the result into a string.
|
// Translate the result into a string.
|
||||||
let str = LEADER.repeat(zeroes);
|
let str = LEADER.repeat(zeroes);
|
||||||
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
|
for (; it2 < size; ++it2) {
|
||||||
|
str += ALPHABET.charAt(b58[it2]);
|
||||||
|
}
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeUnsafe (source) {
|
function decodeUnsafe(source) {
|
||||||
if (typeof source !== "string") { throw new TypeError("Expected String"); }
|
if (typeof source !== "string") {
|
||||||
if (source.length === 0) { return new Uint8Array(); }
|
throw new TypeError("Expected String");
|
||||||
|
}
|
||||||
|
if (source.length === 0) {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
let psz = 0;
|
let psz = 0;
|
||||||
// Skip and count leading '1's.
|
// Skip and count leading '1's.
|
||||||
let zeroes = 0;
|
let zeroes = 0;
|
||||||
@ -161,21 +197,29 @@ goog.scope(function() {
|
|||||||
psz++;
|
psz++;
|
||||||
}
|
}
|
||||||
// Allocate enough space in big-endian base256 representation.
|
// Allocate enough space in big-endian base256 representation.
|
||||||
let size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
|
let size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.
|
||||||
let b256 = new Uint8Array(size);
|
let b256 = new Uint8Array(size);
|
||||||
// Process the characters.
|
// Process the characters.
|
||||||
while (source[psz]) {
|
while (source[psz]) {
|
||||||
// Decode character
|
// Decode character
|
||||||
let carry = BASE_MAP[source.charCodeAt(psz)];
|
let carry = BASE_MAP[source.charCodeAt(psz)];
|
||||||
// Invalid character
|
// Invalid character
|
||||||
if (carry === 255) { return; }
|
if (carry === 255) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
|
for (
|
||||||
|
let it3 = size - 1;
|
||||||
|
(carry !== 0 || i < length) && it3 !== -1;
|
||||||
|
it3--, i++
|
||||||
|
) {
|
||||||
carry += (BASE * b256[it3]) >>> 0;
|
carry += (BASE * b256[it3]) >>> 0;
|
||||||
b256[it3] = (carry % 256) >>> 0;
|
b256[it3] = carry % 256 >>> 0;
|
||||||
carry = (carry / 256) >>> 0;
|
carry = (carry / 256) >>> 0;
|
||||||
}
|
}
|
||||||
if (carry !== 0) { throw new Error("Non-zero carry"); }
|
if (carry !== 0) {
|
||||||
|
throw new Error("Non-zero carry");
|
||||||
|
}
|
||||||
length = i;
|
length = i;
|
||||||
psz++;
|
psz++;
|
||||||
}
|
}
|
||||||
@ -192,20 +236,22 @@ goog.scope(function() {
|
|||||||
return vch;
|
return vch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decode (string) {
|
function decode(string) {
|
||||||
let buffer = decodeUnsafe(string);
|
let buffer = decodeUnsafe(string);
|
||||||
if (buffer) { return buffer; }
|
if (buffer) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
throw new Error("Non-base" + BASE + " character");
|
throw new Error("Non-base" + BASE + " character");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encode: encode,
|
encode: encode,
|
||||||
decodeUnsafe: decodeUnsafe,
|
decodeUnsafe: decodeUnsafe,
|
||||||
decode: decode
|
decode: decode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// MORE bases here: https://github.com/cryptocoinjs/base-x/tree/master
|
// MORE bases here: https://github.com/cryptocoinjs/base-x/tree/master
|
||||||
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const BASE62 =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
self.bufferToBase62 = getBaseCodec(BASE62).encode;
|
self.bufferToBase62 = getBaseCodec(BASE62).encode;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
goog.provide("app.common.svg.path.arc_to_bezier");
|
goog.provide("app.common.svg.path.arc_to_bezier");
|
||||||
|
|
||||||
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
|
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
|
||||||
goog.scope(function() {
|
goog.scope(function () {
|
||||||
const self = app.common.svg.path.arc_to_bezier;
|
const self = app.common.svg.path.arc_to_bezier;
|
||||||
|
|
||||||
var TAU = Math.PI * 2;
|
var TAU = Math.PI * 2;
|
||||||
@ -27,20 +27,23 @@ goog.scope(function() {
|
|||||||
// we can use simplified math (without length normalization)
|
// we can use simplified math (without length normalization)
|
||||||
//
|
//
|
||||||
function unit_vector_angle(ux, uy, vx, vy) {
|
function unit_vector_angle(ux, uy, vx, vy) {
|
||||||
var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
|
var sign = ux * vy - uy * vx < 0 ? -1 : 1;
|
||||||
var dot = ux * vx + uy * vy;
|
var dot = ux * vx + uy * vy;
|
||||||
|
|
||||||
// Add this to work with arbitrary vectors:
|
// Add this to work with arbitrary vectors:
|
||||||
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
||||||
|
|
||||||
// rounding errors, e.g. -1.0000000000000002 can screw up this
|
// rounding errors, e.g. -1.0000000000000002 can screw up this
|
||||||
if (dot > 1.0) { dot = 1.0; }
|
if (dot > 1.0) {
|
||||||
if (dot < -1.0) { dot = -1.0; }
|
dot = 1.0;
|
||||||
|
}
|
||||||
|
if (dot < -1.0) {
|
||||||
|
dot = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
return sign * Math.acos(dot);
|
return sign * Math.acos(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Convert from endpoint to center parameterization,
|
// Convert from endpoint to center parameterization,
|
||||||
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
||||||
//
|
//
|
||||||
@ -53,11 +56,11 @@ goog.scope(function() {
|
|||||||
// points. After that, rotate it to line up ellipse axes with coordinate
|
// points. After that, rotate it to line up ellipse axes with coordinate
|
||||||
// axes.
|
// axes.
|
||||||
//
|
//
|
||||||
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
|
||||||
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
|
||||||
|
|
||||||
var rx_sq = rx * rx;
|
var rx_sq = rx * rx;
|
||||||
var ry_sq = ry * ry;
|
var ry_sq = ry * ry;
|
||||||
var x1p_sq = x1p * x1p;
|
var x1p_sq = x1p * x1p;
|
||||||
var y1p_sq = y1p * y1p;
|
var y1p_sq = y1p * y1p;
|
||||||
|
|
||||||
@ -66,33 +69,33 @@ goog.scope(function() {
|
|||||||
// Compute coordinates of the centre of this ellipse (cx', cy')
|
// Compute coordinates of the centre of this ellipse (cx', cy')
|
||||||
// in the new coordinate system.
|
// in the new coordinate system.
|
||||||
//
|
//
|
||||||
var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);
|
var radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq;
|
||||||
|
|
||||||
if (radicant < 0) {
|
if (radicant < 0) {
|
||||||
// due to rounding errors it might be e.g. -1.3877787807814457e-17
|
// due to rounding errors it might be e.g. -1.3877787807814457e-17
|
||||||
radicant = 0;
|
radicant = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
|
radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq;
|
||||||
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
|
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
|
||||||
|
|
||||||
var cxp = radicant * rx/ry * y1p;
|
var cxp = ((radicant * rx) / ry) * y1p;
|
||||||
var cyp = radicant * -ry/rx * x1p;
|
var cyp = ((radicant * -ry) / rx) * x1p;
|
||||||
|
|
||||||
// Step 3.
|
// Step 3.
|
||||||
//
|
//
|
||||||
// Transform back to get centre coordinates (cx, cy) in the original
|
// Transform back to get centre coordinates (cx, cy) in the original
|
||||||
// coordinate system.
|
// coordinate system.
|
||||||
//
|
//
|
||||||
var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
|
var cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2;
|
||||||
var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;
|
var cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2;
|
||||||
|
|
||||||
// Step 4.
|
// Step 4.
|
||||||
//
|
//
|
||||||
// Compute angles (theta1, delta_theta).
|
// Compute angles (theta1, delta_theta).
|
||||||
//
|
//
|
||||||
var v1x = (x1p - cxp) / rx;
|
var v1x = (x1p - cxp) / rx;
|
||||||
var v1y = (y1p - cyp) / ry;
|
var v1y = (y1p - cyp) / ry;
|
||||||
var v2x = (-x1p - cxp) / rx;
|
var v2x = (-x1p - cxp) / rx;
|
||||||
var v2y = (-y1p - cyp) / ry;
|
var v2y = (-y1p - cyp) / ry;
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ goog.scope(function() {
|
|||||||
delta_theta += TAU;
|
delta_theta += TAU;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ cx, cy, theta1, delta_theta ];
|
return [cx, cy, theta1, delta_theta];
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -114,24 +117,33 @@ goog.scope(function() {
|
|||||||
// see http://math.stackexchange.com/questions/873224
|
// see http://math.stackexchange.com/questions/873224
|
||||||
//
|
//
|
||||||
function approximate_unit_arc(theta1, delta_theta) {
|
function approximate_unit_arc(theta1, delta_theta) {
|
||||||
var alpha = 4/3 * Math.tan(delta_theta/4);
|
var alpha = (4 / 3) * Math.tan(delta_theta / 4);
|
||||||
|
|
||||||
var x1 = Math.cos(theta1);
|
var x1 = Math.cos(theta1);
|
||||||
var y1 = Math.sin(theta1);
|
var y1 = Math.sin(theta1);
|
||||||
var x2 = Math.cos(theta1 + delta_theta);
|
var x2 = Math.cos(theta1 + delta_theta);
|
||||||
var y2 = Math.sin(theta1 + delta_theta);
|
var y2 = Math.sin(theta1 + delta_theta);
|
||||||
|
|
||||||
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
|
return [
|
||||||
|
x1,
|
||||||
|
y1,
|
||||||
|
x1 - y1 * alpha,
|
||||||
|
y1 + x1 * alpha,
|
||||||
|
x2 + y2 * alpha,
|
||||||
|
y2 - x2 * alpha,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculate_beziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
|
function calculate_beziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
|
||||||
var sin_phi = Math.sin(phi * TAU / 360);
|
var sin_phi = Math.sin((phi * TAU) / 360);
|
||||||
var cos_phi = Math.cos(phi * TAU / 360);
|
var cos_phi = Math.cos((phi * TAU) / 360);
|
||||||
|
|
||||||
// Make sure radii are valid
|
// Make sure radii are valid
|
||||||
//
|
//
|
||||||
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
|
||||||
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
|
||||||
|
|
||||||
// console.log("L", x1p, y1p)
|
// console.log("L", x1p, y1p)
|
||||||
|
|
||||||
@ -145,7 +157,6 @@ goog.scope(function() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Compensate out-of-range radii
|
// Compensate out-of-range radii
|
||||||
//
|
//
|
||||||
rx = Math.abs(rx);
|
rx = Math.abs(rx);
|
||||||
@ -157,25 +168,20 @@ goog.scope(function() {
|
|||||||
ry *= Math.sqrt(lambda);
|
ry *= Math.sqrt(lambda);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Get center parameters (cx, cy, theta1, delta_theta)
|
// Get center parameters (cx, cy, theta1, delta_theta)
|
||||||
//
|
//
|
||||||
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
|
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
|
||||||
|
|
||||||
|
|
||||||
var result = [];
|
var result = [];
|
||||||
var theta1 = cc[2];
|
var theta1 = cc[2];
|
||||||
var delta_theta = cc[3];
|
var delta_theta = cc[3];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Split an arc to multiple segments, so each segment
|
// Split an arc to multiple segments, so each segment
|
||||||
// will be less than τ/4 (= 90°)
|
// will be less than τ/4 (= 90°)
|
||||||
//
|
//
|
||||||
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
|
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
|
||||||
delta_theta /= segments;
|
delta_theta /= segments;
|
||||||
|
|
||||||
|
|
||||||
for (var i = 0; i < segments; i++) {
|
for (var i = 0; i < segments; i++) {
|
||||||
var item = approximate_unit_arc(theta1, delta_theta);
|
var item = approximate_unit_arc(theta1, delta_theta);
|
||||||
result.push(item);
|
result.push(item);
|
||||||
@ -195,8 +201,8 @@ goog.scope(function() {
|
|||||||
y *= ry;
|
y *= ry;
|
||||||
|
|
||||||
// rotate
|
// rotate
|
||||||
var xp = cos_phi*x - sin_phi*y;
|
var xp = cos_phi * x - sin_phi * y;
|
||||||
var yp = sin_phi*x + cos_phi*y;
|
var yp = sin_phi * x + cos_phi * y;
|
||||||
|
|
||||||
// translate
|
// translate
|
||||||
curve[i + 0] = xp + cc[0];
|
curve[i + 0] = xp + cc[0];
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -234,7 +234,7 @@
|
|||||||
(dfn-format v "p")
|
(dfn-format v "p")
|
||||||
|
|
||||||
:localized-date-time
|
:localized-date-time
|
||||||
(dfn-format v "PPPp")
|
(dfn-format v "PPP . p")
|
||||||
|
|
||||||
(if (string? fmt)
|
(if (string? fmt)
|
||||||
(dfn-format v fmt)
|
(dfn-format v fmt)
|
||||||
|
|||||||
@ -10,16 +10,18 @@
|
|||||||
goog.require("app.common.encoding_impl");
|
goog.require("app.common.encoding_impl");
|
||||||
goog.provide("app.common.uuid_impl");
|
goog.provide("app.common.uuid_impl");
|
||||||
|
|
||||||
goog.scope(function() {
|
goog.scope(function () {
|
||||||
const global = goog.global;
|
const global = goog.global;
|
||||||
const encoding = app.common.encoding_impl;
|
const encoding = app.common.encoding_impl;
|
||||||
const self = app.common.uuid_impl;
|
const self = app.common.uuid_impl;
|
||||||
|
|
||||||
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
|
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
|
||||||
|
|
||||||
const fill = (() => {
|
const fill = (() => {
|
||||||
if (typeof global.crypto !== "undefined" &&
|
if (
|
||||||
typeof global.crypto.getRandomValues !== "undefined") {
|
typeof global.crypto !== "undefined" &&
|
||||||
|
typeof global.crypto.getRandomValues !== "undefined"
|
||||||
|
) {
|
||||||
return (buf) => {
|
return (buf) => {
|
||||||
global.crypto.getRandomValues(buf);
|
global.crypto.getRandomValues(buf);
|
||||||
return buf;
|
return buf;
|
||||||
@ -30,7 +32,7 @@ goog.scope(function() {
|
|||||||
|
|
||||||
return (buf) => {
|
return (buf) => {
|
||||||
const bytes = randomBytes(buf.length);
|
const bytes = randomBytes(buf.length);
|
||||||
buf.set(bytes)
|
buf.set(bytes);
|
||||||
return buf;
|
return buf;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -39,8 +41,10 @@ goog.scope(function() {
|
|||||||
|
|
||||||
return (buf) => {
|
return (buf) => {
|
||||||
for (let i = 0, r; i < buf.length; i++) {
|
for (let i = 0, r; i < buf.length; i++) {
|
||||||
if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
|
if ((i & 0x03) === 0) {
|
||||||
buf[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
r = Math.random() * 0x100000000;
|
||||||
|
}
|
||||||
|
buf[i] = (r >>> ((i & 0x03) << 3)) & 0xff;
|
||||||
}
|
}
|
||||||
return buf;
|
return buf;
|
||||||
};
|
};
|
||||||
@ -50,31 +54,38 @@ goog.scope(function() {
|
|||||||
function toHexString(buf) {
|
function toHexString(buf) {
|
||||||
const hexMap = encoding.hexMap;
|
const hexMap = encoding.hexMap;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
return (hexMap[buf[i++]] +
|
return (
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] + '-' +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] + '-' +
|
"-" +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] + '-' +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
"-" +
|
||||||
hexMap[buf[i++]] + '-' +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
"-" +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
hexMap[buf[i++]] +
|
||||||
hexMap[buf[i++]] +
|
"-" +
|
||||||
hexMap[buf[i++]]);
|
hexMap[buf[i++]] +
|
||||||
};
|
hexMap[buf[i++]] +
|
||||||
|
hexMap[buf[i++]] +
|
||||||
|
hexMap[buf[i++]] +
|
||||||
|
hexMap[buf[i++]] +
|
||||||
|
hexMap[buf[i++]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getBigUint64(view, byteOffset, le) {
|
function getBigUint64(view, byteOffset, le) {
|
||||||
const a = view.getUint32(byteOffset, le);
|
const a = view.getUint32(byteOffset, le);
|
||||||
const b = view.getUint32(byteOffset + 4, le);
|
const b = view.getUint32(byteOffset + 4, le);
|
||||||
const leMask = Number(!!le);
|
const leMask = Number(!!le);
|
||||||
const beMask = Number(!le);
|
const beMask = Number(!le);
|
||||||
return ((BigInt(a * beMask + b * leMask) << 32n) |
|
return (
|
||||||
(BigInt(a * leMask + b * beMask)));
|
(BigInt(a * beMask + b * leMask) << 32n) | BigInt(a * leMask + b * beMask)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBigUint64(view, byteOffset, value, le) {
|
function setBigUint64(view, byteOffset, value, le) {
|
||||||
@ -83,8 +94,7 @@ goog.scope(function() {
|
|||||||
if (le) {
|
if (le) {
|
||||||
view.setUint32(byteOffset + 4, hi, le);
|
view.setUint32(byteOffset + 4, hi, le);
|
||||||
view.setUint32(byteOffset, lo, le);
|
view.setUint32(byteOffset, lo, le);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
view.setUint32(byteOffset, hi, le);
|
view.setUint32(byteOffset, hi, le);
|
||||||
view.setUint32(byteOffset + 4, lo, le);
|
view.setUint32(byteOffset + 4, lo, le);
|
||||||
}
|
}
|
||||||
@ -104,17 +114,18 @@ goog.scope(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.shortID = (function () {
|
self.shortID = (function () {
|
||||||
const buff = new ArrayBuffer(8);
|
const buff = new ArrayBuffer(8);
|
||||||
const int8 = new Uint8Array(buff);
|
const int8 = new Uint8Array(buff);
|
||||||
const view = new DataView(buff);
|
const view = new DataView(buff);
|
||||||
|
|
||||||
const base = 0x0000_0000_0000_0000n;
|
const base = 0x0000_0000_0000_0000n;
|
||||||
|
|
||||||
return function shortID(ts) {
|
return function shortID(ts) {
|
||||||
const tss = currentTimestamp(timeRef);
|
const tss = currentTimestamp(timeRef);
|
||||||
const msb = (base
|
const msb =
|
||||||
| (nextLong() & 0xffff_ffff_0000_0000n)
|
base |
|
||||||
| (tss & 0x0000_0000_ffff_ffffn));
|
(nextLong() & 0xffff_ffff_0000_0000n) |
|
||||||
|
(tss & 0x0000_0000_ffff_ffffn);
|
||||||
setBigUint64(view, 0, msb, false);
|
setBigUint64(view, 0, msb, false);
|
||||||
return encoding.toBase62(int8);
|
return encoding.toBase62(int8);
|
||||||
};
|
};
|
||||||
@ -139,9 +150,9 @@ goog.scope(function() {
|
|||||||
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
|
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
|
||||||
|
|
||||||
let countCs = 0n;
|
let countCs = 0n;
|
||||||
let lastRd = 0n;
|
let lastRd = 0n;
|
||||||
let lastCs = 0n;
|
let lastCs = 0n;
|
||||||
let lastTs = 0n;
|
let lastTs = 0n;
|
||||||
let baseMsb = 0x0000_0000_0000_8000n;
|
let baseMsb = 0x0000_0000_0000_8000n;
|
||||||
let baseLsb = 0x8000_0000_0000_0000n;
|
let baseLsb = 0x8000_0000_0000_0000n;
|
||||||
|
|
||||||
@ -149,12 +160,9 @@ goog.scope(function() {
|
|||||||
lastCs = nextLong() & maxCs;
|
lastCs = nextLong() & maxCs;
|
||||||
|
|
||||||
const create = function create(ts, lastRd, lastCs) {
|
const create = function create(ts, lastRd, lastCs) {
|
||||||
const msb = (baseMsb
|
const msb = baseMsb | (lastRd & 0xffff_ffff_ffff_0fffn);
|
||||||
| (lastRd & 0xffff_ffff_ffff_0fffn));
|
|
||||||
|
|
||||||
const lsb = (baseLsb
|
const lsb = baseLsb | ((ts << 14n) & 0x3fff_ffff_ffff_c000n) | lastCs;
|
||||||
| ((ts << 14n) & 0x3fff_ffff_ffff_c000n)
|
|
||||||
| lastCs);
|
|
||||||
|
|
||||||
setBigUint64(view, 0, msb, false);
|
setBigUint64(view, 0, msb, false);
|
||||||
setBigUint64(view, 8, lsb, false);
|
setBigUint64(view, 8, lsb, false);
|
||||||
@ -167,10 +175,10 @@ goog.scope(function() {
|
|||||||
let ts = currentTimestamp(timeRef);
|
let ts = currentTimestamp(timeRef);
|
||||||
|
|
||||||
// Protect from clock regression
|
// Protect from clock regression
|
||||||
if ((ts - lastTs) < 0) {
|
if (ts - lastTs < 0) {
|
||||||
lastRd = (lastRd
|
lastRd =
|
||||||
& 0x0000_0000_0000_0f00n
|
(lastRd & 0x0000_0000_0000_0f00n) |
|
||||||
| (nextLong() & 0xffff_ffff_ffff_f0ffn));
|
(nextLong() & 0xffff_ffff_ffff_f0ffn);
|
||||||
countCs = 0n;
|
countCs = 0n;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -209,63 +217,63 @@ goog.scope(function() {
|
|||||||
|
|
||||||
// Parse ........-....-....-####-............
|
// Parse ........-....-....-####-............
|
||||||
int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
|
int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
|
||||||
int8[9] = rest & 0xff,
|
(int8[9] = rest & 0xff),
|
||||||
|
// Parse ........-....-....-....-############
|
||||||
// Parse ........-....-....-....-############
|
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
||||||
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
(int8[10] =
|
||||||
int8[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
|
((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff);
|
||||||
int8[11] = (rest / 0x100000000) & 0xff;
|
int8[11] = (rest / 0x100000000) & 0xff;
|
||||||
int8[12] = (rest >>> 24) & 0xff;
|
int8[12] = (rest >>> 24) & 0xff;
|
||||||
int8[13] = (rest >>> 16) & 0xff;
|
int8[13] = (rest >>> 16) & 0xff;
|
||||||
int8[14] = (rest >>> 8) & 0xff;
|
int8[14] = (rest >>> 8) & 0xff;
|
||||||
int8[15] = rest & 0xff;
|
int8[15] = rest & 0xff;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fromPair = (hi, lo) => {
|
const fromPair = (hi, lo) => {
|
||||||
view.setBigInt64(0, hi);
|
view.setBigInt64(0, hi);
|
||||||
view.setBigInt64(8, lo);
|
view.setBigInt64(8, lo);
|
||||||
return encoding.bufferToHex(int8, true);
|
return encoding.bufferToHex(int8, true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getHi = (uuid) => {
|
const getHi = (uuid) => {
|
||||||
fillBytes(uuid);
|
fillBytes(uuid);
|
||||||
return view.getBigInt64(0);
|
return view.getBigInt64(0);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getLo = (uuid) => {
|
const getLo = (uuid) => {
|
||||||
fillBytes(uuid);
|
fillBytes(uuid);
|
||||||
return view.getBigInt64(8);
|
return view.getBigInt64(8);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getBytes = (uuid) => {
|
const getBytes = (uuid) => {
|
||||||
fillBytes(uuid);
|
fillBytes(uuid);
|
||||||
return Int8Array.from(int8);
|
return Int8Array.from(int8);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getUnsignedParts = (uuid) => {
|
const getUnsignedParts = (uuid) => {
|
||||||
fillBytes(uuid);
|
fillBytes(uuid);
|
||||||
const result = new Uint32Array(4);
|
const result = new Uint32Array(4);
|
||||||
|
|
||||||
result[0] = view.getUint32(0)
|
result[0] = view.getUint32(0);
|
||||||
result[1] = view.getUint32(4);
|
result[1] = view.getUint32(4);
|
||||||
result[2] = view.getUint32(8);
|
result[2] = view.getUint32(8);
|
||||||
result[3] = view.getUint32(12);
|
result[3] = view.getUint32(12);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
const fromUnsignedParts = (a, b, c, d) => {
|
const fromUnsignedParts = (a, b, c, d) => {
|
||||||
view.setUint32(0, a)
|
view.setUint32(0, a);
|
||||||
view.setUint32(4, b)
|
view.setUint32(4, b);
|
||||||
view.setUint32(8, c)
|
view.setUint32(8, c);
|
||||||
view.setUint32(12, d)
|
view.setUint32(12, d);
|
||||||
return encoding.bufferToHex(int8, true);
|
return encoding.bufferToHex(int8, true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const fromArray = (u8data) => {
|
const fromArray = (u8data) => {
|
||||||
int8.set(u8data);
|
int8.set(u8data);
|
||||||
return encoding.bufferToHex(int8, true);
|
return encoding.bufferToHex(int8, true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const setTag = (tag) => {
|
const setTag = (tag) => {
|
||||||
tag = BigInt.asUintN(64, "" + tag);
|
tag = BigInt.asUintN(64, "" + tag);
|
||||||
@ -273,9 +281,9 @@ goog.scope(function() {
|
|||||||
throw new Error("illegal arguments: tag value should fit in 4bits");
|
throw new Error("illegal arguments: tag value should fit in 4bits");
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRd = (lastRd
|
lastRd =
|
||||||
& 0xffff_ffff_ffff_f0ffn
|
(lastRd & 0xffff_ffff_ffff_f0ffn) |
|
||||||
| ((tag << 8) & 0x0000_0000_0000_0f00n));
|
((tag << 8) & 0x0000_0000_0000_0f00n);
|
||||||
};
|
};
|
||||||
|
|
||||||
factory.create = create;
|
factory.create = create;
|
||||||
@ -290,9 +298,9 @@ goog.scope(function() {
|
|||||||
return factory;
|
return factory;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
self.shortV8 = function(uuid) {
|
self.shortV8 = function (uuid) {
|
||||||
const buff = encoding.hexToBuffer(uuid);
|
const buff = encoding.hexToBuffer(uuid);
|
||||||
const short = new Uint8Array(buff, 4);
|
const short = new Uint8Array(buff, 4);
|
||||||
return encoding.bufferToBase62(short);
|
return encoding.bufferToBase62(short);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -307,7 +315,7 @@ goog.scope(function() {
|
|||||||
return self.v8.fromPair(hi, lo);
|
return self.v8.fromPair(hi, lo);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.fromBytes = function(data) {
|
self.fromBytes = function (data) {
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
return self.v8.fromArray(data);
|
return self.v8.fromArray(data);
|
||||||
} else if (data instanceof Int8Array) {
|
} else if (data instanceof Int8Array) {
|
||||||
@ -325,15 +333,15 @@ goog.scope(function() {
|
|||||||
return self.v8.getUnsignedParts(uuid);
|
return self.v8.getUnsignedParts(uuid);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.fromUnsignedParts = function(a,b,c,d) {
|
self.fromUnsignedParts = function (a, b, c, d) {
|
||||||
return self.v8.fromUnsignedParts(a,b,c,d);
|
return self.v8.fromUnsignedParts(a, b, c, d);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.getHi = function (uuid) {
|
self.getHi = function (uuid) {
|
||||||
return self.v8.getHi(uuid);
|
return self.v8.getHi(uuid);
|
||||||
}
|
};
|
||||||
|
|
||||||
self.getLo = function (uuid) {
|
self.getLo = function (uuid) {
|
||||||
return self.v8.getLo(uuid);
|
return self.v8.getLo(uuid);
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -67,8 +67,11 @@ export class WeakEqMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
if (key === null || (typeof key !== 'object' && typeof key !== 'function')) {
|
if (
|
||||||
throw new TypeError('WeakEqMap keys must be objects (like WeakMap).');
|
key === null ||
|
||||||
|
(typeof key !== "object" && typeof key !== "function")
|
||||||
|
) {
|
||||||
|
throw new TypeError("WeakEqMap keys must be objects (like WeakMap).");
|
||||||
}
|
}
|
||||||
const hash = this._hash(key);
|
const hash = this._hash(key);
|
||||||
const bucket = this._getBucket(hash);
|
const bucket = this._getBucket(hash);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#kaocha/v1
|
#kaocha/v1
|
||||||
{:tests [{:id :unit
|
{:tests [{:id :unit
|
||||||
:test-paths ["test"]}]
|
:test-paths ["test"]}]
|
||||||
:kaocha/reporter [kaocha.report/dots]}
|
:kaocha/reporter [kaocha.report/dots]}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ RUN set -ex; \
|
|||||||
curl \
|
curl \
|
||||||
bash \
|
bash \
|
||||||
git \
|
git \
|
||||||
|
ripgrep \
|
||||||
\
|
\
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
|||||||
@ -34,7 +34,8 @@
|
|||||||
"watch": "pnpm run watch:app",
|
"watch": "pnpm run watch:app",
|
||||||
"build:app": "clojure -M:dev:shadow-cljs release main",
|
"build:app": "clojure -M:dev:shadow-cljs release main",
|
||||||
"build": "pnpm run clear:shadow-cache && pnpm run build:app",
|
"build": "pnpm run clear:shadow-cache && pnpm run build:app",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/",
|
"fmt": "cljfmt fix --parallel=true src/",
|
||||||
"lint:clj": "cljfmt check --parallel src/ && clj-kondo --parallel --lint src/"
|
"check-fmt": "cljfmt check --parallel=true src/",
|
||||||
|
"lint": "clj-kondo --parallel --lint src/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,14 +100,12 @@
|
|||||||
|
|
||||||
(def browser-pool-factory
|
(def browser-pool-factory
|
||||||
(letfn [(create []
|
(letfn [(create []
|
||||||
(-> (p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||||
browser (.launch pw/chromium opts)
|
browser (.launch pw/chromium opts)
|
||||||
id (swap! pool-browser-id inc)]
|
id (swap! pool-browser-id inc)]
|
||||||
(l/info :origin "factory" :action "create" :browser-id id)
|
(l/info :origin "factory" :action "create" :browser-id id)
|
||||||
(unchecked-set browser "__id" id)
|
(unchecked-set browser "__id" id)
|
||||||
browser)
|
browser))
|
||||||
(p/catch (fn [cause]
|
|
||||||
(l/error :hint "Cannot launch the headless browser" :cause cause)))))
|
|
||||||
|
|
||||||
(destroy [obj]
|
(destroy [obj]
|
||||||
(let [id (unchecked-get obj "__id")]
|
(let [id (unchecked-get obj "__id")]
|
||||||
|
|||||||
@ -23,12 +23,15 @@
|
|||||||
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
|
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
|
||||||
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
|
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
|
||||||
"build:app": "pnpm run clear:shadow-cache && pnpm run build:app:main && pnpm run build:app:libs",
|
"build:app": "pnpm run clear:shadow-cache && pnpm run build:app:main && pnpm run build:app:libs",
|
||||||
|
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||||
|
"check-fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js",
|
||||||
|
"check-fmt:scss": "prettier -c resources/styles -c src/**/*.scss",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
"fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
"fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||||
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
|
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
|
||||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
"lint:clj": "clj-kondo --parallel --lint ../common/src src/",
|
||||||
"lint:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
"lint:js": "exit 0",
|
||||||
"lint:scss": "prettier -c resources/styles -c src/**/*.scss",
|
"lint:scss": "exit 0",
|
||||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||||
"test": "pnpm run build:test && node target/tests/test.js",
|
"test": "pnpm run build:test && node target/tests/test.js",
|
||||||
"test:storybook": "vitest run --project=storybook",
|
"test:storybook": "vitest run --project=storybook",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,513 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"plugins/runtime",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"variants/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"render-wasm/v1",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~u0b5bcbca-32ab-81eb-8005-a15fc4484678",
|
||||||
|
"~: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": "New File 6",
|
||||||
|
"~:revn": 1,
|
||||||
|
"~:modified-at": "~m1773140377840",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~ueffcbebc-b8c8-802f-8007-b11dd34fe190",
|
||||||
|
"~: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": "~u0b5bcbca-32ab-81eb-8005-a15fc448f334",
|
||||||
|
"~:created-at": "~m1773140371775",
|
||||||
|
"~:backend": "legacy-db",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~ueffcbebc-b8c8-802f-8007-b11dd34fe191"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~ueffcbebc-b8c8-802f-8007-b11dd34fe191": {
|
||||||
|
"~:objects": {
|
||||||
|
"~u00000000-0000-0000-0000-000000000000": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 0,
|
||||||
|
"~:hide-fill-on-export": false,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "Root Frame",
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:type": "~:frame",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 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,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0,
|
||||||
|
"~:width": 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,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:flip-y": null,
|
||||||
|
"~:shapes": [
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79f9",
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79fa",
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79fb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79f9": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 660.000001521671,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:fixed",
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "Rectangle",
|
||||||
|
"~:width": 99.9999986310249,
|
||||||
|
"~:type": "~:rect",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 660.000001521671
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1088.99999863103,
|
||||||
|
"~:y": 660.000001521671
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1088.99999863103,
|
||||||
|
"~:y": 760.000000795896
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 760.000000795896
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:r3": 0,
|
||||||
|
"~:r1": 0,
|
||||||
|
"~:id": "~ub952fb5e-cae5-8054-8007-b11dd63f79f9",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-color": "#000000",
|
||||||
|
"~:stroke-opacity": 1,
|
||||||
|
"~:stroke-width": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 989,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 660.000001521671,
|
||||||
|
"~:width": 99.9999986310249,
|
||||||
|
"~:height": 99.9999992742251,
|
||||||
|
"~:x1": 989,
|
||||||
|
"~:y1": 660.000001521671,
|
||||||
|
"~:x2": 1088.99999863103,
|
||||||
|
"~:y2": 760.000000795896
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#B1B2B5",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 99.9999992742251,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79fa": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 457.999994456768,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:fixed",
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "Ellipse",
|
||||||
|
"~:width": 299.99999499321,
|
||||||
|
"~:type": "~:circle",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1171.99998355202,
|
||||||
|
"~:y": 457.999994456768
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1471.99997854523,
|
||||||
|
"~:y": 457.999994456768
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1471.99997854523,
|
||||||
|
"~:y": 757.999989449978
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1171.99998355202,
|
||||||
|
"~:y": 757.999989449978
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ub952fb5e-cae5-8054-8007-b11dd63f79fa",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-color": "#000000",
|
||||||
|
"~:stroke-opacity": 1,
|
||||||
|
"~:stroke-width": 400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 1171.99998355202,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 1171.99998355202,
|
||||||
|
"~:y": 457.999994456768,
|
||||||
|
"~:width": 299.99999499321,
|
||||||
|
"~:height": 299.99999499321,
|
||||||
|
"~:x1": 1171.99998355202,
|
||||||
|
"~:y1": 457.999994456768,
|
||||||
|
"~:x2": 1471.99997854523,
|
||||||
|
"~:y2": 757.999989449978
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#B1B2B5",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 299.99999499321,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~ub952fb5e-cae5-8054-8007-b11dd63f79fb": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 444,
|
||||||
|
"~:hide-fill-on-export": false,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "Board",
|
||||||
|
"~:width": 100,
|
||||||
|
"~:type": "~:frame",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 444
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1089,
|
||||||
|
"~:y": 444
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1089,
|
||||||
|
"~:y": 544
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 544
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:r3": 0,
|
||||||
|
"~:r1": 0,
|
||||||
|
"~:id": "~ub952fb5e-cae5-8054-8007-b11dd63f79fb",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-color": "#000000",
|
||||||
|
"~:stroke-opacity": 1,
|
||||||
|
"~:stroke-width": 200
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 989,
|
||||||
|
"~:proportion": 1,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 989,
|
||||||
|
"~:y": 444,
|
||||||
|
"~:width": 100,
|
||||||
|
"~:height": 100,
|
||||||
|
"~:x1": 989,
|
||||||
|
"~:y1": 444,
|
||||||
|
"~:x2": 1089,
|
||||||
|
"~:y2": 544
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#FFFFFF",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 100,
|
||||||
|
"~:flip-y": null,
|
||||||
|
"~:shapes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ueffcbebc-b8c8-802f-8007-b11dd34fe191",
|
||||||
|
"~:name": "Page 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ueffcbebc-b8c8-802f-8007-b11dd34fe190",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,737 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"variants/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"fdata/pointer-map",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~u0b5bcbca-32ab-81eb-8005-a15fc4484678",
|
||||||
|
"~: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": "holadios",
|
||||||
|
"~:revn": 54,
|
||||||
|
"~:modified-at": "~m1773136426990",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~ueffcbebc-b8c8-802f-8007-b0ebecd7ebf4",
|
||||||
|
"~: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",
|
||||||
|
"0004-clean-shadow-color",
|
||||||
|
"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",
|
||||||
|
"0016-copy-fills-from-position-data-to-text-node",
|
||||||
|
"0015-clean-shadow-color"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~u0b5bcbca-32ab-81eb-8005-a15fc448f334",
|
||||||
|
"~:created-at": "~m1773127290716",
|
||||||
|
"~:backend": "legacy-db",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~u3e9e17c3-fc57-80ce-8007-101743996fe9"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u3e9e17c3-fc57-80ce-8007-101743996fe9": {
|
||||||
|
"~:id": "~u3e9e17c3-fc57-80ce-8007-101743996fe9",
|
||||||
|
"~:name": "Page 1",
|
||||||
|
"~:objects": {
|
||||||
|
"~u00000000-0000-0000-0000-000000000000": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 0,
|
||||||
|
"~:hide-fill-on-export": false,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:name": "Root Frame",
|
||||||
|
"~:width": 0.01,
|
||||||
|
"~:type": "~:frame",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0.01,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:r2": 0,
|
||||||
|
"~:proportion-lock": false,
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:page-id": "~u3e9e17c3-fc57-80ce-8007-101743996fe9",
|
||||||
|
"~: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,
|
||||||
|
"~:r4": 0,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 0,
|
||||||
|
"~:y": 0,
|
||||||
|
"~:width": 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,
|
||||||
|
"~:height": 0.01,
|
||||||
|
"~:flip-y": null,
|
||||||
|
"~:shapes": [
|
||||||
|
"~u7d004cdb-8305-806a-8007-b0f01ee65230",
|
||||||
|
"~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea",
|
||||||
|
"~u2ae0abdc-99ff-8009-8007-b0f7f45177dc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~u7d004cdb-8305-806a-8007-b0f01ee65230": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": -161.000001410182,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:auto-width",
|
||||||
|
"~:content": {
|
||||||
|
"~:type": "root",
|
||||||
|
"~:key": "6hv3a5x8wb",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:type": "paragraph-set",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:typography-ref-id": null,
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "219sqyfv11",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:typography-ref-file": null,
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:typography-ref-id": null,
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "21rct71nkal",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:typography-ref-file": null,
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:type": "paragraph",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:vertical-align": "top"
|
||||||
|
},
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "HELLO WORLD",
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:type": "~:text",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 109.999998223851,
|
||||||
|
"~:y": -161.000001410182
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1639.00000215977,
|
||||||
|
"~:y": -161.000001410182
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1639.00000215977,
|
||||||
|
"~:y": 138.999988970841
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 109.999998223851,
|
||||||
|
"~:y": 138.999988970841
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u7d004cdb-8305-806a-8007-b0f01ee65230",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:position-data": [
|
||||||
|
{
|
||||||
|
"~:y": 150.869995117188,
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:width": 1528.56005859375,
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:x": 110,
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:direction": "ltr",
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:height": 323.739990234375,
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:center",
|
||||||
|
"~:stroke-width": 50,
|
||||||
|
"~:stroke-color": "#43e50b",
|
||||||
|
"~:stroke-opacity": 0.6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 109.999998223851,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 109.999998223851,
|
||||||
|
"~:y": -161.000001410182,
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:height": 299.999990381022,
|
||||||
|
"~:x1": 109.999998223851,
|
||||||
|
"~:y1": -161.000001410182,
|
||||||
|
"~:x2": 1639.00000215977,
|
||||||
|
"~:y2": 138.999988970841
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 299.999990381022,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": -462.000004970439,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:auto-width",
|
||||||
|
"~:content": {
|
||||||
|
"~:type": "root",
|
||||||
|
"~:key": "6hv3a5x8wb",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:type": "paragraph-set",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "219sqyfv11",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "21rct71nkal",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:type": "paragraph",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:vertical-align": "top"
|
||||||
|
},
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "HELLO WORLD",
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:type": "~:text",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 92.9999982852505,
|
||||||
|
"~:y": -462.000004970439
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1622.00000222117,
|
||||||
|
"~:y": -462.000004970439
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1622.00000222117,
|
||||||
|
"~:y": -162.000040815452
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 92.9999982852505,
|
||||||
|
"~:y": -162.000040815452
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:position-data": [
|
||||||
|
{
|
||||||
|
"~:y": -150.130004882813,
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:width": 1528.56005859375,
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:x": 93,
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:direction": "ltr",
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:height": 323.739990234375,
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:inner",
|
||||||
|
"~:stroke-width": 50,
|
||||||
|
"~:stroke-color": "#43e50b",
|
||||||
|
"~:stroke-opacity": 0.6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 92.9999982852505,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 92.9999982852505,
|
||||||
|
"~:y": -462.000004970439,
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:height": 299.999964154987,
|
||||||
|
"~:x1": 92.9999982852505,
|
||||||
|
"~:y1": -462.000004970439,
|
||||||
|
"~:x2": 1622.00000222117,
|
||||||
|
"~:y2": -162.000040815452
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 299.999964154987,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~u2ae0abdc-99ff-8009-8007-b0f7f45177dc": {
|
||||||
|
"~#shape": {
|
||||||
|
"~:y": 169.999996321908,
|
||||||
|
"~:transform": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:rotation": 0,
|
||||||
|
"~:grow-type": "~:auto-width",
|
||||||
|
"~:content": {
|
||||||
|
"~:type": "root",
|
||||||
|
"~:key": "6hv3a5x8wb",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:type": "paragraph-set",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:children": [
|
||||||
|
{
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "219sqyfv11",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:key": "21rct71nkal",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:type": "paragraph",
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:font-family": "sourcesanspro"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:vertical-align": "top"
|
||||||
|
},
|
||||||
|
"~:hide-in-viewer": false,
|
||||||
|
"~:name": "HELLO WORLD",
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:type": "~:text",
|
||||||
|
"~:points": [
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 92.9999984013972,
|
||||||
|
"~:y": 169.999996321908
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1622.00000233732,
|
||||||
|
"~:y": 169.999996321908
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 1622.00000233732,
|
||||||
|
"~:y": 470.000003392238
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"~#point": {
|
||||||
|
"~:x": 92.9999984013972,
|
||||||
|
"~:y": 470.000003392238
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:transform-inverse": {
|
||||||
|
"~#matrix": {
|
||||||
|
"~:a": 1,
|
||||||
|
"~:b": 0,
|
||||||
|
"~:c": 0,
|
||||||
|
"~:d": 1,
|
||||||
|
"~:e": 0,
|
||||||
|
"~:f": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u2ae0abdc-99ff-8009-8007-b0f7f45177dc",
|
||||||
|
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:position-data": [
|
||||||
|
{
|
||||||
|
"~:y": 481.869995117188,
|
||||||
|
"~:line-height": "1.2",
|
||||||
|
"~:font-style": "normal",
|
||||||
|
"~:text-transform": "none",
|
||||||
|
"~:text-align": "left",
|
||||||
|
"~:font-id": "sourcesanspro",
|
||||||
|
"~:font-size": "250",
|
||||||
|
"~:font-weight": "400",
|
||||||
|
"~:text-direction": "ltr",
|
||||||
|
"~:width": 1528.56005859375,
|
||||||
|
"~:font-variant-id": "regular",
|
||||||
|
"~:text-decoration": "none",
|
||||||
|
"~:letter-spacing": "0",
|
||||||
|
"~:x": 93,
|
||||||
|
"~:fills": [
|
||||||
|
{
|
||||||
|
"~:fill-color": "#000000",
|
||||||
|
"~:fill-opacity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:direction": "ltr",
|
||||||
|
"~:font-family": "sourcesanspro",
|
||||||
|
"~:height": 323.739990234375,
|
||||||
|
"~:text": "HELLO WORLD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||||
|
"~:strokes": [
|
||||||
|
{
|
||||||
|
"~:stroke-style": "~:solid",
|
||||||
|
"~:stroke-alignment": "~:outer",
|
||||||
|
"~:stroke-width": 50,
|
||||||
|
"~:stroke-color": "#43e50b",
|
||||||
|
"~:stroke-opacity": 0.6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"~:x": 92.9999984013973,
|
||||||
|
"~:selrect": {
|
||||||
|
"~#rect": {
|
||||||
|
"~:x": 92.9999984013973,
|
||||||
|
"~:y": 169.999996321908,
|
||||||
|
"~:width": 1529.00000393592,
|
||||||
|
"~:height": 300.00000707033,
|
||||||
|
"~:x1": 92.9999984013973,
|
||||||
|
"~:y1": 169.999996321908,
|
||||||
|
"~:x2": 1622.00000233732,
|
||||||
|
"~:y2": 470.000003392238
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:fills": [],
|
||||||
|
"~:flip-x": null,
|
||||||
|
"~:height": 300.00000707033,
|
||||||
|
"~:flip-y": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~ueffcbebc-b8c8-802f-8007-b0ebecd7ebf4",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
w
|
{}
|
||||||
|
|||||||
@ -456,3 +456,38 @@ test("Check inner stroke artifacts", async ({
|
|||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("BUG 13551 - Blurs affecting other elements", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-file-blurs-affecting-other-elements.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "effcbebc-b8c8-802f-8007-a7dc677169cd",
|
||||||
|
pageId: "a5508528-5928-8008-8007-a7de9feef61bd",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRenderWithoutUI();
|
||||||
|
|
||||||
|
// Stricter comparison: blur is very subtle
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot({
|
||||||
|
maxDiffPixelRatio: 0,
|
||||||
|
threshold: 0.1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("BUG 13610 - Huge inner strokes", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-file-huge-inner-strokes.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "effcbebc-b8c8-802f-8007-b11dd34fe190",
|
||||||
|
pageId: "effcbebc-b8c8-802f-8007-b11dd34fe191",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRenderWithoutUI();
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -587,3 +587,23 @@ test.skip("Updates text alignment edition - part 3", async ({ page }) => {
|
|||||||
|
|
||||||
await expect(workspace.canvas).toHaveScreenshot({ timeout: 10000 });
|
await expect(workspace.canvas).toHaveScreenshot({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("Renders a file with group with strokes and not 100% opacities", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-file-strokes-and-not-100-percent-opacities.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "effcbebc-b8c8-802f-8007-b0ebecd7ebf4",
|
||||||
|
pageId: "3e9e17c3-fc57-80ce-8007-101743996fe9",
|
||||||
|
});
|
||||||
|
|
||||||
|
await workspace.waitForFirstRenderWithoutUI();
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot({
|
||||||
|
maxDiffPixelRatio: 0,
|
||||||
|
threshold: 0.01,
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@ -16,6 +16,29 @@ test.skip("BUG 10867 - Crash when loading comments", async ({ page }) => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("BUG 13541 - Shows error page when WebGL context is lost", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspacePage = new WasmWorkspacePage(page);
|
||||||
|
await workspacePage.setupEmptyFile();
|
||||||
|
await workspacePage.goToWorkspace();
|
||||||
|
await workspacePage.waitForFirstRender();
|
||||||
|
|
||||||
|
// Simulate a WebGL context loss by dispatching the event on the canvas
|
||||||
|
await workspacePage.canvas.evaluate((canvas) => {
|
||||||
|
const event = new Event("webglcontextlost", { cancelable: true });
|
||||||
|
canvas.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText("Oops! The canvas context was lost"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByText("WebGL has stopped working"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText("Reload page")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@ -4,8 +4,6 @@ TARGET=${1:-app};
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
rm -rf node_modules;
|
|
||||||
|
|
||||||
corepack enable;
|
corepack enable;
|
||||||
corepack install;
|
corepack install;
|
||||||
pnpm install;
|
pnpm install;
|
||||||
|
|||||||
@ -127,6 +127,24 @@
|
|||||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||||
(flash :cause cause :type :unhandled))))
|
(flash :cause cause :type :unhandled))))
|
||||||
|
|
||||||
|
(defmethod ptk/handle-error :wasm-non-blocking
|
||||||
|
[error]
|
||||||
|
(when-let [cause (::instance error)]
|
||||||
|
(show-not-blocking-error cause)))
|
||||||
|
|
||||||
|
(defmethod ptk/handle-error :wasm-critical
|
||||||
|
[error]
|
||||||
|
(when-let [cause (::instance error)]
|
||||||
|
(ex/print-throwable cause :prefix "WASM critical error"))
|
||||||
|
(st/emit! (rt/assign-exception error)))
|
||||||
|
|
||||||
|
(defmethod ptk/handle-error :wasm-exception
|
||||||
|
[error]
|
||||||
|
(when-let [cause (::instance error)]
|
||||||
|
(let [prefix (or (:prefix error) "Exception")]
|
||||||
|
(ex/print-throwable cause :prefix prefix)))
|
||||||
|
(st/emit! (rt/assign-exception error)))
|
||||||
|
|
||||||
;; We receive a explicit authentication error; If the uri is for
|
;; We receive a explicit authentication error; If the uri is for
|
||||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||||
;; for show the error page. Otherwise this explicitly clears all
|
;; for show the error page. Otherwise this explicitly clears all
|
||||||
@ -338,6 +356,17 @@
|
|||||||
(str/starts-with? message "invalid props on component")
|
(str/starts-with? message "invalid props on component")
|
||||||
(str/starts-with? message "Unexpected token "))))
|
(str/starts-with? message "Unexpected token "))))
|
||||||
|
|
||||||
|
(handle-uncaught [cause]
|
||||||
|
(when cause
|
||||||
|
(set! last-exception cause)
|
||||||
|
(let [data (ex-data cause)
|
||||||
|
type (get data :type)]
|
||||||
|
(if (#{:wasm-critical :wasm-non-blocking :wasm-exception} type)
|
||||||
|
(on-error cause)
|
||||||
|
(when-not (is-ignorable-exception? cause)
|
||||||
|
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||||
|
(ts/schedule #(show-not-blocking-error cause)))))))
|
||||||
|
|
||||||
(on-unhandled-error [event]
|
(on-unhandled-error [event]
|
||||||
(.preventDefault ^js event)
|
(.preventDefault ^js event)
|
||||||
(when-let [cause (unchecked-get event "error")]
|
(when-let [cause (unchecked-get event "error")]
|
||||||
|
|||||||
@ -114,4 +114,5 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: $b-1 solid var(--tab-panel-outline-color);
|
outline: $b-1 solid var(--tab-panel-outline-color);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,6 @@
|
|||||||
(mf/spread-props props
|
(mf/spread-props props
|
||||||
{:class [class class']
|
{:class [class class']
|
||||||
:data-testid "milestone"})
|
:data-testid "milestone"})
|
||||||
|
|
||||||
open*
|
open*
|
||||||
(mf/use-state false)
|
(mf/use-state false)
|
||||||
|
|
||||||
@ -57,7 +56,13 @@
|
|||||||
(dom/get-data "index")
|
(dom/get-data "index")
|
||||||
(d/parse-integer))]
|
(d/parse-integer))]
|
||||||
(when (fn? on-menu-click)
|
(when (fn? on-menu-click)
|
||||||
(on-menu-click index event)))))]
|
(on-menu-click index event)))))
|
||||||
|
|
||||||
|
snapshots
|
||||||
|
(mf/with-memo [snapshots]
|
||||||
|
(map-indexed (fn [index date]
|
||||||
|
(d/vec2 date index))
|
||||||
|
snapshots))]
|
||||||
|
|
||||||
[:> :div props
|
[:> :div props
|
||||||
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label]
|
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label]
|
||||||
@ -76,14 +81,14 @@
|
|||||||
:icon-arrow-toggled open?)}]]
|
:icon-arrow-toggled open?)}]]
|
||||||
|
|
||||||
(when ^boolean open?
|
(when ^boolean open?
|
||||||
(for [[idx d] (d/enumerate snapshots)]
|
(for [[date index] snapshots]
|
||||||
[:div {:key (dm/str "entry-" idx)
|
[:div {:key (dm/str "entry-" index)
|
||||||
:class (stl/css :version-entry)}
|
:class (stl/css :version-entry)}
|
||||||
[:> date* {:date d :class (stl/css :date) :typography t/body-small}]
|
[:> date* {:date date :class (stl/css :date) :typography t/body-small}]
|
||||||
[:> icon-button* {:class (stl/css :entry-button)
|
[:> icon-button* {:class (stl/css :entry-button)
|
||||||
:variant "ghost"
|
:variant "ghost"
|
||||||
:icon i/menu
|
:icon i/menu
|
||||||
:aria-label (tr "workspace.versions.version-menu")
|
:aria-label (tr "workspace.versions.version-menu")
|
||||||
:data-index idx
|
:data-index index
|
||||||
:on-click on-menu-click}]]))]]))
|
:on-click on-menu-click}]]))]]))
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,8 @@
|
|||||||
|
|
||||||
(ns app.main.ui.ds.utilities.date
|
(ns app.main.ui.ds.utilities.date
|
||||||
(:require-macros
|
(:require-macros
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.main.style :as stl])
|
[app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.main.ui.ds.foundations.typography :as t]
|
[app.main.ui.ds.foundations.typography :as t]
|
||||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
@ -30,15 +28,10 @@
|
|||||||
(mf/defc date*
|
(mf/defc date*
|
||||||
{::mf/schema schema:date}
|
{::mf/schema schema:date}
|
||||||
[{:keys [class date selected typography] :rest props}]
|
[{:keys [class date selected typography] :rest props}]
|
||||||
(let [class (d/append-class class (stl/css-case :date true :is-selected selected))
|
(let [date (cond-> date (not (ct/inst? date)) ct/inst)
|
||||||
date (cond-> date (not (ct/inst? date)) ct/inst)
|
|
||||||
typography (or typography t/body-medium)]
|
typography (or typography t/body-medium)]
|
||||||
[:> text* {:as "time"
|
[:> text* {:as "time"
|
||||||
:typography typography
|
:typography typography
|
||||||
:class class
|
:class [class (stl/css-case :date true :is-selected selected)]
|
||||||
:date-time (ct/format-inst date :iso)}
|
:date-time (ct/format-inst date :iso)}
|
||||||
(dm/str
|
(ct/format-inst date :localized-date-time)]))
|
||||||
(ct/format-inst date :localized-date)
|
|
||||||
" . "
|
|
||||||
(ct/format-inst date :localized-time)
|
|
||||||
"h")]))
|
|
||||||
|
|||||||
@ -477,8 +477,12 @@
|
|||||||
:service-unavailable
|
:service-unavailable
|
||||||
[:> service-unavailable*]
|
[:> service-unavailable*]
|
||||||
|
|
||||||
:webgl-context-lost
|
:wasm-exception
|
||||||
[:> webgl-context-lost*]
|
(case (get data :exception-type)
|
||||||
|
:webgl-context-lost
|
||||||
|
[:> webgl-context-lost*]
|
||||||
|
|
||||||
|
[:> internal-error* props])
|
||||||
|
|
||||||
[:> internal-error* props])))
|
[:> internal-error* props])))
|
||||||
|
|
||||||
|
|||||||
343
frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
Normal file
343
frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
;; 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.main.ui.workspace.shapes.text.v3-editor
|
||||||
|
"Contenteditable DOM element for WASM text editor input"
|
||||||
|
(:require-macros [app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[app.main.data.helpers :as dsh]
|
||||||
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.css-cursors :as cur]
|
||||||
|
[app.render-wasm.api :as wasm.api]
|
||||||
|
[app.render-wasm.text-editor :as text-editor]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def caret-blink-interval-ms 250)
|
||||||
|
|
||||||
|
(defn- sync-wasm-text-editor-content!
|
||||||
|
"Sync WASM text editor content back to the shape via the standard
|
||||||
|
commit pipeline. Called after every text-modifying input."
|
||||||
|
[& {:keys [finalize?]}]
|
||||||
|
(when-let [{:keys [shape-id content]}
|
||||||
|
(text-editor/text-editor-sync-content)]
|
||||||
|
(st/emit! (dwt/v2-update-text-shape-content
|
||||||
|
shape-id content
|
||||||
|
:update-name? true
|
||||||
|
:finalize? finalize?))))
|
||||||
|
|
||||||
|
(defn- font-family-from-font-id [font-id]
|
||||||
|
(if (str/includes? font-id "gfont-noto-sans")
|
||||||
|
(let [lang (str/replace font-id #"gfont\-noto\-sans\-" "")]
|
||||||
|
(if (>= (count lang) 3) (str/capital lang) (str/upper lang)))
|
||||||
|
"Noto Color Emoji"))
|
||||||
|
|
||||||
|
(mf/defc text-editor
|
||||||
|
"Contenteditable element positioned over the text shape to capture input events."
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [shape (obj/get props "shape")
|
||||||
|
shape-id (dm/get-prop shape :id)
|
||||||
|
|
||||||
|
clip-id (dm/str "text-edition-clip" shape-id)
|
||||||
|
|
||||||
|
contenteditable-ref (mf/use-ref nil)
|
||||||
|
composing? (mf/use-state false)
|
||||||
|
|
||||||
|
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
|
||||||
|
fallback-families (map (fn [font]
|
||||||
|
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||||
|
|
||||||
|
[{:keys [x y width height]} transform]
|
||||||
|
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||||
|
selrect-transform (mf/deref refs/workspace-selrect)
|
||||||
|
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||||
|
selrect-height (:height selrect)
|
||||||
|
selrect-width (:width selrect)
|
||||||
|
max-width (max width selrect-width)
|
||||||
|
max-height (max height selrect-height)
|
||||||
|
valign (-> shape :content :vertical-align)
|
||||||
|
y (:y selrect)
|
||||||
|
y (case valign
|
||||||
|
"bottom" (+ y (- selrect-height height))
|
||||||
|
"center" (+ y (/ (- selrect-height height) 2))
|
||||||
|
y)]
|
||||||
|
[(assoc selrect :y y :width max-width :height max-height) transform])
|
||||||
|
|
||||||
|
on-composition-start
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [_event]
|
||||||
|
(reset! composing? true)))
|
||||||
|
|
||||||
|
on-composition-end
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(reset! composing? false)
|
||||||
|
(let [data (.-data event)]
|
||||||
|
(when data
|
||||||
|
(text-editor/text-editor-insert-text data)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-composition"))
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(set! (.-textContent node) "")))))
|
||||||
|
|
||||||
|
on-paste
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(let [clipboard-data (.-clipboardData event)
|
||||||
|
text (.getData clipboard-data "text/plain")]
|
||||||
|
(when (and text (seq text))
|
||||||
|
(text-editor/text-editor-insert-text text)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-paste"))
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(set! (.-textContent node) "")))))
|
||||||
|
|
||||||
|
on-copy
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(when (text-editor/text-editor-is-active?)
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(when (text-editor/text-editor-get-selection)
|
||||||
|
(let [text (text-editor/text-editor-export-selection)]
|
||||||
|
(.setData (.-clipboardData event) "text/plain" text))))))
|
||||||
|
|
||||||
|
on-cut
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(when (text-editor/text-editor-is-active?)
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(when (text-editor/text-editor-get-selection)
|
||||||
|
(let [text (text-editor/text-editor-export-selection)]
|
||||||
|
(.setData (.-clipboardData event) "text/plain" (or text ""))
|
||||||
|
(when (and text (seq text))
|
||||||
|
(text-editor/text-editor-delete-backward)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-cut"))))
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(set! (.-textContent node) "")))))
|
||||||
|
|
||||||
|
on-key-down
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(when (and (text-editor/text-editor-is-active?)
|
||||||
|
(not @composing?))
|
||||||
|
(let [key (.-key event)
|
||||||
|
ctrl? (or (.-ctrlKey event) (.-metaKey event))
|
||||||
|
shift? (.-shiftKey event)]
|
||||||
|
|
||||||
|
(cond
|
||||||
|
;; Escape: finalize and stop
|
||||||
|
(= key "Escape")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(.blur node)))
|
||||||
|
|
||||||
|
;; Ctrl+A: select all (key is "a" or "A" depending on platform)
|
||||||
|
(and ctrl? (= (str/lower key) "a"))
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-select-all)
|
||||||
|
(wasm.api/request-render "text-select-all"))
|
||||||
|
|
||||||
|
;; Enter
|
||||||
|
(= key "Enter")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-insert-paragraph)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-paragraph"))
|
||||||
|
|
||||||
|
;; Backspace
|
||||||
|
(= key "Backspace")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-delete-backward)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-delete-backward"))
|
||||||
|
|
||||||
|
;; Delete
|
||||||
|
(= key "Delete")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-delete-forward)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-delete-forward"))
|
||||||
|
|
||||||
|
;; Arrow keys
|
||||||
|
(= key "ArrowLeft")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 0 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
(= key "ArrowRight")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 1 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
(= key "ArrowUp")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 2 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
(= key "ArrowDown")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 3 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
(= key "Home")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 4 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
(= key "End")
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-move-cursor 5 shift?)
|
||||||
|
(wasm.api/request-render "text-cursor-move"))
|
||||||
|
|
||||||
|
;; Let contenteditable handle text input via on-input
|
||||||
|
:else nil)))))
|
||||||
|
|
||||||
|
on-input
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (.-nativeEvent event)
|
||||||
|
input-type (.-inputType native-event)
|
||||||
|
data (.-data native-event)]
|
||||||
|
;; Skip composition-related input events - composition-end handles those
|
||||||
|
(when (and (not @composing?)
|
||||||
|
(not= input-type "insertCompositionText"))
|
||||||
|
(when (and data (seq data))
|
||||||
|
(text-editor/text-editor-insert-text data)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-input"))
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(set! (.-textContent node) ""))))))
|
||||||
|
|
||||||
|
on-pointer-down
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (dom/event->native-event event)
|
||||||
|
off-pt (dom/get-offset-position native-event)]
|
||||||
|
(wasm.api/text-editor-pointer-down off-pt))))
|
||||||
|
|
||||||
|
on-pointer-move
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (dom/event->native-event event)
|
||||||
|
off-pt (dom/get-offset-position native-event)]
|
||||||
|
(wasm.api/text-editor-pointer-move off-pt))))
|
||||||
|
|
||||||
|
on-pointer-up
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (dom/event->native-event event)
|
||||||
|
off-pt (dom/get-offset-position native-event)]
|
||||||
|
(wasm.api/text-editor-pointer-up off-pt))))
|
||||||
|
|
||||||
|
on-click
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (dom/event->native-event event)
|
||||||
|
off-pt (dom/get-offset-position native-event)]
|
||||||
|
(wasm.api/text-editor-set-cursor-from-offset off-pt))))
|
||||||
|
|
||||||
|
on-double-click
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js event]
|
||||||
|
(let [native-event (dom/event->native-event event)
|
||||||
|
off-pt (dom/get-offset-position native-event)]
|
||||||
|
(wasm.api/text-editor-select-word-boundary off-pt))))
|
||||||
|
|
||||||
|
on-focus
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js _event]
|
||||||
|
(wasm.api/text-editor-start shape-id)))
|
||||||
|
|
||||||
|
on-blur
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js _event]
|
||||||
|
(sync-wasm-text-editor-content! {:finalize? true})
|
||||||
|
(wasm.api/text-editor-stop)))
|
||||||
|
|
||||||
|
style #js {:pointerEvents "all"
|
||||||
|
"--editor-container-width" (dm/str width "px")
|
||||||
|
"--editor-container-height" (dm/str height "px")
|
||||||
|
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")}]
|
||||||
|
|
||||||
|
;; Focus contenteditable on mount
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps contenteditable-ref)
|
||||||
|
(fn []
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(.focus node))
|
||||||
|
;; Explicitly call on-blur here instead of relying on browser blur events,
|
||||||
|
;; because in Firefox blur is not reliably fired when leaving the text editor
|
||||||
|
;; by clicking elsewhere. The component does unmount when the shape is
|
||||||
|
;; deselected, so we can safely call the blur handler here to finalize the editor.
|
||||||
|
on-blur))
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(fn []
|
||||||
|
(let [timeout-id (atom nil)
|
||||||
|
schedule-blink (fn schedule-blink []
|
||||||
|
(when (text-editor/text-editor-is-active?)
|
||||||
|
(wasm.api/request-render "cursor-blink"))
|
||||||
|
(reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))]
|
||||||
|
(schedule-blink)
|
||||||
|
(fn []
|
||||||
|
(when @timeout-id
|
||||||
|
(js/clearTimeout @timeout-id))))))
|
||||||
|
|
||||||
|
;; Composition and input events
|
||||||
|
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
|
||||||
|
:transform (dm/str transform)
|
||||||
|
:data-testid "text-editor"}
|
||||||
|
[:defs
|
||||||
|
[:clipPath {:id clip-id}
|
||||||
|
[:rect {:x x :y y :width width :height height}]]]
|
||||||
|
|
||||||
|
[:foreignObject {:x x :y y :width width :height height}
|
||||||
|
[:div {:on-click on-click
|
||||||
|
:on-double-click on-double-click
|
||||||
|
:on-pointer-down on-pointer-down
|
||||||
|
:on-pointer-move on-pointer-move
|
||||||
|
:on-pointer-up on-pointer-up
|
||||||
|
:class (stl/css :text-editor)
|
||||||
|
:style style}
|
||||||
|
[:div
|
||||||
|
{:ref contenteditable-ref
|
||||||
|
:contentEditable true
|
||||||
|
:suppressContentEditableWarning true
|
||||||
|
:on-composition-start on-composition-start
|
||||||
|
:on-composition-end on-composition-end
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:on-input on-input
|
||||||
|
:on-paste on-paste
|
||||||
|
:on-copy on-copy
|
||||||
|
:on-cut on-cut
|
||||||
|
:on-focus on-focus
|
||||||
|
:on-blur on-blur
|
||||||
|
;; FIXME on-click
|
||||||
|
;; :on-click on-click
|
||||||
|
:id "text-editor-wasm-input"
|
||||||
|
:class (dm/str (cur/get-dynamic "text" (:rotation shape))
|
||||||
|
" "
|
||||||
|
(stl/css :text-editor-container))
|
||||||
|
:data-testid "text-editor-container"}]]]]))
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
.text-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
@ -159,3 +159,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: calc(100vh - deprecated.$s-88);
|
height: calc(100vh - deprecated.$s-88);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-tab {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|||||||
@ -19,13 +19,11 @@
|
|||||||
[app.main.data.workspace.media :as dwm]
|
[app.main.data.workspace.media :as dwm]
|
||||||
[app.main.data.workspace.path :as dwdp]
|
[app.main.data.workspace.path :as dwdp]
|
||||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||||
[app.main.features :as features]
|
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.workspace.sidebar.assets.components :as wsac]
|
[app.main.ui.workspace.sidebar.assets.components :as wsac]
|
||||||
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.render-wasm.wasm :as wasm.wasm]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.dom.dnd :as dnd]
|
[app.util.dom.dnd :as dnd]
|
||||||
[app.util.dom.normalize-wheel :as nw]
|
[app.util.dom.normalize-wheel :as nw]
|
||||||
@ -74,7 +72,6 @@
|
|||||||
shift? (kbd/shift? native-event)
|
shift? (kbd/shift? native-event)
|
||||||
alt? (kbd/alt? native-event)
|
alt? (kbd/alt? native-event)
|
||||||
mod? (kbd/mod? native-event)
|
mod? (kbd/mod? native-event)
|
||||||
off-pt (dom/get-offset-position native-event)
|
|
||||||
|
|
||||||
left-click? (and (not panning) (dom/left-mouse? event))
|
left-click? (and (not panning) (dom/left-mouse? event))
|
||||||
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
||||||
@ -94,23 +91,8 @@
|
|||||||
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
||||||
::dwsp/interrupt)
|
::dwsp/interrupt)
|
||||||
|
|
||||||
(when (wasm.api/text-editor-is-active?)
|
|
||||||
(wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt)))
|
|
||||||
|
|
||||||
(when (and (not= edition id) (or text-editing? grid-editing?))
|
(when (and (not= edition id) (or text-editing? grid-editing?))
|
||||||
(st/emit! (dw/clear-edition-mode))
|
(st/emit! (dw/clear-edition-mode)))
|
||||||
;; FIXME: I think this is not completely correct because this
|
|
||||||
;; is going to happen even when clicking or selecting text.
|
|
||||||
;; Sync and stop WASM text editor when exiting edit mode
|
|
||||||
#_(when (and text-editing?
|
|
||||||
(features/active-feature? @st/state "render-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
|
||||||
(st/emit! (dwt/v2-update-text-shape-content
|
|
||||||
shape-id content
|
|
||||||
:update-name? true
|
|
||||||
:finalize? true)))
|
|
||||||
(wasm.api/text-editor-stop)))
|
|
||||||
|
|
||||||
(when (and (not text-editing?)
|
(when (and (not text-editing?)
|
||||||
(not blocked)
|
(not blocked)
|
||||||
@ -192,8 +174,6 @@
|
|||||||
alt? (kbd/alt? event)
|
alt? (kbd/alt? event)
|
||||||
meta? (kbd/meta? event)
|
meta? (kbd/meta? event)
|
||||||
hovering? (some? @hover)
|
hovering? (some? @hover)
|
||||||
native-event (dom/event->native-event event)
|
|
||||||
off-pt (dom/get-offset-position native-event)
|
|
||||||
raw-pt (dom/get-client-position event)
|
raw-pt (dom/get-client-position event)
|
||||||
pt (uwvv/point->viewport raw-pt)]
|
pt (uwvv/point->viewport raw-pt)]
|
||||||
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
||||||
@ -207,20 +187,6 @@
|
|||||||
(not drawing-tool))
|
(not drawing-tool))
|
||||||
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
||||||
|
|
||||||
;; FIXME: Maybe we can move into a function of the kind
|
|
||||||
;; "text-editor-on-click"
|
|
||||||
;; If clicking on a text shape and wasm render is enabled, forward cursor position
|
|
||||||
(when (and hovering?
|
|
||||||
(not @space?)
|
|
||||||
edition ;; Only when already in edit mode
|
|
||||||
(not drawing-path?)
|
|
||||||
(not drawing-tool))
|
|
||||||
(let [hover-shape @hover]
|
|
||||||
(when (and (= :text (:type hover-shape))
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt)))))
|
|
||||||
|
|
||||||
(when (and @z?
|
(when (and @z?
|
||||||
(not @space?)
|
(not @space?)
|
||||||
(not edition)
|
(not edition)
|
||||||
@ -262,19 +228,7 @@
|
|||||||
(and editable? (not= id edition) (not read-only?))
|
(and editable? (not= id edition) (not read-only?))
|
||||||
(do
|
(do
|
||||||
(st/emit! (dw/select-shape id)
|
(st/emit! (dw/select-shape id)
|
||||||
(dw/start-editing-selected))
|
(dw/start-editing-selected)))
|
||||||
;; If using wasm text-editor, notify WASM to start editing this shape
|
|
||||||
;; and set cursor position from the double-click location
|
|
||||||
(when (and (= type :text)
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-start id)))
|
|
||||||
|
|
||||||
(and editable? (= id edition) (not read-only?)
|
|
||||||
(= type :text)
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-select-all)
|
|
||||||
|
|
||||||
(some? selected-shape)
|
(some? selected-shape)
|
||||||
(do
|
(do
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
[app.main.ui.workspace.shapes.text.editor :as editor-v1]
|
[app.main.ui.workspace.shapes.text.editor :as editor-v1]
|
||||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
||||||
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
||||||
|
[app.main.ui.workspace.shapes.text.v3-editor :as editor-v3]
|
||||||
[app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
|
[app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
[app.main.ui.workspace.viewport.comments :as comments]
|
[app.main.ui.workspace.viewport.comments :as comments]
|
||||||
@ -54,7 +55,6 @@
|
|||||||
[app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]]
|
[app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]]
|
||||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.render-wasm.text-editor-input :refer [text-editor-input]]
|
|
||||||
[app.util.debug :as dbg]
|
[app.util.debug :as dbg]
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
@ -417,14 +417,7 @@
|
|||||||
|
|
||||||
(when picking-color?
|
(when picking-color?
|
||||||
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
|
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
|
||||||
:canvas-ref canvas-ref}])
|
:canvas-ref canvas-ref}])]
|
||||||
|
|
||||||
;; WASM text editor contenteditable (must be outside SVG to work)
|
|
||||||
(when (and show-text-editor?
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1"))
|
|
||||||
[:& text-editor-input {:shape editing-shape
|
|
||||||
:zoom zoom
|
|
||||||
:vbox vbox}])]
|
|
||||||
|
|
||||||
[:canvas {:id "render"
|
[:canvas {:id "render"
|
||||||
:data-testid "canvas-wasm-shapes"
|
:data-testid "canvas-wasm-shapes"
|
||||||
@ -471,14 +464,20 @@
|
|||||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||||
;; Text editor handling:
|
;; Text editor handling:
|
||||||
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
||||||
(when (and show-text-editor?
|
(when show-text-editor?
|
||||||
(not (features/active-feature? @st/state "text-editor-wasm/v1")))
|
(cond
|
||||||
(if (features/active-feature? @st/state "text-editor/v2")
|
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||||
|
[:& editor-v3/text-editor {:shape editing-shape
|
||||||
|
:canvas-ref canvas-ref
|
||||||
|
:ref text-editor-ref}]
|
||||||
|
|
||||||
|
(features/active-feature? @st/state "text-editor/v2")
|
||||||
[:& editor-v2/text-editor {:shape editing-shape
|
[:& editor-v2/text-editor {:shape editing-shape
|
||||||
:canvas-ref canvas-ref
|
:canvas-ref canvas-ref
|
||||||
:ref text-editor-ref}]
|
:ref text-editor-ref}]
|
||||||
[:& editor-v1/text-editor-svg {:shape editing-shape
|
|
||||||
:ref text-editor-ref}]))
|
:else [:& editor-v1/text-editor-svg {:shape editing-shape
|
||||||
|
:ref text-editor-ref}]))
|
||||||
|
|
||||||
(when show-frame-outline?
|
(when show-frame-outline?
|
||||||
(let [outlined-frame-id
|
(let [outlined-frame-id
|
||||||
|
|||||||
@ -86,12 +86,14 @@
|
|||||||
;; Re-export public text editor functions
|
;; Re-export public text editor functions
|
||||||
(def text-editor-start text-editor/text-editor-start)
|
(def text-editor-start text-editor/text-editor-start)
|
||||||
(def text-editor-stop text-editor/text-editor-stop)
|
(def text-editor-stop text-editor/text-editor-stop)
|
||||||
|
(def text-editor-set-cursor-from-offset text-editor/text-editor-set-cursor-from-offset)
|
||||||
(def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point)
|
(def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point)
|
||||||
(def text-editor-pointer-down text-editor/text-editor-pointer-down)
|
(def text-editor-pointer-down text-editor/text-editor-pointer-down)
|
||||||
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
||||||
(def text-editor-pointer-up text-editor/text-editor-pointer-up)
|
(def text-editor-pointer-up text-editor/text-editor-pointer-up)
|
||||||
(def text-editor-is-active? text-editor/text-editor-is-active?)
|
(def text-editor-is-active? text-editor/text-editor-is-active?)
|
||||||
(def text-editor-select-all text-editor/text-editor-select-all)
|
(def text-editor-select-all text-editor/text-editor-select-all)
|
||||||
|
(def text-editor-select-word-boundary text-editor/text-editor-select-word-boundary)
|
||||||
(def text-editor-sync-content text-editor/text-editor-sync-content)
|
(def text-editor-sync-content text-editor/text-editor-sync-content)
|
||||||
|
|
||||||
(def dpr
|
(def dpr
|
||||||
@ -1419,7 +1421,9 @@
|
|||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(reset! wasm/context-lost? true)
|
(reset! wasm/context-lost? true)
|
||||||
(log/warn :hint "WebGL context lost")
|
(log/warn :hint "WebGL context lost")
|
||||||
(ex/raise :type :webgl-context-lost
|
(ex/raise :type :wasm-exception
|
||||||
|
:exception-type :webgl-context-lost
|
||||||
|
:prefix "WebGL context lost"
|
||||||
:hint "WebGL context lost"))
|
:hint "WebGL context lost"))
|
||||||
|
|
||||||
(defn init-canvas-context
|
(defn init-canvas-context
|
||||||
|
|||||||
@ -7,11 +7,30 @@
|
|||||||
(ns app.render-wasm.helpers
|
(ns app.render-wasm.helpers
|
||||||
#?(:cljs (:require-macros [app.render-wasm.helpers])))
|
#?(:cljs (:require-macros [app.render-wasm.helpers])))
|
||||||
|
|
||||||
|
(def ^:export error-code
|
||||||
|
"WASM error code constants (must match render-wasm/src/error.rs and mem.rs)."
|
||||||
|
{0x01 :wasm-non-blocking 0x02 :wasm-critical})
|
||||||
|
|
||||||
(defmacro call
|
(defmacro call
|
||||||
"A helper for easy call wasm defined function in a module."
|
"A helper for calling a wasm function.
|
||||||
|
Catches any exception thrown by the WASM function, reads the error code from
|
||||||
|
WASM when available, and routes it based on the error type:
|
||||||
|
- :wasm-non-blocking: call app.main.errors/on-error (eventually, shows a toast and logs the error)
|
||||||
|
- :wasm-critical or unknown: throws an exception to be handled by the global error handler (eventually, shows the internal error page)"
|
||||||
[module name & params]
|
[module name & params]
|
||||||
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})]
|
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})
|
||||||
|
e-sym (gensym "e")
|
||||||
|
code-sym (gensym "code")]
|
||||||
`(let [~fn-sym (cljs.core/unchecked-get ~module ~name)]
|
`(let [~fn-sym (cljs.core/unchecked-get ~module ~name)]
|
||||||
;; DEBUG
|
(try
|
||||||
;; (println "##" ~name)
|
(~fn-sym ~@params)
|
||||||
(~fn-sym ~@params))))
|
(catch :default ~e-sym
|
||||||
|
(let [read-code# (cljs.core/unchecked-get ~module "_read_error_code")
|
||||||
|
~code-sym (when read-code# (read-code#))
|
||||||
|
type# (or (get app.render-wasm.helpers/error-code ~code-sym) :wasm-critical)
|
||||||
|
ex# (ex-info (str "WASM error (type: " type# ")")
|
||||||
|
{:fn ~name :type type# :message (.-message ~e-sym) :error-code ~code-sym}
|
||||||
|
~e-sym)]
|
||||||
|
(if (= type# :wasm-non-blocking)
|
||||||
|
(@~'app.main.store/on-error ex#)
|
||||||
|
(throw ex#))))))))
|
||||||
|
|||||||
@ -16,29 +16,37 @@
|
|||||||
[id]
|
[id]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(let [buffer (uuid/get-u32 id)]
|
(let [buffer (uuid/get-u32 id)]
|
||||||
(h/call wasm/internal-module "_text_editor_start"
|
(when-not (h/call wasm/internal-module "_text_editor_start"
|
||||||
(aget buffer 0)
|
(aget buffer 0)
|
||||||
(aget buffer 1)
|
(aget buffer 1)
|
||||||
(aget buffer 2)
|
(aget buffer 2)
|
||||||
(aget buffer 3)))))
|
(aget buffer 3))
|
||||||
|
(throw (js/Error. "TextEditor initialization failed"))))))
|
||||||
|
|
||||||
|
(defn text-editor-set-cursor-from-offset
|
||||||
|
"Sets caret position from shape relative coordinates"
|
||||||
|
[{:keys [x y]}]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(h/call wasm/internal-module "_text_editor_set_cursor_from_offset" x y)))
|
||||||
|
|
||||||
(defn text-editor-set-cursor-from-point
|
(defn text-editor-set-cursor-from-point
|
||||||
[x y]
|
"Sets caret position from screen (canvas) coordinates"
|
||||||
|
[{:keys [x y]}]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
||||||
|
|
||||||
(defn text-editor-pointer-down
|
(defn text-editor-pointer-down
|
||||||
[x y]
|
[{:keys [x y]}]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_pointer_down" x y)))
|
(h/call wasm/internal-module "_text_editor_pointer_down" x y)))
|
||||||
|
|
||||||
(defn text-editor-pointer-move
|
(defn text-editor-pointer-move
|
||||||
[x y]
|
[{:keys [x y]}]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_pointer_move" x y)))
|
(h/call wasm/internal-module "_text_editor_pointer_move" x y)))
|
||||||
|
|
||||||
(defn text-editor-pointer-up
|
(defn text-editor-pointer-up
|
||||||
[x y]
|
[{:keys [x y]}]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_pointer_up" x y)))
|
(h/call wasm/internal-module "_text_editor_pointer_up" x y)))
|
||||||
|
|
||||||
@ -92,10 +100,16 @@
|
|||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_select_all")))
|
(h/call wasm/internal-module "_text_editor_select_all")))
|
||||||
|
|
||||||
|
(defn text-editor-select-word-boundary
|
||||||
|
[{:keys [x y]}]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(h/call wasm/internal-module "_text_editor_select_word_boundary" x y)))
|
||||||
|
|
||||||
(defn text-editor-stop
|
(defn text-editor-stop
|
||||||
[]
|
[]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_stop")))
|
(when-not (h/call wasm/internal-module "_text_editor_stop")
|
||||||
|
(throw (js/Error. "TextEditor finalization failed")))))
|
||||||
|
|
||||||
(defn text-editor-is-active?
|
(defn text-editor-is-active?
|
||||||
([id]
|
([id]
|
||||||
@ -160,6 +174,7 @@
|
|||||||
(finally
|
(finally
|
||||||
(mem/free))))))
|
(mem/free))))))
|
||||||
|
|
||||||
|
;; This is used as a intermediate cache between Clojure global state and WASM state.
|
||||||
(def ^:private shape-text-contents (atom {}))
|
(def ^:private shape-text-contents (atom {}))
|
||||||
|
|
||||||
(defn- merge-exported-texts-into-content
|
(defn- merge-exported-texts-into-content
|
||||||
|
|||||||
@ -1,241 +0,0 @@
|
|||||||
;; 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.render-wasm.text-editor-input
|
|
||||||
"Contenteditable DOM element for WASM text editor input"
|
|
||||||
(:require
|
|
||||||
[app.common.geom.shapes :as gsh]
|
|
||||||
[app.main.data.workspace.texts :as dwt]
|
|
||||||
[app.main.store :as st]
|
|
||||||
[app.render-wasm.api :as wasm.api]
|
|
||||||
[app.render-wasm.text-editor :as text-editor]
|
|
||||||
[app.util.dom :as dom]
|
|
||||||
[app.util.object :as obj]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[goog.events :as events]
|
|
||||||
[rumext.v2 :as mf])
|
|
||||||
(:import goog.events.EventType))
|
|
||||||
|
|
||||||
(def caret-blink-interval-ms 250)
|
|
||||||
|
|
||||||
(defn- sync-wasm-text-editor-content!
|
|
||||||
"Sync WASM text editor content back to the shape via the standard
|
|
||||||
commit pipeline. Called after every text-modifying input."
|
|
||||||
[& {:keys [finalize?]}]
|
|
||||||
(when-let [{:keys [shape-id content]} (text-editor/text-editor-sync-content)]
|
|
||||||
(st/emit! (dwt/v2-update-text-shape-content
|
|
||||||
shape-id content
|
|
||||||
:update-name? true
|
|
||||||
:finalize? finalize?))))
|
|
||||||
|
|
||||||
(mf/defc text-editor-input
|
|
||||||
"Contenteditable element positioned over the text shape to capture input events."
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [shape (obj/get props "shape")
|
|
||||||
zoom (obj/get props "zoom")
|
|
||||||
vbox (obj/get props "vbox")
|
|
||||||
|
|
||||||
contenteditable-ref (mf/use-ref nil)
|
|
||||||
composing? (mf/use-state false)
|
|
||||||
|
|
||||||
;; Calculate screen position from shape bounds
|
|
||||||
shape-bounds (gsh/shape->rect shape)
|
|
||||||
screen-x (* (- (:x shape-bounds) (:x vbox)) zoom)
|
|
||||||
screen-y (* (- (:y shape-bounds) (:y vbox)) zoom)
|
|
||||||
screen-w (* (:width shape-bounds) zoom)
|
|
||||||
screen-h (* (:height shape-bounds) zoom)]
|
|
||||||
|
|
||||||
;; Focus contenteditable on mount
|
|
||||||
(mf/use-effect
|
|
||||||
(fn []
|
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
|
||||||
(.focus node))
|
|
||||||
js/undefined))
|
|
||||||
|
|
||||||
(mf/use-effect
|
|
||||||
(fn []
|
|
||||||
(let [timeout-id (atom nil)
|
|
||||||
schedule-blink (fn schedule-blink []
|
|
||||||
(when (text-editor/text-editor-is-active?)
|
|
||||||
(wasm.api/request-render "cursor-blink"))
|
|
||||||
(reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))]
|
|
||||||
(schedule-blink)
|
|
||||||
(fn []
|
|
||||||
(when @timeout-id
|
|
||||||
(js/clearTimeout @timeout-id))))))
|
|
||||||
|
|
||||||
;; Document-level keydown handler for control keys
|
|
||||||
(mf/use-effect
|
|
||||||
(fn []
|
|
||||||
(let [on-doc-keydown
|
|
||||||
(fn [e]
|
|
||||||
(when (and (text-editor/text-editor-is-active?)
|
|
||||||
(not @composing?))
|
|
||||||
(let [key (.-key e)
|
|
||||||
ctrl? (or (.-ctrlKey e) (.-metaKey e))
|
|
||||||
shift? (.-shiftKey e)]
|
|
||||||
(cond
|
|
||||||
;; Escape: finalize and stop
|
|
||||||
(= key "Escape")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(sync-wasm-text-editor-content! :finalize? true)
|
|
||||||
(text-editor/text-editor-stop))
|
|
||||||
|
|
||||||
;; Ctrl+A: select all (key is "a" or "A" depending on platform)
|
|
||||||
(and ctrl? (= (str/lower key) "a"))
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-select-all)
|
|
||||||
(wasm.api/request-render "text-select-all"))
|
|
||||||
|
|
||||||
;; Enter
|
|
||||||
(= key "Enter")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-insert-paragraph)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-paragraph"))
|
|
||||||
|
|
||||||
;; Backspace
|
|
||||||
(= key "Backspace")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-delete-backward)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-delete-backward"))
|
|
||||||
|
|
||||||
;; Delete
|
|
||||||
(= key "Delete")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-delete-forward)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-delete-forward"))
|
|
||||||
|
|
||||||
;; Arrow keys
|
|
||||||
(= key "ArrowLeft")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 0 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowRight")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 1 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowUp")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 2 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowDown")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 3 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "Home")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 4 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "End")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 5 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
;; Let contenteditable handle text input via on-input
|
|
||||||
:else nil))))]
|
|
||||||
(events/listen js/document EventType.KEYDOWN on-doc-keydown true)
|
|
||||||
(fn []
|
|
||||||
(events/unlisten js/document EventType.KEYDOWN on-doc-keydown true)))))
|
|
||||||
|
|
||||||
;; Composition and input events
|
|
||||||
(let [on-composition-start
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [_event]
|
|
||||||
(reset! composing? true)))
|
|
||||||
|
|
||||||
on-composition-end
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [^js event]
|
|
||||||
(reset! composing? false)
|
|
||||||
(let [data (.-data event)]
|
|
||||||
(when data
|
|
||||||
(text-editor/text-editor-insert-text data)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-composition"))
|
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
|
||||||
(set! (.-textContent node) "")))))
|
|
||||||
|
|
||||||
on-paste
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [^js event]
|
|
||||||
(dom/prevent-default event)
|
|
||||||
(let [clipboard-data (.-clipboardData event)
|
|
||||||
text (.getData clipboard-data "text/plain")]
|
|
||||||
(when (and text (seq text))
|
|
||||||
(text-editor/text-editor-insert-text text)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-paste"))
|
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
|
||||||
(set! (.-textContent node) "")))))
|
|
||||||
|
|
||||||
on-copy
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [^js event]
|
|
||||||
(when (text-editor/text-editor-is-active?)
|
|
||||||
(dom/prevent-default event)
|
|
||||||
(when (text-editor/text-editor-get-selection)
|
|
||||||
(let [text (text-editor/text-editor-export-selection)]
|
|
||||||
(.setData (.-clipboardData event) "text/plain" text))))))
|
|
||||||
|
|
||||||
on-input
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [^js event]
|
|
||||||
(let [native-event (.-nativeEvent event)
|
|
||||||
input-type (.-inputType native-event)
|
|
||||||
data (.-data native-event)]
|
|
||||||
;; Skip composition-related input events - composition-end handles those
|
|
||||||
(when (and (not @composing?)
|
|
||||||
(not= input-type "insertCompositionText"))
|
|
||||||
(when (and data (seq data))
|
|
||||||
(text-editor/text-editor-insert-text data)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-input"))
|
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
|
||||||
(set! (.-textContent node) ""))))))]
|
|
||||||
|
|
||||||
[:div
|
|
||||||
{:ref contenteditable-ref
|
|
||||||
:contentEditable true
|
|
||||||
:suppressContentEditableWarning true
|
|
||||||
:on-composition-start on-composition-start
|
|
||||||
:on-composition-end on-composition-end
|
|
||||||
:on-input on-input
|
|
||||||
:on-paste on-paste
|
|
||||||
:on-copy on-copy
|
|
||||||
;; FIXME on-click
|
|
||||||
;; :on-click on-click
|
|
||||||
:id "text-editor-wasm-input"
|
|
||||||
;; FIXME
|
|
||||||
:style {:position "absolute"
|
|
||||||
:left (str screen-x "px")
|
|
||||||
:top (str screen-y "px")
|
|
||||||
:width (str screen-w "px")
|
|
||||||
:height (str screen-h "px")
|
|
||||||
:opacity 0
|
|
||||||
:overflow "hidden"
|
|
||||||
:white-space "pre"
|
|
||||||
:cursor "text"
|
|
||||||
:z-index 10}}])))
|
|
||||||
@ -26,8 +26,9 @@
|
|||||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||||
"build": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs release library",
|
"build": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs release library",
|
||||||
"build:bundle": "./scripts/build",
|
"build:bundle": "./scripts/build",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt": "cljfmt fix --parallel=true src/ test/",
|
||||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
||||||
|
"lint": "clj-kondo --parallel --lint src/",
|
||||||
"test": "node --test",
|
"test": "node --test",
|
||||||
"watch:test": "node --test --watch",
|
"watch:test": "node --test --watch",
|
||||||
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
|
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
|
||||||
|
|||||||
23
render-wasm/Cargo.lock
generated
23
render-wasm/Cargo.lock
generated
@ -297,6 +297,8 @@ name = "macros"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -426,6 +428,7 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"macros",
|
"macros",
|
||||||
"skia-safe",
|
"skia-safe",
|
||||||
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -579,6 +582,26 @@ dependencies = [
|
|||||||
"xattr",
|
"xattr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.0.3+spec-1.1.0"
|
version = "1.0.3+spec-1.1.0"
|
||||||
|
|||||||
@ -32,6 +32,7 @@ skia-safe = { version = "0.93.1", default-features = false, features = [
|
|||||||
"binary-cache",
|
"binary-cache",
|
||||||
"webp",
|
"webp",
|
||||||
] }
|
] }
|
||||||
|
thiserror = "2.0.18"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "js"] }
|
uuid = { version = "1.11.0", features = ["v4", "js"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
2
render-wasm/macros/Cargo.lock
generated
2
render-wasm/macros/Cargo.lock
generated
@ -13,6 +13,8 @@ name = "macros"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "macros"
|
name = "macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
syn = "2.0.106"
|
syn = "2.0.106"
|
||||||
|
|||||||
@ -6,9 +6,109 @@ use std::sync;
|
|||||||
|
|
||||||
use heck::{ToKebabCase, ToPascalCase};
|
use heck::{ToKebabCase, ToPascalCase};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, Block, GenericArgument, ItemFn, ReturnType, Type};
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, String>;
|
type Result<T> = std::result::Result<T, String>;
|
||||||
|
|
||||||
|
/// Attribute macro for WASM-exported functions. The function **must** return
|
||||||
|
/// `std::result::Result<T, E>` where T is a C ABI type and E implements
|
||||||
|
/// `std::error::Error` and `Into<u8>`. The macro:
|
||||||
|
/// - Clears the error code at entry.
|
||||||
|
/// - Runs the body in `std::panic::catch_unwind`.
|
||||||
|
/// - Unwraps the Result: `Ok(x)` → return x; `Err(e)` → set error code in memory and panic
|
||||||
|
/// (so ClojureScript can catch the exception and read the code via `read_error_code`).
|
||||||
|
/// - On panic from the body: sets critical error code (0x02) and resumes unwind.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let mut input = parse_macro_input!(item as ItemFn);
|
||||||
|
let body = (*input.block).clone();
|
||||||
|
|
||||||
|
let (attrs, boxed_ty) = match &input.sig.output {
|
||||||
|
ReturnType::Type(attrs, boxed_ty) => (attrs, boxed_ty),
|
||||||
|
ReturnType::Default => {
|
||||||
|
return quote! {
|
||||||
|
compile_error!(
|
||||||
|
"#[wasm_error] requires the function to return std::result::Result<T, E> where E: std::error::Error + Into<u8>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (inner_ty, error_ty) = match crate_error_result_inner_type(boxed_ty) {
|
||||||
|
Some(t) => (t, quote!(crate::error::Error)),
|
||||||
|
None => {
|
||||||
|
return quote! {
|
||||||
|
compile_error!(
|
||||||
|
"#[wasm_error] requires the function to return crate::error::Result<T>. T must be a C ABI type (u32, u8, bool, (), etc.)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let block: Block = syn::parse2(quote! {
|
||||||
|
{
|
||||||
|
crate::mem::clear_error_code();
|
||||||
|
let __wasm_err_result = std::panic::catch_unwind(|| -> std::result::Result<#inner_ty, #error_ty> {
|
||||||
|
#body
|
||||||
|
});
|
||||||
|
match __wasm_err_result {
|
||||||
|
Ok(__inner) => match __inner {
|
||||||
|
Ok(__val) => __val,
|
||||||
|
Err(__e) => {
|
||||||
|
let _: &dyn std::error::Error = &__e;
|
||||||
|
let __msg = __e.to_string();
|
||||||
|
crate::mem::set_error_code(__e.into());
|
||||||
|
panic!("WASM error: {}",__msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(__payload) => {
|
||||||
|
crate::mem::set_error_code(0x02); // critical, same as Error::Critical
|
||||||
|
std::panic::resume_unwind(__payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("block parse");
|
||||||
|
|
||||||
|
input.sig.output = ReturnType::Type(attrs.clone(), Box::new(inner_ty.clone()));
|
||||||
|
input.block = Box::new(block);
|
||||||
|
quote! { #input }.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the type is crate::error::Result<T> or a single-segment Result<T> (e.g. with
|
||||||
|
/// `use crate::error::Result`), returns Some(T). Otherwise None.
|
||||||
|
fn crate_error_result_inner_type(ty: &Type) -> Option<&Type> {
|
||||||
|
let path = match ty {
|
||||||
|
Type::Path(tp) => &tp.path,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let segs: Vec<_> = path.segments.iter().collect();
|
||||||
|
let last = path.segments.last()?;
|
||||||
|
if last.ident != "Result" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let args = match &last.arguments {
|
||||||
|
syn::PathArguments::AngleBracketed(a) => &a.args,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if args.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Accept crate::error::Result<T> or bare Result<T> (from use)
|
||||||
|
let ok = segs.len() == 1
|
||||||
|
|| (segs.len() == 3 && segs[0].ident == "crate" && segs[1].ident == "error");
|
||||||
|
if !ok {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match &args[0] {
|
||||||
|
GenericArgument::Type(t) => Some(t),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(ToJs)]
|
#[proc_macro_derive(ToJs)]
|
||||||
pub fn derive_to_cljs(input: TokenStream) -> TokenStream {
|
pub fn derive_to_cljs(input: TokenStream) -> TokenStream {
|
||||||
let input = syn::parse_macro_input!(input as syn::DeriveInput);
|
let input = syn::parse_macro_input!(input as syn::DeriveInput);
|
||||||
|
|||||||
25
render-wasm/src/error.rs
Normal file
25
render-wasm/src/error.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub const RECOVERABLE_ERROR: u8 = 0x01;
|
||||||
|
pub const CRITICAL_ERROR: u8 = 0x02;
|
||||||
|
|
||||||
|
// This is not really dead code, #[wasm_error] macro replaces this by something else.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("[Recoverable] {0}")]
|
||||||
|
RecoverableError(String),
|
||||||
|
#[error("[Critical] {0}")]
|
||||||
|
CriticalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for u8 {
|
||||||
|
fn from(error: Error) -> Self {
|
||||||
|
match error {
|
||||||
|
Error::RecoverableError(_) => RECOVERABLE_ERROR,
|
||||||
|
Error::CriticalError(_) => CRITICAL_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
mod emscripten;
|
mod emscripten;
|
||||||
|
mod error;
|
||||||
mod math;
|
mod math;
|
||||||
mod mem;
|
mod mem;
|
||||||
mod options;
|
mod options;
|
||||||
@ -14,12 +15,16 @@ mod view;
|
|||||||
mod wapi;
|
mod wapi;
|
||||||
mod wasm;
|
mod wasm;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use macros::wasm_error;
|
||||||
use math::{Bounds, Matrix};
|
use math::{Bounds, Matrix};
|
||||||
use mem::SerializableResult;
|
use mem::SerializableResult;
|
||||||
use shapes::{StructureEntry, StructureEntryType, TransformEntry};
|
use shapes::{StructureEntry, StructureEntryType, TransformEntry};
|
||||||
use skia_safe as skia;
|
use skia_safe as skia;
|
||||||
use state::State;
|
use state::State;
|
||||||
use std::collections::HashMap;
|
|
||||||
use utils::uuid_from_u32_quartet;
|
use utils::uuid_from_u32_quartet;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -95,22 +100,27 @@ macro_rules! with_state_mut_current_shape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn init(width: i32, height: i32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||||
let state_box = Box::new(State::new(width, height));
|
let state_box = Box::new(State::new(width, height));
|
||||||
unsafe {
|
unsafe {
|
||||||
STATE = Some(state_box);
|
STATE = Some(state_box);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_browser(browser: u8) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_browser(browser: u8) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.set_browser(browser);
|
state.set_browser(browser);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn clean_up() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn clean_up() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
// Cancel the current animation frame if it exists so
|
// Cancel the current animation frame if it exists so
|
||||||
// it won't try to render without context
|
// it won't try to render without context
|
||||||
@ -118,49 +128,60 @@ pub extern "C" fn clean_up() {
|
|||||||
render_state.cancel_animation_frame();
|
render_state.cancel_animation_frame();
|
||||||
});
|
});
|
||||||
unsafe { STATE = None }
|
unsafe { STATE = None }
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_render_options(debug: u32, dpr: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let render_state = state.render_state_mut();
|
let render_state = state.render_state_mut();
|
||||||
render_state.set_debug_flags(debug);
|
render_state.set_debug_flags(debug);
|
||||||
render_state.set_dpr(dpr);
|
render_state.set_dpr(dpr);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_canvas_background(raw_color: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let color = skia::Color::new(raw_color);
|
let color = skia::Color::new(raw_color);
|
||||||
state.set_background_color(color);
|
state.set_background_color(color);
|
||||||
state.rebuild_tiles_shallow();
|
state.rebuild_tiles_shallow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render(_: i32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render(_: i32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.rebuild_touched_tiles();
|
state.rebuild_touched_tiles();
|
||||||
state
|
state
|
||||||
.start_render_loop(performance::get_time())
|
.start_render_loop(performance::get_time())
|
||||||
.expect("Error rendering");
|
.expect("Error rendering");
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render_sync() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render_sync() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.rebuild_tiles();
|
state.rebuild_tiles();
|
||||||
state
|
state
|
||||||
.render_sync(performance::get_time())
|
.render_sync(performance::get_time())
|
||||||
.expect("Error rendering");
|
.expect("Error rendering");
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
state.use_shape(id);
|
state.use_shape(id);
|
||||||
@ -179,34 +200,42 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
|
|||||||
state.rebuild_tiles_from(Some(&id));
|
state.rebuild_tiles_from(Some(&id));
|
||||||
state
|
state
|
||||||
.render_sync_shape(&id, performance::get_time())
|
.render_sync_shape(&id, performance::get_time())
|
||||||
.expect("Error rendering");
|
.map_err(|e| Error::RecoverableError(e.to_string()))?;
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render_from_cache(_: i32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.render_state.cancel_animation_frame();
|
state.render_state.cancel_animation_frame();
|
||||||
state.render_from_cache();
|
state.render_from_cache();
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_preview_mode(enabled: bool) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.render_state.set_preview_mode(enabled);
|
state.render_state.set_preview_mode(enabled);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn render_preview() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn render_preview() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.render_preview(performance::get_time());
|
state.render_preview(performance::get_time());
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn process_animation_frame(timestamp: i32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state
|
state
|
||||||
@ -225,37 +254,45 @@ pub extern "C" fn process_animation_frame(timestamp: i32) {
|
|||||||
std::panic::resume_unwind(err);
|
std::panic::resume_unwind(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn reset_canvas() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn reset_canvas() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.render_state_mut().reset_canvas();
|
state.render_state_mut().reset_canvas();
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn resize_viewbox(width: i32, height: i32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.resize(width, height);
|
state.resize(width, height);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
performance::begin_measure!("set_view");
|
performance::begin_measure!("set_view");
|
||||||
let render_state = state.render_state_mut();
|
let render_state = state.render_state_mut();
|
||||||
render_state.set_view(zoom, x, y);
|
render_state.set_view(zoom, x, y);
|
||||||
performance::end_measure!("set_view");
|
performance::end_measure!("set_view");
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "profile-macros")]
|
#[cfg(feature = "profile-macros")]
|
||||||
static mut VIEW_INTERACTION_START: i32 = 0;
|
static mut VIEW_INTERACTION_START: i32 = 0;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_view_start() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_view_start() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
#[cfg(feature = "profile-macros")]
|
#[cfg(feature = "profile-macros")]
|
||||||
unsafe {
|
unsafe {
|
||||||
@ -265,10 +302,12 @@ pub extern "C" fn set_view_start() {
|
|||||||
state.render_state.options.set_fast_mode(true);
|
state.render_state.options.set_fast_mode(true);
|
||||||
performance::end_measure!("set_view_start");
|
performance::end_measure!("set_view_start");
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_view_end() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_view_end() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let _end_start = performance::begin_timed_log!("set_view_end");
|
let _end_start = performance::begin_timed_log!("set_view_end");
|
||||||
performance::begin_measure!("set_view_end");
|
performance::begin_measure!("set_view_end");
|
||||||
@ -304,17 +343,21 @@ pub extern "C" fn set_view_end() {
|
|||||||
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
|
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn clear_focus_mode() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn clear_focus_mode() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.clear_focus_mode();
|
state.clear_focus_mode();
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_focus_mode() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_focus_mode() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<Uuid> = bytes
|
let entries: Vec<Uuid> = bytes
|
||||||
@ -325,83 +368,111 @@ pub extern "C" fn set_focus_mode() {
|
|||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.set_focus_mode(entries);
|
state.set_focus_mode(entries);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn init_shapes_pool(capacity: usize) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn init_shapes_pool(capacity: usize) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.init_shapes_pool(capacity);
|
state.init_shapes_pool(capacity);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
state.use_shape(id);
|
state.use_shape(id);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
state.touch_shape(shape_id);
|
state.touch_shape(shape_id);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
state.set_parent_for_current_shape(id);
|
state.set_parent_for_current_shape(id);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_masked_group(masked: bool) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_masked_group(masked: bool) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_masked(masked);
|
shape.set_masked(masked);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_selrect(left, top, right, bottom);
|
shape.set_selrect(left, top, right, bottom);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_clip_content(clip_content: bool) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_clip_content(clip_content: bool) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_clip(clip_content);
|
shape.set_clip(clip_content);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_rotation(rotation: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_rotation(rotation: f32) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_rotation(rotation);
|
shape.set_rotation(rotation);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_transform(
|
||||||
|
a: f32,
|
||||||
|
b: f32,
|
||||||
|
c: f32,
|
||||||
|
d: f32,
|
||||||
|
e: f32,
|
||||||
|
f: f32,
|
||||||
|
) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_transform(a, b, c, d, e, f);
|
shape.set_transform(a, b, c, d, e, f);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
shape.add_child(id);
|
shape.add_child(id);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_children_set(entries: Vec<Uuid>) {
|
fn set_children_set(entries: Vec<Uuid>) -> Result<()> {
|
||||||
let mut deleted = Vec::new();
|
let mut deleted = Vec::new();
|
||||||
let mut parent_id = None;
|
let mut parent_id = None;
|
||||||
|
|
||||||
@ -420,7 +491,9 @@ fn set_children_set(entries: Vec<Uuid>) {
|
|||||||
|
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let Some(parent_id) = parent_id else {
|
let Some(parent_id) = parent_id else {
|
||||||
return;
|
return Err(Error::RecoverableError(
|
||||||
|
"set_children_set: Parent ID not found".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
for id in deleted {
|
for id in deleted {
|
||||||
@ -428,21 +501,27 @@ fn set_children_set(entries: Vec<Uuid>) {
|
|||||||
state.touch_shape(id);
|
state.touch_shape(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_children_0() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_children_0() -> Result<()> {
|
||||||
let entries = vec![];
|
let entries = vec![];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_children_1(a1: u32, b1: u32, c1: u32, d1: u32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_children_1(a1: u32, b1: u32, c1: u32, d1: u32) -> Result<()> {
|
||||||
let entries = vec![uuid_from_u32_quartet(a1, b1, c1, d1)];
|
let entries = vec![uuid_from_u32_quartet(a1, b1, c1, d1)];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
pub extern "C" fn set_children_2(
|
pub extern "C" fn set_children_2(
|
||||||
a1: u32,
|
a1: u32,
|
||||||
b1: u32,
|
b1: u32,
|
||||||
@ -452,15 +531,17 @@ pub extern "C" fn set_children_2(
|
|||||||
b2: u32,
|
b2: u32,
|
||||||
c2: u32,
|
c2: u32,
|
||||||
d2: u32,
|
d2: u32,
|
||||||
) {
|
) -> Result<()> {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||||
];
|
];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
pub extern "C" fn set_children_3(
|
pub extern "C" fn set_children_3(
|
||||||
a1: u32,
|
a1: u32,
|
||||||
b1: u32,
|
b1: u32,
|
||||||
@ -474,16 +555,18 @@ pub extern "C" fn set_children_3(
|
|||||||
b3: u32,
|
b3: u32,
|
||||||
c3: u32,
|
c3: u32,
|
||||||
d3: u32,
|
d3: u32,
|
||||||
) {
|
) -> Result<()> {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||||||
];
|
];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
pub extern "C" fn set_children_4(
|
pub extern "C" fn set_children_4(
|
||||||
a1: u32,
|
a1: u32,
|
||||||
b1: u32,
|
b1: u32,
|
||||||
@ -501,17 +584,19 @@ pub extern "C" fn set_children_4(
|
|||||||
b4: u32,
|
b4: u32,
|
||||||
c4: u32,
|
c4: u32,
|
||||||
d4: u32,
|
d4: u32,
|
||||||
) {
|
) -> Result<()> {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||||||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||||||
];
|
];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
pub extern "C" fn set_children_5(
|
pub extern "C" fn set_children_5(
|
||||||
a1: u32,
|
a1: u32,
|
||||||
b1: u32,
|
b1: u32,
|
||||||
@ -533,7 +618,7 @@ pub extern "C" fn set_children_5(
|
|||||||
b5: u32,
|
b5: u32,
|
||||||
c5: u32,
|
c5: u32,
|
||||||
d5: u32,
|
d5: u32,
|
||||||
) {
|
) -> Result<()> {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||||
@ -541,11 +626,13 @@ pub extern "C" fn set_children_5(
|
|||||||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||||||
uuid_from_u32_quartet(a5, b5, c5, d5),
|
uuid_from_u32_quartet(a5, b5, c5, d5),
|
||||||
];
|
];
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_children() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_children() -> Result<()> {
|
||||||
let bytes = mem::bytes_or_empty();
|
let bytes = mem::bytes_or_empty();
|
||||||
|
|
||||||
let entries: Vec<Uuid> = bytes
|
let entries: Vec<Uuid> = bytes
|
||||||
@ -553,58 +640,76 @@ pub extern "C" fn set_children() {
|
|||||||
.map(|data| Uuid::try_from(data).unwrap())
|
.map(|data| Uuid::try_from(data).unwrap())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
set_children_set(entries);
|
set_children_set(entries)?;
|
||||||
|
|
||||||
if !bytes.is_empty() {
|
if !bytes.is_empty() {
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32, is_thumbnail: bool) -> bool {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn is_image_cached(
|
||||||
|
a: u32,
|
||||||
|
b: u32,
|
||||||
|
c: u32,
|
||||||
|
d: u32,
|
||||||
|
is_thumbnail: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
state.render_state().has_image(&id, is_thumbnail)
|
let result = state.render_state().has_image(&id, is_thumbnail);
|
||||||
|
Ok(result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_svg_raw_content() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_svg_raw_content() -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
let svg_raw_content = String::from_utf8(bytes)
|
let svg_raw_content = String::from_utf8(bytes)
|
||||||
.unwrap()
|
.map_err(|e| Error::RecoverableError(e.to_string()))?
|
||||||
.trim_end_matches('\0')
|
.trim_end_matches('\0')
|
||||||
.to_string();
|
.to_string();
|
||||||
shape
|
shape.set_svg_raw_content(svg_raw_content);
|
||||||
.set_svg_raw_content(svg_raw_content)
|
|
||||||
.expect("Failed to set svg raw content");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_opacity(opacity: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_opacity(opacity: f32) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_opacity(opacity);
|
shape.set_opacity(opacity);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_hidden(hidden: bool) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_hidden(hidden: bool) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_hidden(hidden);
|
shape.set_hidden(hidden);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
shape.set_corners((r1, r2, r3, r4));
|
shape.set_corners((r1, r2, r3, r4));
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn get_selection_rect() -> *mut u8 {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<Uuid> = bytes
|
let entries: Vec<Uuid> = bytes
|
||||||
@ -619,40 +724,41 @@ pub extern "C" fn get_selection_rect() -> *mut u8 {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
with_state_mut!(state, {
|
let result_bound = with_state_mut!(state, {
|
||||||
let bbs: Vec<_> = entries
|
let bbs: Vec<_> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|id| state.shapes.get(id).map(|b| b.bounds()))
|
.flat_map(|id| state.shapes.get(id).map(|b| b.bounds()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let result_bound = if bbs.len() == 1 {
|
if bbs.len() == 1 {
|
||||||
bbs[0]
|
bbs[0]
|
||||||
} else {
|
} else {
|
||||||
Bounds::join_bounds(&bbs)
|
Bounds::join_bounds(&bbs)
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let width = result_bound.width();
|
let width = result_bound.width();
|
||||||
let height = result_bound.height();
|
let height = result_bound.height();
|
||||||
let center = result_bound.center();
|
let center = result_bound.center();
|
||||||
let transform = result_bound.transform_matrix().unwrap_or(Matrix::default());
|
let transform = result_bound.transform_matrix().unwrap_or(Matrix::default());
|
||||||
|
|
||||||
let mut bytes = vec![0; 40];
|
let mut bytes = vec![0; 40];
|
||||||
bytes[0..4].clone_from_slice(&width.to_le_bytes());
|
bytes[0..4].clone_from_slice(&width.to_le_bytes());
|
||||||
bytes[4..8].clone_from_slice(&height.to_le_bytes());
|
bytes[4..8].clone_from_slice(&height.to_le_bytes());
|
||||||
bytes[8..12].clone_from_slice(¢er.x.to_le_bytes());
|
bytes[8..12].clone_from_slice(¢er.x.to_le_bytes());
|
||||||
bytes[12..16].clone_from_slice(¢er.y.to_le_bytes());
|
bytes[12..16].clone_from_slice(¢er.y.to_le_bytes());
|
||||||
bytes[16..20].clone_from_slice(&transform[0].to_le_bytes());
|
bytes[16..20].clone_from_slice(&transform[0].to_le_bytes());
|
||||||
bytes[20..24].clone_from_slice(&transform[3].to_le_bytes());
|
bytes[20..24].clone_from_slice(&transform[3].to_le_bytes());
|
||||||
bytes[24..28].clone_from_slice(&transform[1].to_le_bytes());
|
bytes[24..28].clone_from_slice(&transform[1].to_le_bytes());
|
||||||
bytes[28..32].clone_from_slice(&transform[4].to_le_bytes());
|
bytes[28..32].clone_from_slice(&transform[4].to_le_bytes());
|
||||||
bytes[32..36].clone_from_slice(&transform[2].to_le_bytes());
|
bytes[32..36].clone_from_slice(&transform[2].to_le_bytes());
|
||||||
bytes[36..40].clone_from_slice(&transform[5].to_le_bytes());
|
bytes[36..40].clone_from_slice(&transform[5].to_le_bytes());
|
||||||
mem::write_bytes(bytes)
|
Ok(mem::write_bytes(bytes))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_structure_modifiers() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<_> = bytes
|
let entries: Vec<_> = bytes
|
||||||
@ -690,18 +796,22 @@ pub extern "C" fn set_structure_modifiers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn clean_modifiers() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
state.shapes.clean_all();
|
state.shapes.clean_all();
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_modifiers() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_modifiers() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<_> = bytes
|
let entries: Vec<_> = bytes
|
||||||
@ -720,26 +830,31 @@ pub extern "C" fn set_modifiers() {
|
|||||||
state.set_modifiers(modifiers);
|
state.set_modifiers(modifiers);
|
||||||
state.rebuild_modifier_tiles(ids);
|
state.rebuild_modifier_tiles(ids);
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn start_temp_objects() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn start_temp_objects() -> Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
#[allow(static_mut_refs)]
|
#[allow(static_mut_refs)]
|
||||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||||
state = Box::new(state.start_temp_objects());
|
state = Box::new(state.start_temp_objects());
|
||||||
STATE = Some(state);
|
STATE = Some(state);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn end_temp_objects() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn end_temp_objects() -> Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
#[allow(static_mut_refs)]
|
#[allow(static_mut_refs)]
|
||||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||||
state = Box::new(state.end_temp_objects());
|
state = Box::new(state.end_temp_objects());
|
||||||
STATE = Some(state);
|
STATE = Some(state);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
use std::alloc::{alloc, Layout};
|
|
||||||
use std::ptr;
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
const LAYOUT_ALIGN: usize = 4;
|
use crate::error::{Error, Result, CRITICAL_ERROR};
|
||||||
|
|
||||||
static BUFFERU8: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
pub const LAYOUT_ALIGN: usize = 4;
|
||||||
|
|
||||||
|
pub static BUFFERU8: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||||
|
pub static BUFFER_ERROR: Mutex<u8> = Mutex::new(0x00);
|
||||||
|
|
||||||
|
pub fn clear_error_code() {
|
||||||
|
let mut guard = BUFFER_ERROR.lock().unwrap();
|
||||||
|
*guard = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the error buffer from a byte. Used by #[wasm_error] when E: Into<u8>.
|
||||||
|
pub fn set_error_code(code: u8) {
|
||||||
|
let mut guard = BUFFER_ERROR.lock().unwrap();
|
||||||
|
*guard = code;
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn alloc_bytes(len: usize) -> *mut u8 {
|
pub extern "C" fn read_error_code() -> u8 {
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
if let Ok(guard) = BUFFER_ERROR.lock() {
|
||||||
|
*guard
|
||||||
if guard.is_some() {
|
} else {
|
||||||
panic!("Bytes already allocated");
|
CRITICAL_ERROR
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
|
||||||
let ptr = alloc(layout);
|
|
||||||
if ptr.is_null() {
|
|
||||||
panic!("Allocation failed");
|
|
||||||
}
|
|
||||||
// TODO: Maybe this could be removed.
|
|
||||||
ptr::write_bytes(ptr, 0, len);
|
|
||||||
*guard = Some(Vec::from_raw_parts(ptr, len, len));
|
|
||||||
ptr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,13 +40,6 @@ pub fn write_bytes(mut bytes: Vec<u8>) -> *mut u8 {
|
|||||||
ptr
|
ptr
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn free_bytes() {
|
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
|
||||||
*guard = None;
|
|
||||||
std::mem::drop(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bytes() -> Vec<u8> {
|
pub fn bytes() -> Vec<u8> {
|
||||||
let mut guard = BUFFERU8.lock().unwrap();
|
let mut guard = BUFFERU8.lock().unwrap();
|
||||||
guard.take().expect("Buffer is not initialized")
|
guard.take().expect("Buffer is not initialized")
|
||||||
@ -57,6 +50,15 @@ pub fn bytes_or_empty() -> Vec<u8> {
|
|||||||
guard.take().unwrap_or_default()
|
guard.take().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn free_bytes() -> Result<()> {
|
||||||
|
let mut guard = BUFFERU8
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
||||||
|
*guard = None;
|
||||||
|
std::mem::drop(guard);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub trait SerializableResult: From<Self::BytesType> + Into<Self::BytesType> {
|
pub trait SerializableResult: From<Self::BytesType> + Into<Self::BytesType> {
|
||||||
type BytesType;
|
type BytesType;
|
||||||
fn clone_to_slice(&self, slice: &mut [u8]);
|
fn clone_to_slice(&self, slice: &mut [u8]);
|
||||||
|
|||||||
@ -53,6 +53,8 @@ pub struct NodeRenderState {
|
|||||||
visited_mask: bool,
|
visited_mask: bool,
|
||||||
// This bool indicates that we're drawing the mask shape.
|
// This bool indicates that we're drawing the mask shape.
|
||||||
mask: bool,
|
mask: bool,
|
||||||
|
// True when this container was flattened (enter/exit skipped).
|
||||||
|
flattened: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get simplified children of a container, flattening nested flattened containers
|
/// Get simplified children of a container, flattening nested flattened containers
|
||||||
@ -745,16 +747,17 @@ impl RenderState {
|
|||||||
s.canvas().concat(transform);
|
s.canvas().concat(transform);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Hard clip edge (antialias = false) to avoid alpha seam when clipping
|
||||||
|
// semi-transparent content larger than the frame.
|
||||||
if let Some(corners) = corners {
|
if let Some(corners) = corners {
|
||||||
let rrect = RRect::new_rect_radii(*bounds, corners);
|
let rrect = RRect::new_rect_radii(*bounds, corners);
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
s.canvas()
|
s.canvas().clip_rrect(rrect, skia::ClipOp::Intersect, false);
|
||||||
.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
s.canvas()
|
s.canvas()
|
||||||
.clip_rect(*bounds, skia::ClipOp::Intersect, antialias);
|
.clip_rect(*bounds, skia::ClipOp::Intersect, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,7 +868,7 @@ impl RenderState {
|
|||||||
let text_stroke_blur_outset =
|
let text_stroke_blur_outset =
|
||||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||||
let mut stroke_paragraphs_list = shape
|
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
||||||
.visible_strokes()
|
.visible_strokes()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
@ -877,7 +880,7 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.unzip();
|
||||||
if fast_mode {
|
if fast_mode {
|
||||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||||
text::render(
|
text::render(
|
||||||
@ -889,9 +892,13 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
text_fill_inset,
|
text_fill_inset,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
|
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||||
|
.iter_mut()
|
||||||
|
.zip(stroke_opacities.iter())
|
||||||
|
{
|
||||||
text::render_with_bounds_outset(
|
text::render_with_bounds_outset(
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
@ -902,6 +909,7 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
None,
|
None,
|
||||||
|
*layer_opacity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -915,7 +923,10 @@ impl RenderState {
|
|||||||
let blur_filter = shape.image_filter(1.);
|
let blur_filter = shape.image_filter(1.);
|
||||||
let mut paragraphs_with_shadows =
|
let mut paragraphs_with_shadows =
|
||||||
text_content.paragraph_builder_group_from_text(Some(true));
|
text_content.paragraph_builder_group_from_text(Some(true));
|
||||||
let mut stroke_paragraphs_with_shadows_list = shape
|
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
|
||||||
|
Vec<_>,
|
||||||
|
Vec<_>,
|
||||||
|
) = shape
|
||||||
.visible_strokes()
|
.visible_strokes()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
@ -927,7 +938,7 @@ impl RenderState {
|
|||||||
Some(true),
|
Some(true),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.unzip();
|
||||||
|
|
||||||
if let Some(parent_shadows) = parent_shadows {
|
if let Some(parent_shadows) = parent_shadows {
|
||||||
if !shape.has_visible_strokes() {
|
if !shape.has_visible_strokes() {
|
||||||
@ -941,6 +952,7 @@ impl RenderState {
|
|||||||
Some(&shadow),
|
Some(&shadow),
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -967,6 +979,7 @@ impl RenderState {
|
|||||||
Some(shadow),
|
Some(shadow),
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -981,6 +994,7 @@ impl RenderState {
|
|||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
text_fill_inset,
|
text_fill_inset,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Stroke drop shadows
|
// 3. Stroke drop shadows
|
||||||
@ -995,7 +1009,10 @@ impl RenderState {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 4. Stroke fills
|
// 4. Stroke fills
|
||||||
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
|
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
|
||||||
|
.iter_mut()
|
||||||
|
.zip(stroke_opacities.iter())
|
||||||
|
{
|
||||||
text::render_with_bounds_outset(
|
text::render_with_bounds_outset(
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
@ -1006,6 +1023,7 @@ impl RenderState {
|
|||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
None,
|
None,
|
||||||
|
*layer_opacity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1032,6 +1050,7 @@ impl RenderState {
|
|||||||
Some(shadow),
|
Some(shadow),
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1462,6 +1481,7 @@ impl RenderState {
|
|||||||
clip_bounds: None,
|
clip_bounds: None,
|
||||||
visited_mask: true,
|
visited_mask: true,
|
||||||
mask: false,
|
mask: false,
|
||||||
|
flattened: false,
|
||||||
});
|
});
|
||||||
if let Some(&mask_id) = element.mask_id() {
|
if let Some(&mask_id) = element.mask_id() {
|
||||||
self.pending_nodes.push(NodeRenderState {
|
self.pending_nodes.push(NodeRenderState {
|
||||||
@ -1470,6 +1490,7 @@ impl RenderState {
|
|||||||
clip_bounds: None,
|
clip_bounds: None,
|
||||||
visited_mask: false,
|
visited_mask: false,
|
||||||
mask: true,
|
mask: true,
|
||||||
|
flattened: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1999,8 +2020,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if visited_children {
|
if visited_children {
|
||||||
// Skip render_shape_exit for flattened containers
|
if !node_render_state.flattened {
|
||||||
if !element.can_flatten() {
|
|
||||||
self.render_shape_exit(element, visited_mask, clip_bounds);
|
self.render_shape_exit(element, visited_mask, clip_bounds);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -2149,6 +2169,7 @@ impl RenderState {
|
|||||||
clip_bounds: clip_bounds.clone(),
|
clip_bounds: clip_bounds.clone(),
|
||||||
visited_mask: false,
|
visited_mask: false,
|
||||||
mask,
|
mask,
|
||||||
|
flattened: can_flatten,
|
||||||
});
|
});
|
||||||
|
|
||||||
if element.is_recursive() {
|
if element.is_recursive() {
|
||||||
@ -2175,13 +2196,13 @@ impl RenderState {
|
|||||||
ids.reverse();
|
ids.reverse();
|
||||||
}
|
}
|
||||||
// Sort by z_index descending (higher z renders on top).
|
// Sort by z_index descending (higher z renders on top).
|
||||||
// When z_index is equal, absolute children go behind
|
// When z_index is equal, absolute children go above
|
||||||
// non-absolute children (false < true).
|
// non-absolute children
|
||||||
ids.sort_by_key(|id| {
|
ids.sort_by_key(|id| {
|
||||||
let s = tree.get(id);
|
let s = tree.get(id);
|
||||||
let z = s.map(|s| s.z_index()).unwrap_or(0);
|
let z = s.map(|s| s.z_index()).unwrap_or(0);
|
||||||
let abs = s.map(|s| s.is_absolute()).unwrap_or(false);
|
let abs = s.map(|s| s.is_absolute()).unwrap_or(false);
|
||||||
(std::cmp::Reverse(z), abs)
|
(std::cmp::Reverse(z), !abs)
|
||||||
});
|
});
|
||||||
ids
|
ids
|
||||||
} else {
|
} else {
|
||||||
@ -2195,6 +2216,7 @@ impl RenderState {
|
|||||||
clip_bounds: children_clip_bounds.clone(),
|
clip_bounds: children_clip_bounds.clone(),
|
||||||
visited_mask: false,
|
visited_mask: false,
|
||||||
mask: false,
|
mask: false,
|
||||||
|
flattened: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2309,6 +2331,7 @@ impl RenderState {
|
|||||||
clip_bounds: None,
|
clip_bounds: None,
|
||||||
visited_mask: false,
|
visited_mask: false,
|
||||||
mask: false,
|
mask: false,
|
||||||
|
flattened: false,
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,6 +155,7 @@ pub fn render_text_shadows(
|
|||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
||||||
@ -167,6 +168,7 @@ pub fn render_text_shadows(
|
|||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,8 +41,8 @@ fn draw_stroke_on_rect(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// By default just draw the rect. Only dotted inner/outer strokes need
|
// Dotted inner/outer strokes need clipping to prevent the dotted
|
||||||
// clipping to prevent the dotted pattern from appearing in wrong areas.
|
// pattern from appearing in wrong areas.
|
||||||
if let Some(clip_op) = stroke.clip_op() {
|
if let Some(clip_op) = stroke.clip_op() {
|
||||||
// Use a neutral layer (no extra paint) so opacity and filters
|
// Use a neutral layer (no extra paint) so opacity and filters
|
||||||
// come solely from the stroke paint. This avoids applying
|
// come solely from the stroke paint. This avoids applying
|
||||||
@ -60,6 +60,35 @@ fn draw_stroke_on_rect(
|
|||||||
}
|
}
|
||||||
draw_stroke();
|
draw_stroke();
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
} else if stroke.kind == StrokeKind::Inner
|
||||||
|
&& (stroke.width >= rect.width() || stroke.width >= rect.height())
|
||||||
|
{
|
||||||
|
// When the inner stroke width exceeds a shape dimension, the inset
|
||||||
|
// rect goes negative and the stroke overflows outside the shape.
|
||||||
|
// Fall back to the same approach as the SVG renderer: draw with
|
||||||
|
// doubled width centered on the original shape and clip to it.
|
||||||
|
canvas.save();
|
||||||
|
match corners {
|
||||||
|
Some(radii) => {
|
||||||
|
let rrect = RRect::new_rect_radii(*rect, radii);
|
||||||
|
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
canvas.clip_rect(*rect, skia::ClipOp::Intersect, antialias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut inner_paint = paint.clone();
|
||||||
|
inner_paint.set_stroke_width(stroke.width * 2.0);
|
||||||
|
match corners {
|
||||||
|
Some(radii) => {
|
||||||
|
let rrect = RRect::new_rect_radii(*rect, radii);
|
||||||
|
canvas.draw_rrect(rrect, &inner_paint);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
canvas.draw_rect(*rect, &inner_paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
} else {
|
} else {
|
||||||
draw_stroke();
|
draw_stroke();
|
||||||
}
|
}
|
||||||
@ -83,8 +112,8 @@ fn draw_stroke_on_circle(
|
|||||||
let filter = compose_filters(blur, shadow);
|
let filter = compose_filters(blur, shadow);
|
||||||
paint.set_image_filter(filter);
|
paint.set_image_filter(filter);
|
||||||
|
|
||||||
// By default just draw the circle. Only dotted inner/outer strokes need
|
// Dotted inner/outer strokes need clipping to prevent the dotted
|
||||||
// clipping to prevent the dotted pattern from appearing in wrong areas.
|
// pattern from appearing in wrong areas.
|
||||||
if let Some(clip_op) = stroke.clip_op() {
|
if let Some(clip_op) = stroke.clip_op() {
|
||||||
// Use a neutral layer (no extra paint) so opacity and filters
|
// Use a neutral layer (no extra paint) so opacity and filters
|
||||||
// come solely from the stroke paint. This avoids applying
|
// come solely from the stroke paint. This avoids applying
|
||||||
@ -99,6 +128,24 @@ fn draw_stroke_on_circle(
|
|||||||
canvas.clip_path(&clip_path, clip_op, antialias);
|
canvas.clip_path(&clip_path, clip_op, antialias);
|
||||||
canvas.draw_oval(stroke_rect, &paint);
|
canvas.draw_oval(stroke_rect, &paint);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
} else if stroke.kind == StrokeKind::Inner
|
||||||
|
&& (stroke.width >= rect.width() || stroke.width >= rect.height())
|
||||||
|
{
|
||||||
|
// When the inner stroke width exceeds a shape dimension, the inset
|
||||||
|
// rect goes negative and the stroke overflows outside the shape.
|
||||||
|
// Fall back to the same approach as the SVG renderer: draw with
|
||||||
|
// doubled width centered on the original shape and clip to it.
|
||||||
|
canvas.save();
|
||||||
|
let clip_path = {
|
||||||
|
let mut pb = skia::PathBuilder::new();
|
||||||
|
pb.add_oval(rect, None, None);
|
||||||
|
pb.detach()
|
||||||
|
};
|
||||||
|
canvas.clip_path(&clip_path, skia::ClipOp::Intersect, antialias);
|
||||||
|
let mut inner_paint = paint.clone();
|
||||||
|
inner_paint.set_stroke_width(stroke.width * 2.0);
|
||||||
|
canvas.draw_oval(*rect, &inner_paint);
|
||||||
|
canvas.restore();
|
||||||
} else {
|
} else {
|
||||||
canvas.draw_oval(stroke_rect, &paint);
|
canvas.draw_oval(stroke_rect, &paint);
|
||||||
}
|
}
|
||||||
@ -164,16 +211,25 @@ fn draw_stroke_on_path(
|
|||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
antialias: bool,
|
antialias: bool,
|
||||||
) {
|
) {
|
||||||
let skia_path = path
|
|
||||||
.to_skia_path()
|
|
||||||
.make_transform(path_transform.unwrap_or(&Matrix::default()));
|
|
||||||
|
|
||||||
let is_open = path.is_open();
|
let is_open = path.is_open();
|
||||||
|
|
||||||
let mut draw_paint = paint.clone();
|
let mut draw_paint = paint.clone();
|
||||||
let filter = compose_filters(blur, shadow);
|
let filter = compose_filters(blur, shadow);
|
||||||
draw_paint.set_image_filter(filter);
|
draw_paint.set_image_filter(filter);
|
||||||
|
|
||||||
|
// Move path_transform from the path geometry to the canvas so the
|
||||||
|
// stroke width is not distorted by non-uniform shape scaling.
|
||||||
|
// The path coordinates are already in world space, so we draw the
|
||||||
|
// raw path on a canvas where the shape transform has been undone:
|
||||||
|
// canvas * path_transform = View × parents (no shape scale/rotation)
|
||||||
|
// This matches the SVG renderer, which bakes the transform into path
|
||||||
|
// coordinates and never sets a transform attribute on the element.
|
||||||
|
let save_count = canvas.save();
|
||||||
|
if let Some(pt) = path_transform {
|
||||||
|
canvas.concat(pt);
|
||||||
|
}
|
||||||
|
let skia_path = path.to_skia_path();
|
||||||
|
|
||||||
match stroke.render_kind(is_open) {
|
match stroke.render_kind(is_open) {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
draw_inner_stroke_path(canvas, &skia_path, &draw_paint, blur, antialias);
|
draw_inner_stroke_path(canvas, &skia_path, &draw_paint, blur, antialias);
|
||||||
@ -187,6 +243,8 @@ fn draw_stroke_on_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_stroke_caps(&skia_path, stroke, canvas, is_open, paint, blur, antialias);
|
handle_stroke_caps(&skia_path, stroke, canvas, is_open, paint, blur, antialias);
|
||||||
|
|
||||||
|
canvas.restore_to_count(save_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_stroke_cap(
|
fn handle_stroke_cap(
|
||||||
|
|||||||
@ -20,11 +20,12 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
bounds: &Rect,
|
bounds: &Rect,
|
||||||
count_inner_strokes: usize,
|
count_inner_strokes: usize,
|
||||||
use_shadow: Option<bool>,
|
use_shadow: Option<bool>,
|
||||||
) -> Vec<ParagraphBuilderGroup> {
|
) -> (Vec<ParagraphBuilderGroup>, Option<f32>) {
|
||||||
let fallback_fonts = get_fallback_fonts();
|
let fallback_fonts = get_fallback_fonts();
|
||||||
let fonts = get_font_collection();
|
let fonts = get_font_collection();
|
||||||
let mut paragraph_group = Vec::new();
|
let mut paragraph_group = Vec::new();
|
||||||
let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent();
|
let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent();
|
||||||
|
let mut group_layer_opacity: Option<f32> = None;
|
||||||
|
|
||||||
for paragraph in text_content.paragraphs() {
|
for paragraph in text_content.paragraphs() {
|
||||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||||
@ -32,7 +33,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
|
|
||||||
for span in paragraph.children().iter() {
|
for span in paragraph.children().iter() {
|
||||||
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
|
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
|
||||||
let stroke_paints = get_text_stroke_paints(
|
let (stroke_paints, stroke_layer_opacity) = get_text_stroke_paints(
|
||||||
stroke,
|
stroke,
|
||||||
bounds,
|
bounds,
|
||||||
&text_paint,
|
&text_paint,
|
||||||
@ -40,6 +41,10 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
remove_stroke_alpha,
|
remove_stroke_alpha,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if group_layer_opacity.is_none() {
|
||||||
|
group_layer_opacity = stroke_layer_opacity;
|
||||||
|
}
|
||||||
|
|
||||||
let text: String = span.apply_text_transform();
|
let text: String = span.apply_text_transform();
|
||||||
|
|
||||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||||
@ -67,7 +72,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
paragraph_group.push(stroke_paragraphs);
|
paragraph_group.push(stroke_paragraphs);
|
||||||
}
|
}
|
||||||
|
|
||||||
paragraph_group
|
(paragraph_group, group_layer_opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_text_stroke_paints(
|
fn get_text_stroke_paints(
|
||||||
@ -76,8 +81,25 @@ fn get_text_stroke_paints(
|
|||||||
text_paint: &Paint,
|
text_paint: &Paint,
|
||||||
count_inner_strokes: usize,
|
count_inner_strokes: usize,
|
||||||
remove_stroke_alpha: bool,
|
remove_stroke_alpha: bool,
|
||||||
) -> Vec<Paint> {
|
) -> (Vec<Paint>, Option<f32>) {
|
||||||
let mut paints = Vec::new();
|
let mut paints = Vec::new();
|
||||||
|
let mut layer_opacity: Option<f32> = None;
|
||||||
|
|
||||||
|
let stroke_opacity = stroke.fill.opacity();
|
||||||
|
let needs_opacity_layer = stroke_opacity < 1.0 && !remove_stroke_alpha;
|
||||||
|
|
||||||
|
let fill_for_paint = |paint: &mut Paint| {
|
||||||
|
if needs_opacity_layer {
|
||||||
|
let opaque_fill = stroke.fill.with_full_opacity();
|
||||||
|
set_paint_fill(paint, &opaque_fill, bounds, remove_stroke_alpha);
|
||||||
|
} else {
|
||||||
|
set_paint_fill(paint, &stroke.fill, bounds, remove_stroke_alpha);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if needs_opacity_layer {
|
||||||
|
layer_opacity = Some(stroke_opacity);
|
||||||
|
}
|
||||||
|
|
||||||
match stroke.kind {
|
match stroke.kind {
|
||||||
StrokeKind::Inner => {
|
StrokeKind::Inner => {
|
||||||
@ -99,7 +121,7 @@ fn get_text_stroke_paints(
|
|||||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
paint.set_stroke_width(stroke.width * 2.0);
|
paint.set_stroke_width(stroke.width * 2.0);
|
||||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
fill_for_paint(&mut paint);
|
||||||
paints.push(paint);
|
paints.push(paint);
|
||||||
} else {
|
} else {
|
||||||
let mut paint = skia::Paint::default();
|
let mut paint = skia::Paint::default();
|
||||||
@ -108,7 +130,12 @@ fn get_text_stroke_paints(
|
|||||||
paint.set_alpha(255);
|
paint.set_alpha(255);
|
||||||
} else {
|
} else {
|
||||||
paint = text_paint.clone();
|
paint = text_paint.clone();
|
||||||
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
|
if needs_opacity_layer {
|
||||||
|
let opaque_fill = stroke.fill.with_full_opacity();
|
||||||
|
set_paint_fill(&mut paint, &opaque_fill, bounds, false);
|
||||||
|
} else {
|
||||||
|
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paint.set_style(skia::PaintStyle::Fill);
|
paint.set_style(skia::PaintStyle::Fill);
|
||||||
@ -132,7 +159,7 @@ fn get_text_stroke_paints(
|
|||||||
paint.set_style(skia::PaintStyle::Stroke);
|
paint.set_style(skia::PaintStyle::Stroke);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
paint.set_stroke_width(stroke.width);
|
paint.set_stroke_width(stroke.width);
|
||||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
fill_for_paint(&mut paint);
|
||||||
paints.push(paint);
|
paints.push(paint);
|
||||||
}
|
}
|
||||||
StrokeKind::Outer => {
|
StrokeKind::Outer => {
|
||||||
@ -141,7 +168,7 @@ fn get_text_stroke_paints(
|
|||||||
paint.set_blend_mode(skia::BlendMode::DstOver);
|
paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
paint.set_stroke_width(stroke.width * 2.0);
|
paint.set_stroke_width(stroke.width * 2.0);
|
||||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
fill_for_paint(&mut paint);
|
||||||
paints.push(paint);
|
paints.push(paint);
|
||||||
|
|
||||||
let mut paint = skia::Paint::default();
|
let mut paint = skia::Paint::default();
|
||||||
@ -153,7 +180,7 @@ fn get_text_stroke_paints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
paints
|
(paints, layer_opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@ -167,6 +194,7 @@ pub fn render_with_bounds_outset(
|
|||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
stroke_bounds_outset: f32,
|
stroke_bounds_outset: f32,
|
||||||
fill_inset: Option<f32>,
|
fill_inset: Option<f32>,
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
) {
|
) {
|
||||||
if let Some(render_state) = render_state {
|
if let Some(render_state) = render_state {
|
||||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||||
@ -195,6 +223,7 @@ pub fn render_with_bounds_outset(
|
|||||||
shadow,
|
shadow,
|
||||||
Some(&blur_filter_clone),
|
Some(&blur_filter_clone),
|
||||||
fill_inset,
|
fill_inset,
|
||||||
|
layer_opacity,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -204,12 +233,28 @@ pub fn render_with_bounds_outset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||||
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset);
|
render_text_on_canvas(
|
||||||
|
canvas,
|
||||||
|
shape,
|
||||||
|
paragraph_builders,
|
||||||
|
shadow,
|
||||||
|
blur,
|
||||||
|
fill_inset,
|
||||||
|
layer_opacity,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(canvas) = canvas {
|
if let Some(canvas) = canvas {
|
||||||
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset);
|
render_text_on_canvas(
|
||||||
|
canvas,
|
||||||
|
shape,
|
||||||
|
paragraph_builders,
|
||||||
|
shadow,
|
||||||
|
blur,
|
||||||
|
fill_inset,
|
||||||
|
layer_opacity,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +268,7 @@ pub fn render(
|
|||||||
shadow: Option<&Paint>,
|
shadow: Option<&Paint>,
|
||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
fill_inset: Option<f32>,
|
fill_inset: Option<f32>,
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
) {
|
) {
|
||||||
render_with_bounds_outset(
|
render_with_bounds_outset(
|
||||||
render_state,
|
render_state,
|
||||||
@ -234,6 +280,7 @@ pub fn render(
|
|||||||
blur,
|
blur,
|
||||||
0.0,
|
0.0,
|
||||||
fill_inset,
|
fill_inset,
|
||||||
|
layer_opacity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +291,7 @@ fn render_text_on_canvas(
|
|||||||
shadow: Option<&Paint>,
|
shadow: Option<&Paint>,
|
||||||
blur: Option<&ImageFilter>,
|
blur: Option<&ImageFilter>,
|
||||||
fill_inset: Option<f32>,
|
fill_inset: Option<f32>,
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
) {
|
) {
|
||||||
if let Some(blur_filter) = blur {
|
if let Some(blur_filter) = blur {
|
||||||
let mut blur_paint = Paint::default();
|
let mut blur_paint = Paint::default();
|
||||||
@ -255,7 +303,7 @@ fn render_text_on_canvas(
|
|||||||
if let Some(shadow_paint) = shadow {
|
if let Some(shadow_paint) = shadow {
|
||||||
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
||||||
canvas.save_layer(&layer_rec);
|
canvas.save_layer(&layer_rec);
|
||||||
draw_text(canvas, shape, paragraph_builders);
|
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
} else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
} else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
||||||
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
||||||
@ -263,13 +311,13 @@ fn render_text_on_canvas(
|
|||||||
layer_paint.set_image_filter(erode);
|
layer_paint.set_image_filter(erode);
|
||||||
let layer_rec = SaveLayerRec::default().paint(&layer_paint);
|
let layer_rec = SaveLayerRec::default().paint(&layer_paint);
|
||||||
canvas.save_layer(&layer_rec);
|
canvas.save_layer(&layer_rec);
|
||||||
draw_text(canvas, shape, paragraph_builders);
|
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
} else {
|
} else {
|
||||||
draw_text(canvas, shape, paragraph_builders);
|
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
draw_text(canvas, shape, paragraph_builders);
|
draw_text(canvas, shape, paragraph_builders, layer_opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if blur.is_some() {
|
if blur.is_some() {
|
||||||
@ -283,13 +331,20 @@ fn draw_text(
|
|||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
|
||||||
|
layer_opacity: Option<f32>,
|
||||||
) {
|
) {
|
||||||
let text_content = shape.get_text_content();
|
let text_content = shape.get_text_content();
|
||||||
let layout_info =
|
let layout_info =
|
||||||
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
|
||||||
|
|
||||||
let layer_rec = SaveLayerRec::default();
|
if let Some(opacity) = layer_opacity {
|
||||||
canvas.save_layer(&layer_rec);
|
let mut opacity_paint = Paint::default();
|
||||||
|
opacity_paint.set_alpha_f(opacity);
|
||||||
|
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||||
|
canvas.save_layer(&layer_rec);
|
||||||
|
} else {
|
||||||
|
canvas.save_layer(&SaveLayerRec::default());
|
||||||
|
}
|
||||||
|
|
||||||
for para in &layout_info.paragraphs {
|
for para in &layout_info.paragraphs {
|
||||||
para.paragraph.paint(canvas, (para.x, para.y));
|
para.paragraph.paint(canvas, (para.x, para.y));
|
||||||
|
|||||||
@ -66,7 +66,7 @@ fn render_selection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_blend_mode(BlendMode::Multiply);
|
paint.set_blend_mode(BlendMode::default());
|
||||||
paint.set_color(editor_state.theme.selection_color);
|
paint.set_color(editor_state.theme.selection_color);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
|
|
||||||
|
|||||||
@ -705,9 +705,8 @@ impl Shape {
|
|||||||
self.invalidate_extrect();
|
self.invalidate_extrect();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
|
pub fn set_svg_raw_content(&mut self, content: String) {
|
||||||
self.shape_type = Type::SVGRaw(SVGRaw::from_content(content));
|
self.shape_type = Type::SVGRaw(SVGRaw::from_content(content));
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_blend_mode(&mut self, mode: BlendMode) {
|
pub fn set_blend_mode(&mut self, mode: BlendMode) {
|
||||||
|
|||||||
@ -140,6 +140,38 @@ pub enum Fill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Fill {
|
impl Fill {
|
||||||
|
pub fn opacity(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
Fill::Solid(SolidColor(color)) => color.a() as f32 / 255.0,
|
||||||
|
Fill::LinearGradient(g) => g.opacity as f32 / 255.0,
|
||||||
|
Fill::RadialGradient(g) => g.opacity as f32 / 255.0,
|
||||||
|
Fill::Image(i) => i.opacity as f32 / 255.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_full_opacity(&self) -> Fill {
|
||||||
|
match self {
|
||||||
|
Fill::Solid(SolidColor(color)) => Fill::Solid(SolidColor(skia::Color::from_argb(
|
||||||
|
255,
|
||||||
|
color.r(),
|
||||||
|
color.g(),
|
||||||
|
color.b(),
|
||||||
|
))),
|
||||||
|
Fill::LinearGradient(g) => Fill::LinearGradient(Gradient {
|
||||||
|
opacity: 255,
|
||||||
|
..g.clone()
|
||||||
|
}),
|
||||||
|
Fill::RadialGradient(g) => Fill::RadialGradient(Gradient {
|
||||||
|
opacity: 255,
|
||||||
|
..g.clone()
|
||||||
|
}),
|
||||||
|
Fill::Image(i) => Fill::Image(ImageFill {
|
||||||
|
opacity: 255,
|
||||||
|
..i.clone()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint {
|
pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint {
|
||||||
match self {
|
match self {
|
||||||
Self::Solid(SolidColor(color)) => {
|
Self::Solid(SolidColor(color)) => {
|
||||||
|
|||||||
@ -161,6 +161,13 @@ impl TextPositionWithAffinity {
|
|||||||
offset,
|
offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.position_with_affinity.position = 0;
|
||||||
|
self.position_with_affinity.affinity = Affinity::Downstream;
|
||||||
|
self.paragraph = 0;
|
||||||
|
self.offset = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -569,6 +576,7 @@ impl TextContent {
|
|||||||
for paragraph in self.paragraphs() {
|
for paragraph in self.paragraphs() {
|
||||||
let paragraph_style = paragraph.paragraph_to_style();
|
let paragraph_style = paragraph.paragraph_to_style();
|
||||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||||
|
let mut has_text = false;
|
||||||
for span in paragraph.children() {
|
for span in paragraph.children() {
|
||||||
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||||
let text_style = span.to_style(
|
let text_style = span.to_style(
|
||||||
@ -578,9 +586,15 @@ impl TextContent {
|
|||||||
paragraph.line_height(),
|
paragraph.line_height(),
|
||||||
);
|
);
|
||||||
let text: String = span.apply_text_transform();
|
let text: String = span.apply_text_transform();
|
||||||
|
if !text.is_empty() {
|
||||||
|
has_text = true;
|
||||||
|
}
|
||||||
builder.push_style(&text_style);
|
builder.push_style(&text_style);
|
||||||
builder.add_text(&text);
|
builder.add_text(&text);
|
||||||
}
|
}
|
||||||
|
if !has_text {
|
||||||
|
builder.add_text(" ");
|
||||||
|
}
|
||||||
paragraph_group.push(vec![builder]);
|
paragraph_group.push(vec![builder]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,11 @@ impl TextSelection {
|
|||||||
!self.is_collapsed()
|
!self.is_collapsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.anchor.reset();
|
||||||
|
self.focus.reset();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
||||||
self.anchor = cursor;
|
self.anchor = cursor;
|
||||||
self.focus = cursor;
|
self.focus = cursor;
|
||||||
@ -86,7 +91,7 @@ pub enum TextEditorEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: It should be better to get these constants from the frontend through the API.
|
/// FIXME: It should be better to get these constants from the frontend through the API.
|
||||||
const SELECTION_COLOR: Color = Color::from_argb(255, 0, 209, 184);
|
const SELECTION_COLOR: Color = Color::from_argb(127, 0, 209, 184);
|
||||||
const CURSOR_WIDTH: f32 = 1.5;
|
const CURSOR_WIDTH: f32 = 1.5;
|
||||||
const CURSOR_COLOR: Color = Color::BLACK;
|
const CURSOR_COLOR: Color = Color::BLACK;
|
||||||
const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0;
|
const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0;
|
||||||
@ -133,7 +138,7 @@ impl TextEditorState {
|
|||||||
self.active_shape_id = Some(shape_id);
|
self.active_shape_id = Some(shape_id);
|
||||||
self.cursor_visible = true;
|
self.cursor_visible = true;
|
||||||
self.last_blink_time = 0.0;
|
self.last_blink_time = 0.0;
|
||||||
self.selection = TextSelection::new();
|
self.selection.reset();
|
||||||
self.is_pointer_selection_active = false;
|
self.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
}
|
}
|
||||||
@ -142,9 +147,10 @@ impl TextEditorState {
|
|||||||
self.is_active = false;
|
self.is_active = false;
|
||||||
self.active_shape_id = None;
|
self.active_shape_id = None;
|
||||||
self.cursor_visible = false;
|
self.cursor_visible = false;
|
||||||
|
self.last_blink_time = 0.0;
|
||||||
|
self.selection.reset();
|
||||||
self.is_pointer_selection_active = false;
|
self.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
self.reset_blink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_pointer_selection(&mut self) -> bool {
|
pub fn start_pointer_selection(&mut self) -> bool {
|
||||||
@ -193,15 +199,83 @@ impl TextEditorState {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_word_boundary(
|
||||||
|
&mut self,
|
||||||
|
content: &TextContent,
|
||||||
|
position: &TextPositionWithAffinity,
|
||||||
|
) {
|
||||||
|
self.is_pointer_selection_active = false;
|
||||||
|
|
||||||
|
let paragraphs = content.paragraphs();
|
||||||
|
if paragraphs.is_empty() || position.paragraph >= paragraphs.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = ¶graphs[position.paragraph];
|
||||||
|
let paragraph_text: String = paragraph
|
||||||
|
.children()
|
||||||
|
.iter()
|
||||||
|
.map(|span| span.text.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let chars: Vec<char> = paragraph_text.chars().collect();
|
||||||
|
if chars.is_empty() {
|
||||||
|
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
|
||||||
|
position.paragraph,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
self.reset_blink();
|
||||||
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = position.offset.min(chars.len());
|
||||||
|
|
||||||
|
if offset == chars.len() {
|
||||||
|
offset = offset.saturating_sub(1);
|
||||||
|
} else if !is_word_char(chars[offset]) && offset > 0 && is_word_char(chars[offset - 1]) {
|
||||||
|
offset -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_word_char(chars[offset]) {
|
||||||
|
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
|
||||||
|
position.paragraph,
|
||||||
|
position.offset.min(chars.len()),
|
||||||
|
));
|
||||||
|
self.reset_blink();
|
||||||
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start = offset;
|
||||||
|
while start > 0 && is_word_char(chars[start - 1]) {
|
||||||
|
start -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut end = offset + 1;
|
||||||
|
while end < chars.len() && is_word_char(chars[end]) {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
|
||||||
|
position.paragraph,
|
||||||
|
start,
|
||||||
|
));
|
||||||
|
self.extend_selection_from_position(&TextPositionWithAffinity::new_without_affinity(
|
||||||
|
position.paragraph,
|
||||||
|
end,
|
||||||
|
));
|
||||||
|
self.reset_blink();
|
||||||
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||||
self.selection.set_caret(*position);
|
self.selection.set_caret(*position);
|
||||||
self.reset_blink();
|
|
||||||
self.push_event(TextEditorEvent::SelectionChanged);
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||||
self.selection.extend_to(*position);
|
self.selection.extend_to(*position);
|
||||||
self.reset_blink();
|
|
||||||
self.push_event(TextEditorEvent::SelectionChanged);
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,3 +316,7 @@ impl TextEditorState {
|
|||||||
!self.pending_events.is_empty()
|
!self.pending_events.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_word_char(c: char) -> bool {
|
||||||
|
c.is_alphanumeric() || c == '_'
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ pub mod blurs;
|
|||||||
pub mod fills;
|
pub mod fills;
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
pub mod layouts;
|
pub mod layouts;
|
||||||
|
pub mod mem;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod shadows;
|
pub mod shadows;
|
||||||
pub mod shapes;
|
pub mod shapes;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes;
|
use crate::shapes;
|
||||||
@ -67,7 +67,8 @@ pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_fills() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_fills() -> Result<()> {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
// The first byte contains the actual number of fills
|
// The first byte contains the actual number of fills
|
||||||
@ -75,8 +76,9 @@ pub extern "C" fn set_shape_fills() {
|
|||||||
// Skip the first 4 bytes (header with fill count) and parse only the actual fills
|
// Skip the first 4 bytes (header with fill count) and parse only the actual fills
|
||||||
let fills = parse_fills_from_bytes(&bytes[4..], num_fills);
|
let fills = parse_fills_from_bytes(&bytes[4..], num_fills);
|
||||||
shape.set_fills(fills);
|
shape.set_fills(fills);
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
use crate::mem;
|
use crate::mem;
|
||||||
|
use macros::wasm_error;
|
||||||
// use crate::mem::SerializableResult;
|
// use crate::mem::SerializableResult;
|
||||||
|
use crate::error::Error;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
use crate::with_state_mut;
|
use crate::with_state_mut;
|
||||||
use crate::STATE;
|
use crate::STATE;
|
||||||
@ -65,7 +67,8 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn store_image() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn store_image() -> crate::error::Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||||
|
|
||||||
@ -87,7 +90,8 @@ pub extern "C" fn store_image() {
|
|||||||
state.touch_shape(ids.shape_id);
|
state.touch_shape(ids.shape_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores an image from an existing WebGL texture, avoiding re-decoding
|
/// Stores an image from an existing WebGL texture, avoiding re-decoding
|
||||||
@ -99,13 +103,17 @@ pub extern "C" fn store_image() {
|
|||||||
/// - bytes 40-43: width (i32)
|
/// - bytes 40-43: width (i32)
|
||||||
/// - bytes 44-47: height (i32)
|
/// - bytes 44-47: height (i32)
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn store_image_from_texture() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
if bytes.len() < 48 {
|
if bytes.len() < 48 {
|
||||||
|
// FIXME: Review if this should be an critical or a recoverable error.
|
||||||
eprintln!("store_image_from_texture: insufficient data");
|
eprintln!("store_image_from_texture: insufficient data");
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
return;
|
return Err(Error::RecoverableError(
|
||||||
|
"store_image_from_texture: insufficient data".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||||
@ -139,5 +147,6 @@ pub extern "C" fn store_image_from_texture() {
|
|||||||
state.touch_shape(ids.shape_id);
|
state.touch_shape(ids.shape_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes::{FontFamily, FontStyle};
|
use crate::shapes::{FontFamily, FontStyle};
|
||||||
@ -30,6 +30,7 @@ impl From<RawFontStyle> for FontStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
pub extern "C" fn store_font(
|
pub extern "C" fn store_font(
|
||||||
a: u32,
|
a: u32,
|
||||||
b: u32,
|
b: u32,
|
||||||
@ -39,7 +40,7 @@ pub extern "C" fn store_font(
|
|||||||
style: u8,
|
style: u8,
|
||||||
is_emoji: bool,
|
is_emoji: bool,
|
||||||
is_fallback: bool,
|
is_fallback: bool,
|
||||||
) {
|
) -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||||
let font_bytes = mem::bytes();
|
let font_bytes = mem::bytes();
|
||||||
@ -52,8 +53,9 @@ pub extern "C" fn store_font(
|
|||||||
.fonts_mut()
|
.fonts_mut()
|
||||||
.add(family, &font_bytes, is_emoji, is_fallback);
|
.add(family, &font_bytes, is_emoji, is_fallback);
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes::{GridCell, GridDirection, GridTrack, GridTrackType};
|
use crate::shapes::{GridCell, GridDirection, GridTrack, GridTrackType};
|
||||||
@ -7,6 +7,9 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat
|
|||||||
|
|
||||||
use super::align;
|
use super::align;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[repr(C, align(1))]
|
#[repr(C, align(1))]
|
||||||
struct RawGridCell {
|
struct RawGridCell {
|
||||||
@ -168,7 +171,8 @@ pub extern "C" fn set_grid_layout_data(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_grid_columns() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_grid_columns() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<GridTrack> = bytes
|
let entries: Vec<GridTrack> = bytes
|
||||||
@ -181,11 +185,13 @@ pub extern "C" fn set_grid_columns() {
|
|||||||
shape.set_grid_columns(entries);
|
shape.set_grid_columns(entries);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_grid_rows() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_grid_rows() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let entries: Vec<GridTrack> = bytes
|
let entries: Vec<GridTrack> = bytes
|
||||||
@ -198,11 +204,13 @@ pub extern "C" fn set_grid_rows() {
|
|||||||
shape.set_grid_rows(entries);
|
shape.set_grid_rows(entries);
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_grid_cells() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_grid_cells() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
|
|
||||||
let cells: Vec<RawGridCell> = bytes
|
let cells: Vec<RawGridCell> = bytes
|
||||||
@ -215,7 +223,8 @@ pub extern "C" fn set_grid_cells() {
|
|||||||
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());
|
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());
|
||||||
});
|
});
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
38
render-wasm/src/wasm/mem.rs
Normal file
38
render-wasm/src/wasm/mem.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use std::alloc::{alloc, Layout};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::mem::{BUFFERU8, LAYOUT_ALIGN};
|
||||||
|
use macros::wasm_error;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn alloc_bytes(len: usize) -> Result<*mut u8> {
|
||||||
|
let mut guard = BUFFERU8
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
||||||
|
|
||||||
|
if guard.is_some() {
|
||||||
|
return Err(Error::CriticalError("Bytes already allocated".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
||||||
|
let ptr = alloc(layout);
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(Error::CriticalError("Allocation failed".to_string()));
|
||||||
|
}
|
||||||
|
// TODO: Maybe this could be removed.
|
||||||
|
ptr::write_bytes(ptr, 0, len);
|
||||||
|
*guard = Some(Vec::from_raw_parts(ptr, len, len));
|
||||||
|
Ok(ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn free_bytes() -> Result<()> {
|
||||||
|
crate::mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
#![allow(unused_mut, unused_variables)]
|
#![allow(unused_mut, unused_variables)]
|
||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
use mem::SerializableResult;
|
use mem::SerializableResult;
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
@ -161,12 +161,14 @@ pub extern "C" fn start_shape_path_buffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_path_chunk_buffer() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
let buffer = get_path_upload_buffer();
|
let buffer = get_path_upload_buffer();
|
||||||
let mut buffer = buffer.lock().unwrap();
|
let mut buffer = buffer.lock().unwrap();
|
||||||
buffer.extend_from_slice(&bytes);
|
buffer.extend_from_slice(&bytes);
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use super::RawSegmentData;
|
use super::RawSegmentData;
|
||||||
use crate::math;
|
use crate::math;
|
||||||
@ -8,6 +8,9 @@ use crate::{mem, SerializableResult};
|
|||||||
use crate::{with_current_shape_mut, with_state, STATE};
|
use crate::{with_current_shape_mut, with_state, STATE};
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, ToJs)]
|
#[derive(Debug, Clone, Copy, PartialEq, ToJs)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -43,15 +46,19 @@ pub extern "C" fn set_shape_bool_type(raw_bool_type: u8) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn calculate_bool(raw_bool_type: u8) -> Result<*mut u8> {
|
||||||
let bytes = mem::bytes_or_empty();
|
let bytes = mem::bytes_or_empty();
|
||||||
|
|
||||||
let entries: Vec<Uuid> = bytes
|
let entries: Vec<Uuid> = bytes
|
||||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||||
.map(|data| Uuid::try_from(data).unwrap())
|
.map(|data| {
|
||||||
.collect();
|
// FIXME: Review if this should be an critical or a recoverable error.
|
||||||
|
Uuid::try_from(data).map_err(|_| Error::RecoverableError("Invalid UUID".to_string()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<Uuid>>>()?;
|
||||||
|
|
||||||
mem::free_bytes();
|
mem::free_bytes()?;
|
||||||
|
|
||||||
let bool_type = RawBoolType::from(raw_bool_type).into();
|
let bool_type = RawBoolType::from(raw_bool_type).into();
|
||||||
let result;
|
let result;
|
||||||
@ -64,5 +71,5 @@ pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
|
|||||||
.map(RawSegmentData::from_segment)
|
.map(RawSegmentData::from_segment)
|
||||||
.collect();
|
.collect();
|
||||||
});
|
});
|
||||||
mem::write_vec(result)
|
Ok(mem::write_vec(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use macros::ToJs;
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||||
|
|
||||||
@ -9,6 +9,8 @@ use crate::shapes::{
|
|||||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||||
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||||
|
|
||||||
@ -285,16 +287,22 @@ pub extern "C" fn clear_shape_text() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn set_shape_text_content() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
|
||||||
let bytes = mem::bytes();
|
let bytes = mem::bytes();
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||||
|
|
||||||
if shape.add_paragraph(raw_text_data.into()).is_err() {
|
shape.add_paragraph(raw_text_data.into()).map_err(|_| {
|
||||||
println!("Error with set_shape_text_content on {:?}", shape.id);
|
Error::RecoverableError(format!(
|
||||||
}
|
"Error with set_shape_text_content on {:?}",
|
||||||
|
shape.id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
});
|
});
|
||||||
mem::free_bytes();
|
|
||||||
|
mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use macros::{wasm_error, ToJs};
|
||||||
|
|
||||||
use crate::math::{Matrix, Point, Rect};
|
use crate::math::{Matrix, Point, Rect};
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||||
@ -5,7 +7,7 @@ use crate::state::TextSelection;
|
|||||||
use crate::utils::uuid_from_u32_quartet;
|
use crate::utils::uuid_from_u32_quartet;
|
||||||
use crate::utils::uuid_to_u32_quartet;
|
use crate::utils::uuid_to_u32_quartet;
|
||||||
use crate::{with_state, with_state_mut, STATE};
|
use crate::{with_state, with_state_mut, STATE};
|
||||||
use macros::ToJs;
|
use skia_safe::{textlayout::TextDirection, Color};
|
||||||
|
|
||||||
#[derive(PartialEq, ToJs)]
|
#[derive(PartialEq, ToJs)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@ -23,6 +25,21 @@ pub enum CursorDirection {
|
|||||||
// STATE MANAGEMENT
|
// STATE MANAGEMENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn text_editor_apply_theme(
|
||||||
|
selection_color: u32,
|
||||||
|
cursor_width: f32,
|
||||||
|
cursor_color: u32,
|
||||||
|
) {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
// NOTE: In the future could be interesting to fill al this data from
|
||||||
|
// a structure pointer.
|
||||||
|
state.text_editor_state.theme.selection_color = Color::new(selection_color);
|
||||||
|
state.text_editor_state.theme.cursor_width = cursor_width;
|
||||||
|
state.text_editor_state.theme.cursor_color = Color::new(cursor_color);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool {
|
pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
@ -42,10 +59,14 @@ pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_stop() {
|
pub extern "C" fn text_editor_stop() -> bool {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
state.text_editor_state.stop();
|
state.text_editor_state.stop();
|
||||||
});
|
true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@ -101,6 +122,34 @@ pub extern "C" fn text_editor_select_all() -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn text_editor_select_word_boundary(x: f32, y: f32) {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let point = Point::new(x, y);
|
||||||
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.select_word_boundary(text_content, &position);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_poll_event() -> u8 {
|
pub extern "C" fn text_editor_poll_event() -> u8 {
|
||||||
with_state_mut!(state, { state.text_editor_state.poll_event() as u8 })
|
with_state_mut!(state, { state.text_editor_state.poll_event() as u8 })
|
||||||
@ -126,12 +175,8 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let point = Point::new(x, y);
|
let point = Point::new(x, y);
|
||||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
let shape_matrix = shape.get_matrix();
|
|
||||||
state.text_editor_state.start_pointer_selection();
|
state.text_editor_state.start_pointer_selection();
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state.text_editor_state.set_caret_from_position(&position);
|
state.text_editor_state.set_caret_from_position(&position);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -143,7 +188,6 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
let point = Point::new(x, y);
|
let point = Point::new(x, y);
|
||||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
return;
|
return;
|
||||||
@ -151,11 +195,6 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let shape_matrix = shape.get_matrix();
|
|
||||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !state.text_editor_state.is_pointer_selection_active {
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -163,9 +202,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state
|
state
|
||||||
.text_editor_state
|
.text_editor_state
|
||||||
.extend_selection_from_position(&position);
|
.extend_selection_from_position(&position);
|
||||||
@ -179,7 +216,6 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
|||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
let point = Point::new(x, y);
|
let point = Point::new(x, y);
|
||||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
return;
|
return;
|
||||||
@ -187,20 +223,13 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
|||||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let shape_matrix = shape.get_matrix();
|
|
||||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !state.text_editor_state.is_pointer_selection_active {
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Type::Text(text_content) = &shape.shape_type else {
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state
|
state
|
||||||
.text_editor_state
|
.text_editor_state
|
||||||
.extend_selection_from_position(&position);
|
.extend_selection_from_position(&position);
|
||||||
@ -209,6 +238,29 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = Point::new(x, y);
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
|
state.text_editor_state.set_caret_from_position(&position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
@ -240,29 +292,31 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
|||||||
// TEXT OPERATIONS
|
// TEXT OPERATIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// FIXME: Review if all the return Ok(()) should be Err instead.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_insert_text() {
|
#[wasm_error]
|
||||||
|
pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
||||||
let bytes = crate::mem::bytes();
|
let bytes = crate::mem::bytes();
|
||||||
let text = match String::from_utf8(bytes) {
|
let text = match String::from_utf8(bytes) {
|
||||||
Ok(s) => s,
|
Ok(text) => text,
|
||||||
Err(_) => return,
|
Err(_) => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||||
return;
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let selection = state.text_editor_state.selection;
|
let selection = state.text_editor_state.selection;
|
||||||
@ -275,9 +329,7 @@ pub extern "C" fn text_editor_insert_text() {
|
|||||||
|
|
||||||
let cursor = state.text_editor_state.selection.focus;
|
let cursor = state.text_editor_state.selection.focus;
|
||||||
|
|
||||||
if let Some(new_offset) = insert_text_at_cursor(text_content, &cursor, &text) {
|
if let Some(new_cursor) = insert_text_with_newlines(text_content, &cursor, &text) {
|
||||||
let new_cursor =
|
|
||||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, new_offset);
|
|
||||||
state.text_editor_state.selection.set_caret(new_cursor);
|
state.text_editor_state.selection.set_caret(new_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +347,8 @@ pub extern "C" fn text_editor_insert_text() {
|
|||||||
state.render_state.mark_touched(shape_id);
|
state.render_state.mark_touched(shape_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
crate::mem::free_bytes();
|
crate::mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@ -474,7 +527,25 @@ pub extern "C" fn text_editor_move_cursor(direction: CursorDirection, extend_sel
|
|||||||
|
|
||||||
let current = state.text_editor_state.selection.focus;
|
let current = state.text_editor_state.selection.focus;
|
||||||
|
|
||||||
let new_cursor = match direction {
|
// Get the text direction of the span at the current cursor position
|
||||||
|
let span_text_direction = if current.paragraph < paragraphs.len() {
|
||||||
|
get_span_text_direction_at_offset(¶graphs[current.paragraph], current.offset)
|
||||||
|
} else {
|
||||||
|
TextDirection::LTR
|
||||||
|
};
|
||||||
|
|
||||||
|
// For horizontal navigation, swap Backward/Forward when in RTL text
|
||||||
|
let adjusted_direction = if span_text_direction == TextDirection::RTL {
|
||||||
|
match direction {
|
||||||
|
CursorDirection::Backward => CursorDirection::Forward,
|
||||||
|
CursorDirection::Forward => CursorDirection::Backward,
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
direction
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_cursor = match adjusted_direction {
|
||||||
CursorDirection::Backward => move_cursor_backward(¤t, paragraphs),
|
CursorDirection::Backward => move_cursor_backward(¤t, paragraphs),
|
||||||
CursorDirection::Forward => move_cursor_forward(¤t, paragraphs),
|
CursorDirection::Forward => move_cursor_forward(¤t, paragraphs),
|
||||||
CursorDirection::LineBefore => {
|
CursorDirection::LineBefore => {
|
||||||
@ -744,12 +815,10 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
|
|||||||
char_pos += span_len;
|
char_pos += span_len;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !para_text.is_empty() {
|
if para_idx > start.paragraph {
|
||||||
if !result.is_empty() {
|
result.push('\n');
|
||||||
result.push('\n');
|
|
||||||
}
|
|
||||||
result.push_str(¶_text);
|
|
||||||
}
|
}
|
||||||
|
result.push_str(¶_text);
|
||||||
}
|
}
|
||||||
let mut bytes = result.into_bytes();
|
let mut bytes = result.into_bytes();
|
||||||
bytes.push(0);
|
bytes.push(0);
|
||||||
@ -1046,6 +1115,59 @@ fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, u
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert text at a cursor position, splitting on newlines into multiple paragraphs.
|
||||||
|
/// Returns the final cursor position after insertion.
|
||||||
|
fn insert_text_with_newlines(
|
||||||
|
text_content: &mut TextContent,
|
||||||
|
cursor: &TextPositionWithAffinity,
|
||||||
|
text: &str,
|
||||||
|
) -> Option<TextPositionWithAffinity> {
|
||||||
|
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
|
||||||
|
let lines: Vec<&str> = normalized.split('\n').collect();
|
||||||
|
if lines.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current_cursor = *cursor;
|
||||||
|
|
||||||
|
if let Some(new_offset) = insert_text_at_cursor(text_content, ¤t_cursor, lines[0]) {
|
||||||
|
current_cursor =
|
||||||
|
TextPositionWithAffinity::new_without_affinity(current_cursor.paragraph, new_offset);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in lines.iter().skip(1) {
|
||||||
|
if !split_paragraph_at_cursor(text_content, ¤t_cursor) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_cursor =
|
||||||
|
TextPositionWithAffinity::new_without_affinity(current_cursor.paragraph + 1, 0);
|
||||||
|
if let Some(new_offset) = insert_text_at_cursor(text_content, ¤t_cursor, line) {
|
||||||
|
current_cursor = TextPositionWithAffinity::new_without_affinity(
|
||||||
|
current_cursor.paragraph,
|
||||||
|
new_offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(current_cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text direction of the span at a given offset in a paragraph.
|
||||||
|
fn get_span_text_direction_at_offset(
|
||||||
|
para: &Paragraph,
|
||||||
|
char_offset: usize,
|
||||||
|
) -> skia_safe::textlayout::TextDirection {
|
||||||
|
if let Some((span_idx, _)) = find_span_at_offset(para, char_offset) {
|
||||||
|
if let Some(span) = para.children().get(span_idx) {
|
||||||
|
return span.text_direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to paragraph's text direction
|
||||||
|
para.text_direction()
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert text at a cursor position. Returns the new character offset after insertion.
|
/// Insert text at a cursor position. Returns the new character offset after insertion.
|
||||||
fn insert_text_at_cursor(
|
fn insert_text_at_cursor(
|
||||||
text_content: &mut TextContent,
|
text_content: &mut TextContent,
|
||||||
|
|||||||
13
scripts/check-fmt
Executable file
13
scripts/check-fmt
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
cljfmt --parallel=true check \
|
||||||
|
common/src/ \
|
||||||
|
common/test/ \
|
||||||
|
frontend/src/ \
|
||||||
|
frontend/test/ \
|
||||||
|
backend/src/ \
|
||||||
|
backend/test/ \
|
||||||
|
exporter/src/ \
|
||||||
|
library/src;
|
||||||
10
scripts/lint
10
scripts/lint
@ -2,16 +2,6 @@
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
cljfmt check --parallel=true \
|
|
||||||
common/src/ \
|
|
||||||
common/test/ \
|
|
||||||
frontend/src/ \
|
|
||||||
frontend/test/ \
|
|
||||||
backend/src/ \
|
|
||||||
backend/test/ \
|
|
||||||
exporter/src/ \
|
|
||||||
library/src;
|
|
||||||
|
|
||||||
clj-kondo --parallel=true --lint common/src;
|
clj-kondo --parallel=true --lint common/src;
|
||||||
clj-kondo --parallel=true --lint frontend/src;
|
clj-kondo --parallel=true --lint frontend/src;
|
||||||
clj-kondo --parallel=true --lint backend/src;
|
clj-kondo --parallel=true --lint backend/src;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user