From 217d112aae48d647dd3cec450dacc962a25c4ae4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 30 Jun 2025 08:02:22 -0700 Subject: [PATCH 1/6] Update the readme with a new video --- README.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 629c9acdbd..39bc50abaf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ **Graphite is a free, open source vector and raster graphics engine, [available now](https://editor.graphite.rs) in alpha. Get creative with a fully nondestructive editing workflow that combines layer-based compositing with node-based generative design.** -Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies from the [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool. +Having begun life as a vector editor, Graphite continues evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. Photo editing, motion graphics, digital painting, desktop publishing, and VFX compositing are additional competencies on the planned [roadmap](https://graphite.rs/features/#roadmap) making Graphite into a highly versatile content creation tool. Learn more from the [website](https://graphite.rs/), subscribe to the [newsletter](https://graphite.rs/#newsletter), consider [volunteering](https://graphite.rs/volunteer/) or [donating](https://graphite.rs/donate/), and remember to give this repository a ⭐! @@ -58,22 +58,26 @@ Learn more from the [website](https://graphite.rs/), subscribe to the [newslette

+https://github.com/user-attachments/assets/e0b80e09-6f12-41e5-9183-8e9ee04343bd + +## Support our mission ❤️ + +Graphite is 100% community built and funded. Please become a part of keeping the project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our **mission**: + +> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all. +> +> Mission success will come when Graphite is an industry standard. A cohesive product vision and focus on innovation over imitation is the strategy that will make that possible. + ## Screenshots -!["Isometric Fountain" vector artwork](https://static.graphite.rs/content/index/gui-demo-node-graph-isometric-fountain.png) +![Made using nondestructive boolean operations and procedural polka dot patterns](https://github.com/user-attachments/assets/decb7011-18c2-4c68-82af-d1fa5064244a) -!["Marbled Mandelbrot" fractal raster artwork](https://static.graphite.rs/content/index/gui-demo-fractal__3.png) +![Mandelbrot fractal filled with a noise pattern, procedurally generated and infinitely scalable](https://github.com/user-attachments/assets/9e023997-185b-4f43-a724-797d308d9e7b) + +![Design for a magazine spread, a preview of the upcoming focus on desktop publishing](https://github.com/user-attachments/assets/90eca551-5868-4f8d-9016-33958bf96345) ## Contributing/building the code Are you a graphics programmer or Rust developer? Graphite aims to be one of the most approachable projects for putting your engineering skills to use in the world of open source. See [instructions here](https://graphite.rs/volunteer/guide/) for setting up the project and getting started. *By submitting code for inclusion in the project, you are agreeing to license your changes under the Apache 2.0 license, and that you have the authority to do so. Some directories may have other licenses, like dual-licensed MIT/Apache 2.0, and code submissions to those directories mean you agree to the applicable license(s).* - -## Support our mission ❤️ - -Graphite is 100% community built and funded. Please become a part of keeping our project alive and thriving with a [donation](https://graphite.rs/donate/) if you share a belief in our mission: - -> Graphite strives to unshackle the creativity of every budding artist and seasoned professional by building the best comprehensive art and design tool that's accessible to all. -> -> Mission success will come when Graphite is an industry standard. A cohesive product vision and focus on innovation over imitation is the strategy that will make that possible. From d7fbbac95ebaabaaa42d36be790c79e1705b6b63 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 30 Jun 2025 10:04:37 -0700 Subject: [PATCH 2/6] Add videos to the website home page and features page --- website/content/_index.md | 54 ++++++----- website/content/features.md | 17 +++- .../code-quality-guidelines.md | 4 +- website/sass/base.scss | 26 +++--- website/sass/page/features.scss | 10 +++ website/sass/page/index.scss | 90 ++++++++++++++----- 6 files changed, 143 insertions(+), 58 deletions(-) diff --git a/website/content/_index.md b/website/content/_index.md index 98d7c321c9..a22928a9c2 100644 --- a/website/content/_index.md +++ b/website/content/_index.md @@ -164,9 +164,23 @@ meta_description = "Open source free software. A vector graphics creativity suit --- - +
+
-Starting life as a vector editor, Graphite is evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, and beyond. +Starting life as a vector editor, Graphite is evolving into a generalized, all-in-one graphics toolbox that's built more like a game engine than a conventional creative app. The editor's tools wrap its node graph core, providing user-friendly workflows for vector, raster, animation, and beyond. + +Start creating + +
+
+ + + +
+
@@ -276,7 +290,23 @@ Once it's ready to shine, Graphite's code architecture is structured to deliver
- + + + + +
@@ -363,23 +393,7 @@ Graphite's representation of artwork as a node graph lets you customize, compose
- - - - - +
diff --git a/website/content/features.md b/website/content/features.md index 307e8bbc17..ad0e7796c9 100644 --- a/website/content/features.md +++ b/website/content/features.md @@ -2,7 +2,8 @@ title = "Graphite features" [extra] -css = ["/page/features.css", "/component/feature-box.css", "/component/feature-icons.css"] +css = ["/page/features.css", "/component/feature-box.css", "/component/feature-icons.css", "/component/youtube-embed.css"] +js = ["/js/youtube-embed.js"] +++
@@ -17,6 +18,20 @@ In 2025, stay tuned for performance improvements, native multiplatform desktop a
+
+
+ +
+
+
+ Rust-Powered Graphics Editor: How Graphite's Syntax Trees Revolutionize Image Editing +
+
+
+ +
+
+
diff --git a/website/content/volunteer/guide/starting-a-task/code-quality-guidelines.md b/website/content/volunteer/guide/starting-a-task/code-quality-guidelines.md index 375c8cf180..1ea8e0885f 100644 --- a/website/content/volunteer/guide/starting-a-task/code-quality-guidelines.md +++ b/website/content/volunteer/guide/starting-a-task/code-quality-guidelines.md @@ -35,7 +35,9 @@ Comments should usually be placed on a separate line above the code they are ref ## Blank lines -Please make a habit of grouping together related lines of code in blocks separated by blank lines. If you have dozens of lines comprising a single unbroken block of logic, you are likely not splitting it apart enough to aid readability. Find sensible places to partition the logic and insert blank lines between each. Roughly 10% of the code you write should ideally be blank lines, otherwise you are likely underutilizing them at the expense of readability. +Please make a habit of grouping together related lines of code in blocks separated by blank lines. These are like your paragraphs if you were writing a novel — they greatly aid readability and your copy editor would have significant concerns with your writing if they were absent. + +If you have dozens of lines comprising a single unbroken block of logic, you are likely not splitting it apart enough to aid readability. Find sensible places to partition the logic and insert blank lines between each. Roughly 10% of the code you write should ideally be blank lines, otherwise you are likely underutilizing them at the expense of readability. ## Imports diff --git a/website/sass/base.scss b/website/sass/base.scss index b0b7098ea5..d938d26a9a 100644 --- a/website/sass/base.scss +++ b/website/sass/base.scss @@ -151,19 +151,6 @@ body > .page { .heart.heart { // The same color is also used below in the SVG after the `%23` (URL-encoded `#`) color: #cc304f; - - &::after { - content: ""; - background-image: url('data:image/svg+xml;utf8,\ - \ - '); - display: inline-block; - width: 0.75em; - height: 0.75em; - margin-left: 0.25em; - margin-bottom: -0.1em; - vertical-align: baseline; - } } @media screen and (max-width: 1200px) { @@ -683,6 +670,19 @@ hr, } } +.heart::after { + content: ""; + background-image: url('data:image/svg+xml;utf8,\ + \ + '); + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + margin-bottom: -0.1em; + vertical-align: baseline; +} + // blockquote { // padding: 32px 80px; // background: rgba(0, 0, 0, 0.0625); diff --git a/website/sass/page/features.scss b/website/sass/page/features.scss index a305d811fb..41590b908a 100644 --- a/website/sass/page/features.scss +++ b/website/sass/page/features.scss @@ -1,3 +1,13 @@ +.video-container { + background: var(--color-fog); + + > div { + margin: calc(20 * var(--variable-px)) auto; + width: 100%; + max-width: 800px; + } +} + #roadmap { width: 100%; text-align: center; diff --git a/website/sass/page/index.scss b/website/sass/page/index.scss index 0997758b86..9c82d1f0f8 100644 --- a/website/sass/page/index.scss +++ b/website/sass/page/index.scss @@ -18,24 +18,25 @@ #tagline { h1 { span { - position: relative; + position: relative; - &::after { - content: ""; - pointer-events: none; - position: absolute; - left: 0; - right: 0; - top: 100%; - // Dimensions: 480x40 - height: 100%; - margin-top: -0.2em; - background: url("https://static.graphite.rs/textures/text-sketch-underline.png"); - background-repeat: no-repeat; - background-size: contain; + &::after { + content: ""; + pointer-events: none; + position: absolute; + left: 0; + right: 0; + top: 100%; + // Dimensions: 480x40 + height: 100%; + margin-top: -0.2em; + background: url("https://static.graphite.rs/textures/text-sketch-underline.png"); + background-repeat: no-repeat; + background-size: contain; + } } - }} - + } + p { font-size: 1.2rem; @@ -112,15 +113,61 @@ // ▛ OVERVIEW ▜ #overview { background-color: var(--color-cloud); + + .sizzle-video { + display: flex; + flex-wrap: nowrap; + max-width: 100%; + + .block { + min-width: 0; + flex-direction: row; + + &.text { + flex: 1 4 100%; + flex-direction: column; + + p:has(.button) { + margin-top: 20px; + } + } + + &.video { + flex: 0 1 fit-content; + } + } + + @media screen and (max-width: 900px) { + flex-wrap: wrap; + + .block.video { + flex: 1 1 100%; + justify-content: center; + } + } + + @media screen and (max-width: 1100px) { + p:has(.button) { + display: none; + } + } + } } // ▙ OVERVIEW ▟ +// ▛ DONATE ▜ +#donate { + h2 { + color: #cc304f; + } +} +// ▙ DONATE ▟ + // ▛ PROCEDURALISM ▜ #proceduralism { background-color: var(--color-slate); color: white; - margin-top: 0; - + .diptych { background: black; color: var(--color-fog); @@ -183,9 +230,6 @@ } // ▙ PROCEDURALISM ▟ -// ▛ DONATE ▜ -// ▙ DONATE ▟ - // ▛ NEWSLETTER ▜ #newsletter { background-color: var(--color-peach); @@ -355,7 +399,7 @@ margin: calc(20 * var(--variable-px)) auto; width: 100%; max-width: 800px; - + + p { margin-top: 0; } @@ -377,7 +421,7 @@ // ▛ RECENT NEWS ▜ #recent-news { background-color: var(--color-parchment); - + .banner img { width: 100%; height: auto; From d8e15aeb93c49286c11321118c14f93eff7afe1f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 30 Jun 2025 15:53:02 -0700 Subject: [PATCH 3/6] Update the video on the project readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39bc50abaf..e5ceda081e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Learn more from the [website](https://graphite.rs/), subscribe to the [newslette

-https://github.com/user-attachments/assets/e0b80e09-6f12-41e5-9183-8e9ee04343bd +https://github.com/user-attachments/assets/f4604aea-e8f1-45ce-9218-46ddc666f11d ## Support our mission ❤️ From 83773baa003fa4cd0d4a6b57c1d08e2e9e513361 Mon Sep 17 00:00:00 2001 From: Salman Abuhaimed <85521119+BKSalman@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:23:17 +0300 Subject: [PATCH 4/6] Replace Rustybuzz with Parley for text layout, and add text tilt parameter (#2739) * replace rustybuzz with parley for text layout handling change text input direction based on text direction * Code review * change default character spacing to 0 * add shear to text node this also adds migration code for documents that don't have shear * shear migration for text node - add shear property - set character spacing to 0 * use old max_width and max_height in text migration if available * Final code review pass * Add units to the parameters --------- Co-authored-by: Keavon Chambers --- .gitignore | 1 + .nix/flake.nix | 2 +- Cargo.lock | 340 ++++++++++++------ Cargo.toml | 3 +- .../document/graph_operation/utility_types.rs | 1 + .../node_graph/document_node_definitions.rs | 15 + .../utility_types/network_interface.rs | 3 +- .../messages/portfolio/document_migration.rs | 43 ++- .../graph_modification_utils.rs | 2 + .../common_functionality/utility_functions.rs | 6 +- .../messages/tool/tool_messages/text_tool.rs | 15 +- .../src/components/panels/Document.svelte | 4 + .../widgets/inputs/FieldInput.svelte | 1 + node-graph/gcore/Cargo.toml | 3 +- node-graph/gcore/src/text/font_cache.rs | 5 +- node-graph/gcore/src/text/to_path.rs | 310 ++++++++-------- node-graph/gcore/src/transform_nodes.rs | 4 +- node-graph/gstd/src/text.rs | 30 +- 18 files changed, 488 insertions(+), 300 deletions(-) diff --git a/.gitignore b/.gitignore index 5f1f0aaff2..5700f4e89b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ perf.data* profile.json flamegraph.svg .idea/ +.direnv diff --git a/.nix/flake.nix b/.nix/flake.nix index b74b64fb4b..fc0cb6cbe4 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -51,7 +51,7 @@ libraw - # Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/ + # Tauri dependencies: keep in sync with https://v2.tauri.app/start/prerequisites/#system-dependencies (under the NixOS tab) at-spi2-atk atkmm cairo diff --git a/Cargo.lock b/Cargo.lock index c7ae0cdb13..c93d3d4de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.5.2" @@ -486,11 +492,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.0", + "objc2 0.6.1", ] [[package]] @@ -528,9 +534,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] @@ -822,6 +828,12 @@ dependencies = [ "serde", ] +[[package]] +name = "color" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae467d04a8a8aea5d9a49018a6ade2e4221d92968e8ce55a48c0b1164e5f698" + [[package]] name = "color_quant" version = "1.1.0" @@ -1255,6 +1267,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1569,6 +1591,25 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-cache-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f8afb20c8069fd676d27b214559a337cc619a605d25a87baa90b49a06f3b18" +dependencies = [ + "bytemuck", + "thiserror 1.0.69", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -1592,6 +1633,29 @@ dependencies = [ "ttf-parser 0.24.1", ] +[[package]] +name = "fontique" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f97079e1293b8c1e9fb03a2875d328bd2ee8f3b95ce62959c0acc04049c708" +dependencies = [ + "bytemuck", + "fontconfig-cache-parser", + "hashbrown 0.15.4", + "icu_locid", + "memmap2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.1", + "peniko 0.4.0", + "read-fonts 0.29.3", + "roxmltree", + "smallvec", + "windows 0.58.0", + "windows-core 0.58.0", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -2109,7 +2173,7 @@ checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" dependencies = [ "bitflags 2.9.0", "gpu-descriptor-types", - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -2200,13 +2264,14 @@ dependencies = [ "node-macro", "num-derive", "num-traits", + "parley", "petgraph 0.7.1", "rand 0.9.0", "rand_chacha 0.9.0", "rustc-hash 2.1.1", - "rustybuzz 0.20.1", "serde", "serde_json", + "skrifa 0.32.0", "specta", "tinyvec", "tokio", @@ -2470,10 +2535,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2949,7 +3016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] @@ -3204,9 +3271,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" dependencies = [ "arrayvec", "serde", @@ -3282,7 +3349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3506,10 +3573,10 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "once_cell", "png", "serde", @@ -3790,9 +3857,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" dependencies = [ "objc2-encode 4.1.0", "objc2-exception-helper", @@ -3805,15 +3872,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.9.0", - "block2 0.6.0", + "block2 0.6.1", "libc", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", "objc2-core-image", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "objc2-quartz-core 0.3.0", ] @@ -3824,8 +3891,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", - "objc2-foundation 0.3.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] @@ -3835,18 +3902,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", - "objc2-foundation 0.3.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] name = "objc2-core-foundation" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", + "dispatch2", + "objc2 0.6.1", ] [[package]] @@ -3856,7 +3924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-core-foundation", "objc2-io-surface", ] @@ -3867,8 +3935,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" dependencies = [ - "objc2 0.6.0", - "objc2-foundation 0.3.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ba833d4a1cb1aac330f8c973fd92b6ff1858e4aef5cdd00a255eefb28022fb5" +dependencies = [ + "bitflags 2.9.0", + "objc2-core-foundation", ] [[package]] @@ -3906,14 +3984,14 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.0", - "block2 0.6.0", + "block2 0.6.1", "libc", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-core-foundation", ] @@ -3924,7 +4002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-core-foundation", ] @@ -3960,8 +4038,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", - "objc2-foundation 0.3.0", + "objc2 0.6.1", + "objc2-foundation 0.3.1", ] [[package]] @@ -3971,9 +4049,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" dependencies = [ "bitflags 2.9.0", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", ] [[package]] @@ -3983,11 +4061,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" dependencies = [ "bitflags 2.9.0", - "block2 0.6.0", - "objc2 0.6.0", + "block2 0.6.1", + "objc2 0.6.1", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", ] [[package]] @@ -4149,6 +4227,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parley" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e57638545cf2ba4c3e72cc5715e53b1880b829cc3dbefda3d1700c58efe723" +dependencies = [ + "fontique", + "hashbrown 0.15.4", + "peniko 0.4.0", + "skrifa 0.31.3", + "swash", +] + [[package]] name = "paste" version = "1.0.15" @@ -4180,7 +4271,18 @@ name = "peniko" version = "0.2.0" source = "git+https://github.com/linebender/peniko.git?rev=d114c62#d114c6292dbcfb03e7360692198be423168a0edd" dependencies = [ - "color", + "color 0.1.0", + "kurbo", + "smallvec", +] + +[[package]] +name = "peniko" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9" +dependencies = [ + "color 0.3.1", "kurbo", "smallvec", ] @@ -4968,7 +5070,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f" dependencies = [ "bytemuck", - "font-types", + "font-types 0.8.3", +] + +[[package]] +name = "read-fonts" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" +dependencies = [ + "bytemuck", + "font-types 0.9.0", +] + +[[package]] +name = "read-fonts" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91" +dependencies = [ + "bytemuck", + "font-types 0.9.0", ] [[package]] @@ -5261,26 +5383,8 @@ dependencies = [ "log", "smallvec", "ttf-parser 0.24.1", - "unicode-bidi-mirroring 0.3.0", - "unicode-ccc 0.3.0", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.9.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser 0.25.1", - "unicode-bidi-mirroring 0.4.0", - "unicode-ccc 0.4.0", + "unicode-bidi-mirroring", + "unicode-ccc", "unicode-properties", "unicode-script", ] @@ -5415,9 +5519,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -5446,9 +5550,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -5652,7 +5756,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.25.3", +] + +[[package]] +name = "skrifa" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" +dependencies = [ + "bytemuck", + "read-fonts 0.29.3", +] + +[[package]] +name = "skrifa" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97" +dependencies = [ + "bytemuck", + "read-fonts 0.30.1", ] [[package]] @@ -5675,9 +5799,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -5895,6 +6019,17 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "swash" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" +dependencies = [ + "skrifa 0.31.3", + "yazi", + "zeno", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -6005,9 +6140,9 @@ dependencies = [ "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "once_cell", "parking_lot", "raw-window-handle", @@ -6060,9 +6195,9 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "percent-encoding", "plist", "raw-window-handle", @@ -6266,9 +6401,9 @@ dependencies = [ "http", "jni", "log", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "once_cell", "percent-encoding", "raw-window-handle", @@ -6688,11 +6823,11 @@ dependencies = [ "dirs", "libappindicator", "muda", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "once_cell", "png", "serde", @@ -6720,9 +6855,6 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] [[package]] name = "typeid" @@ -6795,24 +6927,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - [[package]] name = "unicode-ccc" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - [[package]] name = "unicode-ident" version = "1.0.18" @@ -6900,7 +7020,7 @@ dependencies = [ "log", "pico-args", "roxmltree", - "rustybuzz 0.18.0", + "rustybuzz", "simplecss", "siphasher 1.0.1", "strict-num", @@ -6971,9 +7091,9 @@ dependencies = [ "bytemuck", "futures-intrusive", "log", - "peniko", + "peniko 0.2.0", "png", - "skrifa", + "skrifa 0.26.6", "static_assertions", "thiserror 2.0.12", "vello_encoding", @@ -6988,8 +7108,8 @@ source = "git+https://github.com/linebender/vello.git?rev=3275ec8#3275ec85d83118 dependencies = [ "bytemuck", "guillotiere", - "peniko", - "skrifa", + "peniko 0.2.0", + "skrifa 0.26.6", "smallvec", ] @@ -7556,10 +7676,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -8077,7 +8197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730" dependencies = [ "base64 0.22.1", - "block2 0.6.0", + "block2 0.6.1", "cookie", "crossbeam-channel", "dpi", @@ -8091,10 +8211,10 @@ dependencies = [ "kuchikiki", "libc", "ndk 0.9.0", - "objc2 0.6.0", + "objc2 0.6.1", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.1", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -8199,6 +8319,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + [[package]] name = "yoke" version = "0.7.5" @@ -8223,6 +8349,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 56c27ce6fe..1dab010003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,8 @@ rand_chacha = "0.9" glam = { version = "0.29", default-features = false, features = ["serde", "scalar-math", "debug-glam-assert"] } base64 = "0.22" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp"] } -rustybuzz = "0.20" +parley = "0.5.0" +skrifa = "0.32.0" pretty_assertions = "1.4.1" fern = { version = "0.7", features = ["colored"] } num_enum = "0.7" diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index d79a60b81d..245b2f41ef 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -190,6 +190,7 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)), Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)), Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)), + Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)), ]); let text_id = NodeId::new(); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index c426ac85a1..63a001ba57 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1556,6 +1556,7 @@ fn static_nodes() -> Vec { NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false), ], ..Default::default() }, @@ -1577,6 +1578,7 @@ fn static_nodes() -> Vec { "Line Height", "TODO", WidgetOverride::Number(NumberInputSettings { + unit: Some("x".to_string()), min: Some(0.), step: Some(0.1), ..Default::default() @@ -1586,6 +1588,7 @@ fn static_nodes() -> Vec { "Character Spacing", "TODO", WidgetOverride::Number(NumberInputSettings { + unit: Some(" px".to_string()), min: Some(0.), step: Some(0.1), ..Default::default() @@ -1595,6 +1598,7 @@ fn static_nodes() -> Vec { "Max Width", "TODO", WidgetOverride::Number(NumberInputSettings { + unit: Some(" px".to_string()), min: Some(1.), blank_assist: false, ..Default::default() @@ -1604,11 +1608,22 @@ fn static_nodes() -> Vec { "Max Height", "TODO", WidgetOverride::Number(NumberInputSettings { + unit: Some(" px".to_string()), min: Some(1.), blank_assist: false, ..Default::default() }), ), + PropertiesRow::with_override( + "Tilt", + "Faux italic", + WidgetOverride::Number(NumberInputSettings { + min: Some(-85.), + max: Some(85.), + unit: Some("°".to_string()), + ..Default::default() + }), + ), ], output_names: vec!["Vector".to_string()], ..Default::default() diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 04e2f0803d..2566650e70 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1161,8 +1161,7 @@ impl NodeNetworkInterface { .and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index)) } - pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId]) { - let row = ("", "TODO").into(); + pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId], row: PropertiesRow) { let _ = self .node_metadata_mut(node_id, network_path) .map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row)); diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index e8724966c1..5612a8a23d 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1,6 +1,7 @@ // TODO: Eventually remove this document upgrade code // This file contains lots of hacky code for upgrading old documents to the new format +use super::document::utility_types::network_interface::{NumberInputSettings, PropertiesRow, WidgetOverride}; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector}; @@ -308,8 +309,8 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ let node_definition = resolve_document_node_type(reference).unwrap(); let document_node = node_definition.default_node_template().document_node; document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); - document.network_interface.insert_input_properties_row(node_id, 8, network_path); - document.network_interface.insert_input_properties_row(node_id, 9, network_path); + document.network_interface.insert_input_properties_row(node_id, 8, network_path, ("", "TODO").into()); + document.network_interface.insert_input_properties_row(node_id, 9, network_path, ("", "TODO").into()); let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); @@ -402,7 +403,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 - if reference == "Text" && inputs_count != 8 { + if reference == "Text" && inputs_count != 9 { let node_definition = resolve_document_node_type(reference).unwrap(); let document_node = node_definition.default_node_template().document_node; document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); @@ -433,12 +434,44 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_ ); document.network_interface.set_input( &InputConnector::node(*node_id, 6), - NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), + if inputs_count >= 7 { + old_inputs[6].clone() + } else { + NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false) + }, network_path, ); document.network_interface.set_input( &InputConnector::node(*node_id, 7), - NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), + if inputs_count >= 8 { + old_inputs[7].clone() + } else { + NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false) + }, + network_path, + ); + document.network_interface.insert_input_properties_row( + node_id, + 9, + network_path, + PropertiesRow::with_override( + "Tilt", + "Faux italic", + WidgetOverride::Number(NumberInputSettings { + min: Some(-85.), + max: Some(85.), + unit: Some("°".to_string()), + ..Default::default() + }), + ), + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 8), + if inputs_count >= 9 { + old_inputs[8].clone() + } else { + NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false) + }, network_path, ); } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index e06d34b9e2..765bfc1eac 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -361,6 +361,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None }; let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None }; let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None }; + let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None }; let typesetting = TypesettingConfig { font_size, @@ -368,6 +369,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter max_width, character_spacing, max_height, + tilt, }; Some((text, font, typesetting)) } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index b7b95123c0..23829271b0 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -11,7 +11,7 @@ use crate::messages::tool::utility_types::ToolType; use bezier_rs::{Bezier, BezierHandles}; use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; -use graphene_std::text::{FontCache, load_face}; +use graphene_std::text::{FontCache, load_font}; use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType}; use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez}; @@ -70,8 +70,8 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH return Quad::from_box([DVec2::ZERO, DVec2::ZERO]); }; - let buzz_face = font_cache.get(font).map(|data| load_face(data)); - let far = graphene_std::text::bounding_box(text, buzz_face.as_ref(), typesetting, false); + let font_data = font_cache.get(font).map(|data| load_font(data)); + let far = graphene_std::text::bounding_box(text, font_data, typesetting, false); Quad::from_box([DVec2::ZERO, far]) } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 44814fc39b..e56e53d477 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -18,7 +18,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::text::{Font, FontCache, TypesettingConfig, lines_clipping, load_face}; +use graphene_std::text::{Font, FontCache, TypesettingConfig, lines_clipping, load_font}; use graphene_std::vector::style::Fill; #[derive(Default)] @@ -35,6 +35,7 @@ pub struct TextOptions { font_name: String, font_style: String, fill: ToolColorOptions, + tilt: f64, } impl Default for TextOptions { @@ -42,10 +43,11 @@ impl Default for TextOptions { Self { font_size: 24., line_height_ratio: 1.2, - character_spacing: 1., + character_spacing: 0., font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(), font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(), fill: ToolColorOptions::new_primary(), + tilt: 0., } } } @@ -468,8 +470,8 @@ impl Fsm for TextToolFsmState { transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(), }); if let Some(editing_text) = tool_data.editing_text.as_mut() { - let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data)); - let far = graphene_std::text::bounding_box(&tool_data.new_text, buzz_face.as_ref(), editing_text.typesetting, false); + let font_data = font_cache.get(&editing_text.font).map(|data| load_font(data)); + let far = graphene_std::text::bounding_box(&tool_data.new_text, font_data, editing_text.typesetting, false); if far.x != 0. && far.y != 0. { let quad = Quad::from_box([DVec2::ZERO, far]); let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad; @@ -517,8 +519,8 @@ impl Fsm for TextToolFsmState { // Draw red overlay if text is clipped let transformed_quad = layer_transform * bounds; if let Some((text, font, typesetting)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) { - let buzz_face = font_cache.get(font).map(|data| load_face(data)); - if lines_clipping(text.as_str(), buzz_face, typesetting) { + let font_data = font_cache.get(font).map(|data| load_font(data)); + if lines_clipping(text.as_str(), font_data, typesetting) { overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.)); } } @@ -784,6 +786,7 @@ impl Fsm for TextToolFsmState { max_width: constraint_size.map(|size| size.x), character_spacing: tool_options.character_spacing, max_height: constraint_size.map(|size| size.y), + tilt: tool_options.tilt, }, font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()), color: tool_options.fill.active_color(), diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index db91741442..70d4ea99ba 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -760,6 +760,8 @@ .text-input { word-break: break-all; + unicode-bidi: plaintext; + text-align: left; } .text-input div { @@ -773,6 +775,8 @@ overflow-wrap: anywhere; white-space: pre-wrap; word-break: normal; + unicode-bidi: plaintext; + text-align: left; display: inline-block; // Workaround to force Chrome to display the flashing text entry cursor when text is empty padding-left: 1px; diff --git a/frontend/src/components/widgets/inputs/FieldInput.svelte b/frontend/src/components/widgets/inputs/FieldInput.svelte index 2efc1353b9..b35279401a 100644 --- a/frontend/src/components/widgets/inputs/FieldInput.svelte +++ b/frontend/src/components/widgets/inputs/FieldInput.svelte @@ -158,6 +158,7 @@ background: none; color: var(--color-e-nearwhite); caret-color: var(--color-e-nearwhite); + unicode-bidi: plaintext; &::selection { background-color: var(--color-4-dimgray); diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 9d7a9dde3b..cc8b30c8b4 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -29,10 +29,11 @@ ctor = { workspace = true } rand_chacha = { workspace = true } bezier-rs = { workspace = true } specta = { workspace = true } -rustybuzz = { workspace = true } image = { workspace = true } half = { workspace = true } tinyvec = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } kurbo = { workspace = true } log = { workspace = true } base64 = { workspace = true } diff --git a/node-graph/gcore/src/text/font_cache.rs b/node-graph/gcore/src/text/font_cache.rs index a872166fb3..37a8bfc505 100644 --- a/node-graph/gcore/src/text/font_cache.rs +++ b/node-graph/gcore/src/text/font_cache.rs @@ -22,11 +22,12 @@ impl Default for Font { /// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)] pub struct FontCache { - /// Actual font file data used for rendering a font with ttf_parser and rustybuzz + /// Actual font file data used for rendering a font font_file_data: HashMap>, /// Web font preview URLs used for showing fonts when live editing preview_urls: HashMap, } + impl FontCache { /// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> { @@ -40,7 +41,7 @@ impl FontCache { } /// Try to get the bytes for a font - pub fn get<'a>(&'a self, font: &Font) -> Option<&'a Vec> { + pub fn get(&self, font: &Font) -> Option<&Vec> { self.resolve_font(font).and_then(|font| self.font_file_data.get(font)) } diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index 9988fcae3e..bfe60da86d 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,29 +1,67 @@ use crate::vector::PointId; use bezier_rs::{ManipulatorGroup, Subpath}; -use glam::DVec2; -use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder}; -use rustybuzz::{GlyphBuffer, UnicodeBuffer}; +use core::cell::RefCell; +use glam::{DAffine2, DVec2}; +use parley::fontique::Blob; +use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; +use skrifa::GlyphId; +use skrifa::instance::{LocationRef, NormalizedCoord, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::FontRef as ReadFontsRef; +use skrifa::{MetadataProvider, OutlineGlyph}; +use std::sync::Arc; -struct Builder { +// Thread-local storage avoids expensive re-initialization of font and layout contexts +// across multiple text rendering operations within the same thread +thread_local! { + static FONT_CONTEXT: RefCell = RefCell::new(FontContext::new()); + static LAYOUT_CONTEXT: RefCell> = RefCell::new(LayoutContext::new()); +} + +struct PathBuilder { current_subpath: Subpath, + glyph_subpaths: Vec>, other_subpaths: Vec>, - text_cursor: DVec2, - offset: DVec2, - ascender: f64, + origin: DVec2, scale: f64, id: PointId, } -impl Builder { +impl PathBuilder { fn point(&self, x: f32, y: f32) -> DVec2 { - self.text_cursor + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale + // Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down) + DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale + } + + fn set_origin(&mut self, x: f64, y: f64) { + self.origin = DVec2::new(x, y); + } + + fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option, skew: DAffine2) { + let location_ref = LocationRef::new(normalized_coords); + let settings = DrawSettings::unhinted(Size::new(size), location_ref); + glyph.draw(settings, self).unwrap(); + + // Apply transforms in correct order: style-based skew first, then user-requested skew + // This ensures font synthesis (italic) is applied before user transformations + for glyph_subpath in &mut self.glyph_subpaths { + if let Some(style_skew) = style_skew { + glyph_subpath.apply_transform(style_skew); + } + + glyph_subpath.apply_transform(skew); + } + + if !self.glyph_subpaths.is_empty() { + self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths)); + } } } -impl OutlineBuilder for Builder { +impl OutlinePen for PathBuilder { fn move_to(&mut self, x: f32, y: f32) { if !self.current_subpath.is_empty() { - self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); + self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); } self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id())); } @@ -47,36 +85,10 @@ impl OutlineBuilder for Builder { fn close(&mut self) { self.current_subpath.set_closed(true); - self.other_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); + self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false))); } } -fn font_properties(buzz_face: &rustybuzz::Face, font_size: f64, line_height_ratio: f64) -> (f64, f64, UnicodeBuffer) { - let scale = (buzz_face.units_per_em() as f64).recip() * font_size; - let line_height = font_size * line_height_ratio; - let buffer = UnicodeBuffer::new(); - (scale, line_height, buffer) -} - -fn push_str(buffer: &mut UnicodeBuffer, word: &str) { - buffer.push_str(word); -} - -fn wrap_word(max_width: Option, glyph_buffer: &GlyphBuffer, font_size: f64, character_spacing: f64, x_pos: f64, space_glyph: Option) -> bool { - if let Some(max_width) = max_width { - // We don't word wrap spaces (to match the browser) - let all_glyphs = glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()); - let non_space_glyphs = all_glyphs.take_while(|(_, info)| space_glyph != Some(GlyphId(info.glyph_id as u16))); - let word_length: f64 = non_space_glyphs.map(|(pos, _)| pos.x_advance as f64 * character_spacing).sum(); - let scaled_word_length = word_length * font_size; - - if scaled_word_length + x_pos > max_width { - return true; - } - } - false -} - #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub struct TypesettingConfig { pub font_size: f64, @@ -84,6 +96,7 @@ pub struct TypesettingConfig { pub character_spacing: f64, pub max_width: Option, pub max_height: Option, + pub tilt: f64, } impl Default for TypesettingConfig { @@ -91,163 +104,130 @@ impl Default for TypesettingConfig { Self { font_size: 24., line_height_ratio: 1.2, - character_spacing: 1., + character_spacing: 0., max_width: None, max_height: None, + tilt: 0., } } } -pub fn to_path(str: &str, buzz_face: Option, typesetting: TypesettingConfig) -> Vec> { - let Some(buzz_face) = buzz_face else { return vec![] }; - let space_glyph = buzz_face.glyph_index(' '); +fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) { + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); - let (scale, line_height, mut buffer) = font_properties(&buzz_face, typesetting.font_size, typesetting.line_height_ratio); + let run = glyph_run.run(); - let mut builder = Builder { + // User-requested tilt applied around baseline to avoid vertical displacement + // Translation ensures rotation point is at the baseline, not origin + let skew = DAffine2::from_translation(DVec2::new(0., run_y as f64)) + * DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.]) + * DAffine2::from_translation(DVec2::new(0., -run_y as f64)); + + let synthesis = run.synthesis(); + + // Font synthesis (e.g., synthetic italic) applied separately from user transforms + // This preserves the distinction between font styling and user transformations + let style_skew = synthesis.skew().map(|angle| { + DAffine2::from_translation(DVec2::new(0., run_y as f64)) + * DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.]) + * DAffine2::from_translation(DVec2::new(0., -run_y as f64)) + }); + + let font = run.font(); + let font_size = run.font_size(); + + let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::>(); + + // TODO: This can be cached for better performance + let font_collection_ref = font.data.as_ref(); + let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap(); + let outlines = font_ref.outline_glyphs(); + + for glyph in glyph_run.glyphs() { + let glyph_x = run_x + glyph.x; + let glyph_y = run_y - glyph.y; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + if let Some(glyph_outline) = outlines.get(glyph_id) { + path_builder.set_origin(glyph_x as f64, glyph_y as f64); + path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew); + } + } +} + +fn layout_text(str: &str, font_data: Option>, typesetting: TypesettingConfig) -> Option> { + let font_cx = FONT_CONTEXT.with(Clone::clone); + let mut font_cx = font_cx.borrow_mut(); + let layout_cx = LAYOUT_CONTEXT.with(Clone::clone); + let mut layout_cx = layout_cx.borrow_mut(); + + let font_family = font_data.and_then(|font_data| { + font_cx + .collection + .register_fonts(font_data, None) + .first() + .and_then(|(family_id, _)| font_cx.collection.family_name(*family_id).map(String::from)) + })?; + + const DISPLAY_SCALE: f32 = 1.; + let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, true); + + builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32)); + builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32)); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family))))); + builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32)); + + let mut layout: Layout<()> = builder.build(str); + + layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32)); + layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default()); + + Some(layout) +} + +pub fn to_path(str: &str, font_data: Option>, typesetting: TypesettingConfig) -> Vec> { + let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() }; + + let mut path_builder = PathBuilder { current_subpath: Subpath::new(Vec::new(), false), + glyph_subpaths: Vec::new(), other_subpaths: Vec::new(), - text_cursor: DVec2::ZERO, - offset: DVec2::ZERO, - ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * typesetting.font_size / scale, - scale, + origin: DVec2::ZERO, + scale: layout.scale() as f64, id: PointId::ZERO, }; - for line in str.split('\n') { - for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() { - push_str(&mut buffer, word); - let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer); - - // Don't wrap the first word - if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, builder.text_cursor.x, space_glyph) { - builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height); + for line in layout.lines() { + for item in line.items() { + if let PositionedLayoutItem::GlyphRun(glyph_run) = item { + render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt); } - - for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { - let glyph_id = GlyphId(glyph_info.glyph_id as u16); - if let Some(max_width) = typesetting.max_width { - if space_glyph != Some(glyph_id) && builder.text_cursor.x + (glyph_position.x_advance as f64 * builder.scale * typesetting.character_spacing) >= max_width { - builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height); - } - } - // Clip when the height is exceeded - if typesetting.max_height.is_some_and(|max_height| builder.text_cursor.y > max_height - line_height) { - return builder.other_subpaths; - } - - builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale; - buzz_face.outline_glyph(glyph_id, &mut builder); - if !builder.current_subpath.is_empty() { - builder.other_subpaths.push(std::mem::replace(&mut builder.current_subpath, Subpath::new(Vec::new(), false))); - } - - builder.text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * builder.scale; - } - - buffer = glyph_buffer.clear(); } - - builder.text_cursor = DVec2::new(0., builder.text_cursor.y + line_height); } - builder.other_subpaths + path_builder.other_subpaths } -pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { - // Show blank layer if font has not loaded - let Some(buzz_face) = buzz_face else { return DVec2::ZERO }; - let space_glyph = buzz_face.glyph_index(' '); - - let (scale, line_height, mut buffer) = font_properties(buzz_face, typesetting.font_size, typesetting.line_height_ratio); - - let [mut text_cursor, mut bounds] = [DVec2::ZERO; 2]; +pub fn bounding_box(str: &str, font_data: Option>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { if !for_clipping_test { if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) { return DVec2::new(max_width, max_height); } } - for line in str.split('\n') { - for (index, word) in SplitWordsIncludingSpaces::new(line).enumerate() { - push_str(&mut buffer, word); + let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO }; - let glyph_buffer = rustybuzz::shape(buzz_face, &[], buffer); - - // Don't wrap the first word - if index != 0 && wrap_word(typesetting.max_width, &glyph_buffer, scale, typesetting.character_spacing, text_cursor.x, space_glyph) { - text_cursor = DVec2::new(0., text_cursor.y + line_height); - } - - for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) { - let glyph_id = GlyphId(glyph_info.glyph_id as u16); - if let Some(max_width) = typesetting.max_width { - if space_glyph != Some(glyph_id) && text_cursor.x + (glyph_position.x_advance as f64 * scale * typesetting.character_spacing) >= max_width { - text_cursor = DVec2::new(0., text_cursor.y + line_height); - } - } - text_cursor += DVec2::new(glyph_position.x_advance as f64 * typesetting.character_spacing, glyph_position.y_advance as f64) * scale; - bounds = bounds.max(text_cursor + DVec2::new(0., line_height)); - } - - buffer = glyph_buffer.clear(); - } - text_cursor = DVec2::new(0., text_cursor.y + line_height); - bounds = bounds.max(text_cursor); - } - - if !for_clipping_test { - if let Some(max_width) = typesetting.max_width { - bounds.x = max_width; - } - if let Some(max_height) = typesetting.max_height { - bounds.y = max_height; - } - } - - bounds + DVec2::new(layout.full_width() as f64, layout.height() as f64) } -pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> { - rustybuzz::Face::from_slice(data, 0).expect("Loading font failed") +pub fn load_font(data: &[u8]) -> Blob { + Blob::new(Arc::new(data.to_vec())) } -pub fn lines_clipping(str: &str, buzz_face: Option, typesetting: TypesettingConfig) -> bool { +pub fn lines_clipping(str: &str, font_data: Option>, typesetting: TypesettingConfig) -> bool { let Some(max_height) = typesetting.max_height else { return false }; - let bounds = bounding_box(str, buzz_face.as_ref(), typesetting, true); + let bounds = bounding_box(str, font_data, typesetting, true); max_height < bounds.y } - -struct SplitWordsIncludingSpaces<'a> { - text: &'a str, - start_byte: usize, -} - -impl<'a> SplitWordsIncludingSpaces<'a> { - pub fn new(text: &'a str) -> Self { - Self { text, start_byte: 0 } - } -} - -impl<'a> Iterator for SplitWordsIncludingSpaces<'a> { - type Item = &'a str; - fn next(&mut self) -> Option { - let mut eaten_chars = self.text[self.start_byte..].char_indices().skip_while(|(_, c)| *c != ' ').skip_while(|(_, c)| *c == ' '); - let start_byte = self.start_byte; - self.start_byte = eaten_chars.next().map_or(self.text.len(), |(offset, _)| self.start_byte + offset); - (self.start_byte > start_byte).then(|| self.text.get(start_byte..self.start_byte)).flatten() - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn split_words_including_spaces() { - let mut split_words = SplitWordsIncludingSpaces::new("hello world ."); - assert_eq!(split_words.next(), Some("hello ")); - assert_eq!(split_words.next(), Some("world ")); - assert_eq!(split_words.next(), Some(".")); - assert_eq!(split_words.next(), None); - } -} diff --git a/node-graph/gcore/src/transform_nodes.rs b/node-graph/gcore/src/transform_nodes.rs index 39024681e6..5e584aa820 100644 --- a/node-graph/gcore/src/transform_nodes.rs +++ b/node-graph/gcore/src/transform_nodes.rs @@ -19,10 +19,10 @@ async fn transform( translate: DVec2, rotate: f64, scale: DVec2, - shear: DVec2, + skew: DVec2, _pivot: DVec2, ) -> Instances { - let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); + let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]); let footprint = ctx.try_footprint().copied(); diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index 0f69ae1bfc..71eea96fba 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -9,23 +9,37 @@ fn text<'i: 'n>( editor: &'i WasmEditorApi, text: String, font_name: Font, - #[default(24.)] font_size: f64, - #[default(1.2)] line_height_ratio: f64, - #[default(1.)] character_spacing: f64, - #[default(None)] max_width: Option, - #[default(None)] max_height: Option, + #[unit(" px")] + #[default(24.)] + font_size: f64, + #[unit("x")] + #[default(1.2)] + line_height_ratio: f64, + #[unit(" px")] + #[default(0.)] + character_spacing: f64, + #[unit(" px")] + #[default(None)] + max_width: Option, + #[unit(" px")] + #[default(None)] + max_height: Option, + #[unit("°")] + #[default(0.)] + tilt: f64, ) -> VectorDataTable { - let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data)); - let typesetting = TypesettingConfig { font_size, line_height_ratio, character_spacing, max_width, max_height, + tilt, }; - let result = VectorData::from_subpaths(to_path(&text, buzz_face, typesetting), false); + let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f)); + + let result = VectorData::from_subpaths(to_path(&text, font_data, typesetting), false); VectorDataTable::new(result) } From c67170594598b3352130472cae0ce729b2506f17 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 30 Jun 2025 08:14:36 -0700 Subject: [PATCH 5/6] New node: 'Index' --- node-graph/gcore/src/graphic_element.rs | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 915d82344a..131c6c04b0 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -551,3 +551,53 @@ impl From for GraphicElement { pub trait ToGraphicElement { fn to_graphic_element(&self) -> GraphicElement; } + +/// Returns the value at the specified index in the collection. +/// If that index has no value, the type's default value is returned. +#[node_macro::node(category("General"))] +fn index( + _: impl Ctx, + /// The collection of data, such as a list or table. + #[implementations( + Vec, + Vec>, + Vec, Vec, + Vec, + VectorDataTable, + RasterDataTable, + GraphicGroupTable, + )] + collection: T, + /// The index of the item to retrieve, starting from 0 for the first item. + index: u32, +) -> T::Output +where + T::Output: Clone + Default, +{ + collection.at_index(index as usize).unwrap_or_default() +} + +pub trait AtIndex { + type Output; + fn at_index(&self, index: usize) -> Option; +} +impl AtIndex for Vec { + type Output = T; + + fn at_index(&self, index: usize) -> Option { + self.get(index).cloned() + } +} +impl AtIndex for Instances { + type Output = Instances; + + fn at_index(&self, index: usize) -> Option { + let mut result_table = Self::default(); + if let Some(row) = self.instance_ref_iter().nth(index) { + result_table.push(row.to_instance_cloned()); + Some(result_table) + } else { + None + } + } +} From 0febfaf142e77cee72676f2dce108c725182bf19 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 30 Jun 2025 16:39:57 -0700 Subject: [PATCH 6/6] Make the node graph unrenderable message less scary --- .../src/messages/portfolio/portfolio_message_handler.rs | 8 ++++---- frontend/src/components/Editor.svelte | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5abb0918b4..7dac84d342 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -924,11 +924,11 @@ impl PortfolioMessageHandler { let result = self.executor.poll_node_graph_evaluation(active_document, responses); if result.is_err() { let error = r#" - + - The document cannot be rendered in its current state. - Check for error details in the node graph, which can be - opened with the viewport's top right Node Graph button. + The document cannot render in its current state. + Undo to go back, if available, or check for error details + by clicking the Node Graph button up at the top right. /text>"# // It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed. .to_string(); diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 9b062f0977..148cd8e4b3 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -104,6 +104,8 @@ --color-f-white-rgb: 255, 255, 255; --color-error-red: #d6536e; --color-error-red-rgb: 214, 83, 110; + --color-warning-yellow: #d5aa43; + --color-warning-yellow-rgb: 213, 170, 67; --color-data-general: #cfcfcf; --color-data-general-dim: #8a8a8a;