Compare commits

...

52 Commits

Author SHA1 Message Date
Andrey Antukh
7bc29c22ed Merge remote-tracking branch 'origin/develop' into staging 2025-07-21 21:07:24 +02:00
Andrey Antukh
b71ec4bfe0 Simplify docker build script (#6924) 2025-07-21 20:45:03 +02:00
Andrey Antukh
827bbf6a7f Merge pull request #6926 from penpot/juanfran-close-libraries-modal-on-esc
🐛 Fix ESC key not closing Add/Manage Libraries modal
2025-07-21 15:48:40 +02:00
Juanfran
2db0cc0cbf 🐛 Fix ESC key not closing Add/Manage Libraries modal 2025-07-21 15:23:54 +02:00
Andrey Antukh
42ef01b339 Merge pull request #6871 from penpot/niwinz-develop-login-enhancements
 Allow login dialog on settings
2025-07-21 15:19:06 +02:00
Aitor Moreno
fdaef2be69 Merge pull request #6891 from penpot/elenatorro-test-style-decoration-blending
🔧 Add text decoration styles
2025-07-21 15:18:18 +02:00
Pablo Alba
ae3213f5d4 🐛 Fix text override corner case 2025-07-21 12:40:03 +02:00
Andrey Antukh
6dfd05fdd1 Merge remote-tracking branch 'origin/staging' into develop 2025-07-21 12:05:24 +02:00
Andrey Antukh
51107c3fc9 🐛 Fix incorrect event name on event constructor 2025-07-21 11:58:48 +02:00
Andrey Antukh
b6863efb3a Merge pull request #6874 from penpot/xaviju-11355-tokens-import-details-layout
 Improve legibility on import token notification details
2025-07-21 11:54:08 +02:00
Andrey Antukh
799bceb8b7 🐛 Check if profile is logged-in on subscriptions internal redirects 2025-07-21 11:40:31 +02:00
Andrey Antukh
9e573128c1 🐛 Fix incorrect event name on event constructor 2025-07-21 11:40:31 +02:00
Andrey Antukh
1f05511add Allow login dialog on settings 2025-07-21 11:40:30 +02:00
Elena Torro
eeee52a738 🐛 Ensure line height is properly handled on line breaks 2025-07-21 11:37:56 +02:00
Xavier Julian
7f53860296 📎 Add warning on feature flag temporary fix for font-size tokens 2025-07-21 11:23:27 +02:00
Andrey Antukh
16d0077393 Merge pull request #6920 from penpot/mdbenito-feature/wheel-scrolling-for-templates
 Enable wheel scrolling over templates-section in the dashboard
2025-07-21 11:22:47 +02:00
Andrey Antukh
622fed2f0d 💄 Add minor formating enhancements to dashboard templates ui code 2025-07-21 10:39:50 +02:00
Andrey Antukh
d22ade3289 Remove duplicated code 2025-07-21 10:38:18 +02:00
Miguel de Benito Delgado
7febf330ac Enable wheel scrolling over templates-section in the dashboard 2025-07-21 10:34:50 +02:00
Andrey Antukh
75a50ac1ac Merge pull request #6912 from penpot/andy-highlight-font-selector
 Highlight first font in font selector search, apply on Enter/click
2025-07-21 10:33:53 +02:00
Andres Gonzalez
e62567d09e Highlight first font in font selector search, apply on Enter/click
[Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579)

 Highlight first font in font selector search, apply on Enter/click
2025-07-21 10:13:36 +02:00
Andrey Antukh
8d80eebeb1 Merge pull request #6906 from penpot/andy-enhance-text-auto-resize
 Switch auto-width to auto-height on horizontal resize on text shapes
2025-07-21 10:11:35 +02:00
Andres Gonzalez
ee9a42238d Switch auto-width to auto-height on horizontal resize on text shapes 2025-07-21 09:56:45 +02:00
Andrey Antukh
758c76d661 Merge pull request #6905 from penpot/andy-enhance-text-resize-behavior
 Allow double-click on text bounding box to set auto-width/auto-height
2025-07-21 09:55:18 +02:00
Andrey Antukh
1dec46cbfa Merge pull request #6903 from penpot/superalex-fix-page-duplication
🐛 Fix error on validating file referential integrity when duplicating a page
2025-07-21 09:46:12 +02:00
Andrey Antukh
ae25d704c1 📎 Add missing use-fn hook 2025-07-21 09:32:44 +02:00
Andres Gonzalez
e05f8c0329 Improve text layer resize behavior
Text layers now only switch to fixed grow-type on vertical resize, not on horizontal resize, for a more intuitive UX. See #4602.
2025-07-21 09:27:42 +02:00
Alejandro Alonso
ce62e11626 🐛 Fix error on validating file referential integrity when duplicating a page 2025-07-21 09:26:23 +02:00
Andrey Antukh
9f04c2fc1d Merge pull request #6901 from penpot/superalex-hide-bb-when-editing-effects
 Hide bounding box while editing visual effects
2025-07-21 09:23:18 +02:00
Andrey Antukh
05a405a82d Merge pull request #6893 from penpot/xaviju-11144-copy-color-attr
 Keep color data when copying from info tab into CSS
2025-07-21 09:22:57 +02:00
Andrey Antukh
3c8c21c378 Merge pull request #6899 from abedef/patch-1
📚 Fix broken link in self-hosting docs
2025-07-21 09:19:39 +02:00
Xavier Julian
2dbeb884a5 Keep color data when copying from info tab into CSS 2025-07-21 09:07:20 +02:00
Andrey Antukh
931d72b41f Merge pull request #6887 from dfelinto/fix-trackpad-swipe
🐛 Fix touchpad swipe back/forward #4246
2025-07-21 08:58:32 +02:00
Alejandro Alonso
2e3cdd872c Revert " Highlight first found font in font list when searching [Taiga #3204]"
This reverts commit 55a13c3139.
2025-07-17 13:01:24 +02:00
Andres Gonzalez
55a13c3139 Highlight first found font in font list when searching [Taiga #3204]
This enhancement highlights the first found font in the font list when searching, and allows pressing Enter to select it, for a more intuitive font selection experience.

See [Taiga #3204](https://tree.taiga.io/project/penpot/issue/3204).
2025-07-17 12:09:50 +02:00
Andrey Antukh
f63d1c87e3 Merge pull request #6904 from penpot/andy-fix-email-change-message
 Update email change confirmation message for clarity
2025-07-17 11:31:21 +02:00
Alejandro Alonso
abbfd44534 Hide bounding box while editing visual effects 2025-07-17 09:33:10 +02:00
Andres Gonzalez
f772724f9a Update email change confirmation message for clarity 2025-07-16 10:22:53 +02:00
Andrey Antukh
f3abd0f190 Merge pull request #6902 from penpot/andy-clarify-invite-member-message
 Clarify invite member message for existing team members
2025-07-15 15:48:45 +02:00
Andres Gonzalez
5d4042c861 Clarify invite member message for existing team members
Update the English message shown when inviting team members whose emails are already part of the team, as suggested in issue #6785.
2025-07-15 14:05:20 +02:00
Dalai Felinto
1fbcec98fb 🐛 Fix touchpad swipe back/forward #4246
This prevents the browser to take over the trackbad swipe gesture both
for the dashboard and the workspace.

At an early attempt I did get the code to work only for the workspace,
but it is too unreliable and I could every now and then get it to misbehave.

I believe it is better to be safe and always prevent the browser from
going back/forth, regardless of workspace/dashboard.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-07-15 00:24:20 +02:00
Alejandro Alonso
abef9f3cf7 Merge pull request #6889 from penpot/niwinz-staging-bugfix
🐛 Fix unexpected exception on processing old texts
2025-07-14 07:08:31 +02:00
Abed Fayyad
6f1958f9f2 📚 Fix broken link in self-hosting docs
Replaced broken Markdown link to the unofficial self-hosting section.

Signed-off-by: Abed Fayyad <yo@abedef.ca>
2025-07-13 09:16:13 -04:00
Andrey Antukh
6b2ce86d5f Merge pull request #6896 from penpot/juanfran-issue-show-main-component-focus
🐛 Fix initialize-page namespace when showing main component
2025-07-11 18:31:37 +02:00
Pablo Alba
0cfd70da2e 🐛 Fix corner cases on variants text overrides 2025-07-11 15:28:55 +02:00
Xavier Julian
4167faf39d 📎 Add blend-mode in code editor feature to CHANGELOG 2025-07-11 15:14:29 +02:00
Pablo Alba
90e6e8c5eb 🐛 Fix double undo on text partial overrides 2025-07-11 15:05:30 +02:00
Elena Torro
b40b1fa2e4 🔧 Refactor ParagraphBuilder and fix auto height 2025-07-11 13:29:22 +02:00
Juanfran
bb1ec109d8 🐛 Fix initialize-page namespace when showing main component 2025-07-11 13:09:20 +02:00
Elena Torro
4c21468850 🔧 Add text decoration styles 2025-07-10 14:26:41 +02:00
Andrey Antukh
40c300fa1a 🐛 Fix unexpected exception on processing old texts 2025-07-10 09:22:00 +02:00
Xavier Julian
77a47e4b2b Improve legibility on import token notification details 2025-07-08 15:09:50 +02:00
54 changed files with 5189 additions and 488 deletions

View File

@@ -8,23 +8,46 @@
### :heart: Community contributions (Thank you!)
- Clarify message when inviting existing team members to make it more user-friendly and clear which invitations will be sent. [Taiga #11441](https://tree.taiga.io/project/penpot/issue/11441) by [@iprithvitharun](https://github.com/iprithvitharun)
- Update email change confirmation message for clarity and correct grammar. [GitHub #6786](https://github.com/penpot/penpot/issues/6786) by [@iprithvitharun](https://github.com/iprithvitharun)
### :sparkles: New features & Enhancements
- Add visual indicator for new comments in the workspace [Taiga #11328](https://tree.taiga.io/project/penpot/issue/11328)
- On components overrides, separate the content of the text from the rest of properties [Taiga #7434](https://tree.taiga.io/project/penpot/us/7434)
- Improve dashboard's sidebar [Taiga #10700](https://tree.taiga.io/project/penpot/us/10700)
- Change "Save color" button to primary button [Taiga #9410](https://tree.taiga.io/project/penpot/issue/9410)
- Support for exif rotated images [GitHub #6767](https://github.com/penpot/penpot/issues/6767)
- Display Blend Mode and Layer Opacity properties in the Inspect tab [Taiga #11283](https://tree.taiga.io/project/penpot/issue/11283)
- Provide CSS `mix-blend-mode` property in code editor when present on shape [Taiga #11282](https://tree.taiga.io/project/penpot/issue/11282)
- Add the option to import tokens in a .zip file. [Taiga #11378](https://tree.taiga.io/project/penpot/us/11378)
- New typography token type - font size token [Taiga #10938](https://tree.taiga.io/project/penpot/us/10938)
- Hide bounding box while editing visual effects [Taiga #11576](https://tree.taiga.io/project/penpot/issue/11576)
- Improved text layer resizing: Allow double-click on text bounding box to set auto-width/auto-height [Taiga #11577](https://tree.taiga.io/project/penpot/issue/11577)
- Improve text layer auto-resize: auto-width switches to auto-height on horizontal resize, and only switches to fixed on vertical resize [Taiga #11578](https://tree.taiga.io/project/penpot/issue/11578)
- Highlight first font in font selector search. Apply only on Enter or click. [Taiga #11579](https://tree.taiga.io/project/penpot/issue/11579)
- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871)
### :bug: Bugs fixed
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)
- Fix text-decoration line-through that displays a wrong property value [Taiga #11145](https://tree.taiga.io/project/penpot/issue/11145)
- Fix display error message on register form [Taiga #11444](https://tree.taiga.io/project/penpot/issue/11444)
- Fix toggle focus mode did not restore viewport and selection upon exit [GitHub #6280](https://github.com/penpot/penpot/issues/6820)
- Fix problem when creating a layout from an existing layout [Taiga #11554](https://tree.taiga.io/project/penpot/issue/11554)
- Fix title button from Title Case to Capitalize [Taiga #11476](https://tree.taiga.io/project/penpot/issue/11476)
- Fix touchpad swipe leading to navigating back/forth [GitHub #4246](https://github.com/penpot/penpot/issues/4246)
- Keep color data when copying from info tab into CSS [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Update HSL values to modern syntax as defined in W3C CSS Color Module Level 4 [Taiga #11144](https://tree.taiga.io/project/penpot/issue/11144)
- Fix main component receives focus and is selected when using 'Show Main Component' [Taiga #11402](https://tree.taiga.io/project/penpot/issue/11402)
- Fix duplicating pages with mainInstance shapes nested inside groups [Taiga #10774](https://tree.taiga.io/project/penpot/issue/10774)
- Fix ESC key not closing Add/Manage Libraries modal [Taiga #11523](https://tree.taiga.io/project/penpot/issue/11523)
## 2.8.1 (Unreleased)
### :bug: Bugs fixed
- Fix unexpected exception on processing old texts [Github #6889](https://github.com/penpot/penpot/pull/6889)
## 2.8.0
@@ -46,6 +69,7 @@ in future versions. Therefore, **migration from Redis to ValKey is recommended f
on-premises instances** that want to keep up to date.
### :heart: Community contributions (Thank you!)
- Add Serbian language [GitHub #5002](https://github.com/penpot/penpot/issues/5002) by [crnobog69](https://github.com/crnobog69)
### :sparkles: New features & Enhancements
@@ -101,7 +125,6 @@ on-premises instances** that want to keep up to date.
- Fix copy in error message [GitHub #6615](https://github.com/penpot/penpot/pull/6615)
- Fix url on invitation link [Taiga #11284](https://tree.taiga.io/project/penpot/issue/11284)
## 2.7.1
### :bug: Bugs fixed
@@ -109,7 +132,6 @@ on-premises instances** that want to keep up to date.
- Fix incorrect handling of strokes with images on importing files
- Fix tokens disappearing after manual additions [Taiga #11063](https://tree.taiga.io/project/penpot/issue/11063)
## 2.7.0
### :rocket: Epics and highlights
@@ -241,7 +263,6 @@ on-premises instances** that want to keep up to date.
- Add character limitation to asset inputs [Taiga #10669](https://tree.taiga.io/project/penpot/issue/10669)
- Fix Storybook link 'list of all available icons' wrong path [Taiga #10705](https://tree.taiga.io/project/penpot/issue/10705)
## 2.5.4
### :heart: Community contributions (Thank you!)
@@ -286,7 +307,7 @@ on-premises instances** that want to keep up to date.
### :boom: Breaking changes & Deprecations
Although this is not a breaking change, we believe its important to highlight it in this
Although this is not a breaking change, we believe it's important to highlight it in this
section:
This release includes a fix for an internal bug in Penpot that caused incorrect handling
@@ -294,9 +315,9 @@ of media assets (e.g., fill images). The issue has been resolved since version 2
no new incorrect references will be generated. However, existing files may still contain
incorrect references.
To address this, weve provided a script to correct these references in existing files.
To address this, we've provided a script to correct these references in existing files.
While having incorrect references generally doesnt result in visible issues, there are
While having incorrect references generally doesn't result in visible issues, there are
rare cases where it can cause problems. For example, if a component library (containing
images) is deleted, and that library is being used in other files, running the FileGC task
(responsible for freeing up space and performing logical deletions) could leave those
@@ -371,7 +392,6 @@ is a number of cores)
- Fix missing methods reference on API Docs
- Fix memory usage issue on file-gc asynchronous task (related to snapshots feature)
## 2.4.1
### :bug: Bugs fixed
@@ -379,7 +399,6 @@ is a number of cores)
- Fix error when importing files with touched components [Taiga #9625](https://tree.taiga.io/project/penpot/issue/9625)
- Fix problem when changing color libraries [Plugins #184](https://github.com/penpot/penpot-plugins/issues/184)
## 2.4.0
### :rocket: Epics and highlights
@@ -433,7 +452,6 @@ is a number of cores)
- Add initial documentation for Kubernetes
## 2.3.1
### :bug: Bugs fixed
@@ -441,7 +459,6 @@ is a number of cores)
- Fix unexpected issue on interaction between plugins sandbox and
internal impl of promise
## 2.3.0
### :rocket: Epics and highlights
@@ -467,7 +484,6 @@ is a number of cores)
You can enable it with the `enable-feature-text-editor-v2` configuration flag.
### :bug: Bugs fixed
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
@@ -507,8 +523,8 @@ is a number of cores)
### :boom: Breaking changes & Deprecations
- Removed "merge assets" option when exporting ".svg + .json" files. After the components changes the option wasn't
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
working properly and we're planning to change the format soon. We think it's better to deprecate the option for the
time being.
### :heart: Community contributions (Thank you!)
@@ -524,7 +540,7 @@ time being.
freeing up space in the database. It can be enabled with the
`enable-enable-tiered-file-data-storage` flag.
*(On-Premise feature, EXPERIMENTAL).*
_(On-Premise feature, EXPERIMENTAL)._
- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372)
@@ -567,7 +583,7 @@ time being.
- **Design System**
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)).
- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329)
@@ -622,11 +638,11 @@ time being.
### :sparkles: New features
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392)
### :bug: Bugs fixed
- Fix the search label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix the "search" label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402)
- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348)
- Fix several issues on the OIDC.
- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398)
@@ -706,22 +722,21 @@ time being.
- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458)
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
## 2.0.1
### :bug: Bugs fixed
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
## 2.0.0 - I Just Can't Get Enough
### :rocket: Epics and highlights
- Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915)
- UI redesign [Taiga #4958](https://tree.taiga.io/project/penpot/epic/4958)
- New components System [Taiga #2662](https://tree.taiga.io/project/penpot/epic/2662)
- Swap components [Taiga #1331](https://tree.taiga.io/project/penpot/us/1331)
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
- Images as fill [Taiga #2983](https://tree.taiga.io/project/penpot/us/2983)
- HTML code generation [Taiga #5277](https://tree.taiga.io/project/penpot/us/5277)
- Light and dark themes [Taiga #2287](https://tree.taiga.io/project/penpot/us/2287)
@@ -730,9 +745,9 @@ time being.
- New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers.
### :heart: Community contributions (Thank you!)
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
- New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534)
- Hide bounding-box when editing shape (by @VasilevsVV) [#3930](https://github.com/penpot/penpot/pull/3930)
- CTRL + "+" to zoom into canvas instead of browser (by @audriu) [#3848](https://github.com/penpot/penpot/pull/3848)
- Add dev deps.edn in the project root (by @PEZ) [#3794](https://github.com/penpot/penpot/pull/3794)
@@ -741,6 +756,7 @@ time being.
- Typo (by StephanEggermont) [#157](https://github.com/penpot/penpot-docs/pull/157)
### :sparkles: New features
- Send comments with Ctrl+Enter / Cmd + Enter [Taiga #6085](https://tree.taiga.io/project/penpot/issue/6085)
- Select through stroke only rectangle [Taiga #5484](https://tree.taiga.io/project/penpot/issue/5484)
- Stroke default position [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847)
@@ -808,6 +824,7 @@ time being.
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
### :bug: Bugs fixed
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
@@ -816,7 +833,7 @@ time being.
- Selecting from Color Palette does not work for board when there is no existing fill [Taiga #6464](https://tree.taiga.io/project/penpot/issue/6464)
- Color thumbnails are consistently rounded in the inspect code mode [Taiga #5886](https://tree.taiga.io/project/penpot/issue/5886)
- Adding vector path points before first point of existing open path not working [Taiga #6593](https://tree.taiga.io/project/penpot/issue/6593)
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
- Some image formats include the extension when importing [Taiga #5485](https://tree.taiga.io/project/penpot/issue/5485)
- Gradient color tool doesn't work properly with flipped items [Taiga #6485](https://tree.taiga.io/project/penpot/issue/6485)
- [TEXT] Align options are not shown when several text are selected [Taiga #5948](https://tree.taiga.io/project/penpot/issue/5948)
- [VIEW MODE] Comments not working properly on multiple pages [Taiga #6281](https://tree.taiga.io/project/penpot/issue/6281)
@@ -860,7 +877,7 @@ time being.
### :sparkles: New features
- Improve selected colors [Taiga #5805]( https://tree.taiga.io/project/penpot/us/5805)
- Improve selected colors [Taiga #5805](https://tree.taiga.io/project/penpot/us/5805)
### :bug: Bugs fixed
@@ -895,7 +912,6 @@ time being.
- Fix deleted pages comments shown in right sidebar [Taiga #5648](https://tree.taiga.io/project/penpot/us/5648)
- Fix tooltip on toggle visibility and toggle lock buttons [Taiga #5141](https://tree.taiga.io/project/penpot/issue/5141)
## 1.19.1
### :bug: Bugs fixed
@@ -1009,7 +1025,6 @@ time being.
- Update google fonts catalog (at 2023/07/06) [Taiga #5592](https://tree.taiga.io/project/penpot/issue/5592)
### :heart: Community contributions by (Thank you!)
- Update Typography palette order (by @akshay-gupta7) [Github #3156](https://github.com/penpot/penpot/pull/3156)
@@ -1163,12 +1178,14 @@ time being.
- Fix problem with opacity in imported SVG's [Taiga #4923](https://tree.taiga.io/project/penpot/issue/4923)
### :heart: Community contributions by (Thank you!)
- To @ondrejkonec: for contributing to the code with:
- Refactor CSS variables [Github #2948](https://github.com/penpot/penpot/pull/2948)
## 1.17.3
### :bug: Bugs fixed
- Fix copy and paste very nested inside itself [Taiga #4848](https://tree.taiga.io/project/penpot/issue/4848)
- Fix custom fonts not rendered correctly [Taiga #4874](https://tree.taiga.io/project/penpot/issue/4874)
- Fix problem with shadows and blur on multiple selection
@@ -1201,6 +1218,7 @@ time being.
## 1.17.1
### :bug: Bugs fixed
- Fix components groups items show the component name in list mode [Taiga #4770](https://tree.taiga.io/project/penpot/issue/4770)
- Fix typing CMD+Z on MacOS turns the cursor into a Zoom cursor [Taiga #4778](https://tree.taiga.io/project/penpot/issue/4778)
- Fix white space on small screens [Taiga #4774](https://tree.taiga.io/project/penpot/issue/4774)
@@ -1315,7 +1333,7 @@ time being.
### :boom: Breaking changes & Deprecations
- Removed the support for v2 internal file data blob format. This
- Removed the support for v2 internal file data blob format. This
version has never been documented nor set as default value so
technically this is not a breaking change because we are removing
a "private API".
@@ -1420,7 +1438,6 @@ time being.
- Fix when ungrouping, the items previously grouped should ALWAYS remain selected [Taiga #4064](https://tree.taiga.io/project/penpot/issue/4064)
- Change shortcut for "Clear undo" [#2219](https://github.com/penpot/penpot/issues/2219)
## 1.15.2-beta
### :bug: Bugs fixed
@@ -1504,6 +1521,7 @@ time being.
- Fix bringing complete file data when launching the export dialog [Taiga #4006](https://tree.taiga.io/project/penpot/issue/4006)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.14.2-beta
@@ -1544,10 +1562,10 @@ time being.
- Prototype connection should be under the rules [Taiga #3384](https://tree.taiga.io/project/penpot/issue/3384)
- Fix problem with empty text boxes events [Taiga #3627](https://tree.taiga.io/project/penpot/issue/3627)
## 1.13.5-beta
### :bug: Bugs fixed
- Fix orientation artboard preset not working with differently sized artboards [Taiga #3548](https://tree.taiga.io/project/penpot/issue/3548)
- Fix background on export arboards [Taiga #1991](https://tree.taiga.io/project/penpot/issue/1991)
@@ -1691,6 +1709,7 @@ time being.
- Fix problem when resizing a group with texts with auto-width/height [#3171](https://tree.taiga.io/project/penpot/issue/3171)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.12.4-beta
@@ -1708,7 +1727,7 @@ time being.
### :bug: Bugs fixed
- Fix issue with shift+select to deselect shapes [Taiga #3154](https://tree.taiga.io/project/penpot/issue/3154)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
- Fix issue with drag-select shapes [Taiga #3165](https://tree.taiga.io/project/penpot/issue/3165)
- Fix issue on password persistence after registration process on private instances
## 1.12.2-beta
@@ -1726,7 +1745,6 @@ time being.
- Fix length of names in sidebar [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
- Fix issues on loki integration
## 1.12.0-beta
### :boom: Breaking changes

View File

@@ -193,7 +193,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Click to the link below to confirm the change:</div>
Click the link below to confirm the change.</div>
</td>
</tr>
<tr>
@@ -217,8 +217,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you received this email by mistake, please consider changing your password for security
reasons.</div>
If you did not request this change, consider changing your password for security reasons.</div>
</td>
</tr>
<tr>

View File

@@ -2,12 +2,11 @@ Hello {{name|abbreviate:25}}!
We received a request to change your current email to {{ pending-email }}.
Click to the link below to confirm the change:
Click the link below to confirm the change.
{{ public-uri }}/#/auth/verify-token?token={{token}}
If you received this email by mistake, please consider changing your password
for security reasons.
If you did not request this change, consider changing your password for security reasons.
Enjoy!
The Penpot team.

View File

@@ -349,7 +349,7 @@
rounded-s (d/format-number (* 100 s) precision)
rounded-l (d/format-number (* 100 l) precision)
rounded-a (d/format-number a precision)]
(str/concat "" rounded-h ", " rounded-s "%, " rounded-l "%, " rounded-a)))
(str/concat "" rounded-h " " rounded-s "% " rounded-l "% / " rounded-a)))
(defn format-rgba
[[r g b a]]

View File

@@ -1727,6 +1727,7 @@
eq-untouched-attrs?
(touched :text-content-structure-same-attrs))))))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1815,10 +1816,15 @@
:else
(get origin-shape attr)))
;; If the final attr-value is the actual value, skip
skip-operations? (or skip-operations?
(= attr-val (get dest-shape attr)))
;; On a text-partial-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
(if text-partial-change?
(if (and text-partial-change? (not skip-operations?))
(add-update-attr-operations :position-data dest-shape roperations uoperations nil)
[roperations uoperations])
@@ -1830,6 +1836,93 @@
roperations'
uoperations')))))))
(defn- switch-text-change-value
[prev-content ;; The :content of the text before the switch
current-content ;; The :content of the text after the switch (a clean copy)
ref-content touched] ;; The :content of the referenced text on the main component
;; before the switch
(let [;; We need the differences between the contents on the main
;; components. current-content is the content of a clean copy,
;; so for all effects its the same as the content on its main
main-comps-diff (cttx/get-diff-type ref-content current-content)
can-keep-text? (not (contains? main-comps-diff :text-content-text))
can-keep-attr? (not (contains? main-comps-diff :text-content-attribute))
main-diff-structure? (contains? main-comps-diff :text-content-structure)
current-attrs (cttx/get-first-paragraph-text-attrs current-content)
;; Have current content an uniform style?
curr-unif-style? (cttx/equal-attrs? current-content current-attrs)
prev-attrs (cttx/get-first-paragraph-text-attrs prev-content)
;; Have prev content an uniform style?
prev-unif-style? (cttx/equal-attrs? prev-content prev-attrs)
ref-attrs (cttx/get-first-paragraph-text-attrs ref-content)
;; Have ref content an uniform style?
ref-unif-style? (cttx/equal-attrs? ref-content ref-attrs)]
(cond
;; When the main components have a difference in structure
;; (different number of paragraph or text entries)
main-diff-structure?
;; Special case for adding or removing paragraphs:
;; If the structure has changed between ref-content and current-content,
;; but each one have uniform attributes, and the attrs on the main
;; components were equal, we keep the touched-content structure and
;; texts, updating its attrs to make them like the current-content
(if (and curr-unif-style?
ref-unif-style?
prev-unif-style?
(= ref-attrs current-attrs))
(cttx/copy-attrs-keys current-content prev-attrs)
;; In any other case of structure change, we discard all
;; the overrides and keep the content of the current-shape
current-content)
;; When the main components are equal, we keep the updated
;; content from previous-shape as is
(and can-keep-text? can-keep-attr?)
prev-content
;; When we can't keep anything, we discard all the
;; overrides and keep the content of the current-shape
(and (not can-keep-text?) (not can-keep-attr?))
current-content
;; Special case for added or removed paragraphs:
;; If the structure has changed on current-content, but it has uniform attributes
;; and the previous-content also has uniform attributes, and we can keep the changes
;; on the text, we keep the touched-content structure and texts, updating
;; its attrs to make them like the current-content
(and (touched :text-content-structure)
curr-unif-style?
prev-unif-style?)
(if can-keep-text?
(cttx/copy-attrs-keys prev-content current-attrs)
(cttx/copy-attrs-keys current-content prev-attrs))
;; In any other case of structure change, we discard all
;; the overrides and keep the content of the current-shape
(touched :text-content-structure)
current-content
;; When there is a change on :text-content-text,
;; and and we can keep it, we copy the texts from
;; previous-shape over the attrs of current-shape
(and
(touched :text-content-text) can-keep-text?)
(cttx/copy-text-keys prev-content current-content)
;; When there is a change on :text-content-attribute,
;; and we can keep it, we copy the texts from current-shape
;; over the attrs of previous-shape
(and
(touched :text-content-attribute) can-keep-attr?)
(cttx/copy-text-keys current-content prev-content)
;; In any other case, we discard all the overrides
;; and keep the content of the current-shape
:else
current-content)))
(defn update-attrs-on-switch
"Copy attributes that have changed in the shape previous to the switch
to the current shape (post switch). Used only on variants switch"
@@ -1883,14 +1976,13 @@
;; and attrs (bold, font, etc) are in the same attr :content.
;; If only one of them is touched, we want to adress this case and
;; only update the untouched one
text-partial-change?
(when (and
(not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched attr-group))
(is-text-partial-change? current-shape previous-shape))
text-change?
(and
(not skip-operations?)
(cfh/text-shape? current-shape)
(cfh/text-shape? previous-shape)
(= :content attr)
(touched attr-group))
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
@@ -1902,20 +1994,34 @@
(not= (:position-data previous-shape) (:position-data current-shape))
(touched :geometry-group))
attr-val (when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
attr-val
(when-not skip-operations?
(cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data?
nil
text-partial-change?
(text-partial-change-value (:content previous-shape)
(:content current-shape)
touched)
text-change?
(switch-text-change-value (:content previous-shape)
(:content current-shape)
(:content origin-ref-shape)
touched)
:else
(get previous-shape attr)))
:else
(get previous-shape attr)))
;; If the final attr-value is the actual value, skip
skip-operations? (or skip-operations?
(= attr-val (get current-shape attr)))
;; On a text-change, we want to force a position-data reset
;; so it's calculated again
[roperations uoperations]
(if (and (not skip-operations?) text-change?)
(add-update-attr-operations :position-data current-shape roperations uoperations nil)
[roperations uoperations])
[roperations' uoperations']
(if skip-operations?

View File

@@ -8,11 +8,14 @@
(:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.logic.variants :as clv]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.text :as txt]
[app.common.types.container :as ctn]
@@ -275,26 +278,36 @@
(defn swap-component
"Swap the specified shape by the component specified by component-tag"
[file shape component-tag & {:keys [page-label propagate-fn]}]
[file shape component-tag & {:keys [page-label propagate-fn keep-touched? new-shape-label]}]
(let [page (if page-label
(thf/get-page file page-label)
(thf/current-page file))
libraries {(:id file) file}
[_ _all-parents changes]
orig-shapes (when keep-touched? (cfh/get-children-with-self (:objects page) (:id shape)))
[new-shape _all-parents changes]
(cll/generate-component-swap (pcb/empty-changes)
(:objects page)
shape
(:data file)
page
{(:id file) file}
libraries
(-> (thc/get-component file component-tag)
:id)
0
nil
{}
false)
(true? keep-touched?))
changes (if keep-touched?
(clv/generate-keep-touched changes new-shape shape orig-shapes page libraries (:data file))
changes)
file' (thf/apply-changes file changes)]
(when new-shape-label
(thi/set-id! new-shape-label (:id new-shape)))
(if propagate-fn
(propagate-fn file')
file')))

View File

@@ -8,7 +8,9 @@
(:require
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]))
[app.common.test-helpers.shapes :as ths]
[app.common.text :as txt]
[app.common.types.shape :as cts]))
(defn add-variant
[file variant-label component1-label root1-label component2-label root2-label
@@ -37,3 +39,48 @@
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v1"} {:name "Property2" :value "p2v1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "p1v2"} {:name "Property2" :value "p2v2"}]}))))
(defn add-variant-with-child
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label
& {:keys [child1-params child2-params]}]
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(ths/add-sample-shape child1-label (assoc child1-params :parent-label root1-label))
(ths/add-sample-shape child2-label (assoc child2-params :parent-label root2-label))
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))
(defn add-variant-with-text
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label text1 text2
& {:keys [text1-params text2-params]}]
(let [text1 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text text1)
(assoc :position-data nil
:parent-label root1-label))
text2 (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text text2)
(assoc :position-data nil
:parent-label root2-label))
file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
variant-id (thi/id variant-label)]
(-> file
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
(ths/add-sample-shape child1-label
(merge text1
text1-params))
(ths/add-sample-shape child2-label
(merge text2
text2-params))
(thc/make-component component1-label root1-label)
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})
(thc/make-component component2-label root2-label)
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value2"}]}))))

View File

@@ -294,8 +294,8 @@
([page component library-data position]
(make-component-instance page component library-data position {}))
([page component library-data position
{:keys [main-instance? force-id force-frame-id keep-ids?]
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false}}]
{:keys [main-instance? force-id force-frame-id keep-ids? force-parent-id]
:or {main-instance? false force-id nil force-frame-id nil keep-ids? false force-parent-id nil}}]
(let [component-page (ctpl/get-page library-data (:main-instance-page component))
component-shape (-> (get-shape component-page (:main-instance-id component))
@@ -303,7 +303,6 @@
(assoc :frame-id uuid/zero)
(remove-swap-keep-attrs))
orig-pos (gpt/point (:x component-shape) (:y component-shape))
delta (gpt/subtract position orig-pos)
@@ -368,7 +367,7 @@
[new-shape new-shapes _]
(ctst/clone-shape component-shape
frame-id
(or force-parent-id frame-id)
(:objects component-page)
:update-new-shape update-new-shape
:force-id force-id

View File

@@ -127,7 +127,8 @@
entries"
[a b]
(cond
(not= (type a) (type b))
(and (not= (type a) (type b))
(not (and (map? a) (map? b)))) ;; Sometimes they are both maps but of different subtypes
false
(map? a)

View File

@@ -135,6 +135,8 @@
(def typography-keys (set/union font-size-keys letter-spacing-keys))
;; TODO: Created to extract the font-size feature from the typography feature flag.
;; Delete this once the typography feature flag is removed.
(def ff-typography-keys (set/difference typography-keys font-size-keys))
(def ^:private schema:number

View File

@@ -8,7 +8,6 @@
(:require
#?(:cljs [goog.color :as gcolors])
[app.common.colors :as colors]
[app.common.data :as d]
[clojure.test :as t]))
(t/deftest valid-hex-color
@@ -52,8 +51,8 @@
(t/is (= [1 2 3] (colors/hex->rgb "#010203"))))
(t/deftest format-hsla
(t/is (= "210, 50%, 0.78%, 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))
(t/is (= "220, 5%, 30%, 0.8" (colors/format-hsla [220.0 0.05 0.3 0.8]))))
(t/is (= "210 50% 0.78% / 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))
(t/is (= "220 5% 30% / 0.8" (colors/format-hsla [220.0 0.05 0.3 0.8]))))
(t/deftest format-rgba
(t/is (= "210, 199, 12, 0.08" (colors/format-rgba [210 199 12 0.08])))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,17 @@
#!/usr/bin/env bash
set -x
DOCKER_CLI_EXPERIMENTAL=enabled
IMAGE=${1:-backend}
OUTPUT="type=registry"
if [ "--local" = "$2" ]; then
OUTPUT="type=docker"
fi
ORG=${PENPOT_DOCKER_NAMESPACE:-penpotapp};
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
IMAGE=${PENPOT_BUILD_IMAGE:-backend}
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64};
PLATFORM=${PENPOT_BUILD_PLATFORM:-linux/amd64,linux/arm64};
VERSION=${PENPOT_BUILD_VERSION:-latest}
DOCKER_IMAGE="$ORG/$IMAGE";
OPTIONS="-t $DOCKER_IMAGE:$VERSION";
@@ -20,7 +23,7 @@ for element in "${TAGS[@]}"; do
done
docker buildx inspect penpot > /dev/null 2>&1;
docker run --privileged --rm tonistiigi/binfmt --install all
docker run --privileged --rm tonistiigi/binfmt --install all > /dev/null;
if [ $? -eq 1 ]; then
docker buildx create --name=penpot --use
@@ -32,4 +35,5 @@ fi
unset IFS;
docker buildx build --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE "$@" .;
shift;
docker buildx build --output $OUTPUT --platform ${PLATFORM// /,} $OPTIONS -f Dockerfile.$IMAGE .;

View File

@@ -28,5 +28,6 @@ Use Docker if you already know the tool, if need full control of the process or
and do not want to depend on any external provider, or need to do any special customization.
</p>
Or you can try <a href="#unofficial-self-host-options">other options</a>,
offered by Penpot community.
Or you can try [other options][1], offered by Penpot community.
[1]: /technical-guide/getting-started/unofficial-options/

View File

File diff suppressed because it is too large Load Diff

View File

@@ -121,7 +121,6 @@ test("Renders a file with styled texts", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts with images", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
@@ -145,6 +144,28 @@ test("Renders a file with texts with images", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with text decoration", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockFileMediaAsset(
[
"d6c33e7b-7b64-80f3-8006-78509a3a2d21",
],
"render-wasm/assets/pattern.png",
);
await mockGetEmojiFont(workspace);
await mockGetJapaneseFont(workspace);
await workspace.mockGetFile("render-wasm/get-file-text-decoration.json");
await workspace.goToWorkspace({
id: "d6c33e7b-7b64-80f3-8006-785098582f1d",
pageId: "d6c33e7b-7b64-80f3-8006-785098582f1e",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with multiple emoji", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -245,4 +245,4 @@
(defn event
[props]
(ptk/data-event ::events props))
(ptk/data-event ::event props))

View File

@@ -36,6 +36,7 @@
[app.main.data.workspace :as-alias dw]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.notifications :as-alias dwn]
[app.main.data.workspace.pages :as-alias dwpg]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.specialized-panel :as dwsp]
@@ -700,7 +701,7 @@
(fn [page-id shape-id]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dw/initialize-page))
(rx/filter (ptk/type? ::dwpg/initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (select-and-zoom shape-id))))

View File

@@ -847,3 +847,25 @@
(if undo-transation?
(rx/of (dwu/commit-undo-transaction undo-id))
(rx/empty))))))))
;; Pure function to determine next grow-type for text layers
(defn next-grow-type [current-grow-type resize-direction]
(cond
(= current-grow-type :fixed)
:fixed
(and (= resize-direction :horizontal)
(= current-grow-type :auto-width))
:auto-height
(and (= resize-direction :horizontal)
(= current-grow-type :auto-height))
:auto-height
(and (= resize-direction :vertical)
(or (= current-grow-type :auto-width)
(= current-grow-type :auto-height)))
:fixed
:else
current-grow-type))

View File

@@ -174,12 +174,15 @@
add-component-copy
(fn [objs id shape]
(let [component (ctkl/get-component fdata (:component-id shape))
parent-id (when (not= (:parent-id shape) uuid/zero) (:parent-id shape))
[new-shape new-shapes]
(ctn/make-component-instance page
component
fdata
(gpt/point (:x shape) (:y shape))
{:keep-ids? true :force-frame-id (:frame-id shape)})
{:keep-ids? true
:force-frame-id (:frame-id shape)
:force-parent-id parent-id})
children (into {} (map (fn [shape] [(:id shape) shape]) new-shapes))
objs (assoc objs id new-shape)]
(merge objs children)))
@@ -201,10 +204,8 @@
(assoc :id id)
(assoc :objects
objects))
changes (-> (pcb/empty-changes it)
(pcb/add-page id page))]
(rx/of (dch/commit-changes changes))))))
(s/def ::rename-page

View File

@@ -479,7 +479,7 @@
;; We don't have the fills attribute. It's an old text without color
;; so need to be black
(and (nil? (:fills node)) (empty? color-attrs))
(update :fills conj txt/default-text-attrs)
(assoc :fills (:fills txt/default-text-attrs))
;; Remove duplicates from the fills
:always

View File

@@ -47,7 +47,7 @@
(ntf/show {:content (tr "workspace.tokens.unknown-token-type-message")
:detail (->> (for [[token-type tokens] type->tokens]
(tr "workspace.tokens.unknown-token-type-section" token-type (count tokens)))
(str/join "\n"))
(str/join "<br>"))
:type :toast
:level :info})))

View File

@@ -218,16 +218,23 @@
(gpt/add resize-origin displacement)
resize-origin)
;; Determine resize direction for grow-type logic
resize-direction (cond
(or (= handler :left) (= handler :right)) :horizontal
(or (= handler :top) (= handler :bottom)) :vertical
:else nil)
;; Calculate new grow-type for text layers
new-grow-type (when (cfh/text-shape? shape)
(dwm/next-grow-type (dm/get-prop shape :grow-type) resize-direction))
;; When the horizontal/vertical scale a flex children with auto/fill
;; we change it too fixed
change-width?
(not (mth/close? (dm/get-prop scalev :x) 1))
change-height?
(not (mth/close? (dm/get-prop scalev :y) 1))
auto-width-text? (and (cfh/text-shape? shape) (= :auto-width (dm/get-prop shape :grow-type)))
auto-height-text? (and (cfh/text-shape? shape) (= :auto-height (dm/get-prop shape :grow-type)))]
(not (mth/close? (dm/get-prop scalev :y) 1))]
(cond-> (ctm/empty)
(some? displacement)
@@ -242,11 +249,9 @@
^boolean change-height?
(ctm/change-property :layout-item-v-sizing :fix)
(and auto-width-text? (or change-width? change-height?))
(ctm/change-property :grow-type :fixed)
(and auto-height-text? change-height?)
(ctm/change-property :grow-type :fixed)
;; Set grow-type if it should change
(and new-grow-type (not= new-grow-type (dm/get-prop shape :grow-type)))
(ctm/change-property :grow-type new-grow-type)
^boolean scale-text
(ctm/scale-content (dm/get-prop scalev :x)))))

View File

@@ -102,23 +102,27 @@
(print-trace! error)
(print-data! error))))
;; We receive a explicit authentication error;
;; If the uri is for workspace, dashboard or view assign the
;; exception for the 'Oops' page. Otherwise this explicitly clears
;; all profile data and redirect the user to the login page. This is
;; here and not in app.main.errors because of circular dependency.
;; We receive a explicit authentication error; If the uri is for
;; workspace, dashboard, viewer or settings, then assign the exception
;; for show the error page. Otherwise this explicitly clears all
;; profile data and redirect the user to the login page. This is here
;; and not in app.main.errors because of circular dependency.
(defmethod ptk/handle-error :authentication
[e]
(let [msg (tr "errors.auth.unable-to-login")
uri (.-href glob/location)
show-oops? (or (str/includes? uri "workspace")
(str/includes? uri "dashboard")
(str/includes? uri "view"))]
(if show-oops?
(st/async-emit! (rt/assign-exception e))
[error]
(let [message (tr "errors.auth.unable-to-login")
uri (rt/get-current-href)
show-error?
(or (str/includes? uri "workspace")
(str/includes? uri "dashboard")
(str/includes? uri "view")
(str/includes? uri "settings"))]
(if show-error?
(st/async-emit! (rt/assign-exception error))
(do
(st/emit! (da/logout))
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
(ts/schedule 500 #(st/emit! (ntf/warn message)))))))
;; Error that happens on an active business model validation does not
;; passes an validation (example: profile can't leave a team). From

View File

@@ -28,6 +28,7 @@
[app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]]
[app.main.ui.releases :refer [release-notes-modal]]
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
@@ -362,6 +363,8 @@
;; initialize themes
(theme/use-initialize profile)
(dom/prevent-browser-gesture-navigation!)
[:& (mf/provider ctx/current-route) {:value route}
[:& (mf/provider ctx/current-profile) {:value profile}
(if edata

View File

@@ -17,6 +17,7 @@
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.dom.normalize-wheel :as nw]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.storage :as storage]
@@ -171,41 +172,48 @@
(mf/defc templates-section*
{::mf/props :obj}
[{:keys [default-project-id profile project-id team-id]}]
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]
(filterv #(and
(not= (:id %) "welcome")
(not= (:id %) "tutorial-for-beginners")) templates))
(let [templates (mf/deref builtin-templates)
templates (mf/with-memo [templates]
(filterv #(and
(not= (:id %) "welcome")
(not= (:id %) "tutorial-for-beginners")) templates))
route (mf/deref refs/route)
route-name (get-in route [:data :name])
section (if (= route-name :dashboard-files)
(if (= project-id default-project-id)
"dashboard-drafts"
"dashboard-project")
(name route-name))
route (mf/deref refs/route)
route-name (get-in route [:data :name])
section (if (= route-name :dashboard-files)
(if (= project-id default-project-id)
"dashboard-drafts"
"dashboard-project")
(name route-name))
collapsed* (mf/use-state
#(get storage/global ::collapsed))
collapsed (deref collapsed*)
collapsed* (mf/use-state
#(get storage/global ::collapsed))
collapsed (deref collapsed*)
can-move (mf/use-state {:left false :right true})
can-move (mf/use-state {:left false :right true})
total (count templates)
total (count templates)
;; We need space for total plus the libraries&templates link
content-ref (mf/use-ref)
move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0))
move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0))
content-ref (mf/use-ref)
on-toggle-collapse
(mf/use-fn
(fn [_event]
(swap! collapsed* not)))
on-wheel
(mf/use-fn
(fn [^js event]
(let [event* (nw/normalize-wheel event)
deltaY (.-spinY event*)
deltaX (.-spinX event*)
node (mf/ref-val content-ref)]
(when (> (abs deltaY) (abs deltaX))
(.scrollBy node #js {:left (* 300 deltaY) :mode "smooth"})))))
on-scroll
(mf/use-fn
(fn [e]
@@ -219,16 +227,10 @@
:right (> scroll-available client-width)}))))
on-move-left
(mf/use-fn #(move-left))
on-move-left-key-down
(mf/use-fn #(move-left))
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) -300 0))
on-move-right
(mf/use-fn #(move-right))
on-move-right-key-down
(mf/use-fn #(move-right))
(mf/use-fn #(dom/scroll-by! (mf/ref-val content-ref) 300 0))
on-import-template
(mf/use-fn
@@ -236,7 +238,7 @@
(fn [template _event]
(import-template! template team-id project-id default-project-id section)))]
(mf/with-effect [content-ref templates]
(mf/with-effect [templates]
(let [content (mf/ref-val content-ref)]
(when (and (some? content) (some? templates))
(dom/scroll-to content #js {:behavior "instant" :left 0 :top 0})
@@ -258,6 +260,7 @@
[:div {:class (stl/css :content)
:on-scroll on-scroll
:on-wheel on-wheel
:ref content-ref}
(for [index (range (count templates))]
@@ -279,13 +282,13 @@
[:button {:class (stl/css :move-button :move-left)
:tab-index (if ^boolean collapsed "-1" "0")
:on-click on-move-left
:on-key-down on-move-left-key-down}
:on-key-down on-move-left}
arrow-icon])
(when (:right @can-move)
[:button {:class (stl/css :move-button :move-right)
:tab-index (if collapsed "-1" "0")
:on-click on-move-right
:aria-label (tr "labels.next")
:on-key-down on-move-right-key-down}
:on-key-down on-move-right
:aria-label (tr "labels.next")}
arrow-icon])]))

View File

@@ -71,4 +71,5 @@
[:div {:on-click on-toggle-detail}
(tr "workspace.notification-pill.detail")]]
(when show-detail
[:div {:class (stl/css :error-detail-content)} detail])])]))
[:div {:class (stl/css :error-detail-content)
:dangerouslySetInnerHTML #js {:__html detail}}])])]))

View File

@@ -7,10 +7,12 @@
(ns app.main.ui.inspect.attributes.text
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.text :as txt]
[app.common.types.color :as types.color]
[app.common.types.color :as ctc]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -18,6 +20,7 @@
[app.main.ui.components.title-bar :refer [inspect-title-bar*]]
[app.main.ui.formats :as fmt]
[app.main.ui.inspect.attributes.common :refer [color-row]]
[app.util.color :as uc]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[okulary.core :as l]
@@ -35,12 +38,56 @@
(get-in state [:viewer-libraries file-id :data :typographies]))]
#(l/derived get-library st/state)))
(defn alpha->hex [alpha]
(-> (mth/round (* 255 alpha))
(js/Number)
(.toString 16)
(.toUpperCase)
(.padStart 2 "0")))
(defn- copy-style-data
[style & properties]
(->> properties
(map #(dm/str (d/name %) ": " (get style %) ";"))
(str/join "\n")))
(defn- format-gradient-css
"Converts a gradient object to a CSS string."
[gradient]
(str "background-image: " (uc/gradient->css gradient) ";"
"background-clip: text;"
"color: transparent;"))
(defn- format-solid-color
"returns a CSS color string based on the provided color and format."
[color format]
(let [color-value (:color color)
opacity (:opacity color 1)
has-opacity? (not (= 1 opacity))]
(case format
:rgba
(let [[r g b a] (cc/hex->rgba color-value opacity)]
(str "color: rgba(" (cc/format-rgba [r g b a]) ");"))
:hex
(str "color: " color-value
(when has-opacity? (alpha->hex opacity)) ";")
:hsla
(let [[h s l a] (cc/hex->hsla color-value opacity)]
(str "color: hsla(" (cc/format-hsla [h s l a]) ");"))
;; Default fallback
(str "color: " color-value ";"))))
(defn- copy-color-data
"Converts a fill object to CSS color string in the specified format."
[fill format]
(let [color (ctc/fill->color fill)]
(if-let [gradient (:gradient color)]
(format-gradient-css gradient)
(format-solid-color color format))))
(mf/defc typography-block
[{:keys [text style]}]
(let [typography-library-ref
@@ -57,7 +104,8 @@
file-library-workspace (get (mf/deref refs/files) (:typography-ref-file style))
typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)])
color-format (mf/use-state :hex)
color-format! (mf/use-state :hex)
color-format* (deref color-format!)
typography (or (get (or typography-library file-typographies-viewer file-typographies-workspace) (:typography-ref-id style)) typography-external-lib)]
@@ -65,10 +113,10 @@
(when (:fills style)
(for [[idx fill] (map-indexed vector (:fills style))]
[:& color-row {:key idx
:format @color-format
:color (types.color/fill->color fill)
:copy-data (copy-style-data fill :fill-color :fill-color-gradient)
:on-change-format #(reset! color-format %)}]))
:format color-format*
:color (ctc/fill->color fill)
:copy-data (copy-color-data fill color-format*)
:on-change-format #(reset! color-format! %)}]))
(when (:typography-ref-id style)
[:div {:class (stl/css :text-row)}

View File

@@ -11,6 +11,7 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.team :as dtm]
[app.main.errors :as errors]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
@@ -130,7 +131,10 @@
(assoc query-params :team-id (:default-team-id profile))))))
:else
(st/emit! (rt/assign-exception {:type :not-found})))))))))
(st/emit! (rt/assign-exception {:type :not-found}))))
(fn [cause]
(errors/on-error cause)))))))
(defn init-routes
[]

View File

@@ -42,7 +42,7 @@
(mf/with-effect [profile]
(when (nil? profile)
(st/emit! (rt/nav :auth-login))))
(st/emit! (rt/assign-exception {:type :authentication}))))
[:*
[:> modal-container*]

View File

@@ -3,6 +3,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.auth :as da]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.profile :as du]
@@ -96,7 +97,7 @@
handle-accept-dialog (mf/use-fn
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
::ev/origin "profile"
::ev/origin "settings"
:section "subscription-management-modal"}))
(let [current-href (rt/get-current-href)
returnUrl (js/encodeURIComponent current-href)
@@ -212,43 +213,69 @@
(mf/defc subscription-page*
[{:keys [profile]}]
(let [route (mf/deref refs/route)
params (:params route)
params-subscription (:subscription (:query params))
show-trial-subscription-modal (or (= params-subscription "subscription-to-penpot-unlimited")
(= params-subscription "subscription-to-penpot-enterprise"))
show-subscription-success-modal (or (= params-subscription "subscribed-to-penpot-unlimited")
(= params-subscription "subscribed-to-penpot-enterprise"))
subscription (:subscription (:props profile))
subscription-type (get-subscription-type subscription)
subscription-is-trial (= (:status subscription) "trialing")
teams* (mf/use-state nil)
teams (deref teams*)
locale (mf/deref i18n/locale)
penpot-member (dt/format-date-locale-short (:created-at profile) {:locale locale})
subscription-member (dt/format-date-locale-short (:start-date subscription) {:locale locale})
go-to-pricing-page (mf/use-fn
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "settings" :section "subscription"}))
(dom/open-new-window "https://penpot.app/pricing")))
go-to-payments (mf/use-fn
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
::ev/origin "profile"
:section "subscription"}))
(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)))))
open-subscription-modal (mf/use-fn
(mf/deps teams)
(fn [subscription-type]
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:in-app"}))
(st/emit!
(modal/show :management-dialog
{:subscription-type subscription-type
:teams teams :subscribe-to-trial (not subscription)}))))]
(let [route (mf/deref refs/route)
authenticated? (da/is-authenticated? profile)
teams* (mf/use-state nil)
teams (deref teams*)
locale (mf/deref i18n/locale)
params-subscription
(-> route :params :query :subscription)
show-trial-subscription-modal?
(or (= params-subscription "subscription-to-penpot-unlimited")
(= params-subscription "subscription-to-penpot-enterprise"))
show-subscription-success-modal?
(or (= params-subscription "subscribed-to-penpot-unlimited")
(= params-subscription "subscribed-to-penpot-enterprise"))
subscription
(-> profile :props :subscription)
subscription-type
(get-subscription-type subscription)
subscription-is-trial?
(= (:status subscription) "trialing")
member-since
(dt/format-date-locale-short (:created-at profile) {:locale locale})
subscribed-since
(dt/format-date-locale-short (:start-date subscription) {:locale locale})
go-to-pricing-page
(mf/use-fn
(fn []
(st/emit! (ev/event {::ev/name "explore-pricing-click"
::ev/origin "settings"
:section "subscription"}))
(dom/open-new-window "https://penpot.app/pricing")))
go-to-payments
(mf/use-fn
(fn []
(st/emit! (ev/event {::ev/name "open-subscription-management"
::ev/origin "settings"
:section "subscription"}))
(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)))))
open-subscription-modal
(mf/use-fn
(mf/deps teams)
(fn [subscription-type]
(st/emit! (ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:in-app"}))
(st/emit!
(modal/show :management-dialog
{:subscription-type subscription-type
:teams teams :subscribe-to-trial (not subscription)}))))]
(mf/with-effect []
(->> (rp/cmd! :get-owned-teams)
@@ -258,33 +285,35 @@
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
(mf/with-effect [show-trial-subscription-modal subscription]
(when show-trial-subscription-modal
(st/emit!
(ptk/event ::ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:from-pricing-page"})
(modal/show :management-dialog
{:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited")
"unlimited"
"enterprise")
:teams teams
:subscribe-to-trial (not subscription)})
(rt/nav :settings-subscription {} {::rt/replace true}))))
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? subscription]
(when ^boolean authenticated?
(cond
^boolean show-trial-subscription-modal?
(mf/with-effect [show-subscription-success-modal subscription]
(when show-subscription-success-modal
(st/emit!
(modal/show :subscription-success
{:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited")
(tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.enterprise-trial"))})
(du/update-profile-props {:subscription
(-> subscription
(assoc :type (if (= params-subscription "subscribed-to-penpot-unlimited")
"unlimited"
"enterprise"))
(assoc :status "trialing"))})
(rt/nav :settings-subscription {} {::rt/replace true}))))
(st/emit!
(ptk/event ::ev/event {::ev/name "open-subscription-modal"
::ev/origin "settings:from-pricing-page"})
(modal/show :management-dialog
{:subscription-type (if (= params-subscription "subscription-to-penpot-unlimited")
"unlimited"
"enterprise")
:teams teams
:subscribe-to-trial (not subscription)})
(rt/nav :settings-subscription {} {::rt/replace true}))
^boolean show-subscription-success-modal?
(st/emit!
(modal/show :subscription-success
{:subscription-name (if (= params-subscription "subscribed-to-penpot-unlimited")
(tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.enterprise-trial"))})
(du/update-profile-props {:subscription
(-> subscription
(assoc :type (if (= params-subscription "subscribed-to-penpot-unlimited")
"unlimited"
"enterprise"))
(assoc :status "trialing"))})
(rt/nav :settings-subscription {} {::rt/replace true})))))
[:section {:class (stl/css :dashboard-section)}
[:div {:class (stl/css :dashboard-content)}
@@ -301,7 +330,7 @@
(tr "subscription.settings.professional.storage")]}]
"unlimited"
(if subscription-is-trial
(if subscription-is-trial?
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
:card-title-icon i/character-u
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
@@ -325,7 +354,7 @@
:editors (-> profile :props :subscription :quantity)}])
"enterprise"
(if subscription-is-trial
(if subscription-is-trial?
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
:card-title-icon i/character-e
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
@@ -344,13 +373,16 @@
:cta-link go-to-payments}]))
[:div {:class (stl/css :membership-container)}
(when subscription-member [:div {:class (stl/css :membership)}
[:span {:class (stl/css :subscription-member)} i/crown]
[:span {:class (stl/css :membership-date)} (tr "subscription.settings.support-us-since" subscription-member)]])
(when subscribed-since
[:div {:class (stl/css :membership)}
[:span {:class (stl/css :subscription-member)} i/crown]
[:span {:class (stl/css :membership-date)}
(tr "subscription.settings.support-us-since" subscribed-since)]])
[:div {:class (stl/css :membership)}
[:span {:class (stl/css :penpot-member)} i/user]
[:span {:class (stl/css :membership-date)} (tr "subscription.settings.member-since" penpot-member)]]]]
[:span {:class (stl/css :membership-date)}
(tr "subscription.settings.member-since" member-since)]]]]
[:div {:class (stl/css :other-subscriptions)}
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.other-plans")]

View File

@@ -69,9 +69,8 @@
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
(mf/defc login-dialog
{::mf/props :obj}
[{:keys [show-dialog]}]
(mf/defc login-dialog*
[]
(let [current-section (mf/use-state :login)
user-email (mf/use-state "")
register-token (mf/use-state "")
@@ -94,9 +93,7 @@
success-login
(mf/use-fn
(fn []
(reset! show-dialog false)
(st/emit! (rt/reload true))))
#(st/emit! (rt/reload true)))
success-register
(mf/use-fn
@@ -117,7 +114,7 @@
(reset! current-section :recovery-email-sent)))
on-nav-root
(mf/use-fn #(st/emit! (rt/nav-root)))]
(mf/use-fn #(st/emit! (rt/nav :auth-login {})))]
[:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog-login)}
@@ -203,11 +200,9 @@
[:button {:on-click on-click} button-text]]]]))
(mf/defc request-access*
[{:keys [file-id team-id is-default is-workspace]}]
(let [profile (mf/deref refs/profile)
requested* (mf/use-state {:sent false :already-requested false})
[{:keys [file-id team-id is-default is-workspace profile]}]
(let [requested* (mf/use-state {:sent false :already-requested false})
requested (deref requested*)
show-dialog (mf/use-state true)
on-close
(mf/use-fn
@@ -237,90 +232,47 @@
(st/emit! (dcm/create-team-access-request
(with-meta params mdata))))))]
[:*
(if (some? file-id)
(if is-workspace
[:div {:class (stl/css :workspace)}
[:div {:class (stl/css :workspace-left)}
i/logo-icon
[:div
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
[:div {:class (stl/css :workspace-right)}]]
(cond
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:div {:class (stl/css :viewer)}
;; FIXME: the viewer header was never designed to be reused
;; from other parts of the application, and this code looks
;; like a fast workaround reusing it as-is without a proper
;; component adaptation for be able to use it easily it on
;; viewer context or static error page context
[:& viewer.header/header {:project
{:name (tr "not-found.no-permission.project-name")}
:index 0
:file {:name (tr "not-found.no-permission.penpot-file")}
:page nil
:frame nil
:permissions {:is-logged true}
:zoom 1
:section :interactions
:shown-thumbnails false
:interactions-mode nil}]])
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
[:> sidebar*
{:team nil
:projects []
:project (:default-project-id profile)
:profile profile
:section :dashboard-projects
:search-term ""}]]])
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(when @show-dialog
(cond
(nil? profile)
[:& login-dialog {:show-dialog show-dialog}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
is-default
[:& request-dialog {:title (tr "not-found.no-permission.project")
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(and (some? file-id) (:already-requested requested))
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file")
:content [(tr "not-found.no-permission.already-requested.or-others.file")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:already-requested requested)
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project")
:content [(tr "not-found.no-permission.already-requested.or-others.project")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(:sent requested)
[:& request-dialog {:title (tr "not-found.no-permission.done.success")
:content [(tr "not-found.no-permission.done.remember")]
:button-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? file-id)
[:& request-dialog {:title (tr "not-found.no-permission.file")
:content [(tr "not-found.no-permission.you-can-ask.file")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}]))]))
(some? team-id)
[:& request-dialog {:title (tr "not-found.no-permission.project")
:content [(tr "not-found.no-permission.you-can-ask.project")
(tr "not-found.no-permission.if-approves")]
:button-text (tr "not-found.no-permission.ask")
:on-button-click on-request-access
:cancel-text (tr "not-found.no-permission.go-dashboard")
:on-close on-close}])))
(mf/defc not-found*
[]
@@ -484,29 +436,77 @@
[:> internal-error* props])))
(mf/defc context-wrapper*
[{:keys [is-workspace is-dashboard is-viewer profile children]}]
[:*
(cond
is-workspace
[:div {:class (stl/css :workspace)}
[:div {:class (stl/css :workspace-left)}
i/logo-icon
[:div
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
[:div {:class (stl/css :workspace-right)}]]
is-viewer
[:div {:class (stl/css :viewer)}
;; FIXME: the viewer header was never designed to be reused
;; from other parts of the application, and this code looks
;; like a fast workaround reusing it as-is without a proper
;; component adaptation for be able to use it easily it on
;; viewer context or static error page context
[:& viewer.header/header {:project
{:name (tr "not-found.no-permission.project-name")}
:index 0
:file {:name (tr "not-found.no-permission.penpot-file")}
:page nil
:frame nil
:permissions {:is-logged true}
:zoom 1
:section :interactions
:shown-thumbnails false
:interactions-mode nil}]]
is-dashboard
[:div {:class (stl/css :dashboard)}
[:div {:class (stl/css :dashboard-sidebar)}
[:> sidebar*
{:team nil
:projects []
:project (:default-project-id profile)
:profile profile
:section :dashboard-projects
:search-term ""}]]])
children])
(mf/defc exception-page*
{::mf/props :obj}
[{:keys [data route] :as props}]
(let [type (:type data)
path (:path route)
(let [type (:type data)
path (:path route)
params (:query-params route)
params (:query-params route)
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view")
;; We store the request access info int this state
info* (mf/use-state nil)
info (deref info*)
info* (mf/use-state nil)
info (deref info*)
loaded? (get info :loaded false)
loaded? (get info :loaded false)
profile (mf/deref refs/profile)
auth-error?
(= type :authentication)
request-access?
(and
(or (= type :not-found)
(= type :authentication))
(or (= type :not-found) auth-error?)
(or workspace? dashboard? view?)
(or (:file-id info)
(:team-id info)))]
@@ -517,11 +517,25 @@
(rx/subs! (partial reset! info*)
(partial reset! info* {:loaded true})))))
(when loaded?
(if request-access?
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:is-workspace workspace?}]
[:> exception-section* props]))))
(if auth-error?
[:> context-wrapper*
{:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> login-dialog* {}]]
(when loaded?
(if request-access?
[:> context-wrapper* {:is-workspace workspace?
:is-dashboard dashboard?
:is-viewer view?
:profile profile}
[:> request-access* {:file-id (:file-id info)
:team-id (:team-id info)
:is-default (:team-default info)
:is-workspace workspace?}]]
[:> exception-section* props])))))

View File

@@ -593,8 +593,7 @@
close-dialog
(mf/use-fn
(fn [_]
(modal/hide!)
(modal/disallow-click-outside!)))
(modal/hide!)))
selected-tab*
(mf/use-state #(d/nilv starting-tab "libraries"))

View File

@@ -129,8 +129,7 @@
(mf/use-fn
(mf/deps file-id)
(fn []
(modal/show! :libraries-dialog {:file-id file-id})
(modal/allow-click-outside!)))
(modal/show! :libraries-dialog {:file-id file-id})))
on-open-menu
(mf/use-fn #(swap! filters* update :open-menu not))

View File

@@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.uuid :as uuid]
[app.main.data.workspace :as udw]
[app.main.data.workspace.shapes :as dwsh]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
@@ -49,20 +50,23 @@
handle-add
(mf/use-fn
(mf/deps change!)
(mf/deps change! ids)
(fn []
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(assoc % :blur (create-blur)))))
handle-delete
(mf/use-fn
(mf/deps change!)
(mf/deps change! ids)
(fn []
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(dissoc % :blur))))
handle-change
(mf/use-fn
(mf/deps change!)
(mf/deps change! ids)
(fn [value]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(cond-> %
(not (contains? % :blur))
(assoc :blur (create-blur))
@@ -72,8 +76,9 @@
handle-toggle-visibility
(mf/use-fn
(mf/deps change!)
(mf/deps change! ids)
(fn []
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(update-in % [:blur :hidden] not))))]
[:div {:class (stl/css :element-set)}

View File

@@ -11,6 +11,7 @@
[app.common.types.fill :as types.fill]
[app.common.types.shape.attrs :refer [default-color]]
[app.config :as cfg]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]]
@@ -114,6 +115,7 @@
(mf/deps ids multiple? empty-fills?)
(fn [_]
(when can-add-fills?
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/add-fill ids {:color default-color
:opacity 1}))
(when (or multiple? empty-fills?)
@@ -124,6 +126,7 @@
(mf/deps ids)
(fn [color index]
(let [color (select-keys color ctc/color-attrs)]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-fill ids color index)))))
on-reorder

View File

@@ -90,6 +90,7 @@
(wasm.api/use-shape (:id shape))
(wasm.api/set-shape-blend-mode value)))
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dw/set-preview-blend-mode ids value))))
handle-blend-mode-leave
@@ -101,33 +102,38 @@
handle-opacity-change
(mf/use-fn
(mf/deps on-change)
(mf/deps on-change ids)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(let [value (/ value 100)]
(on-change :opacity value))))
handle-set-hidden
(mf/use-fn
(mf/deps on-change)
(mf/deps on-change ids)
(fn [_]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(on-change :hidden true)))
handle-set-visible
(mf/use-fn
(mf/deps on-change)
(mf/deps on-change ids)
(fn [_]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(on-change :hidden false)))
handle-set-blocked
(mf/use-fn
(mf/deps on-change)
(mf/deps on-change ids)
(fn [_]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(on-change :blocked true)))
handle-set-unblocked
(mf/use-fn
(mf/deps on-change)
(mf/deps on-change ids)
(fn [_]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(on-change :blocked false)))
options

View File

@@ -12,6 +12,7 @@
[app.common.data.macros :as dm]
[app.common.types.shape.shadow :as ctss]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.undo :as dwu]
@@ -80,31 +81,56 @@
(mf/use-fn (mf/deps index) #(on-remove index))
on-update-offset-x
(mf/use-fn (mf/deps index) #(on-update index :offset-x %))
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-x value)))
on-update-offset-y
(mf/use-fn (mf/deps index) #(on-update index :offset-y %))
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :offset-y value)))
on-update-spread
(mf/use-fn (mf/deps index) #(on-update index :spread %))
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :spread value)))
on-update-blur
(mf/use-fn (mf/deps index) #(on-update index :blur %))
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :blur value)))
on-update-color
(mf/use-fn
(mf/deps index on-update)
(fn [color]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :color color)))
on-detach-color
(mf/use-fn (mf/deps index) #(on-detach-color index))
on-style-change
(mf/use-fn (mf/deps index) #(on-update index :style (keyword %)))
(mf/use-fn
(mf/deps index)
(fn [value]
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-update index :style (keyword value))))
on-toggle-visibility
(mf/use-fn (mf/deps index) #(on-toggle-visibility index))
(mf/use-fn
(mf/deps index)
(fn []
(st/emit! (dw/trigger-bounding-box-cloaking [shadow-id]))
(on-toggle-visibility index)))
on-toggle-open
(mf/use-fn
@@ -242,18 +268,21 @@
(mf/use-fn
(fn []
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dwsh/update-shapes ids #(dissoc % :shadow))))))
handle-reorder
(mf/use-fn
(fn [new-index index]
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/reorder-shadows ids index new-index)))))
on-add-shadow
(mf/use-fn
(fn []
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/add-shadow ids (create-shadow))))))
on-detach-color
@@ -261,18 +290,22 @@
(fn [index]
(let [ids (mf/ref-val ids-ref)
f #(update-in % [:shadow index :color] dissoc :id :file-id :ref-id :ref-file)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dwsh/update-shapes ids f)))))
on-toggle-visibility
(mf/use-fn
(fn [index]
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dwsh/update-shapes ids #(update-in % [:shadow index :hidden] not))))))
on-remove
(mf/use-fn
(mf/deps ids)
(fn [index]
(let [ids (mf/ref-val ids-ref)]
(st/emit! (dw/trigger-bounding-box-cloaking ids))
(st/emit! (dwsh/update-shapes ids #(update % :shadow remove-shadow-by-index index))))))
on-update

View File

@@ -10,6 +10,7 @@
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]]
@@ -56,6 +57,7 @@
(mf/use-fn
(mf/deps ids)
(fn [index color]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-color ids color index))))
@@ -63,18 +65,21 @@
(mf/use-fn
(mf/deps ids)
(fn [index]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/remove-stroke ids index))))
handle-remove-all
(mf/use-fn
(mf/deps ids)
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/remove-all-strokes ids))))
on-color-detach
(mf/use-fn
(mf/deps ids)
(fn [index color]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(let [color (-> color
(dissoc :ref-id :ref-file))]
(st/emit! (dc/change-stroke-color ids color index)))))
@@ -84,22 +89,26 @@
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/reorder-strokes ids index new-index)))))
on-stroke-style-change
(mf/use-fn
(mf/deps ids)
(fn [index value]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-style value} index))))
on-stroke-alignment-change
(fn [index value]
(when-not (str/empty? value)
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-alignment value} index))))
on-stroke-width-change
(fn [index value]
(when-not (str/empty? value)
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-width value} index))))
open-caps-select
@@ -128,10 +137,12 @@
on-stroke-cap-start-change
(fn [index value]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-cap-start value} index)))
on-stroke-cap-end-change
(fn [index value]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-cap-end value} index)))
on-stroke-cap-switch
@@ -140,10 +151,12 @@
stroke-cap-end (get-in values [:strokes index :stroke-cap-end])]
(when (and (not= stroke-cap-start :multiple)
(not= stroke-cap-end :multiple))
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-cap-start stroke-cap-end
:stroke-cap-end stroke-cap-start} index)))))
on-add-stroke
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/add-stroke ids {:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black

View File

@@ -112,7 +112,6 @@
recent-fonts (mf/with-memo [state recent-fonts]
(filter-fonts state recent-fonts))
full-size? (boolean (and full-size show-recent))
select-next
@@ -131,6 +130,13 @@
(dom/prevent-default event)
(swap! selected get-prev-font fonts)))
on-select-and-close
(mf/use-fn
(mf/deps on-select on-close)
(fn [font]
(on-select font)
(on-close)))
on-key-down
(mf/use-fn
(mf/deps fonts)
@@ -139,7 +145,7 @@
(kbd/up-arrow? event) (select-prev event)
(kbd/down-arrow? event) (select-next event)
(kbd/esc? event) (on-close)
(kbd/enter? event) (on-close)
(kbd/enter? event) (do (on-select-and-close @selected))
:else (dom/focus! (mf/ref-val input)))))
on-filter-change
@@ -163,9 +169,6 @@
(when-let [index (:index @selected)]
(.scrollToRow ^js inst index))))
(mf/with-effect [@selected]
(on-select @selected))
(mf/with-effect []
(st/emit! (dsc/push-shortcuts :typography {}))
(fn []
@@ -178,6 +181,10 @@
#(let [offset (.getOffsetForRow ^js inst #js {:alignment "center" :index index})]
(.scrollToPosition ^js inst offset)))))
(mf/with-effect [(:term state) fonts]
(when (and (seq fonts) (not= (:id @selected) (:id (first fonts))))
(reset! selected (first fonts))))
[:div {:class (stl/css :font-selector)}
[:div {:class (stl/css-case :font-selector-dropdown true :font-selector-dropdown-full-size full-size?)}
[:div {:class (stl/css :header)}

View File

@@ -16,6 +16,7 @@
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.main.data.workspace :as dw]
[app.main.data.workspace.shapes :as dwsh]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
@@ -277,7 +278,7 @@
;; The side handler is always rendered horizontally and then rotated
(mf/defc resize-side-handler
{::mf/wrap-props false}
[{:keys [x y length align angle zoom position rotation transform on-resize color show-handler scale-text]}]
[{:keys [x y length align angle zoom position rotation transform on-resize color show-handler scale-text shape-id shape-type]}]
(let [height (/ resize-side-height zoom)
offset-y (if (= align :outside) (- height) (- (/ height 2)))
target-y (+ y offset-y)
@@ -289,7 +290,18 @@
(cur/get-dynamic "resize-ew" rotation))
(if ^boolean scale-text
(cur/get-dynamic "scale-ns" rotation)
(cur/get-dynamic "resize-ns" rotation)))]
(cur/get-dynamic "resize-ns" rotation)))
on-double-click
(mf/use-fn
(mf/deps shape-id position shape-type)
(fn [_event]
(when (= shape-type :text)
(cond
(= position :right)
(st/emit! (dwsh/update-shapes [shape-id] #(assoc % :grow-type :auto-width)))
(= position :bottom)
(st/emit! (dwsh/update-shapes [shape-id] #(assoc % :grow-type :auto-height)))))))]
[:g.resize-handler
(when ^boolean show-handler
@@ -311,6 +323,7 @@
:data-position (name position)
:transform transform-str
:on-pointer-down on-resize
:on-double-click on-double-click
:style {:fill (if (dbg/enabled? :handlers) "yellow" "none")
:stroke-width 0}}]]))
@@ -399,7 +412,9 @@
:on-resize on-resize
:transform transform
:rotation rotation
:color color}
:color color
:shape-id (dm/get-prop shape :id)
:shape-type (dm/get-prop shape :type)}
props)]
(case type
:rotation [:> rotation-handler props]
@@ -482,16 +497,36 @@
{::mf/private true}
[{:keys [shape zoom color disabled]}]
(let [shape-id (dm/get-prop shape :id)
grow-type (dm/get-prop shape :grow-type)
shape-type (dm/get-prop shape :type)
on-resize
(mf/use-fn
(mf/deps shape-id shape)
(mf/deps shape-id shape grow-type shape-type)
(fn [event]
(when (dom/left-mouse? event)
(dom/stop-propagation event)
(let [target (dom/get-current-target event)
position (-> (dom/get-data target "position")
(keyword))]
(cond
;; If text and in auto-width and the resize is horizontal, switch to auto-height and mark direction
(and (= shape-type :text)
(= grow-type :auto-width)
(or (= position :right) (= position :left)))
(st/emit! (dwsh/update-shapes [shape-id] #(-> % (assoc :grow-type :auto-height) (assoc :last-resize-direction :horizontal))))
;; If text and in auto-height and the resize is horizontal, mark direction but do not change grow-type
(and (= shape-type :text)
(= grow-type :auto-height)
(or (= position :right) (= position :left)))
(st/emit! (dwsh/update-shapes [shape-id] #(assoc % :last-resize-direction :horizontal)))
;; If text and in auto-height and the resize is vertical, mark direction
(and (= shape-type :text)
(= grow-type :auto-height)
(or (= position :top) (= position :bottom)))
(st/emit! (dwsh/update-shapes [shape-id] #(assoc % :last-resize-direction :vertical)))
:else
nil)
(st/emit! (dw/start-resize position #{shape-id} shape))))))
on-rotate

View File

@@ -891,3 +891,12 @@
(defn last-child
[^js node]
(.. node -lastChild))
(defn prevent-browser-gesture-navigation!
[]
;; Prevent the browser from interpreting trackpad horizontal swipe as back/forth
;;
;; In theory We could disable this only for the workspace. However gets too unreliable.
;; It is better to be safe and disable for the dashboard as well.
(set! (.. js/document -documentElement -style -overscrollBehaviorX) "none")
(set! (.. js/document -body -style -overscrollBehaviorX) "none"))

View File

@@ -11,6 +11,7 @@
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as dc]
@@ -394,3 +395,60 @@
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 10))
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 4))
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 0)))))))))
(t/deftest duplicate-page-integrity-frame-group-component
;; This test covers the bug fixed in 2.9.0: duplicating a page with a mainInstance inside a group
;; must preserve parent/child referential integrity (parent-id and :shapes).
;;
;; Structure created:
;; Page
;; └─ Group ("group-1")
;; └─ Main component ("frame-1")
;; └─ Shape ("shape-1")
;; The frame is also promoted to a component (main).
;;
;; The test checks:
;; - The group, frame, and shape exist in the duplicated page.
;; - The parent/child relationships are correct (group:shapes contains frame, frame:shapes contains shape, etc).
;; - The duplicated page contains an instance of the component whose main is in the original page.
(t/async done
(with-redefs [uuid/next cthi/next-uuid]
(let [file (-> (cthf/sample-file :file1 :page-label :page-1)
(ctho/add-group :group-1 {:name "group-1"})
(ctho/add-frame :frame-1 :parent-label :group-1 {:name "frame-1"})
(cths/add-sample-shape :shape-1 :parent-label :frame-1 {:name "shape-1"})
(cthc/make-component :component-1 :frame-1))
page-id (cthf/current-page-id file)
store (ths/setup-store file)
events [(app.main.data.workspace.pages/duplicate-page page-id)]]
(ths/run-store
store done events
(fn [new-state]
(let [file' (ths/get-file-from-state new-state)
pages-vec (get-in file' [:data :pages])
pages-index (get-in file' [:data :pages-index])
new-page-id (first (remove #(= page-id %) pages-vec))
new-page (get pages-index new-page-id)
new-objects (:objects new-page)
group (some #(when (= (:name %) "group-1") %) (vals new-objects))
frame (some #(when (= (:name %) "frame-1") %) (vals new-objects))
shape (some #(when (= (:name %) "shape-1") %) (vals new-objects))
component-ids (map :component-id (filter :component-root (vals new-objects)))]
(t/is group "Group exists in duplicated page")
(t/is frame "Frame exists in duplicated page")
(t/is shape "Shape exists in duplicated page")
(t/is (some #(= (:id frame) %) (:shapes group)) "Group's :shapes contains frame's id")
(t/is (some #(= (:id shape) %) (:shapes frame)) "Frame's :shapes contains shape's id")
(t/is (= (:parent-id frame) (:id group)) "Frame's parent is group")
(t/is (= (:parent-id shape) (:id frame)) "Shape's parent is frame")
;; Check the duplicated page must contain an instance of the component whose main is in the original page
(let [original-page (get pages-index page-id)
original-main (some #(when (:component-root %) %) (vals (:objects original-page)))
instance (some #(when (:component-root %) %) (vals (:objects new-page)))
component-id (:component-id original-main)]
(t/is (ctk/instance-of? instance (:id file) component-id)
(str "Duplicated page contains an instance of the original main component (component-id: " component-id ")")))
(done))))))))

View File

@@ -2919,9 +2919,7 @@ msgstr "Emails, comma separated"
#: src/app/main/ui/dashboard/team.cljs:228
msgid "modals.invite-member.repeated-invitation"
msgstr ""
"Some emails are from current team members. Their invitations will not be "
"sent."
msgstr "Some members are already on the team. We'll invite the rest."
#: src/app/main/ui/dashboard/team.cljs:221
msgid "modals.invite-team-member.text"

View File

@@ -460,37 +460,44 @@ impl RenderState {
});
let text_content = text_content.new_bounds(shape.selrect());
let paragraphs = text_content.get_skia_paragraphs(self.fonts.font_collection());
let mut paragraphs = text_content.get_skia_paragraphs();
shadows::render_text_drop_shadows(self, &shape, &paragraphs, antialias);
text::render(self, &shape, &paragraphs, None);
shadows::render_text_drop_shadows(self, &shape, &mut paragraphs, antialias);
text::render(self, &shape, &mut paragraphs, None);
if shape.has_inner_strokes() {
// Inner strokes paints need the text fill to apply correctly their blend modes
// (e.g., SrcATop, DstOver)
text::render(self, &shape, &paragraphs, Some(SurfaceId::Strokes));
text::render(self, &shape, &mut paragraphs, Some(SurfaceId::Strokes));
}
for stroke in shape.strokes().rev() {
let stroke_paragraphs = text_content.get_skia_stroke_paragraphs(
stroke,
&shape.selrect(),
self.fonts.font_collection(),
let mut stroke_paragraphs =
text_content.get_skia_stroke_paragraphs(stroke, &shape.selrect());
shadows::render_text_drop_shadows(
self,
&shape,
&mut stroke_paragraphs,
antialias,
);
shadows::render_text_drop_shadows(self, &shape, &stroke_paragraphs, antialias);
strokes::render(
self,
&shape,
stroke,
None,
None,
Some(&stroke_paragraphs),
Some(&mut stroke_paragraphs),
antialias,
);
shadows::render_text_inner_shadows(
self,
&shape,
&mut stroke_paragraphs,
antialias,
);
shadows::render_text_inner_shadows(self, &shape, &stroke_paragraphs, antialias);
}
shadows::render_text_inner_shadows(self, &shape, &paragraphs, antialias);
shadows::render_text_inner_shadows(self, &shape, &mut paragraphs, antialias);
}
_ => {
let surface_ids = SurfaceId::Strokes as u32

View File

@@ -2,7 +2,8 @@ use super::{RenderState, SurfaceId};
use crate::render::strokes;
use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type};
use skia_safe::{canvas::SaveLayerRec, textlayout::Paragraph, Paint, Path};
use skia_safe::textlayout::ParagraphBuilder;
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
// Fill Shadows
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
@@ -88,7 +89,7 @@ pub fn render_stroke_inner_shadows(
pub fn render_text_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paragraphs: &mut [ParagraphBuilder],
antialias: bool,
) {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
@@ -123,7 +124,7 @@ pub fn render_text_drop_shadow(
render_state: &mut RenderState,
shape: &Shape,
shadow: &Shadow,
paragraphs: &[Vec<Paragraph>],
paragraphs: &mut [ParagraphBuilder],
antialias: bool,
) {
let paint = shadow.get_drop_shadow_paint(antialias);
@@ -145,7 +146,7 @@ pub fn render_text_drop_shadow(
pub fn render_text_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paragraphs: &mut [ParagraphBuilder],
antialias: bool,
) {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
@@ -157,7 +158,7 @@ pub fn render_text_inner_shadow(
render_state: &mut RenderState,
shape: &Shape,
shadow: &Shadow,
paragraphs: &[Vec<Paragraph>],
paragraphs: &mut [ParagraphBuilder],
antialias: bool,
) {
let paint = shadow.get_inner_shadow_paint(antialias);

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use crate::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
use skia_safe::{self as skia, textlayout::Paragraph, ImageFilter, RRect};
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, RRect};
use super::{RenderState, SurfaceId};
use crate::render::text::{self};
@@ -485,7 +485,7 @@ pub fn render(
stroke: &Stroke,
surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>,
paragraphs: Option<&[Vec<Paragraph>]>,
paragraphs: Option<&mut [ParagraphBuilder]>,
antialias: bool,
) {
let scale = render_state.get_scale();

View File

@@ -1,35 +1,129 @@
use super::{RenderState, Shape, SurfaceId};
use crate::shapes::VerticalAlign;
use skia_safe::{textlayout::Paragraph, Paint, Path};
use skia_safe::{textlayout::ParagraphBuilder, FontMetrics, Paint, Path};
pub fn render(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &[Vec<Paragraph>],
paragraphs: &mut [ParagraphBuilder],
surface_id: Option<SurfaceId>,
) {
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
let mut offset_y = 0.0;
let container_height = shape.selrect().height();
for group in paragraphs {
let total_paragraphs_height: f32 = group.iter().map(|p| p.height()).sum();
let mut offset_y = match shape.vertical_align() {
VerticalAlign::Center => (container_height - total_paragraphs_height) / 2.0,
VerticalAlign::Bottom => container_height - total_paragraphs_height,
for builder in paragraphs {
let mut skia_paragraph = builder.build();
skia_paragraph.layout(shape.bounds().width());
let paragraph_height: f32 = skia_paragraph.height();
let paragraph_offset_y = match shape.vertical_align() {
VerticalAlign::Center => (container_height - paragraph_height) / 2.0,
VerticalAlign::Bottom => container_height - paragraph_height,
_ => 0.0,
};
for skia_paragraph in group {
let xy = (shape.selrect().x(), shape.selrect.y() + offset_y);
skia_paragraph.paint(canvas, xy);
offset_y += skia_paragraph.height();
offset_y += paragraph_offset_y;
let xy = (shape.selrect().x(), shape.selrect().y() + offset_y);
skia_paragraph.paint(canvas, xy);
offset_y += paragraph_height;
for line_metrics in skia_paragraph.get_line_metrics().iter() {
let style_metrics: Vec<_> = line_metrics
.get_style_metrics(line_metrics.start_index..line_metrics.end_index)
.into_iter()
.collect();
let mut current_x_offset = 0.0;
let total_line_width = line_metrics.width as f32;
let total_chars = line_metrics.end_index - line_metrics.start_index;
// No text decoration for empty lines
if total_chars == 0 || style_metrics.is_empty() {
continue;
}
for (i, (index, style_metric)) in style_metrics.iter().enumerate() {
let text_style = style_metric.text_style;
let font_metrics = style_metric.font_metrics;
let next_index = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line_metrics.end_index);
let char_count = next_index - index;
let segment_width = if total_chars > 0 {
(char_count as f32 / total_chars as f32) * total_line_width
} else {
char_count as f32 * font_metrics.avg_char_width
};
if text_style.decoration().ty
!= skia_safe::textlayout::TextDecoration::NO_DECORATION
{
let decoration_type = text_style.decoration().ty;
let text_left = xy.0 + current_x_offset;
let text_top = xy.1 + line_metrics.baseline as f32 - line_metrics.ascent as f32;
let text_width = segment_width;
let line_height = line_metrics.height as f32;
let r = calculate_text_decoration_rect(
decoration_type,
font_metrics,
text_left,
text_top,
text_width,
line_height,
);
if let Some(decoration_rect) = r {
let decoration_paint = text_style.foreground();
canvas.draw_rect(decoration_rect, &decoration_paint);
}
}
current_x_offset += segment_width;
}
}
}
}
pub fn calculate_text_decoration_rect(
decoration: skia_safe::textlayout::TextDecoration,
font_metrics: FontMetrics,
blob_left: f32,
blob_offset_y: f32,
text_width: f32,
blob_height: f32,
) -> Option<skia_safe::Rect> {
let thickness = font_metrics.underline_thickness().unwrap_or(1.0);
match decoration {
skia_safe::textlayout::TextDecoration::LINE_THROUGH => {
let line_position = blob_height / 2.0;
Some(skia_safe::Rect::new(
blob_left,
blob_offset_y + line_position - thickness / 2.0,
blob_left + text_width,
blob_offset_y + line_position + thickness / 2.0,
))
}
skia_safe::textlayout::TextDecoration::UNDERLINE => {
let underline_y = blob_offset_y + blob_height - thickness;
Some(skia_safe::Rect::new(
blob_left,
underline_y,
blob_left + text_width,
underline_y + thickness,
))
}
_ => None,
}
}
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_as_path(

View File

@@ -164,8 +164,6 @@ fn propagate_transform(
};
let shapes = &state.shapes;
let font_col = state.render_state.fonts.font_collection();
let shape_bounds_before = bounds.find(shape);
let mut shape_bounds_after = shape_bounds_before.transform(&entry.transform);
@@ -173,9 +171,9 @@ fn propagate_transform(
if let Type::Text(content) = &shape.shape_type {
if content.grow_type() == GrowType::AutoHeight {
let mut paragraphs = content.get_skia_paragraphs(font_col);
let mut paragraphs = content.get_skia_paragraphs();
set_paragraphs_width(shape_bounds_after.width(), &mut paragraphs);
let height = auto_height(&paragraphs);
let height = auto_height(&mut paragraphs, shape_bounds_after.width());
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,

View File

@@ -5,13 +5,13 @@ use crate::{
use skia_safe::{
self as skia,
paint::Paint,
textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle},
textlayout::{ParagraphBuilder, ParagraphStyle},
};
use std::collections::HashSet;
use super::FontFamily;
use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind};
use crate::utils::{get_fallback_fonts, uuid_from_u32};
use crate::utils::{get_fallback_fonts, get_font_collection, uuid_from_u32};
use crate::wasm::fills::parse_fills_from_bytes;
use crate::Uuid;
@@ -40,15 +40,14 @@ pub struct TextContent {
pub grow_type: GrowType,
}
pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec<Vec<skia::textlayout::Paragraph>>) {
for group in paragraphs {
for paragraph in group {
// We first set max so we can get the min_intrinsic_width (this is the min word size)
// then after we set either the real with or the min.
// This is done this way so the words are not break into lines.
paragraph.layout(f32::MAX);
paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
}
pub fn set_paragraphs_width(width: f32, paragraphs: &mut [ParagraphBuilder]) {
for p in paragraphs {
// We first set max so we can get the min_intrinsic_width (this is the min word size)
// then after we set either the real with or the min.
// This is done this way so the words are not break into lines.
let mut paragraph = p.build();
paragraph.layout(f32::MAX);
paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil()));
}
}
@@ -94,67 +93,61 @@ impl TextContent {
self.paragraphs.push(paragraph);
}
pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec<Vec<skia::textlayout::Paragraph>> {
pub fn to_paragraphs(&self) -> Vec<ParagraphBuilder> {
let fonts = get_font_collection();
let fallback_fonts = get_fallback_fonts();
let mut paragraph_group = Vec::new();
let paragraphs = self
.paragraphs
self.paragraphs
.iter()
.map(|p| {
let paragraph_style = p.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in &p.children {
let text_style = leaf.to_style(p, &self.bounds, fallback_fonts); // FIXME
let text_style = leaf.to_style(p, &self.bounds, fallback_fonts);
let text = leaf.apply_text_transform();
builder.push_style(&text_style);
builder.add_text(&text);
builder.pop();
}
builder.build()
builder
})
.collect();
paragraph_group.push(paragraphs);
paragraph_group
.collect()
}
pub fn to_stroke_paragraphs(
&self,
stroke: &Stroke,
bounds: &Rect,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
pub fn to_stroke_paragraphs(&self, stroke: &Stroke, bounds: &Rect) -> Vec<ParagraphBuilder> {
let fallback_fonts = get_fallback_fonts();
let mut paragraph_group = Vec::new();
let stroke_paints = get_text_stroke_paints(stroke, bounds);
let fonts = get_font_collection();
for stroke_paint in stroke_paints {
let mut stroke_paragraphs = Vec::new();
for paragraph in &self.paragraphs {
let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in &paragraph.children {
let stroke_style =
leaf.to_stroke_style(paragraph, &stroke_paint, fallback_fonts);
let text: String = leaf.apply_text_transform();
builder.push_style(&stroke_style);
builder.add_text(&text);
builder.pop();
}
let p = builder.build();
stroke_paragraphs.push(p);
}
paragraph_group.push(stroke_paragraphs);
}
paragraph_group
stroke_paints
.into_iter()
.flat_map(|stroke_paint| {
self.paragraphs
.iter()
.map(|paragraph| {
let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in &paragraph.children {
let stroke_style =
leaf.to_stroke_style(paragraph, &stroke_paint, fallback_fonts);
let text: String = leaf.apply_text_transform();
builder.push_style(&stroke_style);
builder.add_text(&text);
builder.pop();
}
builder
})
.collect::<Vec<_>>()
})
.collect()
}
pub fn collect_paragraphs(
&self,
mut paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
mut paragraphs: Vec<ParagraphBuilder>,
) -> Vec<ParagraphBuilder> {
if self.grow_type() == GrowType::AutoWidth {
set_paragraphs_width(f32::MAX, &mut paragraphs);
let max_width = auto_width(&paragraphs).ceil();
let max_width = auto_width(&mut paragraphs).ceil();
set_paragraphs_width(max_width, &mut paragraphs);
} else {
set_paragraphs_width(self.width(), &mut paragraphs);
@@ -162,20 +155,16 @@ impl TextContent {
paragraphs
}
pub fn get_skia_paragraphs(
&self,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
self.collect_paragraphs(self.to_paragraphs(fonts))
pub fn get_skia_paragraphs(&self) -> Vec<ParagraphBuilder> {
self.collect_paragraphs(self.to_paragraphs())
}
pub fn get_skia_stroke_paragraphs(
&self,
stroke: &Stroke,
bounds: &Rect,
fonts: &FontCollection,
) -> Vec<Vec<skia::textlayout::Paragraph>> {
self.collect_paragraphs(self.to_stroke_paragraphs(stroke, bounds, fonts))
) -> Vec<ParagraphBuilder> {
self.collect_paragraphs(self.to_stroke_paragraphs(stroke, bounds))
}
pub fn grow_type(&self) -> GrowType {
@@ -286,6 +275,26 @@ impl Paragraph {
1 => skia::textlayout::TextDirection::RTL,
_ => skia::textlayout::TextDirection::LTR,
});
// Force minimum line height for empty lines using strut style
if !self.children.is_empty() {
let reference_child = self
.children
.iter()
.find(|child| !child.text.trim().is_empty())
.unwrap_or(&self.children[0]);
let mut strut_style = skia::textlayout::StrutStyle::default();
strut_style.set_font_size(reference_child.font_size);
strut_style.set_height(self.line_height);
strut_style.set_height_override(true);
strut_style.set_half_leading(false);
strut_style.set_leading(0.0);
strut_style.set_strut_enabled(true);
strut_style.set_force_strut_height(true);
style.set_strut_style(strut_style);
}
style
}
@@ -350,6 +359,8 @@ impl TextLeaf {
style.set_letter_spacing(paragraph.letter_spacing);
style.set_height(paragraph.line_height);
style.set_height_override(true);
style.set_half_leading(false);
style.set_decoration_type(match self.text_decoration {
0 => skia::textlayout::TextDecoration::NO_DECORATION,
1 => skia::textlayout::TextDecoration::UNDERLINE,
@@ -358,8 +369,8 @@ impl TextLeaf {
_ => skia::textlayout::TextDecoration::NO_DECORATION,
});
// FIXME fix decoration styles
style.set_decoration_color(paint.color());
// Trick to avoid showing the text decoration
style.set_decoration_thickness_multiplier(0.0);
let mut font_families = vec![
self.serialized_font_family(),
@@ -626,24 +637,28 @@ impl From<&Vec<u8>> for RawTextData {
}
}
pub fn auto_width(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> f32 {
paragraphs.iter().flatten().fold(0.0, |auto_width, p| {
f32::max(p.max_intrinsic_width(), auto_width)
pub fn auto_width(paragraphs: &mut [ParagraphBuilder]) -> f32 {
paragraphs.iter_mut().fold(0.0, |auto_width, p| {
let mut paragraph = p.build();
paragraph.layout(f32::MAX);
f32::max(paragraph.max_intrinsic_width(), auto_width)
})
}
pub fn max_width(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> f32 {
paragraphs
.iter()
.flatten()
.fold(0.0, |max_width, p| f32::max(p.max_width(), max_width))
pub fn max_width(paragraphs: &mut [ParagraphBuilder]) -> f32 {
paragraphs.iter_mut().fold(0.0, |max_width, p| {
let mut paragraph = p.build();
paragraph.layout(f32::MAX);
f32::max(paragraph.max_width(), max_width)
})
}
pub fn auto_height(paragraphs: &[Vec<skia::textlayout::Paragraph>]) -> f32 {
paragraphs
.iter()
.flatten()
.fold(0.0, |auto_height, p| auto_height + p.height())
pub fn auto_height(paragraphs: &mut [ParagraphBuilder], width: f32) -> f32 {
paragraphs.iter_mut().fold(0.0, |auto_height, p| {
let mut paragraph = p.build();
paragraph.layout(width);
auto_height + paragraph.height()
})
}
fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec<Paint> {
@@ -651,14 +666,9 @@ fn get_text_stroke_paints(stroke: &Stroke, bounds: &Rect) -> Vec<Paint> {
match stroke.kind {
StrokeKind::Inner => {
let mut paint = skia::Paint::default();
paint.set_blend_mode(skia::BlendMode::DstOver);
paint.set_anti_alias(true);
paints.push(paint);
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_blend_mode(skia::BlendMode::SrcATop);
paint.set_blend_mode(skia::BlendMode::SrcIn);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);

View File

@@ -1,4 +1,4 @@
use skia_safe::{self as skia, Path, Point};
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
use std::collections::HashMap;
mod shapes_pool;
@@ -167,6 +167,10 @@ impl State {
.rebuild_modifier_tiles(&self.shapes, &self.modifiers);
}
pub fn font_collection(&self) -> &FontCollection {
self.render_state.fonts().font_collection()
}
pub fn get_grid_coords(&self, pos_x: f32, pos_y: f32) -> Option<(i32, i32)> {
let shape = self.current_shape()?;
let bounds = shape.bounds();

View File

@@ -1,3 +1,4 @@
use crate::skia::textlayout::FontCollection;
use crate::skia::Image;
use crate::uuid::Uuid;
use crate::with_state_mut;
@@ -31,3 +32,7 @@ pub fn get_image(image_id: &Uuid) -> Option<&Image> {
pub fn get_fallback_fonts() -> &'static HashSet<String> {
with_state_mut!(state, { state.render_state().fonts().get_fallback() })
}
pub fn get_font_collection() -> &'static FontCollection {
with_state_mut!(state, { state.font_collection() })
}

View File

@@ -2,7 +2,7 @@ use crate::mem;
use crate::shapes::{auto_height, auto_width, max_width, GrowType, RawTextData, Type};
use crate::STATE;
use crate::{with_current_shape, with_current_shape_mut, with_state_mut};
use crate::{with_current_shape, with_current_shape_mut};
#[no_mangle]
pub extern "C" fn clear_shape_text() {
@@ -35,11 +35,6 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) {
#[no_mangle]
pub extern "C" fn get_text_dimensions() -> *mut u8 {
let font_col;
with_state_mut!(state, {
font_col = state.render_state.fonts.font_collection();
});
let mut width = 0.01;
let mut height = 0.01;
let mut m_width = 0.01;
@@ -49,16 +44,16 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
height = shape.selrect.height();
if let Type::Text(content) = &shape.shape_type {
let paragraphs = content.get_skia_paragraphs(font_col);
m_width = max_width(&paragraphs);
let mut paragraphs = content.get_skia_paragraphs();
m_width = max_width(&mut paragraphs);
match content.grow_type() {
GrowType::AutoHeight => {
height = auto_height(&paragraphs).ceil();
height = auto_height(&mut paragraphs, width).ceil();
}
GrowType::AutoWidth => {
width = auto_width(&paragraphs).ceil();
height = auto_height(&paragraphs).ceil();
width = auto_width(&mut paragraphs).ceil();
height = auto_height(&mut paragraphs, width).ceil();
}
GrowType::Fixed => {}
}