mirror of
https://github.com/penpot/penpot.git
synced 2026-01-20 12:20:16 -05:00
Compare commits
16 Commits
2.12.0-RC1
...
test-inner
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369979ffe6 | ||
|
|
db0cbbbc2e | ||
|
|
48304bd26f | ||
|
|
60e32bbc71 | ||
|
|
54451608dc | ||
|
|
b7727122d5 | ||
|
|
8880f07a6a | ||
|
|
aaca2c41d8 | ||
|
|
33417a4b20 | ||
|
|
2640889dc8 | ||
|
|
dd5f3396d1 | ||
|
|
dedeae8641 | ||
|
|
a7552d412a | ||
|
|
f58475a7c9 | ||
|
|
00bbb0bfb6 | ||
|
|
d93fe89c12 |
344
.github/workflows/tests.yml
vendored
344
.github/workflows/tests.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "CI: Tests"
|
||||
name: "CI"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -16,125 +16,140 @@ on:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# lint:
|
||||
# name: "Code Linter"
|
||||
# runs-on: ubuntu-24.04
|
||||
# container: penpotapp/devenv:latest
|
||||
lint:
|
||||
name: "Code Linter"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - name: Check clojure code format
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# yarn install
|
||||
# yarn run fmt:clj:check
|
||||
- name: Check clojure code format
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install
|
||||
yarn run fmt:clj:check
|
||||
|
||||
# test-common:
|
||||
# name: "Common Tests"
|
||||
# runs-on: ubuntu-24.04
|
||||
# container: penpotapp/devenv:latest
|
||||
test-common:
|
||||
name: "Common Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - name: Run tests on JVM
|
||||
# working-directory: ./common
|
||||
# run: |
|
||||
# clojure -M:dev:test
|
||||
- name: Run tests on JVM
|
||||
working-directory: ./common
|
||||
run: |
|
||||
clojure -M:dev:test
|
||||
|
||||
# - name: Run tests on NODE
|
||||
# working-directory: ./common
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# yarn install;
|
||||
# yarn run test;
|
||||
- name: Run tests on NODE
|
||||
working-directory: ./common
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
|
||||
# test-frontend:
|
||||
# name: "Frontend Tests"
|
||||
# runs-on: ubuntu-24.04
|
||||
# container: penpotapp/devenv:latest
|
||||
test-frontend:
|
||||
name: "Frontend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - name: Unit Tests
|
||||
# working-directory: ./frontend
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# yarn install;
|
||||
# yarn run test;
|
||||
- name: Unit Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run test;
|
||||
|
||||
# - name: Component Tests
|
||||
# working-directory: ./frontend
|
||||
# run: |
|
||||
# yarn run playwright install chromium --with-deps;
|
||||
# yarn run build:storybook
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run build:storybook
|
||||
|
||||
# npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
||||
# "npx http-server storybook-static --port 6006 --silent" \
|
||||
# "npx wait-on tcp:6006 && yarn test:storybook"
|
||||
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
|
||||
"npx http-server storybook-static --port 6006 --silent" \
|
||||
"npx wait-on tcp:6006 && yarn test:storybook"
|
||||
|
||||
# - name: Check SCSS Format
|
||||
# working-directory: ./frontend
|
||||
# run: |
|
||||
# yarn run lint:scss;
|
||||
- name: Check SCSS Format
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run lint:scss;
|
||||
|
||||
# test-backend:
|
||||
# name: "Backend Tests"
|
||||
# runs-on: ubuntu-24.04
|
||||
# container: penpotapp/devenv:latest
|
||||
test-backend:
|
||||
name: "Backend Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres:17
|
||||
# # Provide the password for postgres
|
||||
# env:
|
||||
# POSTGRES_USER: penpot_test
|
||||
# POSTGRES_PASSWORD: penpot_test
|
||||
# POSTGRES_DB: penpot_test
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_USER: penpot_test
|
||||
POSTGRES_PASSWORD: penpot_test
|
||||
POSTGRES_DB: penpot_test
|
||||
|
||||
# # Set health checks to wait until postgres has started
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
# redis:
|
||||
# image: valkey/valkey:9
|
||||
redis:
|
||||
image: valkey/valkey:9
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# test-library:
|
||||
# name: "Library Tests"
|
||||
# runs-on: ubuntu-24.04
|
||||
# container: penpotapp/devenv:latest
|
||||
- name: Run tests
|
||||
working-directory: ./backend
|
||||
env:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
|
||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
|
||||
PENPOT_TEST_REDIS_URI: "redis://redis/1"
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
run: |
|
||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
||||
|
||||
# - name: Run tests
|
||||
# working-directory: ./library
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# yarn install;
|
||||
# yarn run build:bundle;
|
||||
# yarn run test;
|
||||
test-library:
|
||||
name: "Library Tests"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
test-integration:
|
||||
name: "Integration Tests"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./library
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run build:bundle;
|
||||
yarn run test;
|
||||
|
||||
build-integration:
|
||||
name: "Build Integration Bundle"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
@@ -157,17 +172,144 @@ jobs:
|
||||
run: |
|
||||
./build release
|
||||
|
||||
- name: Store Bundle Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
test-integration-1:
|
||||
name: "Integration Tests 1/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run playwright install chromium --with-deps
|
||||
yarn run test:e2e -x --workers=1 --reporter=line
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard="1/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result
|
||||
name: integration-tests-result-1
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
test-integration-2:
|
||||
name: "Integration Tests 2/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "2/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-2
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
test-integration-3:
|
||||
name: "Integration Tests 3/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "3/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-3
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
test-integration-4:
|
||||
name: "Integration Tests 3/4"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install;
|
||||
yarn run playwright install chromium --with-deps;
|
||||
yarn run test:e2e -x --workers=2 --reporter=list --shard "4/4";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-4
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
@@ -71,6 +71,7 @@ example. It's still usable as before, we just removed the example.
|
||||
- Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696)
|
||||
- Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353)
|
||||
- Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313)
|
||||
- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -85,6 +86,7 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -1679,7 +1679,7 @@ Will return a value that matches this schema:
|
||||
["id" {:optional true} :string]
|
||||
["name" :string]
|
||||
["description" :string]
|
||||
["isSource" :boolean]
|
||||
["isSource" {:optional true} :boolean]
|
||||
["selectedTokenSets"
|
||||
[:map-of :string [:enum "enabled" "disabled"]]]]]]
|
||||
["$metadata" {:optional true}
|
||||
|
||||
@@ -115,6 +115,11 @@ services:
|
||||
volumes:
|
||||
- "valkey_data:/data"
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- redis
|
||||
|
||||
mailer:
|
||||
image: sj26/mailcatcher:latest
|
||||
restart: always
|
||||
@@ -123,6 +128,12 @@ services:
|
||||
ports:
|
||||
- "1080:1080"
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- mailer
|
||||
|
||||
|
||||
# https://github.com/rroemhild/docker-test-openldap
|
||||
ldap:
|
||||
image: rroemhild/test-openldap:2.1
|
||||
@@ -136,3 +147,9 @@ services:
|
||||
nofile:
|
||||
soft: 1024
|
||||
hard: 1024
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ldap
|
||||
|
||||
|
||||
@@ -314,7 +314,7 @@ If you're using the official <code class="language-bash">docker-compose.yml</cod
|
||||
|
||||
## Email configuration
|
||||
|
||||
By default, <code class="language-bash">smpt</code> flag is disabled, the email will be
|
||||
By default, <code class="language-bash">smtp</code> flag is disabled, the email will be
|
||||
printed to the console, which means that the emails will be shown in the stdout.
|
||||
|
||||
Note that if you plan to invite members to a team, it is recommended that you enable SMTP
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 3.07. Abstraction levels
|
||||
desc: "Penpot Technical Guide: organize data and logic in clear abstraction layers—ADTs, file ops, event-sourced changes, business rules, and data events."
|
||||
---
|
||||
|
||||
# Code organization in abstraction levels
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 3.06. Backend Guide
|
||||
desc: "Penpot Technical Guide: Backend basics - REPL setup, loading fixtures, database migrations, and clj-kondo linting to speed development workflows."
|
||||
---
|
||||
|
||||
# Backend guide #
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 1.2 Install with Elestio
|
||||
desc: "Step-by-step guide to deploy a self-hosted Penpot on Elestio: 3-minute setup, managed DNS/SMTP/SSL/backups, Docker Compose config, updates & support."
|
||||
---
|
||||
|
||||
# Install with Elestio
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Design Tokens
|
||||
order: 5
|
||||
desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG format, with sets, themes, aliases, equations and JSON import/export.
|
||||
---
|
||||
|
||||
<h1 id="design-tokens">Design Tokens</h1>
|
||||
|
||||
@@ -195,7 +195,9 @@
|
||||
(ptk/reify ::login-from-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
|
||||
(->> (dp/on-fetch-profile-success profile)
|
||||
(rx/map (fn [profile]
|
||||
(logged-in (with-meta profile {::ev/source "login-with-token"}))))
|
||||
;; NOTE: we need this to be asynchronous because the effect
|
||||
;; should be called before proceed with the login process
|
||||
(rx/observe-on :async)))))
|
||||
|
||||
@@ -77,22 +77,25 @@
|
||||
(rx/of (rt/nav-raw :href href)))
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn on-fetch-profile-success
|
||||
[profile]
|
||||
(if (and (contains? cf/flags :subscriptions)
|
||||
(is-authenticated? profile))
|
||||
(->> (rp/cmd! :get-subscription-usage {})
|
||||
(rx/map (fn [{:keys [editors]}]
|
||||
(update-in profile [:props :subscription] assoc :editors editors)))
|
||||
(rx/catch (fn [cause]
|
||||
(js/console.error "unexpected error on obtaining subscription usage" cause)
|
||||
(rx/of profile))))
|
||||
(rx/of profile)))
|
||||
|
||||
(defn fetch-profile
|
||||
[]
|
||||
(ptk/reify ::fetch-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/mapcat (fn [profile]
|
||||
(if (and (contains? cf/flags :subscriptions)
|
||||
(is-authenticated? profile))
|
||||
(->> (rp/cmd! :get-subscription-usage {})
|
||||
(rx/map (fn [{:keys [editors]}]
|
||||
(update-in profile [:props :subscription] assoc :editors editors)))
|
||||
(rx/catch (fn [cause]
|
||||
(js/console.error "unexpected error on obtaining subscription usage" cause)
|
||||
(rx/of profile))))
|
||||
(rx/of profile))))
|
||||
(rx/mapcat on-fetch-profile-success)
|
||||
(rx/map (partial ptk/data-event ::profile-fetched))
|
||||
(rx/catch on-fetch-profile-exception)))))
|
||||
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
(declare ^:private paste-svg-text)
|
||||
(declare ^:private paste-shapes)
|
||||
|
||||
(def ^:private default-options
|
||||
#js {:decodeTransit t/decode-str
|
||||
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
|
||||
|
||||
(defn create-paste-from-blob
|
||||
[in-viewport?]
|
||||
(fn [blob]
|
||||
@@ -290,7 +294,7 @@
|
||||
(ptk/reify ::paste-from-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (clipboard/from-navigator)
|
||||
(->> (clipboard/from-navigator default-options)
|
||||
(rx/mapcat default-paste-from-blob)
|
||||
(rx/take 1)))))
|
||||
|
||||
@@ -308,7 +312,7 @@
|
||||
;; we forbid that scenario so the default behaviour is executed
|
||||
(if is-editing?
|
||||
(rx/empty)
|
||||
(->> (clipboard/from-synthetic-clipboard-event event)
|
||||
(->> (clipboard/from-synthetic-clipboard-event event default-options)
|
||||
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
|
||||
|
||||
(defn copy-selected-svg
|
||||
@@ -478,7 +482,7 @@
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(->> (clipboard/from-navigator)
|
||||
(->> (clipboard/from-navigator default-options)
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/map decode-entry)
|
||||
(rx/take 1)
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
(defn render-thumbnail
|
||||
[file-id revn]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:width thumbnail-width}))
|
||||
(mw/ask! {:cmd :thumbnails/generate-for-file-wasm
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:width thumbnail-width})
|
||||
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
(mf/deps team-id selected files)
|
||||
(fn []
|
||||
(swap! state* assoc :status :exporting)
|
||||
(->> (fexp/export-files :files files :type type)
|
||||
(->> (fexp/export-files :files files :type selected)
|
||||
(rx/subs!
|
||||
(fn [{:keys [file-id error filename uri] :as result}]
|
||||
(if error
|
||||
|
||||
@@ -64,93 +64,132 @@
|
||||
:on-click cta-link-with-icon} cta-text-with-icon
|
||||
[:> icon* {:icon-id "open-link"
|
||||
:size "s"}]])
|
||||
(when (and cta-link cta-text (not show-button-cta)) [:button {:class (stl/css-case :cta-button true
|
||||
:bottom-link (not (and cta-link-trial cta-text-trial)))
|
||||
:on-click cta-link} cta-text])
|
||||
(when (and cta-link cta-text show-button-cta) [:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial)))
|
||||
:on-click cta-link} cta-text])
|
||||
(when (and cta-link-trial cta-text-trial) [:button {:class (stl/css :cta-button :bottom-link)
|
||||
:on-click cta-link-trial} cta-text-trial])])
|
||||
(when (and cta-link cta-text (not show-button-cta))
|
||||
[:button {:class (stl/css-case :cta-button true
|
||||
:bottom-link (not (and cta-link-trial cta-text-trial)))
|
||||
:on-click cta-link} cta-text])
|
||||
(when (and cta-link cta-text show-button-cta)
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:class (stl/css-case :bottom-button (not (and cta-link-trial cta-text-trial)))
|
||||
:on-click cta-link} cta-text])
|
||||
(when (and cta-link-trial cta-text-trial)
|
||||
[:button {:class (stl/css :cta-button :bottom-link)
|
||||
:on-click cta-link-trial} cta-text-trial])])
|
||||
|
||||
(defn schema:seats-form [min-editors]
|
||||
[:map {:title "SeatsForm"}
|
||||
[:min-members [::sm/number {:min min-editors
|
||||
:max 9999}]]])
|
||||
:max 9999}]]
|
||||
[:redirect-to-payment-details :boolean]])
|
||||
|
||||
(mf/defc subscribe-management-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :management-dialog}
|
||||
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
|
||||
|
||||
(let [unlimited-modal-step* (mf/use-state 1)
|
||||
unlimited-modal-step (deref unlimited-modal-step*)
|
||||
subscription-name (if subscribe-to-trial
|
||||
(if (= subscription-type "unlimited")
|
||||
(tr "subscription.settings.unlimited-trial")
|
||||
(tr "subscription.settings.enterprise-trial"))
|
||||
(case subscription-type
|
||||
"professional" (tr "subscription.settings.professional")
|
||||
"unlimited" (tr "subscription.settings.unlimited")
|
||||
"enterprise" (tr "subscription.settings.enterprise")))
|
||||
min-editors (if (seq editors) (count editors) 1)
|
||||
initial (mf/with-memo [min-editors]
|
||||
{:min-members min-editors})
|
||||
form (fm/use-form :schema (schema:seats-form min-editors)
|
||||
:initial initial)
|
||||
submit-in-progress* (mf/use-state false)
|
||||
subscribe-to-unlimited (mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [add-payment-details]
|
||||
(when (not @submit-in-progress*)
|
||||
(let [data (:clean-data @form)
|
||||
return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (if add-payment-details
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" (:min-members data) "&returnUrl=" return-url)
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" (:min-members data) "&returnUrl=" return-url))]
|
||||
(reset! submit-in-progress* true)
|
||||
(reset! form nil)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "unlimited"
|
||||
:quantity (:min-members data)})
|
||||
(rt/nav-raw :href href))))))
|
||||
(let [unlimited-modal-step*
|
||||
(mf/use-state 1)
|
||||
|
||||
subscribe-to-enterprise (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "enterprise"}))
|
||||
(let [return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
unlimited-modal-step
|
||||
(deref unlimited-modal-step*)
|
||||
|
||||
handle-accept-dialog (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "settings"
|
||||
:section "subscription-management-modal"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))
|
||||
(modal/hide!)))
|
||||
handle-close-dialog (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
|
||||
(modal/hide!)))
|
||||
subscription-name
|
||||
(if subscribe-to-trial
|
||||
(if (= subscription-type "unlimited")
|
||||
(tr "subscription.settings.unlimited-trial")
|
||||
(tr "subscription.settings.enterprise-trial"))
|
||||
(case subscription-type
|
||||
"professional" (tr "subscription.settings.professional")
|
||||
"unlimited" (tr "subscription.settings.unlimited")
|
||||
"enterprise" (tr "subscription.settings.enterprise")))
|
||||
|
||||
handle-unlimited-modal-step (mf/use-fn
|
||||
(mf/deps unlimited-modal-step)
|
||||
(fn []
|
||||
(if (= unlimited-modal-step 1)
|
||||
(reset! unlimited-modal-step* 2)
|
||||
(reset! unlimited-modal-step* 1))))
|
||||
min-editors
|
||||
(if (seq editors) (count editors) 1)
|
||||
|
||||
show-editors-list* (mf/use-state false)
|
||||
show-editors-list (deref show-editors-list*)
|
||||
handle-click (mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-editors-list* not)))]
|
||||
initial
|
||||
(mf/with-memo [min-editors]
|
||||
{:min-members min-editors
|
||||
:redirect-to-payment-details false})
|
||||
|
||||
form
|
||||
(fm/use-form :schema (schema:seats-form min-editors)
|
||||
:initial initial)
|
||||
|
||||
submit-in-progress
|
||||
(mf/use-ref false)
|
||||
|
||||
subscribe-to-unlimited
|
||||
(mf/use-fn
|
||||
(fn [min-members add-payment-details?]
|
||||
(when-not (mf/ref-val submit-in-progress)
|
||||
(mf/set-ref-val! submit-in-progress true)
|
||||
(let [return-url (-> (rt/get-current-href)
|
||||
(rt/encode-url))
|
||||
href (dm/str "payments/subscriptions/create?type=unlimited&show="
|
||||
add-payment-details? "&quantity="
|
||||
min-members "&returnUrl=" return-url)]
|
||||
(reset! form nil)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "unlimited"
|
||||
:quantity min-members})
|
||||
(rt/nav-raw :href href))))))
|
||||
|
||||
subscribe-to-enterprise
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "enterprise"}))
|
||||
(let [return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
|
||||
handle-accept-dialog
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "settings"
|
||||
:section "subscription-management-modal"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))
|
||||
(modal/hide!)))
|
||||
|
||||
handle-close-dialog
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
|
||||
(modal/hide!)))
|
||||
|
||||
show-editors-list*
|
||||
(mf/use-state false)
|
||||
|
||||
show-editors-list
|
||||
(deref show-editors-list*)
|
||||
|
||||
handle-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-editors-list* not)))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps current-subscription unlimited-modal-step*)
|
||||
(fn [form]
|
||||
(let [clean-data (get @form :clean-data)
|
||||
min-members (get clean-data :min-members)
|
||||
redirect? (get clean-data :redirect-to-payment-details)]
|
||||
(if (or (contains? #{"unpaid" "canceled"} (:status current-subscription))
|
||||
(= @unlimited-modal-step* 2))
|
||||
(subscribe-to-unlimited min-members redirect?)
|
||||
(swap! unlimited-modal-step* inc)))))
|
||||
|
||||
on-add-payments-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(swap! form update :data assoc :redirect-to-payment-details true)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
@@ -180,7 +219,8 @@
|
||||
[:li {:key (dm/str (:id editor)) :class (stl/css :team-name)} "- " (:name editor)])]])])
|
||||
|
||||
(when (and
|
||||
(or (and (= subscription-type "professional") (contains? #{"unlimited" "enterprise"} (:type current-subscription)))
|
||||
(or (and (= subscription-type "professional")
|
||||
(contains? #{"unlimited" "enterprise"} (:type current-subscription)))
|
||||
(and (= subscription-type "unlimited") (= (:type current-subscription) "enterprise")))
|
||||
(not (contains? #{"unpaid" "canceled"} (:status current-subscription)))
|
||||
(not subscribe-to-trial))
|
||||
@@ -189,7 +229,7 @@
|
||||
|
||||
(if (and (= subscription-type "unlimited")
|
||||
(or subscribe-to-trial (contains? #{"unpaid" "canceled"} (:status current-subscription))))
|
||||
[:& fm/form {:on-submit handle-unlimited-modal-step
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :seats-form)
|
||||
:form form}
|
||||
(when (= unlimited-modal-step 1)
|
||||
@@ -232,7 +272,9 @@
|
||||
:on-click handle-close-dialog}]
|
||||
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "labels.continue")
|
||||
{:label (if (contains? #{"unpaid" "canceled"} (:status current-subscription))
|
||||
(tr "subscription.settings.subscribe")
|
||||
(tr "labels.continue"))
|
||||
:class (stl/css :primary-button)}]]]])
|
||||
|
||||
(when (= unlimited-modal-step 2)
|
||||
@@ -245,15 +287,14 @@
|
||||
|
||||
[:input
|
||||
{:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "ubscription.settings.management-dialog.step-2-skip-button")
|
||||
:on-click #(subscribe-to-unlimited false)}]
|
||||
:type "submit"
|
||||
:value (tr "subscription.settings.management-dialog.step-2-skip-button")}]
|
||||
|
||||
[:input
|
||||
{:class (stl/css :primary-button)
|
||||
:type "button"
|
||||
:type "submit"
|
||||
:value (tr "subscription.settings.management-dialog.step-2-add-payment-button")
|
||||
:on-click #(subscribe-to-unlimited true)}]]]])]
|
||||
:on-click on-add-payments-click}]]]])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.dashboard-section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -20,10 +22,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
max-width: deprecated.$s-500;
|
||||
max-inline-size: $sz-500;
|
||||
margin-block-end: var(--sp-xxxl);
|
||||
width: deprecated.$s-580;
|
||||
margin: deprecated.$s-92 auto deprecated.$s-120 auto;
|
||||
inline-size: px2rem(580);
|
||||
margin: px2rem(92) auto px2rem(120) auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -98,8 +100,8 @@
|
||||
.plan-title-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--color-foreground-primary);
|
||||
height: var(--sp-xl);
|
||||
width: var(--sp-xl);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
border-radius: 6px;
|
||||
border: 1.75px solid var(--color-foreground-primary);
|
||||
stroke-width: 2.25px;
|
||||
@@ -140,7 +142,7 @@
|
||||
}
|
||||
|
||||
.other-subscriptions {
|
||||
margin-block-start: deprecated.$s-52;
|
||||
margin-block-start: px2rem(52);
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
@@ -155,8 +157,8 @@
|
||||
|
||||
.cta-button svg {
|
||||
@extend .button-icon;
|
||||
height: var(--sp-l);
|
||||
width: var(--sp-l);
|
||||
block-size: var(--sp-l);
|
||||
inline-size: var(--sp-l);
|
||||
stroke: var(--color-accent-primary);
|
||||
margin-inline-start: var(--sp-xs);
|
||||
}
|
||||
@@ -179,14 +181,12 @@
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: initial;
|
||||
min-width: deprecated.$s-548;
|
||||
max-block-size: initial;
|
||||
min-inline-size: px2rem(548);
|
||||
}
|
||||
|
||||
.modal-dialog.subscription-success {
|
||||
min-width: deprecated.$s-648;
|
||||
min-inline-size: px2rem(648);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -232,11 +232,11 @@
|
||||
|
||||
.modal-success-content {
|
||||
display: flex;
|
||||
gap: deprecated.$s-40;
|
||||
gap: $sz-40;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-block-start: deprecated.$s-40;
|
||||
margin-block-start: $sz-40;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -249,23 +249,28 @@
|
||||
|
||||
.primary-button {
|
||||
@extend .modal-accept-btn;
|
||||
min-block-size: $sz-32;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
min-block-size: $sz-32;
|
||||
white-space: break-spaces;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.modal-start {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: deprecated.$s-220;
|
||||
max-inline-size: $sz-224;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
inline-size: 100%;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-inline-size: 992px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -285,19 +290,19 @@
|
||||
list-style-position: inside;
|
||||
list-style-type: none;
|
||||
margin-inline-start: var(--sp-xl);
|
||||
max-height: deprecated.$s-216;
|
||||
max-block-size: px2rem(216);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
--input-icon-padding: var(--sp-s);
|
||||
width: deprecated.$s-80;
|
||||
inline-size: px2rem(80);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-error);
|
||||
margin-block-start: deprecated.$s-8;
|
||||
margin-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.editors-wrapper {
|
||||
@@ -316,7 +321,7 @@
|
||||
@include t.use-typography("body-small");
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: var(--sp-s);
|
||||
margin-block-start: deprecated.$s-40;
|
||||
margin-block-start: $sz-40;
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline: var(--sp-m);
|
||||
}
|
||||
|
||||
@@ -313,15 +313,17 @@
|
||||
(let [{:keys [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)
|
||||
max-height (max height selrect-height)
|
||||
valign (-> shape :content :vertical-align)
|
||||
|
||||
y (:y selrect)
|
||||
y (case valign
|
||||
"bottom" (- y (- height (:height selrect)))
|
||||
"center" (- y (/ (- height (:height selrect)) 2))
|
||||
y (if (> height selrect-height)
|
||||
(case valign
|
||||
"bottom" (- y (- height selrect-height))
|
||||
"center" (- y (/ (- height selrect-height) 2))
|
||||
"top" y)
|
||||
y)]
|
||||
[(assoc selrect :y y :width (:width selrect) :height (max height (:height selrect))) transform])
|
||||
[(assoc selrect :y y :width (:width selrect) :height max-height) transform])
|
||||
|
||||
(let [bounds (gst/shape->rect shape)
|
||||
x (mth/min (dm/get-prop bounds :x)
|
||||
|
||||
@@ -216,6 +216,18 @@
|
||||
on-frame-leave (actions/on-frame-leave frame-hover)
|
||||
on-frame-select (actions/on-frame-select selected read-only?)
|
||||
|
||||
;; Text Editor Event Handlers
|
||||
on-text-keydown (fn [event]
|
||||
(when (and text-editing? (.-key event))
|
||||
(.preventDefault event)
|
||||
(wasm.api/handle-text-keydown (.-key event))))
|
||||
on-text-mousedown (fn [event]
|
||||
(when text-editing?
|
||||
(let [rect (.getBoundingClientRect (.-currentTarget event))
|
||||
x (- (.-clientX event) (.-left rect))
|
||||
y (- (.-clientY event) (.-top rect))]
|
||||
(wasm.api/handle-text-mousedown x y))))
|
||||
|
||||
disable-events? (contains? layout :comments)
|
||||
show-comments? (= drawing-tool :comments)
|
||||
show-cursor-tooltip? tooltip
|
||||
@@ -231,8 +243,9 @@
|
||||
|
||||
show-pixel-grid? (and (contains? layout :show-pixel-grid)
|
||||
(>= zoom 8))
|
||||
show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
||||
;; show-text-editor? (and editing-shape (= :text (:type editing-shape)))
|
||||
|
||||
show-text-editor? false
|
||||
hover-grid? (and (some? @hover-top-frame-id)
|
||||
(ctl/grid-layout? objects @hover-top-frame-id))
|
||||
|
||||
@@ -419,7 +432,9 @@
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-up on-pointer-up}
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-key-down on-text-keydown
|
||||
:on-mouse-down on-text-mousedown}
|
||||
|
||||
[:defs
|
||||
;; This clip is so the handlers are not over the rulers
|
||||
|
||||
@@ -128,11 +128,12 @@
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)}))
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
|
||||
(defn- ensure-text-content
|
||||
@@ -198,70 +199,71 @@
|
||||
(defn set-shape-children
|
||||
[children]
|
||||
(perf/begin-measure "set-shape-children")
|
||||
(case (count children)
|
||||
0
|
||||
(h/call wasm/internal-module "_set_children_0")
|
||||
(let [children (into [] (filter uuid?) children)]
|
||||
(case (count children)
|
||||
0
|
||||
(h/call wasm/internal-module "_set_children_0")
|
||||
|
||||
1
|
||||
(let [[c1] children
|
||||
c1 (uuid/get-u32 c1)]
|
||||
(h/call wasm/internal-module "_set_children_1"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
|
||||
1
|
||||
(let [[c1] children
|
||||
c1 (uuid/get-u32 c1)]
|
||||
(h/call wasm/internal-module "_set_children_1"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
|
||||
|
||||
2
|
||||
(let [[c1 c2] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)]
|
||||
(h/call wasm/internal-module "_set_children_2"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
|
||||
2
|
||||
(let [[c1 c2] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)]
|
||||
(h/call wasm/internal-module "_set_children_2"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
|
||||
|
||||
3
|
||||
(let [[c1 c2 c3] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)]
|
||||
(h/call wasm/internal-module "_set_children_3"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
|
||||
3
|
||||
(let [[c1 c2 c3] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)]
|
||||
(h/call wasm/internal-module "_set_children_3"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
|
||||
|
||||
4
|
||||
(let [[c1 c2 c3 c4] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)]
|
||||
(h/call wasm/internal-module "_set_children_4"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
|
||||
4
|
||||
(let [[c1 c2 c3 c4] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)]
|
||||
(h/call wasm/internal-module "_set_children_4"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
|
||||
|
||||
5
|
||||
(let [[c1 c2 c3 c4 c5] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)
|
||||
c5 (uuid/get-u32 c5)]
|
||||
(h/call wasm/internal-module "_set_children_5"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
|
||||
(aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
|
||||
5
|
||||
(let [[c1 c2 c3 c4 c5] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)
|
||||
c5 (uuid/get-u32 c5)]
|
||||
(h/call wasm/internal-module "_set_children_5"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
|
||||
(aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
|
||||
|
||||
;; Dynamic call for children > 5
|
||||
(let [heap (mem/get-heap-u32)
|
||||
size (mem/get-alloc-size children UUID-U8-SIZE)
|
||||
offset (mem/alloc->offset-32 size)]
|
||||
(reduce
|
||||
(fn [offset id]
|
||||
(mem.h32/write-uuid offset heap id))
|
||||
offset
|
||||
children)
|
||||
(h/call wasm/internal-module "_set_children")))
|
||||
;; Dynamic call for children > 5
|
||||
(let [heap (mem/get-heap-u32)
|
||||
size (mem/get-alloc-size children UUID-U8-SIZE)
|
||||
offset (mem/alloc->offset-32 size)]
|
||||
(reduce
|
||||
(fn [offset id]
|
||||
(mem.h32/write-uuid offset heap id))
|
||||
offset
|
||||
children)
|
||||
(h/call wasm/internal-module "_set_children"))))
|
||||
(perf/end-measure "set-shape-children")
|
||||
nil)
|
||||
|
||||
@@ -468,14 +470,14 @@
|
||||
[attrs]
|
||||
(let [style (:style attrs)
|
||||
;; Filter to only supported attributes
|
||||
allowed-keys #{:fill :fillRule :strokeLinecap :strokeLinejoin}
|
||||
allowed-keys #{:fill :fillRule :fill-rule :strokeLinecap :stroke-linecap :strokeLinejoin :stroke-linejoin}
|
||||
attrs (-> attrs
|
||||
(dissoc :style)
|
||||
(merge style)
|
||||
(select-keys allowed-keys))
|
||||
fill-rule (-> attrs :fillRule sr/translate-fill-rule)
|
||||
stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap)
|
||||
stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin)
|
||||
fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
|
||||
stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
|
||||
stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
|
||||
fill-none (= "none" (-> attrs :fill))]
|
||||
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
|
||||
|
||||
@@ -739,7 +741,7 @@
|
||||
|
||||
(d/nilv align-self 0)
|
||||
is-absolute
|
||||
(d/nilv z-index))))
|
||||
(d/nilv z-index 0))))
|
||||
|
||||
(defn clear-layout
|
||||
[]
|
||||
@@ -1031,8 +1033,9 @@
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full render-callback
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(when render-callback (render-callback))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
@@ -1209,6 +1212,7 @@
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
|
||||
(let [gl (unchecked-get wasm/internal-module "GL")
|
||||
flags (debug-flags)
|
||||
context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2")
|
||||
@@ -1255,6 +1259,19 @@
|
||||
(h/call wasm/internal-module "_hide_grid")
|
||||
(request-render "clear-grid"))
|
||||
|
||||
;; Text Editor Functions
|
||||
(defn handle-text-keydown
|
||||
[key]
|
||||
(when (and wasm/internal-module key)
|
||||
(h/call wasm/internal-module "_handle_keydown" key)
|
||||
(request-render "text-editor-keydown")))
|
||||
|
||||
(defn handle-text-mousedown
|
||||
[x y]
|
||||
(when (and wasm/internal-module x y)
|
||||
(h/call wasm/internal-module "_handle_mousedown" x y)
|
||||
(request-render "text-editor-mousedown")))
|
||||
|
||||
(defn get-grid-coords
|
||||
[position]
|
||||
(let [offset (h/call wasm/internal-module
|
||||
|
||||
@@ -83,12 +83,13 @@
|
||||
|
||||
(defn update-text-layout
|
||||
[id]
|
||||
(let [shape-id-buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
||||
(aget shape-id-buffer 0)
|
||||
(aget shape-id-buffer 1)
|
||||
(aget shape-id-buffer 2)
|
||||
(aget shape-id-buffer 3))))
|
||||
(when wasm/context-initialized?
|
||||
(let [shape-id-buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
||||
(aget shape-id-buffer 0)
|
||||
(aget shape-id-buffer 1)
|
||||
(aget shape-id-buffer 2)
|
||||
(aget shape-id-buffer 3)))))
|
||||
|
||||
;; IMPORTANT: Only TTF fonts can be stored.
|
||||
(defn- store-font-buffer
|
||||
|
||||
@@ -18,47 +18,56 @@
|
||||
"image/svg+xml"])
|
||||
|
||||
(def ^:private default-options
|
||||
#js {:decodeTransit t/decode-str})
|
||||
#js {:decodeTransit t/decode-str
|
||||
:allowHTMLPaste false})
|
||||
|
||||
(defn- from-data-transfer
|
||||
"Get clipboard stream from DataTransfer instance"
|
||||
[data-transfer]
|
||||
(->> (rx/from (impl/fromDataTransfer data-transfer default-options))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
([data-transfer]
|
||||
(from-data-transfer data-transfer default-options))
|
||||
([data-transfer options]
|
||||
(->> (rx/from (impl/fromDataTransfer data-transfer options))
|
||||
(rx/mapcat #(rx/from %)))))
|
||||
|
||||
(defn from-navigator
|
||||
[]
|
||||
(->> (rx/from (impl/fromNavigator default-options))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
([]
|
||||
(from-navigator default-options))
|
||||
([options]
|
||||
(->> (rx/from (impl/fromNavigator options))
|
||||
(rx/mapcat #(rx/from %)))))
|
||||
|
||||
(defn from-clipboard-event
|
||||
"Get clipboard stream from clipboard event"
|
||||
[event]
|
||||
(let [cdata (.-clipboardData ^js event)]
|
||||
(from-data-transfer cdata)))
|
||||
([event]
|
||||
(from-clipboard-event event default-options))
|
||||
([event options]
|
||||
(let [cdata (.-clipboardData ^js event)]
|
||||
(from-data-transfer cdata options))))
|
||||
|
||||
(defn from-synthetic-clipboard-event
|
||||
"Get clipboard stream from syntetic clipboard event"
|
||||
[event]
|
||||
(let [target
|
||||
(dom/get-target event)
|
||||
([event options]
|
||||
(let [target
|
||||
(dom/get-target event)
|
||||
|
||||
content-editable?
|
||||
(dom/is-content-editable? target)
|
||||
content-editable?
|
||||
(dom/is-content-editable? target)
|
||||
|
||||
is-input?
|
||||
(= (dom/get-tag-name target) "INPUT")]
|
||||
is-input?
|
||||
(= (dom/get-tag-name target) "INPUT")]
|
||||
|
||||
;; ignore when pasting into an editable control
|
||||
(when-not (or content-editable? is-input?)
|
||||
(-> event
|
||||
(dom/event->browser-event)
|
||||
(from-clipboard-event)))))
|
||||
(when-not (or content-editable? is-input?)
|
||||
(-> event
|
||||
(dom/event->browser-event)
|
||||
(from-clipboard-event options))))))
|
||||
|
||||
(defn from-drop-event
|
||||
"Get clipboard stream from drop event"
|
||||
[event]
|
||||
(from-data-transfer (.-dataTransfer ^js event)))
|
||||
([event]
|
||||
(from-drop-event event default-options))
|
||||
([event options]
|
||||
(from-data-transfer (.-dataTransfer ^js event) options)))
|
||||
|
||||
;; FIXME: rename to `write-text`
|
||||
(defn to-clipboard
|
||||
|
||||
@@ -27,6 +27,7 @@ const exclusiveTypes = [
|
||||
/**
|
||||
* @typedef {Object} ClipboardSettings
|
||||
* @property {Function} [decodeTransit]
|
||||
* @property {boolean} [allowHTMLPaste]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -38,9 +39,7 @@ const exclusiveTypes = [
|
||||
*/
|
||||
function parseText(text, options) {
|
||||
options = options || {};
|
||||
|
||||
const decodeTransit = options["decodeTransit"];
|
||||
|
||||
if (decodeTransit) {
|
||||
try {
|
||||
decodeTransit(text);
|
||||
@@ -57,18 +56,85 @@ function parseText(text, options) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters ClipboardItem types
|
||||
*
|
||||
* @param {ClipboardSettings} options
|
||||
* @returns {Function<AllowedTypesFilterFunction>}
|
||||
*/
|
||||
function filterAllowedTypes(options) {
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {boolean}
|
||||
*/
|
||||
return function filter(type) {
|
||||
if (
|
||||
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
|
||||
type === "text/html"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return allowedTypes.includes(type);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters DataTransferItems
|
||||
*
|
||||
* @param {ClipboardSettings} options
|
||||
* @returns {Function<AllowedTypesFilterFunction>}
|
||||
*/
|
||||
function filterAllowedItems(options) {
|
||||
/**
|
||||
* @param {DataTransferItem}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
return function filter(item) {
|
||||
if (
|
||||
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
|
||||
item.type === "text/html"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return allowedTypes.includes(item.type);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts ClipboardItem types
|
||||
*
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortTypes(a, b) {
|
||||
return allowedTypes.indexOf(a) - allowedTypes.indexOf(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts DataTransferItems
|
||||
*
|
||||
* @param {DataTransferItem} a
|
||||
* @param {DataTransferItem} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortItems(a, b) {
|
||||
return allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromNavigator(options) {
|
||||
options = options || {};
|
||||
const items = await navigator.clipboard.read();
|
||||
return Promise.all(
|
||||
Array.from(items).map(async (item) => {
|
||||
const itemAllowedTypes = Array.from(item.types)
|
||||
.filter((type) => allowedTypes.includes(type))
|
||||
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b));
|
||||
.filter(filterAllowedTypes(options))
|
||||
.sort(sortTypes);
|
||||
|
||||
if (
|
||||
itemAllowedTypes.length === 1 &&
|
||||
@@ -96,12 +162,11 @@ export async function fromNavigator(options) {
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromDataTransfer(dataTransfer, options) {
|
||||
options = options || {};
|
||||
const items = await Promise.all(
|
||||
Array.from(dataTransfer.items)
|
||||
.filter((item) => allowedTypes.includes(item.type))
|
||||
.sort(
|
||||
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
|
||||
)
|
||||
.filter(filterAllowedItems(options))
|
||||
.sort(sortItems)
|
||||
.map(async (item) => {
|
||||
if (item.kind === "file") {
|
||||
return Promise.resolve(item.getAsFile());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.color :as cc]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.render :as render]
|
||||
@@ -125,58 +126,75 @@
|
||||
|
||||
(def thumbnail-aspect-ratio (/ 2 3))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate-for-file-wasm
|
||||
[{:keys [file-id revn width] :as message} _]
|
||||
(defn render-canvas-blob
|
||||
[canvas width height background-color]
|
||||
(-> (.convertToBlob canvas)
|
||||
(p/then
|
||||
(fn [blob]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element
|
||||
svg-wrapper
|
||||
#js {:data-uri (blob->uri blob)
|
||||
:width width
|
||||
:height height
|
||||
:background background-color}))))))
|
||||
|
||||
(defn process-wasm-thumbnail
|
||||
[{:keys [id file-id revn width] :as message}]
|
||||
(->> (rx/from @init-wasm)
|
||||
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
|
||||
(rx/mapcat
|
||||
(fn [{:keys [page] :as file}]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(try
|
||||
(let [background-color (or (:background page) cc/canvas)
|
||||
height (* width thumbnail-aspect-ratio)
|
||||
canvas (js/OffscreenCanvas. width height)
|
||||
init? (wasm.api/init-canvas-context canvas)]
|
||||
(if init?
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
vbox (if frame
|
||||
(-> (gsb/get-object-bounds objects frame)
|
||||
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
|
||||
(render/calculate-dimensions objects thumbnail-aspect-ratio))
|
||||
zoom (/ width (:width vbox))]
|
||||
(let [background-color (or (:background page) cc/canvas)
|
||||
height (* width thumbnail-aspect-ratio)
|
||||
canvas (js/OffscreenCanvas. width height)
|
||||
init? (wasm.api/init-canvas-context canvas)]
|
||||
(if init?
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
vbox (if frame
|
||||
(-> (gsb/get-object-bounds objects frame)
|
||||
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
|
||||
(render/calculate-dimensions objects thumbnail-aspect-ratio))
|
||||
zoom (/ width (:width vbox))]
|
||||
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox background-color
|
||||
(fn []
|
||||
(if frame
|
||||
(wasm.api/render-sync-shape (:id frame))
|
||||
(wasm.api/render-sync))
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox background-color
|
||||
(fn []
|
||||
(if frame
|
||||
(wasm.api/render-sync-shape (:id frame))
|
||||
(wasm.api/render-sync))
|
||||
|
||||
(-> (.convertToBlob canvas)
|
||||
(p/then
|
||||
(fn [blob]
|
||||
(let [data
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element
|
||||
svg-wrapper
|
||||
#js {:data-uri (blob->uri blob)
|
||||
:width width
|
||||
:height height
|
||||
:background background-color}))]
|
||||
(rx/push! subs {:data data :file-id file-id :revn revn}))))
|
||||
(p/catch #(do (.error js/console %)
|
||||
(rx/error! subs %)))
|
||||
(p/finally #(rx/end! subs))))))
|
||||
(-> (render-canvas-blob canvas width height background-color)
|
||||
(p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn}))
|
||||
(p/catch #(rx/error! subs %))
|
||||
(p/finally #(rx/end! subs))))))
|
||||
|
||||
(do (rx/error! subs "Error loading webgl context")
|
||||
(rx/end! subs)))
|
||||
(rx/end! subs))
|
||||
|
||||
nil)
|
||||
nil)))))))
|
||||
|
||||
(catch :default err
|
||||
(.error js/console err)
|
||||
(rx/error! subs err)
|
||||
(rx/end! subs)))))))))
|
||||
(defonce thumbs-subject (rx/subject))
|
||||
|
||||
(defonce thumbs-stream
|
||||
(->> thumbs-subject
|
||||
(rx/mapcat process-wasm-thumbnail)
|
||||
(rx/share)))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate-for-file-wasm
|
||||
[message _]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(let [id (uuid/next)
|
||||
sid
|
||||
(->> thumbs-stream
|
||||
(rx/filter #(= id (:id %)))
|
||||
(rx/subs!
|
||||
#(do
|
||||
(rx/push! subs %)
|
||||
(rx/end! subs))))]
|
||||
(rx/push! thumbs-subject (assoc message :id id))
|
||||
|
||||
#(rx/dispose! sid)))))
|
||||
|
||||
9
render-wasm/Cargo.lock
generated
9
render-wasm/Cargo.lock
generated
@@ -432,6 +432,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"macros",
|
||||
"skia-safe",
|
||||
"text_editor",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -577,6 +578,14 @@ dependencies = [
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text_editor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"skia-safe",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
|
||||
@@ -32,6 +32,7 @@ skia-safe = { version = "0.87.0", default-features = false, features = [
|
||||
"binary-cache",
|
||||
"webp",
|
||||
] }
|
||||
text_editor = { path = "src/text_editor" }
|
||||
uuid = { version = "1.11.0", features = ["v4", "js"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -650,6 +650,38 @@ pub extern "C" fn set_modifiers() {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn handle_keydown(key_ptr: *const std::os::raw::c_char) {
|
||||
if key_ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let key = unsafe {
|
||||
match std::ffi::CStr::from_ptr(key_ptr).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return, // Invalid UTF-8, skip
|
||||
}
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.render_state.text_editor.handle_keydown(key);
|
||||
});
|
||||
render_sync();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn handle_mousedown(x: f32, y: f32) {
|
||||
// Basic sanity checks
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return;
|
||||
}
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.render_state.text_editor.handle_mousedown(x, y);
|
||||
});
|
||||
render_sync();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
|
||||
@@ -215,6 +215,7 @@ pub(crate) struct RenderState {
|
||||
pub options: RenderOptions,
|
||||
pub surfaces: Surfaces,
|
||||
pub fonts: FontStore,
|
||||
pub text_editor: ::text_editor::TextEditor,
|
||||
pub viewbox: Viewbox,
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub cached_target_snapshot: Option<skia::Image>,
|
||||
@@ -288,12 +289,14 @@ impl RenderState {
|
||||
|
||||
let viewbox = Viewbox::new(width as f32, height as f32);
|
||||
let tiles = tiles::TileHashMap::new();
|
||||
let text_editor = ::text_editor::TextEditor::new(fonts.debug_font.clone());
|
||||
|
||||
RenderState {
|
||||
gpu_state: gpu_state.clone(),
|
||||
options: RenderOptions::default(),
|
||||
surfaces,
|
||||
fonts,
|
||||
text_editor,
|
||||
viewbox,
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
cached_target_snapshot: None,
|
||||
@@ -916,6 +919,8 @@ impl RenderState {
|
||||
ui::render(self, shapes);
|
||||
debug::render_wasm_label(self);
|
||||
|
||||
self.text_editor.render(self.surfaces.canvas(SurfaceId::Target));
|
||||
|
||||
self.flush_and_submit();
|
||||
}
|
||||
}
|
||||
@@ -1641,9 +1646,9 @@ impl RenderState {
|
||||
}
|
||||
|
||||
children_ids.sort_by(|id1, id2| {
|
||||
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
|
||||
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());
|
||||
z1.cmp(&z2)
|
||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||
z2.cmp(&z1)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1791,6 +1796,8 @@ impl RenderState {
|
||||
ui::render(self, tree);
|
||||
debug::render_wasm_label(self);
|
||||
|
||||
self.text_editor.render(self.surfaces.canvas(SurfaceId::Target));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct FontStore {
|
||||
font_mgr: FontMgr,
|
||||
font_provider: textlayout::TypefaceFontProvider,
|
||||
font_collection: textlayout::FontCollection,
|
||||
debug_font: Font,
|
||||
pub debug_font: Font,
|
||||
fallback_fonts: HashSet<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ fn propagate_reflow(
|
||||
}
|
||||
_ => {
|
||||
// Other shapes don't have to be reflown
|
||||
reflow_parent = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
|
||||
use skia_safe::{self as skia, Path, Point, textlayout::FontCollection};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod shapes_pool;
|
||||
mod text_editor;
|
||||
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::render::RenderState;
|
||||
use crate::shapes::Shape;
|
||||
@@ -20,7 +18,6 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
/// must not be shared between different Web Workers.
|
||||
pub(crate) struct State<'a> {
|
||||
pub render_state: RenderState,
|
||||
pub text_editor_state: TextEditorState,
|
||||
pub current_id: Option<Uuid>,
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool<'a>,
|
||||
@@ -28,9 +25,9 @@ pub(crate) struct State<'a> {
|
||||
|
||||
impl<'a> State<'a> {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
let render_state = RenderState::new(width, height);
|
||||
State {
|
||||
render_state: RenderState::new(width, height),
|
||||
text_editor_state: TextEditorState::new(),
|
||||
render_state,
|
||||
current_id: None,
|
||||
current_browser: 0,
|
||||
shapes: ShapesPool::new(),
|
||||
@@ -49,16 +46,6 @@ impl<'a> State<'a> {
|
||||
&self.render_state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn text_editor_state_mut(&mut self) -> &mut TextEditorState {
|
||||
&mut self.text_editor_state
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn text_editor_state(&self) -> &TextEditorState {
|
||||
&self.text_editor_state
|
||||
}
|
||||
|
||||
pub fn render_from_cache(&mut self) {
|
||||
self.render_state.render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
8
render-wasm/src/text_editor/Cargo.toml
Normal file
8
render-wasm/src/text_editor/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "text_editor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
skia-safe = { version = "0.87.0", default-features = false, features = ["gl"] }
|
||||
wasm-bindgen = "0.2"
|
||||
12
render-wasm/src/text_editor/src/events.rs
Normal file
12
render-wasm/src/text_editor/src/events.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn handle_keydown(key: String) {
|
||||
// TODO: Handle keydown event
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn handle_mousedown(x: f32, y: f32) {
|
||||
// TODO: Handle mousedown event
|
||||
}
|
||||
153
render-wasm/src/text_editor/src/lib.rs
Normal file
153
render-wasm/src/text_editor/src/lib.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
pub mod events;
|
||||
|
||||
use skia_safe::{Canvas, Font, Paint, Point, Color};
|
||||
|
||||
pub struct TextEditor {
|
||||
text: Vec<String>,
|
||||
font: Font,
|
||||
cursor_pos: Point,
|
||||
}
|
||||
|
||||
impl TextEditor {
|
||||
pub fn new(font: Font) -> Self {
|
||||
TextEditor {
|
||||
text: vec!["Hello, Skia!".to_string()],
|
||||
font,
|
||||
cursor_pos: Point::new(0.0, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, canvas: &Canvas) {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(Color::BLACK);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
for (i, line) in self.text.iter().enumerate() {
|
||||
canvas.draw_str(line, (20.0, 20.0 + (i as f32 * 18.0)), &self.font, &paint);
|
||||
}
|
||||
|
||||
// Draw cursor - with bounds checking
|
||||
let mut cursor_paint = Paint::default();
|
||||
cursor_paint.set_color(Color::BLACK);
|
||||
cursor_paint.set_anti_alias(true);
|
||||
|
||||
let y_idx = self.cursor_pos.y as usize;
|
||||
let x_idx = self.cursor_pos.x as usize;
|
||||
|
||||
if y_idx < self.text.len() {
|
||||
let line = &self.text[y_idx];
|
||||
let safe_x_idx = x_idx.min(line.len());
|
||||
|
||||
let (x, _) = if safe_x_idx > 0 {
|
||||
self.font.measure_str(&line[..safe_x_idx], None)
|
||||
} else {
|
||||
(0.0, skia_safe::Rect::new_empty())
|
||||
};
|
||||
|
||||
let cursor_rect = skia_safe::Rect::from_xywh(
|
||||
20.0 + x,
|
||||
20.0 + (y_idx as f32 * 18.0) - 18.0,
|
||||
1.0,
|
||||
18.0
|
||||
);
|
||||
canvas.draw_rect(cursor_rect, &cursor_paint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_keydown(&mut self, key: &str) {
|
||||
if self.text.is_empty() {
|
||||
self.text.push(String::new());
|
||||
}
|
||||
|
||||
let y = self.cursor_pos.y as usize;
|
||||
let x = self.cursor_pos.x as usize;
|
||||
|
||||
if y >= self.text.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
match key {
|
||||
"ArrowLeft" => {
|
||||
if x > 0 {
|
||||
self.cursor_pos.x -= 1.0;
|
||||
} else if y > 0 {
|
||||
self.cursor_pos.y -= 1.0;
|
||||
self.cursor_pos.x = self.text[y - 1].len() as f32;
|
||||
}
|
||||
}
|
||||
"ArrowRight" => {
|
||||
if x < self.text[y].len() {
|
||||
self.cursor_pos.x += 1.0;
|
||||
} else if y < self.text.len() - 1 {
|
||||
self.cursor_pos.y += 1.0;
|
||||
self.cursor_pos.x = 0.0;
|
||||
}
|
||||
}
|
||||
"ArrowUp" => {
|
||||
if y > 0 {
|
||||
self.cursor_pos.y -= 1.0;
|
||||
let new_y = y - 1;
|
||||
self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32);
|
||||
}
|
||||
}
|
||||
"ArrowDown" => {
|
||||
if y < self.text.len() - 1 {
|
||||
self.cursor_pos.y += 1.0;
|
||||
let new_y = y + 1;
|
||||
self.cursor_pos.x = self.cursor_pos.x.min(self.text[new_y].len() as f32);
|
||||
}
|
||||
}
|
||||
"Backspace" => {
|
||||
if x > 0 {
|
||||
self.text[y].remove(x - 1);
|
||||
self.cursor_pos.x -= 1.0;
|
||||
} else if y > 0 {
|
||||
let line = self.text.remove(y);
|
||||
self.cursor_pos.y -= 1.0;
|
||||
self.cursor_pos.x = self.text[y - 1].len() as f32;
|
||||
self.text[y - 1].push_str(&line);
|
||||
}
|
||||
}
|
||||
"Enter" => {
|
||||
let line = self.text[y].split_off(x);
|
||||
self.text.insert(y + 1, line);
|
||||
self.cursor_pos.y += 1.0;
|
||||
self.cursor_pos.x = 0.0;
|
||||
}
|
||||
_ => {
|
||||
if key.len() == 1 {
|
||||
if let Some(ch) = key.chars().next() {
|
||||
self.text[y].insert(x, ch);
|
||||
self.cursor_pos.x += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mousedown(&mut self, x: f32, y: f32) {
|
||||
println!("@@@ Mouse down at: ({}, {})", x, y);
|
||||
if self.text.is_empty() {
|
||||
self.text.push(String::new());
|
||||
}
|
||||
|
||||
let line_height = 18.0;
|
||||
let y_pos = ((y - 20.0) / line_height).floor().max(0.0) as usize;
|
||||
let y_pos = y_pos.min(self.text.len() - 1);
|
||||
self.cursor_pos.y = y_pos as f32;
|
||||
|
||||
let line = &self.text[y_pos];
|
||||
let mut closest_pos = 0;
|
||||
let mut min_dist = f32::MAX;
|
||||
|
||||
for i in 0..=line.len() {
|
||||
let (width, _) = self.font.measure_str(&line[..i], None);
|
||||
let dist = (x - (20.0 + width)).abs();
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
closest_pos = i;
|
||||
}
|
||||
}
|
||||
self.cursor_pos.x = closest_pos as f32;
|
||||
}
|
||||
}
|
||||
@@ -291,9 +291,10 @@ pub extern "C" fn set_shape_text_content() {
|
||||
let bytes = mem::bytes();
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||
shape
|
||||
.add_paragraph(raw_text_data.into())
|
||||
.expect("Failed to add paragraph");
|
||||
|
||||
if let Err(_) = shape.add_paragraph(raw_text_data.into()) {
|
||||
println!("Error with set_shape_text_content on {:?}", shape.id);
|
||||
}
|
||||
});
|
||||
mem::free_bytes();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user