diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 64e6076..46c15a9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,10 @@ updates: interval: weekly day: monday open-pull-requests-limit: 5 + # Font measurement must track Iced's shaping engine. Upgrade as one stack. + ignore: + - dependency-name: cosmic-text + versions: [">=0.20"] groups: rust-dependencies: patterns: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cf364c..26c283c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,9 @@ jobs: - name: Check Files follow callback races run: python3 scripts/test-files-follow.py + - name: Check measurement and rendering font stack + run: python3 scripts/check-font-stack.py + - name: Check formatting run: cargo fmt --all -- --check @@ -204,6 +207,8 @@ jobs: sudo apt-get update sudo apt-get install --no-install-recommends -y \ pkg-config \ + fonts-dejavu-core \ + fonts-noto-cjk \ libfontconfig1-dev \ libwayland-dev \ libx11-dev \ diff --git a/Cargo.lock b/Cargo.lock index f606382..f8d4761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,9 +763,9 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.15.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173852283a9a57a3cbe365d86e74dc428a09c50421477d5ad6fe9d9509e37737" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ "bitflags 2.13.1", "fontdb", @@ -773,10 +773,10 @@ dependencies = [ "linebender_resource_handle", "log", "rangemap", - "rustc-hash 1.1.0", + "rustc-hash 2.1.3", "self_cell", - "skrifa 0.37.0", - "smol_str", + "skrifa 0.40.0", + "smol_str 0.3.7", "swash", "sys-locale", "unicode-bidi", @@ -796,9 +796,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] @@ -900,6 +900,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1053,7 +1062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1175,9 +1184,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.10.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" dependencies = [ "bytemuck", ] @@ -1249,7 +1258,7 @@ dependencies = [ "base64", "cosmic-text", "crossbeam-channel", - "dirs", + "dirs 7.0.0", "env_logger", "fuzzy-matcher", "iced", @@ -1548,14 +1557,14 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.3.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" dependencies = [ "bitflags 2.13.1", "bytemuck", "core_maths", - "read-fonts 0.35.0", + "read-fonts 0.37.0", "smallvec", ] @@ -1636,7 +1645,7 @@ dependencies = [ "log", "num-traits", "rustc-hash 2.1.3", - "smol_str", + "smol_str 0.2.2", "thiserror 2.0.20", "web-time", ] @@ -1670,8 +1679,6 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234ca1c2cec4155055f68fa5fad1b5242c496ac8238d80a259bca382fb44a102" dependencies = [ "bitflags 2.13.1", "bytemuck", @@ -1727,9 +1734,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0acf8b75a3bc914aff5f2329fdffc1b36eeaea29dda0e4bd232f1c62e9cc3d" +version = "0.14.1" dependencies = [ "bytemuck", "cosmic-text", @@ -2007,7 +2012,7 @@ version = "0.2.0" source = "git+https://github.com/beamiter/jterm_core.git?rev=3b378da1dceadd2c0d6c6676a89d685e146b163f#3b378da1dceadd2c0d6c6676a89d685e146b163f" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "fuzzy-matcher", "jagent", "libc", @@ -2068,9 +2073,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libfuzzer-sys" @@ -3319,13 +3324,13 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.35.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ "bytemuck", "core_maths", - "font-types 0.10.1", + "font-types 0.11.3", ] [[package]] @@ -3474,7 +3479,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3567,9 +3572,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -3660,12 +3665,12 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "skrifa" -version = "0.37.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", - "read-fonts 0.35.0", + "read-fonts 0.37.0", ] [[package]] @@ -3695,9 +3700,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "smithay-client-toolkit" @@ -3769,6 +3774,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb7109d23f319619d4cc0de5464613c8b7b1a09db2774a58101459cf6be2f10" + [[package]] name = "softbuffer" version = "0.4.8" @@ -3882,10 +3893,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4015,18 +4026,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "pin-project-lite", ] [[package]] name = "toml" -version = "1.1.4+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "indexmap", "serde_core", @@ -4145,9 +4156,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" dependencies = [ "tinyvec", ] @@ -4184,9 +4195,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4621,7 +4632,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4925,7 +4936,7 @@ dependencies = [ "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit 0.19.2", - "smol_str", + "smol_str 0.2.2", "tracing", "unicode-segmentation", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 8f00b9c..339d81c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ categories = ["command-line-utilities"] iced = { version = "0.14", features = ["canvas", "tokio", "advanced", "image", "wgpu"] } # Direct handle on the shaper iced embeds (same locked version), used to # measure the primary font's real advance for the terminal grid. -cosmic-text = "0.15" +# Upgrade only together with iced_graphics, iced_tiny_skia and cryoglyph. +cosmic-text = "=0.19.0" # Exact native boundary paired with the pinned local Iced shell patch. iced_winit = { version = "=0.14.0", default-features = false } libc = "0.2" @@ -34,7 +35,7 @@ once_cell = "1.21" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"] } toml = "1.1" -dirs = "6.0" +dirs = "7.0" unicode-normalization = "0.1" regex = "1.13" fuzzy-matcher = "0.3" @@ -59,6 +60,8 @@ opt-level = 3 # cryoglyph 0.1.0 requires the unsound lru 0.16 line. This Rust-source-identical # local copy changes only that dependency to the RustSec-fixed 0.18.2 release. [patch.crates-io] +iced_graphics = { path = "vendor/iced_graphics" } +iced_tiny_skia = { path = "vendor/iced_tiny_skia" } # X11 legacy wheel release must not emit a second scroll notch. winit = { path = "vendor/winit" } cryoglyph = { path = "vendor/cryoglyph" } diff --git a/docs/dependency-pr22-audit.md b/docs/dependency-pr22-audit.md new file mode 100644 index 0000000..543fb2b --- /dev/null +++ b/docs/dependency-pr22-audit.md @@ -0,0 +1,109 @@ +# PR #22 font-stack audit + +Audited PR head `8bc0aa13264fec5b7c285c4575dc0be398a8a724` against +base `1e96427550688fff42e388f875216c54a11dcc9e` on 2026-10-23. + +## Confirmed failure and coordinated upgrade + +The original PR's [release job](https://github.com/beamiter/frost/actions/runs/39813505715/job/119945797398) +fails with E0061 at `terminal_view.rs` calls to `Buffer::set_size` and +`Buffer::set_text`. The [test job](https://github.com/beamiter/frost/actions/runs/39813505715/job/119945797558) +reports eight E0061 errors, including the layout tests. cosmic-text 0.19 +removed the FontSystem argument from these setters and made setters lazy. +This is evidence of compilation failure, not evidence of a rendered regression. + +The requested final solution is a coordinated upgrade to cosmic-text `=0.19.0`. +Iced stable remains 0.14.0; use iced_tiny_skia 0.14.1. Vendor the published +iced_graphics 0.14.0 and iced_tiny_skia 0.14.1 crates, adapt the graphics Buffer +setters and explicitly flush lazy shaping before reading/aligning layout. +Upgrade the existing cryoglyph 0.1 patch to the same shaper, preserving its lru +security fix. The lockfile contains no cosmic-text 0.15. Retain the other nine +Dependabot dependency upgrades. See `vendor/FONT_STACK.md` for patch provenance. + +| Option | Cost and risk | Decision | +| --- | --- | --- | +| Retain 0.15.0 | Smallest fix; no rendering algorithm change, compiles against existing Iced | Superseded by request to use latest font stack | +| Upgrade together to 0.19.0 | Two local Iced crate patches; lazy-layout/API adaptation; newer harfrust/skrifa behavior requires regression coverage | Implemented | + +A lock-graph check and a Rust Buffer type-identity test enforce the shared +version. Dependabot is prevented from independently moving cosmic-text beyond +this coordinated release. Future stack updates must change this policy too. + +## Data flow + +Frost's `measure_mono_ascii_advance` uses Basic shaping of printable ASCII, +checks uniform advances and passes the resulting scalar cell width into Metrics. +Unresolved/nonuniform fonts use the existing per-cell fallback. Frost uses a +separate FontSystem from Iced; matching versions does not by itself guarantee +identical font databases, font loading order or fallback selection. + +Iced's text cache constructs cosmic-text Buffers. `iced_graphics` 0.14 and +`iced_tiny_skia` 0.14 require cosmic-text 0.15; the local cryoglyph 0.1 manifest +does too. cryoglyph reexports Buffer, LayoutGlyph and CacheKey and consumes this +model for WGPU rendering. All four lockfile consumers now resolve cosmic-text 0.19.0 through local patches. + +Vim native rendering retains Vim's authoritative row/column/cell counts and +the shared projection for text, cursor, hit testing and IME anchors. Non-ASCII +text uses Advanced shaping; UTF-8 byte cluster boundaries are not cell counts. +No conversion between 0.15 and 0.19 glyph/cache types is introduced; all consumers compile against the 0.19 model. + +## Other updates + +The Linux implementation `dirs/src/lin.rs` is byte-identical between downloaded +6.0.0 and 7.0.0 sources; both use dirs-sys 0.5.0. Frost's home/config/data/local +data/state functions retain `Option`. A compile-time API test covers +these calls. Keeping core's dirs 6 and Frost's dirs 7 is not the font-engine +problem: these APIs exchange standard-library paths, not versioned glyph types. + +The original CI reached Frost after compiling the updated dependencies; the +fixed locked all-target check also validates libc, tokio, smallvec, serde_json, +toml, unicode-normalization, uuid and crossbeam-channel together. Runtime +semantics outside the font/path tests are not inferred from compilation alone. + +## Verification + +Only explicitly completed results below are passes. + +- `cargo check --locked --all-targets`: passed on Rust 1.99.1 with the unified 0.19 stack. +- `cargo fmt --all -- --check`: passed. +- `python3 scripts/check-font-stack.py`: passed; the original PR lockfile fails + this same check because it contains 0.15.0 and 0.19.0. +- `python3 scripts/test-font-sync.py`: three tests passed. +- New `tests/font_stack.rs`: shared Buffer identity, dirs API types, actual + shaping of ASCII/CJK/fullwidth/combining text across sizes 12/20/32 and scales + 1/1.25/1.5/2. CI installs DejaVu and Noto CJK; missing glyphs fail explicitly. +- Added Iced cache/paragraph tests for initial layout, narrow wrapping and wide resize restoration, covering lazy-layout invalidation. +- `cargo test --locked --test font_stack`: 4 passed (new tests above). +- `cargo test --locked --bin frost terminal_view::tests`: 42 passed, including + actual ASCII batch shaping and styled CJK glyph coverage with Noto CJK. +- `cargo test --locked --bin frost vim9_buffer::tests`: 35 passed, including + fractional scrolling, text/caret/IME continuity, CJK source-cell anchors, + mouse hit projections and split geometry at fractional scales. +- Font synchronization harness: 3 passed on the final sources. +- Projection harness: 14 passed; this standalone harness uses its own pinned + dependencies and is supporting geometry evidence, not a whole-PR test. +- Native GUI smoke attempted with the compiled binary and Xvfb. Blocked before + mapping a window: Xvfb cannot establish local/Unix listening sockets in this + execution environment; Frost reports XOpenDisplayFailed. No pixel or real + monitor HiDPI pass is claimed. +- The earlier broad 0.15 test run was stopped after the scope changed. No full + suite pass or release-profile build pass is claimed for the final migration. + +Runtime test commands used `CARGO_PROFILE_DEV_DEBUG=0`, +`CARGO_PROFILE_TEST_DEBUG=0`, and `CARGO_INCREMENTAL=0` to fit scratch disk limits; +these change build artifacts, not the committed dependency graph. + +Existing blockers, deliberately not hidden by weakening checks: + +- `scripts/test-font-groups.py` fails its source assertion at line 30 on both + the exact base and this branch, before its Rust harness runs. +- Original dependency-policy CI reports unmaintained zbus_names 4.3.4, + zvariant 5.15.0 and zvariant_derive 5.15.0 (RUSTSEC-2026-0371/0370/0372). + These locked packages were not introduced or changed by PR #22. +- Vendor iced_winit emits existing deprecated `try_next` warnings. + +Headless shaping/projection tests cannot certify actual GPU pixels, compositor +scale changes or subjective scroll smoothness. Before merge, run the native +Vim GUI smoke with Noto CJK and inspect CJK/combining/fullwidth text, caret/IME +alignment, fractional scrolling and monitor transitions at 100/125/150/200%. +Do not interpret this targeted repair as a claim that all CI is green. diff --git a/scripts/check-font-stack.py b/scripts/check-font-stack.py new file mode 100644 index 0000000..1d174e9 --- /dev/null +++ b/scripts/check-font-stack.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Check the committed font graph without downloading/building dependencies.""" +from pathlib import Path +import sys +import tomllib + +root = Path(__file__).resolve().parents[1] +lock = tomllib.loads((Path(sys.argv[1]) if len(sys.argv) > 1 else root / "Cargo.lock").read_text()) +packages = lock["package"] +shapers = [p for p in packages if p["name"] == "cosmic-text"] +assert len(shapers) == 1, f"measurement/rendering shaper split: {[p['version'] for p in shapers]}" +shaper = shapers[0] +manifest = tomllib.loads((root / "Cargo.toml").read_text()) +assert manifest["dependencies"]["cosmic-text"] == "=" + shaper["version"] +for name in ("frost", "iced_graphics", "iced_tiny_skia", "cryoglyph"): + package, = [p for p in packages if p["name"] == name] + edges = [d for d in package["dependencies"] if d.split()[0] == "cosmic-text"] + assert edges in (["cosmic-text"], ["cosmic-text " + shaper["version"]]), (name, edges) +# Every lockfile edge must resolve uniquely, including after pruning old shapers. +for package in packages: + for dependency in package.get("dependencies", []): + parts = dependency.split() + matches = [p for p in packages if p["name"] == parts[0] + and (len(parts) == 1 or p["version"] == parts[1])] + assert len(matches) == 1, (package["name"], dependency) +print("font-stack lock graph OK: cosmic-text " + shaper["version"]) diff --git a/src/terminal_view.rs b/src/terminal_view.rs index d19114c..fe1e003 100644 --- a/src/terminal_view.rs +++ b/src/terminal_view.rs @@ -1934,7 +1934,7 @@ fn measure_mono_ascii_advance(font: iced::Font, font_size: f32) -> Option { with_metrics_font_system(|font_system| { let mut buffer = Buffer::new(font_system, TextMetrics::new(font_size, font_size)); - buffer.set_size(font_system, Some(4096.0), Some(font_size * 2.0)); + buffer.set_size(Some(4096.0), Some(font_size * 2.0)); let attrs = Attrs::new() .family(family) .weight(Weight(weight)) @@ -1944,7 +1944,7 @@ fn measure_mono_ascii_advance(font: iced::Font, font_size: f32) -> Option { // hmtx advances (no kerning or ligatures), identical to what iced's // Basic shaper produces for the same run. const PROBE: &str = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; - buffer.set_text(font_system, PROBE, &attrs, Shaping::Basic, None); + buffer.set_text(PROBE, &attrs, Shaping::Basic, None); buffer.shape_until_scroll(font_system, false); let mut advance: Option = None; let mut glyphs = 0usize; @@ -3266,17 +3266,16 @@ mod tests { let mut buffer = Buffer::new(&mut font_system, TextMetrics::new(size, size * 1.2)); buffer.set_size( - &mut font_system, Some(run_layout_width(cw, run.chars().count()) * scale), Some(size * 1.2), ); buffer.set_text( - &mut font_system, run, &Attrs::new().family(Family::Monospace), Shaping::Basic, None, ); + buffer.shape_until_scroll(&mut font_system, false); assert_eq!( buffer.layout_runs().count(), 1, @@ -3323,8 +3322,9 @@ mod tests { iced::font::Style::Normal => Style::Normal, }); let mut buffer = Buffer::new(&mut system, TextMetrics::new(20.0, 25.0)); - buffer.set_size(&mut system, Some(f32::INFINITY), Some(25.0)); - buffer.set_text(&mut system, "中文界", &attrs, Shaping::Advanced, None); + buffer.set_size(Some(f32::INFINITY), Some(25.0)); + buffer.set_text("中文界", &attrs, Shaping::Advanced, None); + buffer.shape_until_scroll(&mut system, false); let runs: Vec<_> = buffer.layout_runs().collect(); assert_eq!(runs.len(), 1); assert_eq!(runs[0].glyphs.len(), 3); @@ -3361,14 +3361,14 @@ mod tests { // finite glyph positions, however narrow the pane is. let mut font_system = FontSystem::new(); let mut buffer = Buffer::new(&mut font_system, TextMetrics::new(18.9, 25.2)); - buffer.set_size(&mut font_system, Some(f32::INFINITY), Some(25.2)); + buffer.set_size(Some(f32::INFINITY), Some(25.2)); buffer.set_text( - &mut font_system, "▸ 1234 output rows hidden — click to expand", &Attrs::new().family(Family::Monospace), Shaping::Advanced, None, ); + buffer.shape_until_scroll(&mut font_system, false); let runs: Vec<_> = buffer.layout_runs().collect(); assert!(runs.len() <= 1); for run in runs { diff --git a/tests/font_stack.rs b/tests/font_stack.rs new file mode 100644 index 0000000..4c8e08c --- /dev/null +++ b/tests/font_stack.rs @@ -0,0 +1,120 @@ +//! Exercise the exact shaper shared with Iced, not a substitute width model. +use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + +#[test] +fn measurement_and_iced_use_identical_buffer_types() { + fn renderer_buffer(buffer: &iced::advanced::graphics::text::cosmic_text::Buffer) -> &Buffer { + buffer + } + let mut fonts = FontSystem::new(); + let buffer = Buffer::new(&mut fonts, Metrics::new(16.0, 20.0)); + assert!(std::ptr::eq(&buffer, renderer_buffer(&buffer))); +} + +#[test] +fn unicode_layout_scales_without_changing_cluster_boundaries() { + let mut fonts = FontSystem::new(); + assert!( + fonts.db().faces().next().is_some(), + "install fonts-dejavu-core and fonts-noto-cjk" + ); + for text in [ + "ASCII grid", + "中文界A", + "e\u{301}o\u{308}", + "A界e\u{301}B", + ] { + for font_size in [12.0, 20.0, 32.0] { + let mut reference = None; + for scale in [1.0, 1.25, 1.5, 2.0] { + let size = font_size * scale; + let mut buffer = Buffer::new(&mut fonts, Metrics::new(size, size * 1.2)); + buffer.set_size(Some(4096.0), Some(size * 2.0)); + buffer.set_text( + text, + &Attrs::new().family(Family::Monospace), + Shaping::Advanced, + None, + ); + buffer.shape_until_scroll(&mut fonts, false); + let runs: Vec<_> = buffer.layout_runs().collect(); + assert_eq!(runs.len(), 1, "{text:?} at {size}"); + let glyphs: Vec<_> = runs[0] + .glyphs + .iter() + .map(|g| { + assert_ne!( + g.glyph_id, 0, + "missing glyph for {text:?}; install fonts-noto-cjk" + ); + assert!(text.is_char_boundary(g.start) && text.is_char_boundary(g.end)); + (g.start, g.end, g.x / scale, g.w / scale) + }) + .collect(); + if let Some(ref baseline) = reference { + let baseline: &Vec<(usize, usize, f32, f32)> = baseline; + assert_eq!(baseline.len(), glyphs.len()); + for (a, b) in baseline.iter().zip(&glyphs) { + assert_eq!((a.0, a.1), (b.0, b.1)); + assert!((a.2 - b.2).abs() < 0.02 && (a.3 - b.3).abs() < 0.02); + } + } else { + reference = Some(glyphs); + } + } + } + } +} + +#[test] +fn dirs_seven_retains_path_api_types() { + let _: [fn() -> Option; 5] = [ + dirs::home_dir, + dirs::config_dir, + dirs::data_dir, + dirs::data_local_dir, + dirs::state_dir, + ]; +} + +#[test] +fn iced_cache_and_paragraph_flush_lazy_layout_before_reading() { + use iced::advanced::graphics::text::{cache, Cache, Paragraph}; + use iced::advanced::text::{self, Paragraph as _, Text}; + use iced::{alignment, Font, Pixels, Size}; + let mut fonts = FontSystem::new(); + let mut cache = Cache::new(); + for size in [12.0, 20.0, 32.0] { + let (_, entry) = cache.allocate( + &mut fonts, + cache::Key { + content: "界 e\u{301} A", + size, + line_height: size * 1.2, + font: Font::MONOSPACE, + bounds: Size::new(600.0, 100.0), + shaping: text::Shaping::Advanced, + align_x: text::Alignment::Left, + }, + ); + assert!(entry.min_bounds.width > 0.0); + assert_eq!(entry.buffer.layout_runs().count(), 1); + let mut paragraph = Paragraph::with_text(Text { + content: "hello world hello world", + bounds: Size::new(600.0, 200.0), + size: Pixels(size), + line_height: text::LineHeight::Absolute(Pixels(size * 1.2)), + font: Font::MONOSPACE, + align_x: text::Alignment::Center, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::Word, + }); + assert_eq!(paragraph.buffer().layout_runs().count(), 1); + paragraph.resize(Size::new(70.0, 500.0)); + assert!(paragraph.buffer().layout_runs().count() > 1); + assert!(paragraph.min_bounds().width > 0.0); + paragraph.resize(Size::new(600.0, 200.0)); + assert_eq!(paragraph.buffer().layout_runs().count(), 1); + } +} diff --git a/vendor/FONT_STACK.md b/vendor/FONT_STACK.md new file mode 100644 index 0000000..083c9cb --- /dev/null +++ b/vendor/FONT_STACK.md @@ -0,0 +1,31 @@ +# Coordinated cosmic-text 0.19 migration + +Sources are the published crates.io archives: + +- iced_graphics 0.14.0 (Iced stable 0.14.0) +- iced_tiny_skia 0.14.1 (latest stable software backend at audit time) +- existing cryoglyph 0.1.0, retaining Frost's lru 0.18.2 security patch + +On 2026-10-23, crates.io reported cosmic-text 0.19.0, iced/iced_graphics/ +iced_wgpu 0.14.0, iced_tiny_skia 0.14.1 and cryoglyph 0.1.0 as the latest +non-yanked stable releases. The Iced crates still require cosmic-text 0.15; +these local patches coordinate the upgrade without importing Iced git master. + +Changes from the published graphics/software-backend sources: + +1. Require cosmic-text =0.19.0 in both manifests and cryoglyph. +2. Adapt graphics Buffer setters to the lazy 0.19 API. +3. Shape before text measurement/alignment and after alignment invalidation; + this covers the text cache, plain/rich paragraphs and paragraph resizing. +4. Shape the initial Iced editor buffer explicitly. Subsequent editor updates + already finish with `shape_as_needed`. + +The software renderer and cryoglyph keep their rendering code: compilation +checks their LayoutGlyph, CacheKey and rasterization API against the same +cosmic-text version. Keep the package identities coordinated when updating. +Do not restore a second cosmic-text version to satisfy an older consumer. + +Verification lives in `tests/font_stack.rs`, the production terminal/Vim +tests and `scripts/check-font-stack.py`. The latter checks all four consumers, +the exact direct pin and lockfile edge integrity without needing a compiler. +See `docs/dependency-pr22-audit.md` for evidence and remaining limitations. diff --git a/vendor/cryoglyph/Cargo.toml b/vendor/cryoglyph/Cargo.toml index 24f7ba8..0646ce7 100644 --- a/vendor/cryoglyph/Cargo.toml +++ b/vendor/cryoglyph/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0 OR Zlib" [dependencies] wgpu = { version = "27", default-features = false, features = ["wgsl"] } etagere = "0.2" -cosmic-text = "0.15" +cosmic-text = "=0.19.0" lru = { version = "0.18.2", default-features = false } rustc-hash = "2" diff --git a/vendor/iced_graphics/Cargo.toml b/vendor/iced_graphics/Cargo.toml new file mode 100644 index 0000000..f973282 --- /dev/null +++ b/vendor/iced_graphics/Cargo.toml @@ -0,0 +1,133 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "iced_graphics" +version = "0.14.0" +authors = ["Héctor Ramón Jiménez "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for iced" +homepage = "https://iced.rs" +readme = false +keywords = [ + "gui", + "ui", + "graphics", + "interface", + "widgets", +] +categories = ["gui"] +license = "MIT" +repository = "https://github.com/iced-rs/iced" + +[package.metadata.docs.rs] +rustdoc-args = [ + "--cfg", + "docsrs", +] +all-features = true + +[features] +fira-sans = [] +geometry = ["lyon_path"] +image = [ + "dep:image", + "kamadak-exif", +] +svg = [] +web-colors = [] + +[lib] +name = "iced_graphics" +path = "src/lib.rs" + +[dependencies.bitflags] +version = "2.0" + +[dependencies.bytemuck] +version = "1.0" +features = ["derive"] + +[dependencies.cosmic-text] +version = "=0.19.0" + +[dependencies.half] +version = "2.2" + +[dependencies.iced_core] +version = "0.14.0" + +[dependencies.iced_futures] +version = "0.14.0" + +[dependencies.image] +version = "0.25" +optional = true +default-features = false + +[dependencies.kamadak-exif] +version = "0.6" +optional = true + +[dependencies.log] +version = "0.4" + +[dependencies.lyon_path] +version = "1.0" +optional = true + +[dependencies.raw-window-handle] +version = "0.6" + +[dependencies.rustc-hash] +version = "2.0" + +[dependencies.thiserror] +version = "2" + +[dependencies.unicode-segmentation] +version = "1.0" + +[lints.clippy] +default_trait_access = "deny" +filter_map_next = "deny" +from_over_into = "deny" +large-enum-variant = "allow" +manual_let_else = "deny" +map-entry = "allow" +match-wildcard-for-single-variants = "deny" +needless_borrow = "deny" +new_without_default = "deny" +redundant-closure-for-method-calls = "deny" +result_large_err = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +type-complexity = "allow" +unused_async = "deny" +useless_conversion = "deny" + +[lints.rust] +missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.rust.rust_2018_idioms] +level = "deny" +priority = -1 + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/vendor/iced_graphics/LICENSE b/vendor/iced_graphics/LICENSE new file mode 100644 index 0000000..f1148e2 --- /dev/null +++ b/vendor/iced_graphics/LICENSE @@ -0,0 +1,18 @@ +Copyright 2019 Héctor Ramón, Iced contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/iced_graphics/fonts/FiraSans-Regular.ttf b/vendor/iced_graphics/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000..6f80647 Binary files /dev/null and b/vendor/iced_graphics/fonts/FiraSans-Regular.ttf differ diff --git a/vendor/iced_graphics/fonts/Iced-Icons.ttf b/vendor/iced_graphics/fonts/Iced-Icons.ttf new file mode 100644 index 0000000..f6b5a4e Binary files /dev/null and b/vendor/iced_graphics/fonts/Iced-Icons.ttf differ diff --git a/vendor/iced_graphics/src/antialiasing.rs b/vendor/iced_graphics/src/antialiasing.rs new file mode 100644 index 0000000..7631c97 --- /dev/null +++ b/vendor/iced_graphics/src/antialiasing.rs @@ -0,0 +1,24 @@ +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + /// Returns the amount of samples of the [`Antialiasing`]. + pub fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/vendor/iced_graphics/src/cache.rs b/vendor/iced_graphics/src/cache.rs new file mode 100644 index 0000000..7db80a0 --- /dev/null +++ b/vendor/iced_graphics/src/cache.rs @@ -0,0 +1,190 @@ +//! Cache computations and efficiently reuse them. +use std::cell::RefCell; +use std::fmt; +use std::mem; +use std::sync::atomic::{self, AtomicU64}; + +/// A simple cache that stores generated values to avoid recomputation. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache { + group: Group, + state: RefCell>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::singleton(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) { + let mut state = self.state.borrow_mut(); + + let previous = + mem::replace(&mut *state, State::Empty { previous: None }); + + let previous = match previous { + State::Empty { previous } => previous, + State::Filled { current } => Some(current), + }; + + *state = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group { + id: u64, + is_singleton: bool, +} + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } + } +} + +impl fmt::Debug for Cache +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option, + ) -> Self::Cache { + } +} diff --git a/vendor/iced_graphics/src/color.rs b/vendor/iced_graphics/src/color.rs new file mode 100644 index 0000000..92448a6 --- /dev/null +++ b/vendor/iced_graphics/src/color.rs @@ -0,0 +1,46 @@ +//! Manage colors for shaders. +use crate::core::Color; + +use bytemuck::{Pod, Zeroable}; + +/// A color packed as 4 floats representing RGBA channels. +#[derive(Debug, Clone, Copy, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct Packed([f32; 4]); + +impl Packed { + /// Returns the internal components of the [`Packed`] color. + pub fn components(self) -> [f32; 4] { + self.0 + } +} + +/// A flag that indicates whether the renderer should perform gamma correction. +pub const GAMMA_CORRECTION: bool = internal::GAMMA_CORRECTION; + +/// Packs a [`Color`]. +pub fn pack(color: impl Into) -> Packed { + Packed(internal::pack(color.into())) +} + +#[cfg(not(feature = "web-colors"))] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = true; + + pub fn pack(color: Color) -> [f32; 4] { + color.into_linear() + } +} + +#[cfg(feature = "web-colors")] +mod internal { + use crate::core::Color; + + pub const GAMMA_CORRECTION: bool = false; + + pub fn pack(color: Color) -> [f32; 4] { + [color.r, color.g, color.b, color.a] + } +} diff --git a/vendor/iced_graphics/src/compositor.rs b/vendor/iced_graphics/src/compositor.rs new file mode 100644 index 0000000..90c7b6f --- /dev/null +++ b/vendor/iced_graphics/src/compositor.rs @@ -0,0 +1,227 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. +use crate::core::Color; +use crate::futures::{MaybeSend, MaybeSync}; +use crate::{Error, Settings, Shell, Viewport}; + +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use thiserror::Error; + +use std::borrow::Cow; + +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { + /// The iced renderer of the backend. + type Renderer; + + /// The surface of the backend. + type Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Settings, + display: impl Display + Clone, + compatible_window: impl Window + Clone, + shell: Shell, + ) -> impl Future> { + Self::with_backend(settings, display, compatible_window, shell, None) + } + + /// Creates a new [`Compositor`] with a backend preference. + /// + /// If the backend does not match the preference, it will return + /// [`Error::GraphicsAdapterNotFound`]. + fn with_backend( + settings: Settings, + display: impl Display + Clone, + compatible_window: impl Window + Clone, + shell: Shell, + backend: Option<&str>, + ) -> impl Future>; + + /// Creates a [`Self::Renderer`] for the [`Compositor`]. + fn create_renderer(&self) -> Self::Renderer; + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface; + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ); + + /// Returns [`Information`] used by this [`Compositor`]. + fn information(&self) -> Information; + + /// Loads a font from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>) { + crate::text::font_system() + .write() + .expect("Write to font system") + .load_font(font); + } + + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. + /// + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface + fn present( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + on_pre_present: impl FnOnce(), + ) -> Result<(), SurfaceError>; + + /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of + /// the texture ordered as `RGBA` in the `sRGB` color space. + /// + /// [`Renderer`]: Self::Renderer + fn screenshot( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + background_color: Color, + ) -> Vec; +} + +/// A window that can be used in a [`Compositor`]. +/// +/// This is just a convenient super trait of the `raw-window-handle` +/// traits. +pub trait Window: + HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +impl Window for T where + T: HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +/// An owned display handle that can be used in a [`Compositor`]. +/// +/// This is just a convenient super trait of the `raw-window-handle` +/// trait. +pub trait Display: HasDisplayHandle + MaybeSend + MaybeSync + 'static {} + +impl Display for T where T: HasDisplayHandle + MaybeSend + MaybeSync + 'static +{} + +/// Defines the default compositor of a renderer. +pub trait Default { + /// The compositor of the renderer. + type Compositor: Compositor; +} + +/// Result of an unsuccessful call to [`Compositor::present`]. +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum SurfaceError { + /// A timeout was encountered while trying to acquire the next frame. + #[error("A timeout was encountered while trying to acquire the next frame")] + Timeout, + /// The underlying surface has changed, and therefore the surface must be updated. + #[error( + "The underlying surface has changed, and therefore the surface must be updated." + )] + Outdated, + /// The swap chain has been lost and needs to be recreated. + #[error("The surface has been lost and needs to be recreated")] + Lost, + /// There is no more memory left to allocate a new frame. + #[error("There is no more memory left to allocate a new frame")] + OutOfMemory, + /// Acquiring a texture failed with a generic error. + #[error("Acquiring a texture failed with a generic error")] + Other, +} + +/// Contains information about the graphics (e.g. graphics adapter, graphics backend). +#[derive(Debug)] +pub struct Information { + /// Contains the graphics adapter. + pub adapter: String, + /// Contains the graphics backend. + pub backend: String, +} + +#[cfg(debug_assertions)] +impl Compositor for () { + type Renderer = (); + type Surface = (); + + async fn with_backend( + _settings: Settings, + _display: impl Display, + _compatible_window: impl Window + Clone, + _shell: Shell, + _preferred_backend: Option<&str>, + ) -> Result { + Ok(()) + } + + fn create_renderer(&self) -> Self::Renderer {} + + fn create_surface( + &mut self, + _window: W, + _width: u32, + _height: u32, + ) -> Self::Surface { + } + + fn configure_surface( + &mut self, + _surface: &mut Self::Surface, + _width: u32, + _height: u32, + ) { + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + + fn information(&self) -> Information { + Information { + adapter: String::from("Null Renderer"), + backend: String::from("Null"), + } + } + + fn present( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + _on_pre_present: impl FnOnce(), + ) -> Result<(), SurfaceError> { + Ok(()) + } + + fn screenshot( + &mut self, + _renderer: &mut Self::Renderer, + _viewport: &Viewport, + _background_color: Color, + ) -> Vec { + vec![] + } +} + +#[cfg(debug_assertions)] +impl Default for () { + type Compositor = (); +} diff --git a/vendor/iced_graphics/src/damage.rs b/vendor/iced_graphics/src/damage.rs new file mode 100644 index 0000000..8aa4279 --- /dev/null +++ b/vendor/iced_graphics/src/damage.rs @@ -0,0 +1,78 @@ +//! Compute the damage between frames. +use crate::core::{Point, Rectangle}; + +/// Diffs the damage regions given some previous and current primitives. +pub fn diff( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + diff: impl Fn(&T, &T) -> Vec, +) -> Vec { + let damage = previous.iter().zip(current).flat_map(|(a, b)| diff(a, b)); + + if previous.len() == current.len() { + damage.collect() + } else { + let (smaller, bigger) = if previous.len() < current.len() { + (previous, current) + } else { + (current, previous) + }; + + // Extend damage by the added/removed primitives + damage + .chain(bigger[smaller.len()..].iter().flat_map(bounds)) + .collect() + } +} + +/// Computes the damage regions given some previous and current primitives. +pub fn list( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + are_equal: impl Fn(&T, &T) -> bool, +) -> Vec { + diff(previous, current, &bounds, |a, b| { + if are_equal(a, b) { + vec![] + } else { + bounds(a).into_iter().chain(bounds(b)).collect() + } + }) +} + +/// Groups the given damage regions that are close together inside the given +/// bounds. +pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { + const AREA_THRESHOLD: f32 = 20_000.0; + + damage.sort_by(|a, b| { + a.center() + .distance(Point::ORIGIN) + .total_cmp(&b.center().distance(Point::ORIGIN)) + }); + + let mut output = Vec::new(); + let mut scaled = damage + .into_iter() + .filter_map(|region| region.intersection(&bounds)) + .filter(|region| region.width >= 1.0 && region.height >= 1.0); + + if let Some(mut current) = scaled.next() { + for region in scaled { + let union = current.union(®ion); + + if union.area() - current.area() - region.area() <= AREA_THRESHOLD { + current = union; + } else { + output.push(current); + current = region; + } + } + + output.push(current); + } + + output +} diff --git a/vendor/iced_graphics/src/error.rs b/vendor/iced_graphics/src/error.rs new file mode 100644 index 0000000..6ea1d3a --- /dev/null +++ b/vendor/iced_graphics/src/error.rs @@ -0,0 +1,42 @@ +//! See what can go wrong when creating graphical backends. + +/// An error that occurred while creating an application's graphical context. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The requested backend version is not supported. + #[error("the requested backend version is not supported")] + VersionNotSupported, + + /// Failed to find any pixel format that matches the criteria. + #[error("failed to find any pixel format that matches the criteria")] + NoAvailablePixelFormat, + + /// A suitable graphics adapter or device could not be found. + #[error("a suitable graphics adapter or device could not be found")] + GraphicsAdapterNotFound { + /// The name of the backend where the error happened + backend: &'static str, + /// The reason why this backend could not be used + reason: Reason, + }, + + /// An error occurred in the context's internal backend + #[error("an error occurred in the context's internal backend")] + BackendError(String), + + /// Multiple errors occurred + #[error("multiple errors occurred: {0:?}")] + List(Vec), +} + +/// The reason why a graphics adapter could not be found +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Reason { + /// The backend did not match the preference + DidNotMatch { + /// The preferred backend + preferred_backend: String, + }, + /// The request to create the backend failed + RequestFailed(String), +} diff --git a/vendor/iced_graphics/src/geometry.rs b/vendor/iced_graphics/src/geometry.rs new file mode 100644 index 0000000..b32387f --- /dev/null +++ b/vendor/iced_graphics/src/geometry.rs @@ -0,0 +1,48 @@ +//! Build and draw geometry. +pub mod fill; +pub mod frame; +pub mod path; +pub mod stroke; + +mod cache; +mod style; +mod text; + +pub use cache::Cache; +pub use fill::Fill; +pub use frame::Frame; +pub use path::Path; +pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; +pub use text::Text; + +pub use crate::core::{Image, Svg}; +pub use crate::gradient::{self, Gradient}; + +use crate::cache::Cached; +use crate::core::{self, Rectangle}; + +/// A renderer capable of drawing some [`Self::Geometry`]. +pub trait Renderer: core::Renderer { + /// The kind of geometry this renderer can draw. + type Geometry: Cached; + + /// The kind of [`Frame`] this renderer supports. + type Frame: frame::Backend; + + /// Creates a new [`Self::Frame`]. + fn new_frame(&self, bounds: Rectangle) -> Self::Frame; + + /// Draws the given [`Self::Geometry`]. + fn draw_geometry(&mut self, geometry: Self::Geometry); +} + +#[cfg(debug_assertions)] +impl Renderer for () { + type Geometry = (); + type Frame = (); + + fn new_frame(&self, _bounds: Rectangle) -> Self::Frame {} + + fn draw_geometry(&mut self, _geometry: Self::Geometry) {} +} diff --git a/vendor/iced_graphics/src/geometry/cache.rs b/vendor/iced_graphics/src/geometry/cache.rs new file mode 100644 index 0000000..e17506a --- /dev/null +++ b/vendor/iced_graphics/src/geometry/cache.rs @@ -0,0 +1,130 @@ +use crate::cache::{self, Cached}; +use crate::core::{Rectangle, Size}; +use crate::geometry::{self, Frame}; + +pub use cache::Group; + +/// A simple cache that stores generated geometry to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +pub struct Cache +where + Renderer: geometry::Renderer, +{ + raw: crate::Cache::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data { + bounds: Rectangle, + geometry: T, +} + +impl Cache +where + Renderer: geometry::Renderer, +{ + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + self.raw.clear(); + } + + /// Draws geometry using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the size has changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored geometry will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored geometry if needed. + pub fn draw( + &self, + renderer: &Renderer, + size: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Renderer::Geometry { + self.draw_with_bounds(renderer, Rectangle::with_size(size), draw_fn) + } + + /// Draws geometry using the provided closure and stores it in the + /// [`Cache`]. + /// + /// Analogous to [`draw`](Self::draw), but takes a clipping [`Rectangle`] instead of + /// a [`Size`]. + pub fn draw_with_bounds( + &self, + renderer: &Renderer, + bounds: Rectangle, + draw_fn: impl FnOnce(&mut Frame), + ) -> Renderer::Geometry { + use std::ops::Deref; + + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); + } + + Some(current.geometry.clone()) + } + }; + + let mut frame = Frame::with_bounds(renderer, bounds); + draw_fn(&mut frame); + + let geometry = frame.into_geometry().cache(self.raw.group(), previous); + let result = Cached::load(&geometry); + + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; + + result + } +} + +impl std::fmt::Debug for Cache +where + Renderer: geometry::Renderer, + ::Cache: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self.raw) + } +} + +impl Default for Cache +where + Renderer: geometry::Renderer, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/vendor/iced_graphics/src/geometry/fill.rs b/vendor/iced_graphics/src/geometry/fill.rs new file mode 100644 index 0000000..b79a258 --- /dev/null +++ b/vendor/iced_graphics/src/geometry/fill.rs @@ -0,0 +1,75 @@ +//! Fill [`Geometry`] with a certain style. +//! +//! [`Geometry`]: super::Renderer::Geometry +pub use crate::geometry::Style; + +use crate::core::Color; +use crate::gradient::{self, Gradient}; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +impl From for Fill { + fn from(gradient: gradient::Linear) -> Self { + Fill { + style: Style::Gradient(Gradient::Linear(gradient)), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/vendor/iced_graphics/src/geometry/frame.rs b/vendor/iced_graphics/src/geometry/frame.rs new file mode 100644 index 0000000..fb7aae8 --- /dev/null +++ b/vendor/iced_graphics/src/geometry/frame.rs @@ -0,0 +1,308 @@ +//! Draw and generate geometry. +use crate::core::{Point, Radians, Rectangle, Size, Vector}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; + +/// The region of a surface that can be used to draw geometry. +pub struct Frame +where + Renderer: geometry::Renderer, +{ + raw: Renderer::Frame, +} + +impl Frame +where + Renderer: geometry::Renderer, +{ + /// Creates a new [`Frame`] with the given dimensions. + /// + /// Any geometry drawn outside of the dimensions will be clipped. + /// If you need further control, use [`with_bounds`](Self::with_bounds). + pub fn new(renderer: &Renderer, size: Size) -> Self { + Self::with_bounds(renderer, Rectangle::with_size(size)) + } + + /// Creates a new [`Frame`] with the given clip bounds. + pub fn with_bounds(renderer: &Renderer, bounds: Rectangle) -> Self { + Self { + raw: renderer.new_frame(bounds), + } + } + + /// Returns the width of the [`Frame`]. + pub fn width(&self) -> f32 { + self.raw.width() + } + + /// Returns the height of the [`Frame`]. + pub fn height(&self) -> f32 { + self.raw.height() + } + + /// Returns the dimensions of the [`Frame`]. + pub fn size(&self) -> Size { + self.raw.size() + } + + /// Returns the coordinate of the center of the [`Frame`]. + pub fn center(&self) -> Point { + self.raw.center() + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + self.raw.fill(path, fill); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + self.raw.fill_rectangle(top_left, size, fill); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + self.raw.stroke(path, stroke); + } + + /// Draws the stroke of an axis-aligned rectangle with the provided style + /// given its top-left corner coordinate and its `Size` on the [`Frame`] . + pub fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ All text will be rendered on top of all the layers of + /// a `Canvas`. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + pub fn fill_text(&mut self, text: impl Into) { + self.raw.fill_text(text); + } + + /// Draws the given [`Image`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "image")] + pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { + self.raw.draw_image(bounds, image); + } + + /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "svg")] + pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { + self.raw.draw_svg(bounds, svg); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.push_transform(); + + let result = f(self); + + self.pop_transform(); + + result + } + + /// Pushes the current transform in the transform stack. + pub fn push_transform(&mut self) { + self.raw.push_transform(); + } + + /// Pops a transform from the transform stack and sets it as the current transform. + pub fn pop_transform(&mut self) { + self.raw.pop_transform(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip( + &mut self, + region: Rectangle, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + let mut frame = self.draft(region); + + let result = f(&mut frame); + self.paste(frame); + + result + } + + /// Creates a new [`Frame`] with the given [`Size`]. + /// + /// Draw its contents back to this [`Frame`] with [`paste`]. + /// + /// [`paste`]: Self::paste + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self { + raw: self.raw.draft(clip_bounds), + } + } + + /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); + } + + /// Applies a translation to the current transform of the [`Frame`]. + pub fn translate(&mut self, translation: Vector) { + self.raw.translate(translation); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + pub fn rotate(&mut self, angle: impl Into) { + self.raw.rotate(angle); + } + + /// Applies a uniform scaling to the current transform of the [`Frame`]. + pub fn scale(&mut self, scale: impl Into) { + self.raw.scale(scale); + } + + /// Applies a non-uniform scaling to the current transform of the [`Frame`]. + pub fn scale_nonuniform(&mut self, scale: impl Into) { + self.raw.scale_nonuniform(scale); + } + + /// Turns the [`Frame`] into its underlying geometry. + pub fn into_geometry(self) -> Renderer::Geometry { + self.raw.into_geometry() + } +} + +/// The internal implementation of a [`Frame`]. +/// +/// Analogous to [`Frame`]. See [`Frame`] for the documentation +/// of each method. +#[allow(missing_docs)] +pub trait Backend: Sized { + type Geometry; + + fn width(&self) -> f32; + fn height(&self) -> f32; + fn size(&self) -> Size; + fn center(&self) -> Point; + + fn push_transform(&mut self); + fn pop_transform(&mut self); + + fn translate(&mut self, translation: Vector); + fn rotate(&mut self, angle: impl Into); + fn scale(&mut self, scale: impl Into); + fn scale_nonuniform(&mut self, scale: impl Into); + + fn draft(&mut self, clip_bounds: Rectangle) -> Self; + fn paste(&mut self, frame: Self); + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ); + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ); + + fn fill(&mut self, path: &Path, fill: impl Into); + fn fill_text(&mut self, text: impl Into); + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ); + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into); + + fn into_geometry(self) -> Self::Geometry; +} + +#[cfg(debug_assertions)] +impl Backend for () { + type Geometry = (); + + fn width(&self) -> f32 { + 0.0 + } + + fn height(&self) -> f32 { + 0.0 + } + + fn size(&self) -> Size { + Size::ZERO + } + + fn center(&self) -> Point { + Point::ORIGIN + } + + fn push_transform(&mut self) {} + fn pop_transform(&mut self) {} + + fn translate(&mut self, _translation: Vector) {} + fn rotate(&mut self, _angle: impl Into) {} + fn scale(&mut self, _scale: impl Into) {} + fn scale_nonuniform(&mut self, _scale: impl Into) {} + + fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} + fn paste(&mut self, _frame: Self) {} + + fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into>, + ) { + } + fn stroke_text<'a>( + &mut self, + _text: impl Into, + _stroke: impl Into>, + ) { + } + + fn fill(&mut self, _path: &Path, _fill: impl Into) {} + fn fill_text(&mut self, _text: impl Into) {} + fn fill_rectangle( + &mut self, + _top_left: Point, + _size: Size, + _fill: impl Into, + ) { + } + + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into) {} + + fn into_geometry(self) -> Self::Geometry {} +} diff --git a/vendor/iced_graphics/src/geometry/path.rs b/vendor/iced_graphics/src/geometry/path.rs new file mode 100644 index 0000000..e72732f --- /dev/null +++ b/vendor/iced_graphics/src/geometry/path.rs @@ -0,0 +1,83 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::core::border; +use crate::core::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a rounded rectangle given its top-left + /// corner coordinate, its [`Size`] and [`border::Radius`]. + pub fn rounded_rectangle( + top_left: Point, + size: Size, + radius: border::Radius, + ) -> Self { + Self::new(|p| p.rounded_rectangle(top_left, size, radius)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| { + p.circle(center, radius); + p.close(); + }) + } + + /// Returns the internal [`lyon_path::Path`]. + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + /// Returns the current [`Path`] with the given transform applied to it. + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/vendor/iced_graphics/src/geometry/path/arc.rs b/vendor/iced_graphics/src/geometry/path/arc.rs new file mode 100644 index 0000000..2600497 --- /dev/null +++ b/vendor/iced_graphics/src/geometry/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use iced_core::{Point, Radians, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation from positive x-axis. + pub start_angle: Radians, + /// The end of the segment's angle, clockwise rotation from positive x-axis. + pub end_angle: Radians, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse. The horizontal and vertical half-dimensions of the ellipse will match the x and y values of the radii vector. + pub radii: Vector, + /// The clockwise rotation of the arc's ellipse. + pub rotation: Radians, + /// The start of the segment's angle, clockwise rotation from positive x-axis. + pub start_angle: Radians, + /// The end of the segment's angle, clockwise rotation from positive x-axis. + pub end_angle: Radians, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: Radians(0.0), + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/vendor/iced_graphics/src/geometry/path/builder.rs b/vendor/iced_graphics/src/geometry/path/builder.rs new file mode 100644 index 0000000..1a017ac --- /dev/null +++ b/vendor/iced_graphics/src/geometry/path/builder.rs @@ -0,0 +1,265 @@ +use crate::geometry::path::{Arc, Path, arc}; + +use crate::core::border; +use crate::core::{Point, Radians, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation.0), + start_angle: math::Angle::radians(arc.start_angle.0), + sweep_angle: math::Angle::radians( + (arc.end_angle - arc.start_angle).0, + ), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.cast::().for_each_quadratic_bezier(&mut |curve| { + let curve = curve.cast::(); + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a rounded rectangle to the [`Path`] given its top-left + /// corner coordinate its [`Size`] and [`border::Radius`]. + #[inline] + pub fn rounded_rectangle( + &mut self, + top_left: Point, + size: Size, + radius: border::Radius, + ) { + let min_size = (size.height / 2.0).min(size.width / 2.0); + let [ + top_left_corner, + top_right_corner, + bottom_right_corner, + bottom_left_corner, + ] = radius.into(); + + self.move_to(Point::new( + top_left.x + min_size.min(top_left_corner), + top_left.y, + )); + self.line_to(Point::new( + top_left.x + size.width - min_size.min(top_right_corner), + top_left.y, + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y), + Point::new( + top_left.x + size.width, + top_left.y + min_size.min(top_right_corner), + ), + min_size.min(top_right_corner), + ); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height - min_size.min(bottom_right_corner), + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y + size.height), + Point::new( + top_left.x + size.width - min_size.min(bottom_right_corner), + top_left.y + size.height, + ), + min_size.min(bottom_right_corner), + ); + self.line_to(Point::new( + top_left.x + min_size.min(bottom_left_corner), + top_left.y + size.height, + )); + self.arc_to( + Point::new(top_left.x, top_left.y + size.height), + Point::new( + top_left.x, + top_left.y + size.height - min_size.min(bottom_left_corner), + ), + min_size.min(bottom_left_corner), + ); + self.line_to(Point::new( + top_left.x, + top_left.y + min_size.min(top_left_corner), + )); + self.arc_to( + Point::new(top_left.x, top_left.y), + Point::new(top_left.x + min_size.min(top_left_corner), top_left.y), + min_size.min(top_left_corner), + ); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: Radians(0.0), + end_angle: Radians(2.0 * std::f32::consts::PI), + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close(); + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/vendor/iced_graphics/src/geometry/stroke.rs b/vendor/iced_graphics/src/geometry/stroke.rs new file mode 100644 index 0000000..88a5fd7 --- /dev/null +++ b/vendor/iced_graphics/src/geometry/stroke.rs @@ -0,0 +1,98 @@ +//! Create lines from a [`Path`] and assigns them various attributes/styles. +//! +//! [`Path`]: super::Path +pub use crate::geometry::Style; + +use iced_core::Color; + +/// The style of a stroke. +#[derive(Debug, Clone, Copy)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl Stroke<'_> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl Default for Stroke<'_> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy, Default)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + #[default] + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy, Default)] +pub enum LineJoin { + /// A sharp corner. + #[default] + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/vendor/iced_graphics/src/geometry/style.rs b/vendor/iced_graphics/src/geometry/style.rs new file mode 100644 index 0000000..de77ecc --- /dev/null +++ b/vendor/iced_graphics/src/geometry/style.rs @@ -0,0 +1,24 @@ +use crate::core::Color; +use crate::geometry::Gradient; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/vendor/iced_graphics/src/geometry/text.rs b/vendor/iced_graphics/src/geometry/text.rs new file mode 100644 index 0000000..867ae4f --- /dev/null +++ b/vendor/iced_graphics/src/geometry/text.rs @@ -0,0 +1,197 @@ +use crate::core; +use crate::core::alignment; +use crate::core::text::{Alignment, LineHeight, Paragraph, Shaping, Wrapping}; +use crate::core::{Color, Font, Pixels, Point, Size, Vector}; +use crate::geometry::Path; +use crate::text; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The maximum horizontal space available for this [`Text`]. + /// + /// Text will break into new lines when the width is reached. + pub max_width: f32, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: Pixels, + /// The line height of the text. + pub line_height: LineHeight, + /// The font of the text + pub font: Font, + /// The horizontal alignment of the text + pub align_x: Alignment, + /// The vertical alignment of the text + pub align_y: alignment::Vertical, + /// The shaping strategy of the text. + pub shaping: Shaping, +} + +impl Text { + /// Computes the [`Path`]s of the [`Text`] and draws them using + /// the given closure. + pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { + let paragraph = text::Paragraph::with_text(core::text::Text { + content: &self.content, + bounds: Size::new(self.max_width, f32::INFINITY), + size: self.size, + line_height: self.line_height, + font: self.font, + align_x: self.align_x, + align_y: self.align_y, + shaping: self.shaping, + wrapping: Wrapping::default(), + }); + + let translation_x = match self.align_x { + Alignment::Default | Alignment::Left | Alignment::Justified => { + self.position.x + } + Alignment::Center => self.position.x - paragraph.min_width() / 2.0, + Alignment::Right => self.position.x - paragraph.min_width(), + }; + + let translation_y = { + match self.align_y { + alignment::Vertical::Top => self.position.y, + alignment::Vertical::Center => { + self.position.y - paragraph.min_height() / 2.0 + } + alignment::Vertical::Bottom => { + self.position.y - paragraph.min_height() + } + } + }; + + let buffer = paragraph.buffer(); + let mut swash_cache = cosmic_text::SwashCache::new(); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + for run in buffer.layout_runs() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + run.line_y; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_glyph.cache_key, + ) { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo(control_a, control_b, to) => { + path.bezier_curve_to( + Point::new(control_a.x, -control_a.y) + + offset, + Point::new(control_b.x, -control_b.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + f(glyph, self.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = self.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + f( + Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ); + } + } + } + } +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + max_width: f32::INFINITY, + color: Color::BLACK, + size: Pixels(16.0), + line_height: LineHeight::Relative(1.2), + font: Font::default(), + align_x: Alignment::Default, + align_y: alignment::Vertical::Top, + shaping: Shaping::default(), + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/vendor/iced_graphics/src/gradient.rs b/vendor/iced_graphics/src/gradient.rs new file mode 100644 index 0000000..5426172 --- /dev/null +++ b/vendor/iced_graphics/src/gradient.rs @@ -0,0 +1,191 @@ +//! A gradient that can be used as a fill for some geometry. +//! +//! For a gradient that you can use as a background variant for a widget, see [`Gradient`]. +use crate::color; +use crate::core::gradient::ColorStop; +use crate::core::{self, Color, Point, Rectangle}; + +use bytemuck::{Pod, Zeroable}; +use half::f16; +use std::cmp::Ordering; + +#[derive(Debug, Clone, Copy, PartialEq)] +/// A fill which linearly interpolates colors along a direction. +/// +/// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl From for Gradient { + fn from(gradient: Linear) -> Self { + Self::Linear(gradient) + } +} + +impl Gradient { + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + match self { + Gradient::Linear(linear) => linear.pack(), + } + } +} + +/// A linear gradient. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Linear { + /// The absolute starting position of the gradient. + pub start: Point, + + /// The absolute ending position of the gradient. + pub end: Point, + + /// [`ColorStop`]s along the linear gradient direction. + pub stops: [Option; 8], +} + +impl Linear { + /// Creates a new [`Linear`] builder. + pub fn new(start: Point, end: Point) -> Self { + Self { + start, + end, + stops: [None; 8], + } + } + + /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient. + /// + /// Any `offset` that is not within `0.0..=1.0` will be silently ignored. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + let (Ok(index) | Err(index)) = + self.stops.binary_search_by(|stop| match stop { + None => Ordering::Greater, + Some(stop) => stop.offset.partial_cmp(&offset).unwrap(), + }); + + if index < 8 { + self.stops[index] = Some(ColorStop { offset, color }); + } + } else { + log::warn!("Gradient: ColorStop must be within 0.0..=1.0 range."); + }; + + self + } + + /// Adds multiple [`ColorStop`]s to the gradient. + /// + /// Any stop added after the 8th will be silently ignored. + pub fn add_stops( + mut self, + stops: impl IntoIterator, + ) -> Self { + for stop in stops { + self = self.add_stop(stop.offset, stop.color); + } + + self + } + + /// Packs the [`Gradient`] for use in shader code. + pub fn pack(&self) -> Packed { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in self.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = + stop.map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let direction = [self.start.x, self.start.y, self.end.x, self.end.y]; + + Packed { + colors, + offsets, + direction, + } + } +} + +/// Packed [`Gradient`] data for use in shader code. +#[derive(Debug, Copy, Clone, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct Packed { + // 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32 + colors: [[u32; 2]; 8], + // 8 offsets, 8x 16 bit floats packed into 4 u32s + offsets: [u32; 4], + direction: [f32; 4], +} + +/// Creates a new [`Packed`] gradient for use in shader code. +pub fn pack(gradient: &core::Gradient, bounds: Rectangle) -> Packed { + match gradient { + core::Gradient::Linear(linear) => { + let mut colors = [[0u32; 2]; 8]; + let mut offsets = [f16::from(0u8); 8]; + + for (index, stop) in linear.stops.iter().enumerate() { + let [r, g, b, a] = + color::pack(stop.map_or(Color::default(), |s| s.color)) + .components(); + + colors[index] = [ + pack_f16s([f16::from_f32(r), f16::from_f32(g)]), + pack_f16s([f16::from_f32(b), f16::from_f32(a)]), + ]; + + offsets[index] = stop + .map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset)); + } + + let offsets = [ + pack_f16s([offsets[0], offsets[1]]), + pack_f16s([offsets[2], offsets[3]]), + pack_f16s([offsets[4], offsets[5]]), + pack_f16s([offsets[6], offsets[7]]), + ]; + + let (start, end) = linear.angle.to_distance(&bounds); + + let direction = [start.x, start.y, end.x, end.y]; + + Packed { + colors, + offsets, + direction, + } + } + } +} + +/// Packs two f16s into one u32. +fn pack_f16s(f: [f16; 2]) -> u32 { + let one = (f[0].to_bits() as u32) << 16; + let two = f[1].to_bits() as u32; + + one | two +} diff --git a/vendor/iced_graphics/src/image.rs b/vendor/iced_graphics/src/image.rs new file mode 100644 index 0000000..c05d749 --- /dev/null +++ b/vendor/iced_graphics/src/image.rs @@ -0,0 +1,173 @@ +//! Load and operate on images. +#[cfg(feature = "image")] +use crate::core::Bytes; + +use crate::core::Rectangle; +use crate::core::image; +use crate::core::svg; + +/// A raster or vector image. +#[allow(missing_docs)] +#[derive(Debug, Clone, PartialEq)] +pub enum Image { + /// A raster image. + Raster { + image: image::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + }, + + /// A vector image. + Vector { + svg: svg::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + }, +} + +impl Image { + /// Returns the bounds of the [`Image`]. + pub fn bounds(&self) -> Rectangle { + match self { + Image::Raster { image, bounds, .. } => { + bounds.rotate(image.rotation) + } + Image::Vector { svg, bounds, .. } => bounds.rotate(svg.rotation), + } + } +} + +/// An image buffer. +#[cfg(feature = "image")] +pub type Buffer = ::image::ImageBuffer<::image::Rgba, Bytes>; + +#[cfg(feature = "image")] +/// Tries to load an image by its [`Handle`]. +/// +/// [`Handle`]: image::Handle +pub fn load(handle: &image::Handle) -> Result { + use bitflags::bitflags; + + bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b1; + const ROTATE_180 = 0b10; + const FLIP_VERTICALLY= 0b100; + const ROTATE_90 = 0b1000; + const ROTATE_270 = 0b10000; + } + } + + impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif(reader: &mut R) -> Result + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .map(|value| match value { + 1 => Operation::empty(), + 2 => Operation::FLIP_HORIZONTALLY, + 3 => Operation::ROTATE_180, + 4 => Operation::FLIP_VERTICALLY, + 5 => Operation::ROTATE_90 | Operation::FLIP_HORIZONTALLY, + 6 => Operation::ROTATE_90, + 7 => Operation::ROTATE_90 | Operation::FLIP_VERTICALLY, + 8 => Operation::ROTATE_270, + _ => Operation::empty(), + }) + .unwrap_or_else(Self::empty)) + } + + fn perform( + self, + mut image: ::image::DynamicImage, + ) -> ::image::DynamicImage { + use ::image::imageops; + + if self.contains(Operation::ROTATE_90) { + image = imageops::rotate90(&image).into(); + } + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Operation::ROTATE_270) { + image = imageops::rotate270(&image).into(); + } + + if self.contains(Self::FLIP_VERTICALLY) { + imageops::flip_vertical_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } + } + + let (width, height, pixels) = match handle { + image::Handle::Path(_, path) => { + let image = ::image::open(path).map_err(to_error)?; + + let operation = std::fs::File::open(path) + .ok() + .map(std::io::BufReader::new) + .and_then(|mut reader| Operation::from_exif(&mut reader).ok()) + .unwrap_or_else(Operation::empty); + + let rgba = operation.perform(image).into_rgba8(); + + (rgba.width(), rgba.height(), Bytes::from(rgba.into_raw())) + } + image::Handle::Bytes(_, bytes) => { + let image = ::image::load_from_memory(bytes).map_err(to_error)?; + + let operation = + Operation::from_exif(&mut std::io::Cursor::new(bytes)) + .ok() + .unwrap_or_else(Operation::empty); + + let rgba = operation.perform(image).into_rgba8(); + + (rgba.width(), rgba.height(), Bytes::from(rgba.into_raw())) + } + image::Handle::Rgba { + width, + height, + pixels, + .. + } => (*width, *height, pixels.clone()), + }; + + if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { + Ok(image) + } else { + Err(to_error(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, + ), + ))) + } +} + +#[cfg(feature = "image")] +fn to_error(error: ::image::ImageError) -> image::Error { + use std::sync::Arc; + + match error { + ::image::ImageError::IoError(error) => { + image::Error::Inaccessible(Arc::new(error)) + } + error => image::Error::Invalid(Arc::new(error)), + } +} diff --git a/vendor/iced_graphics/src/image/storage.rs b/vendor/iced_graphics/src/image/storage.rs new file mode 100644 index 0000000..4caa614 --- /dev/null +++ b/vendor/iced_graphics/src/image/storage.rs @@ -0,0 +1,31 @@ +//! Store images. +use iced_core::Size; + +use std::fmt::Debug; + +/// Stores cached image data for use in rendering +pub trait Storage { + /// The type of an [`Entry`] in the [`Storage`]. + type Entry: Entry; + + /// State provided to upload or remove a [`Self::Entry`]. + type State<'a>; + + /// Upload the image data of a [`Self::Entry`]. + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + state: &mut Self::State<'_>, + ) -> Option; + + /// Remove a [`Self::Entry`] from the [`Storage`]. + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); +} + +/// An entry in some [`Storage`], +pub trait Entry: Debug { + /// The [`Size`] of the [`Entry`]. + fn size(&self) -> Size; +} diff --git a/vendor/iced_graphics/src/layer.rs b/vendor/iced_graphics/src/layer.rs new file mode 100644 index 0000000..31cfe68 --- /dev/null +++ b/vendor/iced_graphics/src/layer.rs @@ -0,0 +1,221 @@ +//! Draw and stack layers of graphical primitives. +use crate::core::{Rectangle, Transformation}; + +/// A layer of graphical primitives. +/// +/// Layers normally dictate a set of primitives that are +/// rendered in a specific order. +pub trait Layer: Default { + /// Creates a new [`Layer`] with the given bounds. + fn with_bounds(bounds: Rectangle) -> Self; + + /// Returns the current bounds of the [`Layer`]. + fn bounds(&self) -> Rectangle; + + /// Flushes and settles any pending group of primitives in the [`Layer`]. + /// + /// This will be called when a [`Layer`] is finished. It allows layers to efficiently + /// record primitives together and defer grouping until the end. + fn flush(&mut self); + + /// Resizes the [`Layer`] to the given bounds. + fn resize(&mut self, bounds: Rectangle); + + /// Clears all the layers contents and resets its bounds. + fn reset(&mut self); + + /// Returns the start level of the [`Layer`]. + /// + /// A level is a "sublayer" index inside of a [`Layer`]. + /// + /// A [`Layer`] may draw multiple primitive types in a certain order. + /// The level represents the lowest index of the primitive types it + /// contains. + /// + /// Two layers A and B can therefore be merged if they have the same bounds, + /// and the end level of A is lower or equal than the start level of B. + fn start(&self) -> usize; + + /// Returns the end level of the [`Layer`]. + fn end(&self) -> usize; + + /// Merges a [`Layer`] with the current one. + fn merge(&mut self, _layer: &mut Self); +} + +/// A stack of layers used for drawing. +#[derive(Debug)] +pub struct Stack { + layers: Vec, + transformations: Vec, + previous: Vec, + current: usize, + active_count: usize, +} + +impl Stack { + /// Creates a new empty [`Stack`]. + pub fn new() -> Self { + Self { + layers: vec![T::default()], + transformations: vec![Transformation::IDENTITY], + previous: vec![], + current: 0, + active_count: 1, + } + } + + /// Returns a mutable reference to the current [`Layer`] of the [`Stack`], together with + /// the current [`Transformation`]. + #[inline] + pub fn current_mut(&mut self) -> (&mut T, Transformation) { + let transformation = self.transformation(); + + (&mut self.layers[self.current], transformation) + } + + /// Returns the current [`Transformation`] of the [`Stack`]. + #[inline] + pub fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + /// Pushes a new clipping region in the [`Stack`]; creating a new layer in the + /// process. + pub fn push_clip(&mut self, bounds: Rectangle) { + self.previous.push(self.current); + + self.current = self.active_count; + self.active_count += 1; + + let bounds = bounds * self.transformation(); + + if self.current == self.layers.len() { + self.layers.push(T::with_bounds(bounds)); + } else { + self.layers[self.current].resize(bounds); + } + } + + /// Pops the current clipping region from the [`Stack`] and restores the previous one. + /// + /// The current layer will be recorded for drawing. + pub fn pop_clip(&mut self) { + self.flush(); + + self.current = self.previous.pop().unwrap(); + } + + /// Pushes a new [`Transformation`] in the [`Stack`]. + /// + /// Future drawing operations will be affected by this new [`Transformation`] until + /// it is popped using [`pop_transformation`]. + /// + /// [`pop_transformation`]: Self::pop_transformation + pub fn push_transformation(&mut self, transformation: Transformation) { + self.transformations + .push(self.transformation() * transformation); + } + + /// Pops the current [`Transformation`] in the [`Stack`]. + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + /// Returns an iterator over immutable references to the layers in the [`Stack`]. + pub fn iter(&self) -> impl Iterator { + self.layers[..self.active_count].iter() + } + + /// Returns the slice of layers in the [`Stack`]. + pub fn as_slice(&self) -> &[T] { + &self.layers[..self.active_count] + } + + /// Flushes and settles any primitives in the [`Stack`]. + pub fn flush(&mut self) { + self.layers[self.current].flush(); + } + + /// Performs layer merging wherever possible. + /// + /// Flushes and settles any primitives in the [`Stack`]. + pub fn merge(&mut self) { + self.flush(); + + // These are the layers left to process + let mut left = self.active_count; + + // There must be at least 2 or more layers to merge + while left > 1 { + // We set our target as the topmost layer left to process + let mut current = left - 1; + let mut target = &self.layers[current]; + let mut target_start = target.start(); + let mut target_index = current; + + // We scan downwards for a contiguous block of mergeable layer candidates + while current > 0 { + let candidate = &self.layers[current - 1]; + let start = candidate.start(); + let end = candidate.end(); + + // We skip empty layers + if end == 0 { + current -= 1; + continue; + } + + // Candidate can be merged if primitive sublayers do not overlap with + // previous targets and the clipping bounds match + if end > target_start || candidate.bounds() != target.bounds() { + break; + } + + // Candidate is not empty and can be merged into + target = candidate; + target_start = start; + target_index = current; + current -= 1; + } + + // We merge all the layers scanned into the target + // + // Since we use `target_index` instead of `current`, we + // deliberately avoid merging into empty layers. + // + // If no candidates were mergeable, this is a no-op. + let (head, tail) = self.layers.split_at_mut(target_index + 1); + let layer = &mut head[target_index]; + + for middle in &mut tail[0..left - target_index - 1] { + layer.merge(middle); + } + + // Empty layers found after the target can be skipped + left = current; + } + } + + /// Clears the layers of the [`Stack`], allowing reuse. + /// + /// It resizes the base layer bounds to the `new_bounds`. + /// + /// This will normally keep layer allocations for future drawing operations. + pub fn reset(&mut self, new_bounds: Rectangle) { + for layer in self.layers[..self.active_count].iter_mut() { + layer.reset(); + } + + self.layers[0].resize(new_bounds); + self.current = 0; + self.active_count = 1; + self.previous.clear(); + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} diff --git a/vendor/iced_graphics/src/lib.rs b/vendor/iced_graphics/src/lib.rs new file mode 100644 index 0000000..ff5f39c --- /dev/null +++ b/vendor/iced_graphics/src/lib.rs @@ -0,0 +1,44 @@ +//! A bunch of backend-agnostic types that can be leveraged to build a renderer +//! for [`iced`]. +//! +//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) +//! +//! [`iced`]: https://github.com/iced-rs/iced +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] +mod antialiasing; +mod settings; +mod viewport; + +pub mod cache; +pub mod color; +pub mod compositor; +pub mod damage; +pub mod error; +pub mod gradient; +pub mod image; +pub mod layer; +pub mod mesh; +pub mod shell; +pub mod text; + +#[cfg(feature = "geometry")] +pub mod geometry; + +pub use antialiasing::Antialiasing; +pub use cache::Cache; +pub use compositor::Compositor; +pub use error::Error; +pub use gradient::Gradient; +pub use image::Image; +pub use layer::Layer; +pub use mesh::Mesh; +pub use settings::Settings; +pub use shell::Shell; +pub use text::Text; +pub use viewport::Viewport; + +pub use iced_core as core; +pub use iced_futures as futures; diff --git a/vendor/iced_graphics/src/mesh.rs b/vendor/iced_graphics/src/mesh.rs new file mode 100644 index 0000000..9e11635 --- /dev/null +++ b/vendor/iced_graphics/src/mesh.rs @@ -0,0 +1,210 @@ +//! Draw triangles! +use crate::color; +use crate::core::{Rectangle, Transformation}; +use crate::gradient; + +use bytemuck::{Pod, Zeroable}; + +use std::sync::atomic::{self, AtomicU64}; +use std::sync::{Arc, Weak}; + +/// A low-level primitive to render a mesh of triangles. +#[derive(Debug, Clone, PartialEq)] +pub enum Mesh { + /// A mesh with a solid color. + Solid { + /// The vertices and indices of the mesh. + buffers: Indexed, + + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// A mesh with a gradient. + Gradient { + /// The vertices and indices of the mesh. + buffers: Indexed, + + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, +} + +impl Mesh { + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { + match self { + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the [`Transformation`] of the [`Mesh`]. + pub fn transformation(&self) -> Transformation { + match self { + Self::Solid { transformation, .. } + | Self::Gradient { transformation, .. } => *transformation, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { + match self { + Self::Solid { + clip_bounds, + transformation, + .. + } + | Self::Gradient { + clip_bounds, + transformation, + .. + } => *clip_bounds * *transformation, + } + } +} + +/// A set of vertices and indices representing a list of triangles. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Indexed { + /// The vertices of the mesh + pub vertices: Vec, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec, +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct SolidVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: color::Packed, +} + +/// A vertex which contains 2D position & packed gradient data. +#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)] +#[repr(C)] +pub struct GradientVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The packed vertex data of the gradient. + pub gradient: gradient::Packed, +} + +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of solid meshes. + pub solids: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of gradient meshes. + pub gradients: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of(meshes: &[Mesh]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solids += 1; + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradients += 1; + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} + +/// A cache of multiple meshes. +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + batch: Arc<[Mesh]>, + version: usize, +} + +/// The unique id of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + /// Creates a new [`Cache`] for the given meshes. + pub fn new(meshes: Arc<[Mesh]>) -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + batch: meshes, + version: 0, + } + } + + /// Returns the [`Id`] of the [`Cache`]. + pub fn id(&self) -> Id { + self.id + } + + /// Returns the current version of the [`Cache`]. + pub fn version(&self) -> usize { + self.version + } + + /// Returns the batch of meshes in the [`Cache`]. + pub fn batch(&self) -> &[Mesh] { + &self.batch + } + + /// Returns a [`Weak`] reference to the contents of the [`Cache`]. + pub fn downgrade(&self) -> Weak<[Mesh]> { + Arc::downgrade(&self.batch) + } + + /// Returns true if the [`Cache`] is empty. + pub fn is_empty(&self) -> bool { + self.batch.is_empty() + } + + /// Updates the [`Cache`] with the given meshes. + pub fn update(&mut self, meshes: Arc<[Mesh]>) { + self.batch = meshes; + self.version += 1; + } +} + +/// A renderer capable of drawing a [`Mesh`]. +pub trait Renderer { + /// Draws the given [`Mesh`]. + fn draw_mesh(&mut self, mesh: Mesh); + + /// Draws the given [`Cache`]. + fn draw_mesh_cache(&mut self, cache: Cache); +} diff --git a/vendor/iced_graphics/src/settings.rs b/vendor/iced_graphics/src/settings.rs new file mode 100644 index 0000000..7432a78 --- /dev/null +++ b/vendor/iced_graphics/src/settings.rs @@ -0,0 +1,54 @@ +use crate::Antialiasing; +use crate::core::{self, Font, Pixels}; + +/// The settings of a renderer. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: Pixels, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, + + /// Whether or not to synchronize frames. + /// + /// By default, it is `true`. + pub vsync: bool, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::default(), + default_text_size: Pixels(16.0), + antialiasing: None, + vsync: true, + } + } +} + +impl From for Settings { + fn from(settings: core::Settings) -> Self { + Self { + default_font: if cfg!(all( + target_arch = "wasm32", + feature = "fira-sans" + )) && settings.default_font == Font::default() + { + Font::with_name("Fira Sans") + } else { + settings.default_font + }, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing.then_some(Antialiasing::MSAAx4), + vsync: settings.vsync, + } + } +} diff --git a/vendor/iced_graphics/src/shell.rs b/vendor/iced_graphics/src/shell.rs new file mode 100644 index 0000000..55004d7 --- /dev/null +++ b/vendor/iced_graphics/src/shell.rs @@ -0,0 +1,45 @@ +//! Control the windowing runtime from a renderer. +use std::sync::Arc; + +/// A windowing shell. +#[derive(Clone)] +pub struct Shell(Arc); + +impl Shell { + /// Creates a new [`Shell`]. + pub fn new(notifier: impl Notifier) -> Self { + Self(Arc::new(notifier)) + } + + /// Creates a headless [`Shell`]. + pub fn headless() -> Self { + struct Headless; + + impl Notifier for Headless { + fn request_redraw(&self) {} + + fn invalidate_layout(&self) {} + } + + Self::new(Headless) + } + + /// Requests for all windows of the [`Shell`] to be redrawn. + pub fn request_redraw(&self) { + self.0.request_redraw(); + } + + /// Requests for all layouts of the [`Shell`] to be recomputed. + pub fn invalidate_layout(&self) { + self.0.invalidate_layout(); + } +} + +/// A type that can notify a shell of certain events. +pub trait Notifier: Send + Sync + 'static { + /// Requests for all windows of the [`Shell`] to be redrawn. + fn request_redraw(&self); + + /// Requests for all layouts of the [`Shell`] to be recomputed. + fn invalidate_layout(&self); +} diff --git a/vendor/iced_graphics/src/text.rs b/vendor/iced_graphics/src/text.rs new file mode 100644 index 0000000..486c32b --- /dev/null +++ b/vendor/iced_graphics/src/text.rs @@ -0,0 +1,362 @@ +//! Draw text. +pub mod cache; +pub mod editor; +pub mod paragraph; + +pub use cache::Cache; +pub use editor::Editor; +pub use paragraph::Paragraph; + +pub use cosmic_text; + +use crate::core::alignment; +use crate::core::font::{self, Font}; +use crate::core::text::{Alignment, Shaping, Wrapping}; +use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; + +use std::borrow::Cow; +use std::collections::HashSet; +use std::sync::{Arc, OnceLock, RwLock, Weak}; + +/// A text primitive. +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { + paragraph: paragraph::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// Some cached text. + Cached { + /// The contents of the text. + content: String, + /// The bounds of the text. + bounds: Rectangle, + /// The color of the text. + color: Color, + /// The size of the text in logical pixels. + size: Pixels, + /// The line height of the text. + line_height: Pixels, + /// The font of the text. + font: Font, + /// The horizontal alignment of the text. + align_x: Alignment, + /// The vertical alignment of the text. + align_y: alignment::Vertical, + /// The shaping strategy of the text. + shaping: Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, + }, + /// Some raw text. + #[allow(missing_docs)] + Raw { + raw: Raw, + transformation: Transformation, + }, +} + +impl Text { + /// Returns the visible bounds of the [`Text`]. + pub fn visible_bounds(&self) -> Option { + match self { + Text::Paragraph { + position, + paragraph, + clip_bounds, + transformation, + .. + } => Rectangle::new(*position, paragraph.min_bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + Text::Editor { + editor, + position, + clip_bounds, + transformation, + .. + } => Rectangle::new(*position, editor.bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + Text::Cached { + bounds, + clip_bounds, + .. + } => bounds.intersection(clip_bounds), + Text::Raw { raw, .. } => Some(raw.clip_bounds), + } + } +} + +/// The regular variant of the [Fira Sans] font. +/// +/// It is loaded as part of the default fonts when the `fira-sans` +/// feature is enabled. +/// +/// [Fira Sans]: https://mozilla.github.io/Fira/ +#[cfg(feature = "fira-sans")] +pub const FIRA_SANS_REGULAR: &[u8] = + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(); + +/// Returns the global [`FontSystem`]. +pub fn font_system() -> &'static RwLock { + static FONT_SYSTEM: OnceLock> = OnceLock::new(); + + FONT_SYSTEM.get_or_init(|| { + RwLock::new(FontSystem { + raw: cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), + )), + #[cfg(feature = "fira-sans")] + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(), + )), + ]), + loaded_fonts: HashSet::new(), + version: Version::default(), + }) + }) +} + +/// A set of system fonts. +pub struct FontSystem { + raw: cosmic_text::FontSystem, + loaded_fonts: HashSet, + version: Version, +} + +impl FontSystem { + /// Returns the raw [`cosmic_text::FontSystem`]. + pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.raw + } + + /// Loads a font from its bytes. + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + if let Cow::Borrowed(bytes) = bytes { + let address = bytes.as_ptr() as usize; + + if !self.loaded_fonts.insert(address) { + return; + } + } + + let _ = self.raw.db_mut().load_font_source( + cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), + ); + + self.version = Version(self.version.0 + 1); + } + + /// Returns the current [`Version`] of the [`FontSystem`]. + /// + /// Loading a font will increase the version of a [`FontSystem`]. + pub fn version(&self) -> Version { + self.version + } +} + +/// A version number. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Version(u32); + +/// A weak reference to a [`cosmic_text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { + /// A weak reference to a [`cosmic_text::Buffer`]. + pub buffer: Weak, + /// The position of the text. + pub position: Point, + /// The color of the text. + pub color: Color, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { + fn eq(&self, _other: &Self) -> bool { + // TODO: There is no proper way to compare raw buffers + // For now, no two instances of `Raw` text will be equal. + // This should be fine, but could trigger unnecessary redraws + // in the future. + false + } +} + +/// Measures the dimensions of the given [`cosmic_text::Buffer`]. +pub fn measure(buffer: &cosmic_text::Buffer) -> (Size, bool) { + let (width, height, has_rtl) = buffer.layout_runs().fold( + (0.0, 0.0, false), + |(width, height, has_rtl), run| { + ( + run.line_w.max(width), + height + run.line_height, + has_rtl || run.rtl, + ) + }, + ); + + (Size::new(width, height), has_rtl) +} + +/// Aligns the given [`cosmic_text::Buffer`] with the given [`Alignment`] +/// and returns its minimum [`Size`]. +pub fn align( + buffer: &mut cosmic_text::Buffer, + font_system: &mut cosmic_text::FontSystem, + alignment: Alignment, +) -> Size { + // cosmic-text 0.19 setters defer shaping; measurement must see fresh runs. + buffer.shape_until_scroll(font_system, false); + let (min_bounds, has_rtl) = measure(buffer); + let mut needs_relayout = has_rtl; + + if let Some(align) = to_align(alignment) { + let has_multiple_lines = buffer.lines.len() > 1 + || buffer.lines.first().is_some_and(|line| { + line.layout_opt().is_some_and(|layout| layout.len() > 1) + }); + + if has_multiple_lines { + for line in &mut buffer.lines { + let _ = line.set_align(Some(align)); + } + + needs_relayout = true; + } else if let Some(line) = buffer.lines.first_mut() { + needs_relayout = line.set_align(None); + } + } + + // TODO: Avoid relayout with some changes to `cosmic-text` (?) + if needs_relayout { + log::trace!("Relayouting paragraph..."); + + buffer.set_size( + Some(min_bounds.width), + Some(min_bounds.height), + ); + } + + buffer.shape_until_scroll(font_system, false); + min_bounds +} + +/// Returns the attributes of the given [`Font`]. +pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { + cosmic_text::Attrs::new() + .family(to_family(font.family)) + .weight(to_weight(font.weight)) + .stretch(to_stretch(font.stretch)) + .style(to_style(font.style)) +} + +fn to_family(family: font::Family) -> cosmic_text::Family<'static> { + match family { + font::Family::Name(name) => cosmic_text::Family::Name(name), + font::Family::SansSerif => cosmic_text::Family::SansSerif, + font::Family::Serif => cosmic_text::Family::Serif, + font::Family::Cursive => cosmic_text::Family::Cursive, + font::Family::Fantasy => cosmic_text::Family::Fantasy, + font::Family::Monospace => cosmic_text::Family::Monospace, + } +} + +fn to_weight(weight: font::Weight) -> cosmic_text::Weight { + match weight { + font::Weight::Thin => cosmic_text::Weight::THIN, + font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT, + font::Weight::Light => cosmic_text::Weight::LIGHT, + font::Weight::Normal => cosmic_text::Weight::NORMAL, + font::Weight::Medium => cosmic_text::Weight::MEDIUM, + font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD, + font::Weight::Bold => cosmic_text::Weight::BOLD, + font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD, + font::Weight::Black => cosmic_text::Weight::BLACK, + } +} + +fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch { + match stretch { + font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed, + font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed, + font::Stretch::Condensed => cosmic_text::Stretch::Condensed, + font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed, + font::Stretch::Normal => cosmic_text::Stretch::Normal, + font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded, + font::Stretch::Expanded => cosmic_text::Stretch::Expanded, + font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded, + font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded, + } +} + +fn to_style(style: font::Style) -> cosmic_text::Style { + match style { + font::Style::Normal => cosmic_text::Style::Normal, + font::Style::Italic => cosmic_text::Style::Italic, + font::Style::Oblique => cosmic_text::Style::Oblique, + } +} + +fn to_align(alignment: Alignment) -> Option { + match alignment { + Alignment::Default => None, + Alignment::Left => Some(cosmic_text::Align::Left), + Alignment::Center => Some(cosmic_text::Align::Center), + Alignment::Right => Some(cosmic_text::Align::Right), + Alignment::Justified => Some(cosmic_text::Align::Justified), + } +} + +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. +pub fn to_shaping(shaping: Shaping, text: &str) -> cosmic_text::Shaping { + match shaping { + Shaping::Auto => { + if text.is_ascii() { + cosmic_text::Shaping::Basic + } else { + cosmic_text::Shaping::Advanced + } + } + Shaping::Basic => cosmic_text::Shaping::Basic, + Shaping::Advanced => cosmic_text::Shaping::Advanced, + } +} + +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + +/// Converts some [`Color`] to a [`cosmic_text::Color`]. +pub fn to_color(color: Color) -> cosmic_text::Color { + let [r, g, b, a] = color.into_rgba8(); + + cosmic_text::Color::rgba(r, g, b, a) +} + +/// A text renderer coupled to `iced_graphics`. +pub trait Renderer { + /// Draws the given [`Raw`] text. + fn fill_raw(&mut self, raw: Raw); +} diff --git a/vendor/iced_graphics/src/text/cache.rs b/vendor/iced_graphics/src/text/cache.rs new file mode 100644 index 0000000..38c1c1d --- /dev/null +++ b/vendor/iced_graphics/src/text/cache.rs @@ -0,0 +1,146 @@ +//! Cache text. +use crate::core::{Font, Size}; +use crate::text; + +use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; +use std::collections::hash_map; +use std::hash::{Hash, Hasher}; + +/// A store of recently used sections of text. +#[derive(Debug, Default)] +pub struct Cache { + entries: FxHashMap, + aliases: FxHashMap, + recently_used: FxHashSet, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Self::default() + } + + /// Gets the text [`Entry`] with the given [`KeyHash`]. + pub fn get(&self, key: &KeyHash) -> Option<&Entry> { + self.entries.get(key) + } + + /// Allocates a text [`Entry`] if it is not already present in the [`Cache`]. + pub fn allocate( + &mut self, + font_system: &mut cosmic_text::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut Entry) { + let hash = key.hash(FxHasher::default()); + + if let Some(hash) = self.aliases.get(&hash) { + let _ = self.recently_used.insert(*hash); + + return (*hash, self.entries.get_mut(hash).unwrap()); + } + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { + let metrics = cosmic_text::Metrics::new( + key.size, + key.line_height.max(f32::MIN_POSITIVE), + ); + let mut buffer = cosmic_text::Buffer::new(font_system, metrics); + + buffer.set_size( + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), + ); + buffer.set_text( + key.content, + &text::to_attributes(key.font), + text::to_shaping(key.shaping, key.content), + None, + ); + + let bounds = text::align(&mut buffer, font_system, key.align_x); + + let _ = entry.insert(Entry { + buffer, + min_bounds: bounds, + }); + + for bounds in [ + bounds, + Size { + width: key.bounds.width, + ..bounds + }, + ] { + if key.bounds != bounds { + let _ = self.aliases.insert( + Key { bounds, ..key }.hash(FxHasher::default()), + hash, + ); + } + } + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + /// Trims the [`Cache`]. + /// + /// This will clear the sections of text that have not been used since the last `trim`. + pub fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.aliases + .retain(|_, value| self.recently_used.contains(value)); + + self.recently_used.clear(); + } +} + +/// A cache key representing a section of text. +#[derive(Debug, Clone, Copy)] +pub struct Key<'a> { + /// The content of the text. + pub content: &'a str, + /// The size of the text. + pub size: f32, + /// The line height of the text. + pub line_height: f32, + /// The [`Font`] of the text. + pub font: Font, + /// The bounds of the text. + pub bounds: Size, + /// The shaping strategy of the text. + pub shaping: text::Shaping, + /// The alignment of the text. + pub align_x: text::Alignment, +} + +impl Key<'_> { + fn hash(self, mut hasher: H) -> KeyHash { + self.content.hash(&mut hasher); + self.size.to_bits().hash(&mut hasher); + self.line_height.to_bits().hash(&mut hasher); + self.font.hash(&mut hasher); + self.bounds.width.to_bits().hash(&mut hasher); + self.bounds.height.to_bits().hash(&mut hasher); + self.shaping.hash(&mut hasher); + self.align_x.hash(&mut hasher); + + hasher.finish() + } +} + +/// The hash of a [`Key`]. +pub type KeyHash = u64; + +/// A cache entry. +#[derive(Debug)] +pub struct Entry { + /// The buffer of text, ready for drawing. + pub buffer: cosmic_text::Buffer, + /// The minimum bounds of the text. + pub min_bounds: Size, +} diff --git a/vendor/iced_graphics/src/text/editor.rs b/vendor/iced_graphics/src/text/editor.rs new file mode 100644 index 0000000..7fb3d6b --- /dev/null +++ b/vendor/iced_graphics/src/text/editor.rs @@ -0,0 +1,883 @@ +//! Draw and edit text. +use crate::core::text::editor::{ + self, Action, Cursor, Direction, Edit, Motion, Position, Selection, +}; +use crate::core::text::highlighter::{self, Highlighter}; +use crate::core::text::{LineHeight, Wrapping}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; +use crate::text; + +use cosmic_text::Edit as _; + +use std::borrow::Cow; +use std::fmt; +use std::sync::{self, Arc, RwLock}; + +/// A multi-line text editor. +#[derive(Debug, PartialEq)] +pub struct Editor(Option>); + +struct Internal { + editor: cosmic_text::Editor<'static>, + selection: RwLock>, + font: Font, + bounds: Size, + topmost_line_changed: Option, + version: text::Version, +} + +impl Editor { + /// Creates a new empty [`Editor`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the buffer of the [`Editor`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + buffer_from_editor(&self.internal().editor) + } + + /// Creates a [`Weak`] reference to the [`Editor`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let editor = self.internal(); + + Weak { + raw: Arc::downgrade(editor), + bounds: editor.bounds, + } + } + + fn internal(&self) -> &Arc { + self.0 + .as_ref() + .expect("Editor should always be initialized") + } + + fn with_internal_mut( + &mut self, + f: impl FnOnce(&mut Internal) -> T, + ) -> T { + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + // Clear cursor cache + let _ = internal + .selection + .write() + .expect("Write to cursor cache") + .take(); + + let result = f(&mut internal); + + self.0 = Some(Arc::new(internal)); + + result + } +} + +impl editor::Editor for Editor { + type Font = Font; + + fn with_text(text: &str) -> Self { + let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + buffer.set_text( + text, + &cosmic_text::Attrs::new(), + cosmic_text::Shaping::Advanced, + None, + ); + + buffer.shape_until_scroll(font_system.raw(), false); + + Editor(Some(Arc::new(Internal { + editor: cosmic_text::Editor::new(buffer), + version: font_system.version(), + ..Default::default() + }))) + } + + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + + fn line(&self, index: usize) -> Option> { + self.buffer().lines.get(index).map(|line| editor::Line { + text: Cow::Borrowed(line.text()), + ending: match line.ending() { + cosmic_text::LineEnding::Lf => editor::LineEnding::Lf, + cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf, + cosmic_text::LineEnding::Cr => editor::LineEnding::Cr, + cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr, + cosmic_text::LineEnding::None => editor::LineEnding::None, + }, + }) + } + + fn line_count(&self) -> usize { + self.buffer().lines.len() + } + + fn copy(&self) -> Option { + self.internal().editor.copy_selection() + } + + fn selection(&self) -> editor::Selection { + let internal = self.internal(); + + if let Ok(Some(cursor)) = internal.selection.read().as_deref() { + return cursor.clone(); + } + + let cursor = internal.editor.cursor(); + let buffer = buffer_from_editor(&internal.editor); + + let cursor = match internal.editor.selection_bounds() { + Some((start, end)) => { + let line_height = buffer.metrics().line_height; + let selected_lines = end.line - start.line + 1; + + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: (visual_line as i32 + visual_lines_offset) + as f32 + * line_height + - buffer.scroll().vertical, + height: line_height, + }) + } else { + None + } + }) + .collect(); + + Selection::Range(regions) + } + _ => { + let line_height = buffer.metrics().line_height; + + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); + + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = + line.layout_opt().expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (visual_line, offset) = lines + .find_map(|(i, line)| { + let start = line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_before_start = start > cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => cursor.index < end, + }; + + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| cursor.index > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + Some((i, offset)) + } else { + None + } + }) + .unwrap_or(( + layout.len().saturating_sub(1), + layout.last().map(|line| line.w).unwrap_or(0.0), + )); + + Selection::Caret(Point::new( + offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height + - buffer.scroll().vertical, + )) + } + }; + + *internal.selection.write().expect("Write to cursor cache") = + Some(cursor.clone()); + + cursor + } + + fn cursor(&self) -> Cursor { + let editor = &self.internal().editor; + + let position = { + let cursor = editor.cursor(); + + Position { + line: cursor.line, + column: cursor.index, + } + }; + + let selection = match editor.selection() { + cosmic_text::Selection::None => None, + cosmic_text::Selection::Normal(cursor) + | cosmic_text::Selection::Line(cursor) + | cosmic_text::Selection::Word(cursor) => Some(Position { + line: cursor.line, + column: cursor.index, + }), + }; + + Cursor { + position, + selection, + } + } + + fn perform(&mut self, action: Action) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + self.with_internal_mut(|internal| { + let editor = &mut internal.editor; + + match action { + // Motion events + Action::Move(motion) => { + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); + + match motion { + // These motions are performed as-is even when a selection + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion( + motion, + )), + ); + } + // Other motions simply move the cursor to one end of the selection + _ => editor.set_cursor(match motion.direction() { + Direction::Left => start, + Direction::Right => end, + }), + } + } else { + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); + } + } + + // Selection events + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.selection_bounds().is_none() { + editor.set_selection(cosmic_text::Selection::Normal( + cursor, + )); + } + + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); + + // Deselect if selection matches cursor position + if let Some((start, end)) = editor.selection_bounds() + && start.line == end.line + && start.index == end.index + { + editor.set_selection(cosmic_text::Selection::None); + } + } + Action::SelectWord => { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Word(cursor)); + } + Action::SelectLine => { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); + + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); + + editor.action( + font_system.raw(), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), + ); + } + } + + // Editing events + Action::Edit(edit) => { + let topmost_line_before_edit = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or_else(|| editor.cursor()) + .line; + + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Indent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Indent, + ); + } + Edit::Unindent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Unindent, + ); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); + + internal.topmost_line_changed = Some( + selection_start.line.min(topmost_line_before_edit), + ); + } + + // Mouse events + Action::Click(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + if let Some((start, end)) = editor.selection_bounds() + && start.line == end.line + && start.index == end.index + { + editor.set_selection(cosmic_text::Selection::None); + } + } + Action::Scroll { lines } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { + pixels: lines as f32 + * buffer_from_editor(editor) + .metrics() + .line_height, + }, + ); + } + } + }); + } + + fn move_to(&mut self, cursor: Cursor) { + self.with_internal_mut(|internal| { + // TODO: Expose `Affinity` + internal.editor.set_cursor(cosmic_text::Cursor { + line: cursor.position.line, + index: cursor.position.column, + affinity: cosmic_text::Affinity::Before, + }); + + if let Some(selection) = cursor.selection { + internal + .editor + .set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: selection.line, + index: selection.column, + affinity: cosmic_text::Affinity::Before, + }, + )); + } + }); + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn min_bounds(&self) -> Size { + let internal = self.internal(); + + let (bounds, _has_rtl) = + text::measure(buffer_from_editor(&internal.editor)); + + bounds + } + + fn update( + &mut self, + new_bounds: Size, + new_font: Font, + new_size: Pixels, + new_line_height: LineHeight, + new_wrapping: Wrapping, + new_highlighter: &mut impl Highlighter, + ) { + self.with_internal_mut(|internal| { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let buffer = buffer_mut_from_editor(&mut internal.editor); + + if font_system.version() != internal.version { + log::trace!("Updating `FontSystem` of `Editor`..."); + + for line in buffer.lines.iter_mut() { + line.reset(); + } + + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); + } + + if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); + + for line in buffer.lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + &text::to_attributes(new_font), + )); + } + + internal.font = new_font; + internal.topmost_line_changed = Some(0); + } + + let metrics = buffer.metrics(); + let new_line_height = new_line_height.to_absolute(new_size); + + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + log::trace!("Updating `Metrics` of `Editor`..."); + + buffer.set_metrics( + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ); + } + + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(new_wrap); + } + + if new_bounds != internal.bounds { + log::trace!("Updating size of `Editor`..."); + + buffer.set_size( + Some(new_bounds.width), + Some(new_bounds.height), + ); + + internal.bounds = new_bounds; + } + + if let Some(topmost_line_changed) = + internal.topmost_line_changed.take() + { + log::trace!( + "Notifying highlighter of line \ + change: {topmost_line_changed}" + ); + + new_highlighter.change_line(topmost_line_changed); + } + + internal.editor.shape_as_needed(font_system.raw(), false); + }); + } + + fn highlight( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, + ) { + let internal = self.internal(); + let buffer = buffer_from_editor(&internal.editor); + + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; + + let last_visible_line = buffer.lines[scroll.line..] + .iter() + .enumerate() + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(scroll.line + i) + } + }) + .unwrap_or(buffer.lines.len().saturating_sub(1)); + + let current_line = highlighter.current_line(); + + if current_line > last_visible_line { + return; + } + + let editor = + self.0.take().expect("Editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let attributes = text::to_attributes(font); + + for line in &mut buffer_mut_from_editor(&mut internal.editor).lines + [current_line..=last_visible_line] + { + let mut list = cosmic_text::AttrsList::new(&attributes); + + for (range, highlight) in highlighter.highlight_line(line.text()) { + let format = format_highlight(&highlight); + + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + &cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes.clone() + } + }, + ); + } + } + + let _ = line.set_attrs_list(list); + } + + internal.editor.shape_as_needed(font_system.raw(), false); + + self.0 = Some(Arc::new(internal)); + } +} + +impl Default for Editor { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.bounds == other.bounds + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty( + cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }, + )), + selection: RwLock::new(None), + font: Font::default(), + bounds: Size::ZERO, + topmost_line_changed: None, + version: text::Version::default(), + } + } +} + +impl fmt::Debug for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Internal") + .field("font", &self.font) + .field("bounds", &self.bounds) + .finish() + } +} + +/// A weak reference to an [`Editor`]. +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + /// The bounds of the [`Editor`]. + pub bounds: Size, +} + +impl Weak { + /// Tries to update the reference into an [`Editor`]. + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Some).map(Editor) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} + +fn highlight_line( + line: &cosmic_text::BufferLine, + from: usize, + to: usize, +) -> impl Iterator + '_ { + let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default(); + + layout.iter().map(move |visual_line| { + let start = visual_line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = visual_line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let range = start.max(from)..end.min(to); + + if range.is_empty() { + (0.0, 0.0) + } else if range.start == start && range.end == end { + (0.0, visual_line.w) + } else { + let first_glyph = visual_line + .glyphs + .iter() + .position(|glyph| range.start <= glyph.start) + .unwrap_or(0); + + let mut glyphs = visual_line.glyphs.iter(); + + let x = + glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum(); + + let width: f32 = glyphs + .take_while(|glyph| range.end > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + (x, width) + } + }) +} + +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + let scroll = buffer.scroll(); + + let start = scroll.line.min(line); + let end = scroll.line.max(line); + + let visual_lines_offset: usize = buffer.lines[start..] + .iter() + .take(end - start) + .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default()) + .sum(); + + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } +} + +fn to_motion(motion: Motion) -> cosmic_text::Motion { + match motion { + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), + } +} diff --git a/vendor/iced_graphics/src/text/paragraph.rs b/vendor/iced_graphics/src/text/paragraph.rs new file mode 100644 index 0000000..f41f621 --- /dev/null +++ b/vendor/iced_graphics/src/text/paragraph.rs @@ -0,0 +1,467 @@ +//! Draw paragraphs. +use crate::core; +use crate::core::alignment; +use crate::core::text::{ + Alignment, Hit, LineHeight, Shaping, Span, Text, Wrapping, +}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; +use crate::text; + +use std::fmt; +use std::sync::{self, Arc}; + +/// A bunch of text. +#[derive(Clone, PartialEq)] +pub struct Paragraph(Arc); + +#[derive(Clone)] +struct Internal { + buffer: cosmic_text::Buffer, + font: Font, + shaping: Shaping, + wrapping: Wrapping, + align_x: Alignment, + align_y: alignment::Vertical, + bounds: Size, + min_bounds: Size, + version: text::Version, +} + +impl Paragraph { + /// Creates a new empty [`Paragraph`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the buffer of the [`Paragraph`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + /// Creates a [`Weak`] reference to the [`Paragraph`]. + /// + /// This is useful to avoid cloning the [`Paragraph`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + align_x: paragraph.align_x, + align_y: paragraph.align_y, + } + } + + fn internal(&self) -> &Arc { + &self.0 + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn with_text(text: Text<&str>) -> Self { + log::trace!("Allocating plain paragraph: {}", text.content); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_wrap(text::to_wrap(text.wrapping)); + + buffer.set_text( + text.content, + &text::to_attributes(text.font), + text::to_shaping(text.shaping, text.content), + None, + ); + + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); + + Self(Arc::new(Internal { + buffer, + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + wrapping: text.wrapping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) + } + + fn with_spans(text: Text<&[Span<'_, Link>]>) -> Self { + log::trace!("Allocating rich paragraph: {} spans", text.content.len()); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_wrap(text::to_wrap(text.wrapping)); + + buffer.set_rich_text( + text.content.iter().enumerate().map(|(i, span)| { + let attrs = text::to_attributes(span.font.unwrap_or(text.font)); + + let attrs = match (span.size, span.line_height) { + (None, None) => attrs, + _ => { + let size = span.size.unwrap_or(text.size); + + attrs.metrics(cosmic_text::Metrics::new( + size.into(), + span.line_height + .unwrap_or(text.line_height) + .to_absolute(size) + .into(), + )) + } + }; + + let attrs = if let Some(color) = span.color { + attrs.color(text::to_color(color)) + } else { + attrs + }; + + (span.text.as_ref(), attrs.metadata(i)) + }), + &text::to_attributes(text.font), + cosmic_text::Shaping::Advanced, + None, + ); + + let min_bounds = + text::align(&mut buffer, font_system.raw(), text.align_x); + + Self(Arc::new(Internal { + buffer, + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + wrapping: text.wrapping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) + } + + fn resize(&mut self, new_bounds: Size) { + let paragraph = Arc::make_mut(&mut self.0); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + paragraph.buffer.set_size( + Some(new_bounds.width), + Some(new_bounds.height), + ); + + let min_bounds = text::align( + &mut paragraph.buffer, + font_system.raw(), + paragraph.align_x, + ); + + paragraph.bounds = new_bounds; + paragraph.min_bounds = min_bounds; + } + + fn compare(&self, text: Text<()>) -> core::text::Difference { + let font_system = text::font_system().read().expect("Read font system"); + let paragraph = self.internal(); + let metrics = paragraph.buffer.metrics(); + + if paragraph.version != font_system.version + || metrics.font_size != text.size.0 + || metrics.line_height != text.line_height.to_absolute(text.size).0 + || paragraph.font != text.font + || paragraph.shaping != text.shaping + || paragraph.wrapping != text.wrapping + || paragraph.align_x != text.align_x + || paragraph.align_y != text.align_y + { + core::text::Difference::Shape + } else if paragraph.bounds != text.bounds { + core::text::Difference::Bounds + } else { + core::text::Difference::None + } + } + + fn size(&self) -> Pixels { + Pixels(self.0.buffer.metrics().font_size) + } + + fn font(&self) -> Font { + self.0.font + } + + fn line_height(&self) -> LineHeight { + LineHeight::Absolute(Pixels(self.0.buffer.metrics().line_height)) + } + + fn align_x(&self) -> Alignment { + self.internal().align_x + } + + fn align_y(&self) -> alignment::Vertical { + self.internal().align_y + } + + fn wrapping(&self) -> Wrapping { + self.0.wrapping + } + + fn shaping(&self) -> Shaping { + self.0.shaping + } + + fn bounds(&self) -> Size { + self.0.bounds + } + + fn min_bounds(&self) -> Size { + self.internal().min_bounds + } + + fn hit_test(&self, point: Point) -> Option { + let cursor = self.internal().buffer.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + } + + fn hit_span(&self, point: Point) -> Option { + let internal = self.internal(); + + let cursor = internal.buffer.hit(point.x, point.y)?; + let line = internal.buffer.lines.get(cursor.line)?; + + if cursor.index >= line.text().len() { + return None; + } + + let index = match cursor.affinity { + cosmic_text::Affinity::Before => cursor.index.saturating_sub(1), + cosmic_text::Affinity::After => cursor.index, + }; + + let mut hit = None; + let glyphs = line + .layout_opt() + .as_ref()? + .iter() + .flat_map(|line| line.glyphs.iter()); + + for glyph in glyphs { + if glyph.start <= index && index < glyph.end { + hit = Some(glyph); + break; + } + } + + Some(hit?.metadata) + } + + fn span_bounds(&self, index: usize) -> Vec { + let internal = self.internal(); + + let mut bounds = Vec::new(); + let mut current_bounds = None; + + let glyphs = internal + .buffer + .layout_runs() + .flat_map(|run| { + let line_top = run.line_top; + let line_height = run.line_height; + + run.glyphs + .iter() + .map(move |glyph| (line_top, line_height, glyph)) + }) + .skip_while(|(_, _, glyph)| glyph.metadata != index) + .take_while(|(_, _, glyph)| glyph.metadata == index); + + for (line_top, line_height, glyph) in glyphs { + let y = line_top + glyph.y; + + let new_bounds = || { + Rectangle::new( + Point::new(glyph.x, y), + Size::new( + glyph.w, + glyph.line_height_opt.unwrap_or(line_height), + ), + ) + }; + + match current_bounds.as_mut() { + None => { + current_bounds = Some(new_bounds()); + } + Some(current_bounds) if y != current_bounds.y => { + bounds.push(*current_bounds); + *current_bounds = new_bounds(); + } + Some(current_bounds) => { + current_bounds.width += glyph.w; + } + } + } + + bounds.extend(current_bounds); + bounds + } + + fn grapheme_position(&self, line: usize, index: usize) -> Option { + use unicode_segmentation::UnicodeSegmentation; + + let run = self.internal().buffer.layout_runs().nth(line)?; + + // index represents a grapheme, not a glyph + // Let's find the first glyph for the given grapheme cluster + let mut last_start = None; + let mut last_grapheme_count = 0; + let mut graphemes_seen = 0; + + let glyph = run + .glyphs + .iter() + .find(|glyph| { + if Some(glyph.start) != last_start { + last_grapheme_count = run.text[glyph.start..glyph.end] + .graphemes(false) + .count(); + last_start = Some(glyph.start); + graphemes_seen += last_grapheme_count; + } + + graphemes_seen >= index + }) + .or_else(|| run.glyphs.last())?; + + let advance = if index == 0 { + 0.0 + } else { + glyph.w + * (1.0 + - graphemes_seen.saturating_sub(index) as f32 + / last_grapheme_count.max(1) as f32) + }; + + Some(Point::new( + glyph.x + glyph.x_offset * glyph.font_size + advance, + glyph.y - glyph.y_offset * glyph.font_size, + )) + } +} + +impl Default for Paragraph { + fn default() -> Self { + Self(Arc::new(Internal::default())) + } +} + +impl fmt::Debug for Paragraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let paragraph = self.internal(); + + f.debug_struct("Paragraph") + .field("font", ¶graph.font) + .field("shaping", ¶graph.shaping) + .field("horizontal_alignment", ¶graph.align_x) + .field("vertical_alignment", ¶graph.align_y) + .field("bounds", ¶graph.bounds) + .field("min_bounds", ¶graph.min_bounds) + .finish() + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.shaping == other.shaping + && self.align_x == other.align_x + && self.align_y == other.align_y + && self.bounds == other.bounds + && self.min_bounds == other.min_bounds + && self.buffer.metrics() == other.buffer.metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }), + font: Font::default(), + shaping: Shaping::default(), + wrapping: Wrapping::default(), + align_x: Alignment::Default, + align_y: alignment::Vertical::Top, + bounds: Size::ZERO, + min_bounds: Size::ZERO, + version: text::Version::default(), + } + } +} + +/// A weak reference to a [`Paragraph`]. +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak, + /// The minimum bounds of the [`Paragraph`]. + pub min_bounds: Size, + /// The horizontal alignment of the [`Paragraph`]. + pub align_x: Alignment, + /// The vertical alignment of the [`Paragraph`]. + pub align_y: alignment::Vertical, +} + +impl Weak { + /// Tries to update the reference into a [`Paragraph`]. + pub fn upgrade(&self) -> Option { + self.raw.upgrade().map(Paragraph) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} diff --git a/vendor/iced_graphics/src/viewport.rs b/vendor/iced_graphics/src/viewport.rs new file mode 100644 index 0000000..ea4cd8b --- /dev/null +++ b/vendor/iced_graphics/src/viewport.rs @@ -0,0 +1,56 @@ +use crate::core::{Size, Transformation}; + +/// A viewing region for displaying computer graphics. +#[derive(Debug, Clone)] +pub struct Viewport { + physical_size: Size, + logical_size: Size, + scale_factor: f32, + projection: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given physical dimensions and scale + /// factor. + pub fn with_physical_size(size: Size, scale_factor: f32) -> Viewport { + Viewport { + physical_size: size, + logical_size: Size::new( + size.width as f32 / scale_factor, + size.height as f32 / scale_factor, + ), + scale_factor, + projection: Transformation::orthographic(size.width, size.height), + } + } + + /// Returns the physical size of the [`Viewport`]. + pub fn physical_size(&self) -> Size { + self.physical_size + } + + /// Returns the physical width of the [`Viewport`]. + pub fn physical_width(&self) -> u32 { + self.physical_size.width + } + + /// Returns the physical height of the [`Viewport`]. + pub fn physical_height(&self) -> u32 { + self.physical_size.height + } + + /// Returns the logical size of the [`Viewport`]. + pub fn logical_size(&self) -> Size { + self.logical_size + } + + /// Returns the scale factor of the [`Viewport`]. + pub fn scale_factor(&self) -> f32 { + self.scale_factor + } + + /// Returns the projection transformation of the [`Viewport`]. + pub fn projection(&self) -> Transformation { + self.projection + } +} diff --git a/vendor/iced_tiny_skia/Cargo.toml b/vendor/iced_tiny_skia/Cargo.toml new file mode 100644 index 0000000..2eeb83c --- /dev/null +++ b/vendor/iced_tiny_skia/Cargo.toml @@ -0,0 +1,127 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "iced_tiny_skia" +version = "0.14.1" +authors = ["Héctor Ramón Jiménez "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A software renderer for iced on top of tiny-skia" +homepage = "https://iced.rs" +readme = false +keywords = [ + "gui", + "ui", + "graphics", + "interface", + "widgets", +] +categories = ["gui"] +license = "MIT" +repository = "https://github.com/iced-rs/iced" + +[features] +default = [ + "x11", + "wayland", +] +geometry = ["iced_graphics/geometry"] +image = ["iced_graphics/image"] +svg = [ + "iced_graphics/svg", + "resvg", +] +wayland = [ + "softbuffer/wayland", + "softbuffer/wayland-dlopen", +] +x11 = [ + "softbuffer/x11", + "softbuffer/x11-dlopen", +] + +[lib] +name = "iced_tiny_skia" +path = "src/lib.rs" + +[dependencies.bytemuck] +version = "1.0" +features = ["derive"] + +[dependencies.cosmic-text] +version = "=0.19.0" + +[dependencies.iced_debug] +version = "0.14.0" + +[dependencies.iced_graphics] +version = "0.14.0" + +[dependencies.kurbo] +version = "0.10" + +[dependencies.log] +version = "0.4" + +[dependencies.resvg] +version = "0.45" +optional = true + +[dependencies.rustc-hash] +version = "2.0" + +[dependencies.softbuffer] +version = "0.4" +default-features = false + +[dependencies.tiny-skia] +version = "0.11" +features = [ + "std", + "simd", +] +default-features = false + +[lints.clippy] +default_trait_access = "deny" +filter_map_next = "deny" +from_over_into = "deny" +large-enum-variant = "allow" +manual_let_else = "deny" +map-entry = "allow" +match-wildcard-for-single-variants = "deny" +needless_borrow = "deny" +new_without_default = "deny" +redundant-closure-for-method-calls = "deny" +result_large_err = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +type-complexity = "allow" +unused_async = "deny" +useless_conversion = "deny" + +[lints.rust] +missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.rust.rust_2018_idioms] +level = "deny" +priority = -1 + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/vendor/iced_tiny_skia/LICENSE b/vendor/iced_tiny_skia/LICENSE new file mode 100644 index 0000000..f1148e2 --- /dev/null +++ b/vendor/iced_tiny_skia/LICENSE @@ -0,0 +1,18 @@ +Copyright 2019 Héctor Ramón, Iced contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/iced_tiny_skia/src/engine.rs b/vendor/iced_tiny_skia/src/engine.rs new file mode 100644 index 0000000..d23a40c --- /dev/null +++ b/vendor/iced_tiny_skia/src/engine.rs @@ -0,0 +1,860 @@ +use crate::Primitive; +use crate::core::renderer::Quad; +use crate::core::{ + Background, Color, Gradient, Rectangle, Size, Transformation, Vector, +}; +use crate::graphics::{Image, Text}; +use crate::text; + +#[derive(Debug)] +pub struct Engine { + text_pipeline: text::Pipeline, + + #[cfg(feature = "image")] + pub(crate) raster_pipeline: crate::raster::Pipeline, + #[cfg(feature = "svg")] + pub(crate) vector_pipeline: crate::vector::Pipeline, +} + +impl Engine { + pub fn new() -> Self { + Self { + text_pipeline: text::Pipeline::new(), + #[cfg(feature = "image")] + raster_pipeline: crate::raster::Pipeline::new(), + #[cfg(feature = "svg")] + vector_pipeline: crate::vector::Pipeline::new(), + } + } + + pub fn draw_quad( + &mut self, + quad: &Quad, + background: &Background, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + let physical_bounds = quad.bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let transform = into_transform(transformation); + + // Make sure the border radius is not larger than the bounds + let border_width = quad + .border + .width + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); + + for radius in &mut fill_border_radius { + *radius = (*radius) + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + } + + let path = rounded_rectangle(quad.bounds, fill_border_radius); + + let shadow = quad.shadow; + + if shadow.color.a > 0.0 { + let shadow_bounds = Rectangle { + x: quad.bounds.x + shadow.offset.x - shadow.blur_radius, + y: quad.bounds.y + shadow.offset.y - shadow.blur_radius, + width: quad.bounds.width + shadow.blur_radius * 2.0, + height: quad.bounds.height + shadow.blur_radius * 2.0, + } * transformation; + + let radii = fill_border_radius + .into_iter() + .map(|radius| radius * transformation.scale_factor()) + .collect::>(); + let (x, y, width, height) = ( + shadow_bounds.x as u32, + shadow_bounds.y as u32, + shadow_bounds.width as u32, + shadow_bounds.height as u32, + ); + let half_width = physical_bounds.width / 2.0; + let half_height = physical_bounds.height / 2.0; + + let colors = (y..y + height) + .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32))) + .filter_map(|(x, y)| { + tiny_skia::Size::from_wh(half_width, half_height).map( + |size| { + let shadow_distance = rounded_box_sdf( + Vector::new( + x - physical_bounds.position().x + - (shadow.offset.x + * transformation.scale_factor()) + - half_width, + y - physical_bounds.position().y + - (shadow.offset.y + * transformation.scale_factor()) + - half_height, + ), + size, + &radii, + ) + .max(0.0); + let shadow_alpha = 1.0 + - smoothstep( + -shadow.blur_radius + * transformation.scale_factor(), + shadow.blur_radius + * transformation.scale_factor(), + shadow_distance, + ); + + let mut color = into_color(shadow.color); + color.apply_opacity(shadow_alpha); + + color.to_color_u8().premultiply() + }, + ) + }) + .collect(); + + if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height) + .and_then(|size| { + tiny_skia::Pixmap::from_vec( + bytemuck::cast_vec(colors), + size, + ) + }) + { + pixels.draw_pixmap( + x as i32, + y as i32, + pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::default(), + None, + ); + } + } + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color(*color)) + } + Background::Gradient(Gradient::Linear(linear)) => { + let (start, end) = + linear.angle.to_distance(&quad.bounds); + + let stops: Vec = linear + .stops + .into_iter() + .flatten() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(); + + tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: start.x, + y: start.y, + }, + tiny_skia::Point { x: end.x, y: end.y }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } + }, + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + if border_width > 0.0 { + // Border path is offset by half the border width + let border_bounds = Rectangle { + x: quad.bounds.x + border_width / 2.0, + y: quad.bounds.y + border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + // Make sure the border radius is correct + let mut border_radius = <[f32; 4]>::from(quad.border.radius); + let mut is_simple_border = true; + + for radius in &mut border_radius { + *radius = if *radius == 0.0 { + // Path should handle this fine + 0.0 + } else if *radius > border_width / 2.0 { + *radius - border_width / 2.0 + } else { + is_simple_border = false; + 0.0 + } + .min(border_bounds.width / 2.0) + .min(border_bounds.height / 2.0); + } + + // Stroking a path works well in this case + if is_simple_border { + let border_path = + rounded_rectangle(border_bounds, border_radius); + + pixels.stroke_path( + &border_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } else { + // Draw corners that have too small border radii as having no border radius, + // but mask them with the rounded rectangle with the correct border radius. + let mut temp_pixmap = tiny_skia::Pixmap::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let mut quad_mask = tiny_skia::Mask::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let zero_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: quad.bounds.width, + height: quad.bounds.height, + }; + let path = rounded_rectangle(zero_bounds, fill_border_radius); + + quad_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + true, + transform, + ); + let path_bounds = Rectangle { + x: border_width / 2.0, + y: border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + let border_radius_path = + rounded_rectangle(path_bounds, border_radius); + + temp_pixmap.stroke_path( + &border_radius_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + Some(&quad_mask), + ); + + pixels.draw_pixmap( + quad.bounds.x as i32, + quad.bounds.y as i32, + temp_pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + transform, + clip_mask, + ); + } + } + } + + pub fn draw_text( + &mut self, + text: &Text, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + match text { + Text::Paragraph { + paragraph, + position, + color, + clip_bounds: local_clip_bounds, + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + let Some(clip_bounds) = clip_bounds + .intersection(&(*local_clip_bounds * transformation)) + else { + return; + }; + + let physical_bounds = + Rectangle::new(*position, paragraph.min_bounds) + * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = match physical_bounds.is_within(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; + + self.text_pipeline.draw_paragraph( + paragraph, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Editor { + editor, + position, + color, + clip_bounds: local_clip_bounds, + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + let Some(clip_bounds) = clip_bounds + .intersection(&(*local_clip_bounds * transformation)) + else { + return; + }; + + let physical_bounds = + Rectangle::new(*position, editor.bounds) * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = match physical_bounds.is_within(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; + + self.text_pipeline.draw_editor( + editor, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Cached { + content, + bounds, + color, + size, + line_height, + font, + align_x, + align_y, + shaping, + clip_bounds: local_clip_bounds, + } => { + let physical_bounds = *local_clip_bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = match physical_bounds.is_within(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; + + self.text_pipeline.draw_cached( + content, + *bounds, + *color, + *size, + *line_height, + *font, + *align_x, + *align_y, + *shaping, + pixels, + clip_mask, + transformation, + ); + } + Text::Raw { + raw, + transformation: local_transformation, + } => { + let Some(buffer) = raw.buffer.upgrade() else { + return; + }; + + let transformation = transformation * *local_transformation; + let (width, height) = buffer.size(); + + let physical_bounds = Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(clip_bounds.width), + height.unwrap_or(clip_bounds.height), + ), + ) * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + raw.position, + raw.color, + pixels, + clip_mask, + transformation, + ); + } + } + } + + pub fn draw_primitive( + &mut self, + primitive: &Primitive, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + match primitive { + Primitive::Fill { path, paint, rule } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } * transformation + }; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + pixels.fill_path( + path, + paint, + *rule, + into_transform(transformation), + clip_mask, + ); + } + Primitive::Stroke { + path, + paint, + stroke, + } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x() - stroke.width / 2.0, + y: bounds.y() - stroke.width / 2.0, + width: bounds.width() + stroke.width, + height: bounds.height() + stroke.width, + } * transformation + }; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + pixels.stroke_path( + path, + paint, + stroke, + into_transform(transformation), + clip_mask, + ); + } + } + } + + pub fn draw_image( + &mut self, + image: &Image, + _transformation: Transformation, + _pixels: &mut tiny_skia::PixmapMut<'_>, + _clip_mask: &mut tiny_skia::Mask, + _clip_bounds: Rectangle, + ) { + match image { + #[cfg(feature = "image")] + Image::Raster { image, bounds, .. } => { + let physical_bounds = *bounds * _transformation; + + if !_clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(image.rotation); + + let transform = into_transform(_transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); + + self.raster_pipeline.draw( + &image.handle, + image.filter_method, + *bounds, + image.opacity, + _pixels, + transform, + clip_mask, + ); + } + #[cfg(feature = "svg")] + Image::Vector { svg, bounds, .. } => { + let physical_bounds = *bounds * _transformation; + + if !_clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(svg.rotation); + + let transform = into_transform(_transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); + + self.vector_pipeline.draw( + &svg.handle, + svg.color, + *bounds, + svg.opacity, + _pixels, + transform, + clip_mask, + ); + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + } + } + + pub fn trim(&mut self) { + self.text_pipeline.trim_cache(); + + #[cfg(feature = "image")] + self.raster_pipeline.trim_cache(); + + #[cfg(feature = "svg")] + self.vector_pipeline.trim_cache(); + } +} + +pub fn into_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Convert color from iced to tiny_skia") +} + +fn into_transform(transformation: Transformation) -> tiny_skia::Transform { + let translation = transformation.translation(); + + tiny_skia::Transform { + sx: transformation.scale_factor(), + kx: 0.0, + ky: 0.0, + sy: transformation.scale_factor(), + tx: translation.x, + ty: translation.y, + } +} + +fn rounded_rectangle( + bounds: Rectangle, + border_radius: [f32; 4], +) -> tiny_skia::Path { + let [top_left, top_right, bottom_right, bottom_left] = border_radius; + + if top_left == 0.0 + && top_right == 0.0 + && bottom_right == 0.0 + && bottom_left == 0.0 + { + return tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Build quad rectangle"), + ); + } + + if top_left == top_right + && top_left == bottom_right + && top_left == bottom_left + && top_left == bounds.width / 2.0 + && top_left == bounds.height / 2.0 + { + return tiny_skia::PathBuilder::from_circle( + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + top_left, + ) + .expect("Build circle path"); + } + + let mut builder = tiny_skia::PathBuilder::new(); + + builder.move_to(bounds.x + top_left, bounds.y); + builder.line_to(bounds.x + bounds.width - top_right, bounds.y); + + if top_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width - top_right, + bounds.y, + bounds.x + bounds.width, + bounds.y + top_right, + top_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + + if bottom_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height, + bottom_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + ); + + if bottom_left > 0.0 { + arc_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bottom_left, + bottom_left, + ); + } + + maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); + + if top_left > 0.0 { + arc_to( + &mut builder, + bounds.x, + bounds.y + top_left, + bounds.x + top_left, + bounds.y, + top_left, + ); + } + + builder.finish().expect("Build rounded rectangle path") +} + +fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { + if path.last_point() != Some(tiny_skia::Point { x, y }) { + path.line_to(x, y); + } +} + +fn arc_to( + path: &mut tiny_skia::PathBuilder, + x_from: f32, + y_from: f32, + x_to: f32, + y_to: f32, + radius: f32, +) { + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), + to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), + radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), + x_rotation: 0.0, + large_arc: false, + sweep: true, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + arc.to_cubic_beziers(0.1, |p1, p2, p| { + path.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }); + } + None => { + path.line_to(x_to, y_to); + } + } +} + +fn smoothstep(a: f32, b: f32, x: f32) -> f32 { + let x = ((x - a) / (b - a)).clamp(0.0, 1.0); + + x * x * (3.0 - 2.0 * x) +} + +fn rounded_box_sdf( + to_center: Vector, + size: tiny_skia::Size, + radii: &[f32], +) -> f32 { + let radius = match (to_center.x > 0.0, to_center.y > 0.0) { + (true, true) => radii[2], + (true, false) => radii[1], + (false, true) => radii[3], + (false, false) => radii[0], + }; + + let x = (to_center.x.abs() - size.width() + radius).max(0.0); + let y = (to_center.y.abs() - size.height() + radius).max(0.0); + + (x.powf(2.0) + y.powf(2.0)).sqrt() - radius +} + +pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { + clip_mask.clear(); + + let path = { + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .unwrap(), + ); + + builder.finish().unwrap() + }; + + clip_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + false, + tiny_skia::Transform::default(), + ); +} diff --git a/vendor/iced_tiny_skia/src/geometry.rs b/vendor/iced_tiny_skia/src/geometry.rs new file mode 100644 index 0000000..a45d952 --- /dev/null +++ b/vendor/iced_tiny_skia/src/geometry.rs @@ -0,0 +1,513 @@ +use crate::Primitive; +use crate::core::text::LineHeight; +use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector}; +use crate::graphics::cache::{self, Cached}; +use crate::graphics::geometry::fill::{self, Fill}; +use crate::graphics::geometry::stroke::{self, Stroke}; +use crate::graphics::geometry::{self, Path, Style}; +use crate::graphics::{self, Gradient, Image, Text}; + +use std::sync::Arc; + +#[derive(Debug)] +pub enum Geometry { + Live { + text: Vec, + images: Vec, + primitives: Vec, + clip_bounds: Rectangle, + }, + Cache(Cache), +} + +#[derive(Debug, Clone)] +pub struct Cache { + pub text: Arc<[Text]>, + pub images: Arc<[graphics::Image]>, + pub primitives: Arc<[Primitive]>, + pub clip_bounds: Rectangle, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Cache) -> Self { + Self::Cache(cache.clone()) + } + + fn cache(self, _group: cache::Group, _previous: Option) -> Cache { + match self { + Self::Live { + primitives, + images, + text, + clip_bounds, + } => Cache { + primitives: Arc::from(primitives), + images: Arc::from(images), + text: Arc::from(text), + clip_bounds, + }, + Self::Cache(cache) => cache, + } + } +} + +#[derive(Debug)] +pub struct Frame { + clip_bounds: Rectangle, + transform: tiny_skia::Transform, + stack: Vec, + primitives: Vec, + images: Vec, + text: Vec, +} + +impl Frame { + pub fn new(bounds: Rectangle) -> Self { + Self { + clip_bounds: bounds, + stack: Vec::new(), + primitives: Vec::new(), + images: Vec::new(), + text: Vec::new(), + transform: tiny_skia::Transform::identity(), + } + } +} + +impl geometry::frame::Backend for Frame { + type Geometry = Geometry; + + fn width(&self) -> f32 { + self.clip_bounds.width + } + + fn height(&self) -> f32 { + self.clip_bounds.height + } + + fn size(&self) -> Size { + self.clip_bounds.size() + } + + fn center(&self) -> Point { + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) + } + + fn fill(&mut self, path: &Path, fill: impl Into) { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { + return; + }; + + let fill = fill.into(); + + let mut paint = into_paint(fill.style); + paint.shader.transform(self.transform); + + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); + } + + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Some(path) = convert_path(&Path::rectangle(top_left, size)) + .and_then(|path| path.transform(self.transform)) + else { + return; + }; + + let fill = fill.into(); + + let mut paint = tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }; + paint.shader.transform(self.transform); + + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); + } + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { + return; + }; + + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + let mut paint = into_paint(stroke.style); + paint.shader.transform(self.transform); + + self.primitives.push(Primitive::Stroke { + path, + paint, + stroke: skia_stroke, + }); + } + + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + self.stroke(&Path::rectangle(top_left, size), stroke); + } + + fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let (scale_x, scale_y) = self.transform.get_scale(); + + if !self.transform.has_skew() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (bounds, size, line_height) = if self.transform.is_identity() { + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) + } else { + let mut position = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut position); + + let size = text.size.0 * scale_y; + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + ( + Rectangle { + x: position[0].x, + y: position[0].y, + width: text.max_width * scale_x, + height: f32::INFINITY, + }, + size.into(), + line_height, + ) + }; + + // TODO: Honor layering! + self.text.push(Text::Cached { + content: text.content, + bounds, + color: text.color, + size, + line_height: line_height.to_absolute(size), + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITE), + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } + } + + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ) { + let text = text.into(); + let stroke = stroke.into(); + + text.draw_with(|path, _color| self.stroke(&path, stroke)); + } + + fn push_transform(&mut self) { + self.stack.push(self.transform); + } + + fn pop_transform(&mut self) { + self.transform = self.stack.pop().expect("Pop transform"); + } + + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self::new(clip_bounds) + } + + fn paste(&mut self, frame: Self) { + self.primitives.extend(frame.primitives); + self.text.extend(frame.text); + self.images.extend(frame.images); + } + + fn translate(&mut self, translation: Vector) { + self.transform = + self.transform.pre_translate(translation.x, translation.y); + } + + fn rotate(&mut self, angle: impl Into) { + self.transform = self.transform.pre_concat( + tiny_skia::Transform::from_rotate(angle.into().0.to_degrees()), + ); + } + + fn scale(&mut self, scale: impl Into) { + let scale = scale.into(); + + self.scale_nonuniform(Vector { x: scale, y: scale }); + } + + fn scale_nonuniform(&mut self, scale: impl Into) { + let scale = scale.into(); + + self.transform = self.transform.pre_scale(scale.x, scale.y); + } + + fn into_geometry(self) -> Geometry { + Geometry::Live { + primitives: self.primitives, + images: self.images, + text: self.text, + clip_bounds: self.clip_bounds, + } + } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { + let mut image = image.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + image.rotation += external_rotation; + + self.images.push(graphics::Image::Raster { + image, + bounds, + clip_bounds: self.clip_bounds, + }); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector { + svg, + bounds, + clip_bounds: self.clip_bounds, + }); + } +} + +fn transform_rectangle( + rectangle: Rectangle, + transform: tiny_skia::Transform, +) -> (Rectangle, Radians) { + let mut top_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y, + }; + + let mut top_right = tiny_skia::Point { + x: rectangle.x + rectangle.width, + y: rectangle.y, + }; + + let mut bottom_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y + rectangle.height, + }; + + transform.map_point(&mut top_left); + transform.map_point(&mut top_right); + transform.map_point(&mut bottom_left); + + Rectangle::with_vertices( + Point::new(top_left.x, top_left.y), + Point::new(top_right.x, top_right.y), + Point::new(bottom_left.x, bottom_left.y), + ) +} + +fn convert_path(path: &Path) -> Option { + use iced_graphics::geometry::path::lyon_path; + + let mut builder = tiny_skia::PathBuilder::new(); + let mut last_point = lyon_path::math::Point::default(); + + for event in path.raw() { + match event { + lyon_path::Event::Begin { at } => { + builder.move_to(at.x, at.y); + + last_point = at; + } + lyon_path::Event::Line { from, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.line_to(to.x, to.y); + + last_point = to; + } + lyon_path::Event::Quadratic { from, ctrl, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::Cubic { + from, + ctrl1, + ctrl2, + to, + } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder + .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::End { close, .. } => { + if close { + builder.close(); + } + } + } + } + + let result = builder.finish(); + + #[cfg(debug_assertions)] + if result.is_none() { + log::warn!("Invalid path: {:?}", path.raw()); + } + + result +} + +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { + tiny_skia::Paint { + shader: match style { + Style::Solid(color) => tiny_skia::Shader::SolidColor( + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Create color"), + ), + Style::Gradient(gradient) => match gradient { + Gradient::Linear(linear) => { + let stops: Vec = linear + .stops + .into_iter() + .flatten() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(); + + tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: linear.start.x, + y: linear.start.y, + }, + tiny_skia::Point { + x: linear.end.x, + y: linear.end.y, + }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } + }, + }, + anti_alias: true, + ..Default::default() + } +} + +pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { + match rule { + fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, + fill::Rule::NonZero => tiny_skia::FillRule::Winding, + } +} + +pub fn into_stroke(stroke: &Stroke<'_>) -> tiny_skia::Stroke { + tiny_skia::Stroke { + width: stroke.width, + line_cap: match stroke.line_cap { + stroke::LineCap::Butt => tiny_skia::LineCap::Butt, + stroke::LineCap::Square => tiny_skia::LineCap::Square, + stroke::LineCap::Round => tiny_skia::LineCap::Round, + }, + line_join: match stroke.line_join { + stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, + stroke::LineJoin::Round => tiny_skia::LineJoin::Round, + stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, + }, + dash: if stroke.line_dash.segments.is_empty() { + None + } else { + tiny_skia::StrokeDash::new( + stroke.line_dash.segments.into(), + stroke.line_dash.offset as f32, + ) + }, + ..Default::default() + } +} diff --git a/vendor/iced_tiny_skia/src/layer.rs b/vendor/iced_tiny_skia/src/layer.rs new file mode 100644 index 0000000..92ef6a9 --- /dev/null +++ b/vendor/iced_tiny_skia/src/layer.rs @@ -0,0 +1,420 @@ +use crate::Primitive; +use crate::core::renderer::Quad; +use crate::core::{ + self, Background, Color, Point, Rectangle, Svg, Transformation, +}; +use crate::graphics::damage; +use crate::graphics::layer; +use crate::graphics::text::{Editor, Paragraph, Text}; +use crate::graphics::{self, Image}; + +use std::sync::Arc; + +pub type Stack = layer::Stack; + +#[derive(Debug, Clone)] +pub struct Layer { + pub bounds: Rectangle, + pub quads: Vec<(Quad, Background)>, + pub primitives: Vec>, + pub images: Vec, + pub text: Vec>, +} + +impl Layer { + pub fn draw_quad( + &mut self, + mut quad: Quad, + background: Background, + transformation: Transformation, + ) { + quad.bounds = quad.bounds * transformation; + self.quads.push((quad, background)); + } + + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(paragraph)); + } + + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(editor)); + } + + pub fn draw_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.text.push(Item::Live(text)); + } + + pub fn draw_text_raw( + &mut self, + raw: graphics::text::Raw, + transformation: Transformation, + ) { + let raw = Text::Raw { + raw, + transformation, + }; + + self.text.push(Item::Live(raw)); + } + + pub fn draw_text_group( + &mut self, + text: Vec, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Group(text, clip_bounds, transformation)); + } + + pub fn draw_text_cache( + &mut self, + text: Arc<[Text]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Cached(text, clip_bounds, transformation)); + } + + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster { + image, + bounds, + clip_bounds, + } => { + self.draw_raster(image, bounds, clip_bounds, transformation); + } + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + self.draw_svg(svg, bounds, clip_bounds, transformation); + } + } + } + + pub fn draw_raster( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let image = Image::Raster { + image: core::Image { + border_radius: image.border_radius + * transformation.scale_factor(), + ..image + }, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + svg: Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let svg = Image::Vector { + svg, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; + + self.images.push(svg); + } + + pub fn draw_primitive_group( + &mut self, + primitives: Vec, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Group( + primitives, + clip_bounds * transformation, + transformation, + )); + } + + pub fn draw_primitive_cache( + &mut self, + primitives: Arc<[Primitive]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Cached( + primitives, + clip_bounds * transformation, + transformation, + )); + } + + pub fn damage(previous: &Self, current: &Self) -> Vec { + if previous.bounds != current.bounds { + return vec![previous.bounds, current.bounds]; + } + + let mut damage = damage::list( + &previous.quads, + ¤t.quads, + |(quad, _)| { + quad.bounds + .expand(1.0) + .intersection(¤t.bounds) + .into_iter() + .collect() + }, + |(quad_a, background_a), (quad_b, background_b)| { + quad_a == quad_b && background_a == background_b + }, + ); + + let text = damage::diff( + &previous.text, + ¤t.text, + |item| { + item.as_slice() + .iter() + .filter_map(Text::visible_bounds) + .map(|bounds| bounds * item.transformation()) + .collect() + }, + |text_a, text_b| { + damage::list( + text_a.as_slice(), + text_b.as_slice(), + |text| { + text.visible_bounds() + .into_iter() + .map(|bounds| bounds * text_a.transformation()) + .collect() + }, + |text_a, text_b| text_a == text_b, + ) + }, + ); + + let primitives = damage::list( + &previous.primitives, + ¤t.primitives, + |item| match item { + Item::Live(primitive) => vec![primitive.visible_bounds()], + Item::Group(primitives, group_bounds, transformation) => { + primitives + .as_slice() + .iter() + .map(Primitive::visible_bounds) + .map(|bounds| bounds * *transformation) + .filter_map(|bounds| bounds.intersection(group_bounds)) + .collect() + } + Item::Cached(_primitives, bounds, _transformation) => { + vec![*bounds] + } + }, + |primitive_a, primitive_b| match (primitive_a, primitive_b) { + ( + Item::Cached(cache_a, bounds_a, transformation_a), + Item::Cached(cache_b, bounds_b, transformation_b), + ) => { + Arc::ptr_eq(cache_a, cache_b) + && bounds_a == bounds_b + && transformation_a == transformation_b + } + _ => false, + }, + ); + + let images = damage::list( + &previous.images, + ¤t.images, + |image| vec![image.bounds().expand(1.0)], + Image::eq, + ); + + damage.extend(text); + damage.extend(primitives); + damage.extend(images); + damage + } +} + +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, + quads: Vec::new(), + primitives: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } +} + +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { + Self { + bounds, + ..Self::default() + } + } + + fn bounds(&self) -> Rectangle { + self.bounds + } + + fn flush(&mut self) {} + + fn resize(&mut self, bounds: Rectangle) { + self.bounds = bounds; + } + + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; + + self.quads.clear(); + self.primitives.clear(); + self.text.clear(); + self.images.clear(); + } + + fn start(&self) -> usize { + if !self.quads.is_empty() { + return 1; + } + + if !self.primitives.is_empty() { + return 2; + } + + if !self.images.is_empty() { + return 3; + } + + if !self.text.is_empty() { + return 4; + } + + usize::MAX + } + + fn end(&self) -> usize { + if !self.text.is_empty() { + return 4; + } + + if !self.images.is_empty() { + return 3; + } + + if !self.primitives.is_empty() { + return 2; + } + + if !self.quads.is_empty() { + return 1; + } + + 0 + } + + fn merge(&mut self, layer: &mut Self) { + self.quads.append(&mut layer.quads); + self.primitives.append(&mut layer.primitives); + self.text.append(&mut layer.text); + self.images.append(&mut layer.images); + } +} + +#[derive(Debug, Clone)] +pub enum Item { + Live(T), + Group(Vec, Rectangle, Transformation), + Cached(Arc<[T]>, Rectangle, Transformation), +} + +impl Item { + pub fn transformation(&self) -> Transformation { + match self { + Item::Live(_) => Transformation::IDENTITY, + Item::Group(_, _, transformation) + | Item::Cached(_, _, transformation) => *transformation, + } + } + + pub fn clip_bounds(&self) -> Rectangle { + match self { + Item::Live(_) => Rectangle::INFINITE, + Item::Group(_, clip_bounds, _) + | Item::Cached(_, clip_bounds, _) => *clip_bounds, + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Item::Live(item) => std::slice::from_ref(item), + Item::Group(group, _, _) => group.as_slice(), + Item::Cached(cache, _, _) => cache, + } + } +} diff --git a/vendor/iced_tiny_skia/src/lib.rs b/vendor/iced_tiny_skia/src/lib.rs new file mode 100644 index 0000000..453fc34 --- /dev/null +++ b/vendor/iced_tiny_skia/src/lib.rs @@ -0,0 +1,463 @@ +#![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg))] +pub mod window; + +mod engine; +mod layer; +mod primitive; +mod settings; +mod text; + +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + +#[cfg(feature = "geometry")] +pub mod geometry; + +use iced_debug as debug; +pub use iced_graphics as graphics; +pub use iced_graphics::core; + +pub use layer::Layer; +pub use primitive::Primitive; +pub use settings::Settings; + +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::renderer; +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::engine::Engine; +use crate::graphics::Viewport; +use crate::graphics::compositor; +use crate::graphics::text::{Editor, Paragraph}; + +/// A [`tiny-skia`] graphics renderer for [`iced`]. +/// +/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia +/// [`iced`]: https://github.com/iced-rs/iced +#[derive(Debug)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + engine: Engine, // TODO: Shared engine +} + +impl Renderer { + pub fn new(default_font: Font, default_text_size: Pixels) -> Self { + Self { + default_font, + default_text_size, + layers: layer::Stack::new(), + engine: Engine::new(), + } + } + + pub fn layers(&mut self) -> &[Layer] { + self.layers.flush(); + self.layers.as_slice() + } + + pub fn draw( + &mut self, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + viewport: &Viewport, + damage: &[Rectangle], + background_color: Color, + ) { + let scale_factor = viewport.scale_factor(); + + self.layers.flush(); + + for &damage_bounds in damage { + let damage_bounds = damage_bounds * scale_factor; + + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + damage_bounds.x, + damage_bounds.y, + damage_bounds.width, + damage_bounds.height, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(engine::into_color( + background_color, + )), + anti_alias: false, + blend_mode: tiny_skia::BlendMode::Source, + ..Default::default() + }, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), + None, + ); + + for layer in self.layers.iter() { + let Some(layer_bounds) = + damage_bounds.intersection(&(layer.bounds * scale_factor)) + else { + continue; + }; + + engine::adjust_clip_mask(clip_mask, layer_bounds); + + if !layer.quads.is_empty() { + let render_span = debug::render(debug::Primitive::Quad); + for (quad, background) in &layer.quads { + self.engine.draw_quad( + quad, + background, + Transformation::scale(scale_factor), + pixels, + clip_mask, + layer_bounds, + ); + } + render_span.finish(); + } + + if !layer.primitives.is_empty() { + let render_span = debug::render(debug::Primitive::Triangle); + + for group in &layer.primitives { + let Some(group_bounds) = (group.clip_bounds() + * scale_factor) + .intersection(&layer_bounds) + else { + continue; + }; + + engine::adjust_clip_mask(clip_mask, group_bounds); + + for primitive in group.as_slice() { + self.engine.draw_primitive( + primitive, + Transformation::scale(scale_factor) + * group.transformation(), + pixels, + clip_mask, + group_bounds, + ); + } + + engine::adjust_clip_mask(clip_mask, layer_bounds); + } + + render_span.finish(); + } + + if !layer.images.is_empty() { + let render_span = debug::render(debug::Primitive::Image); + + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + layer_bounds, + ); + } + + render_span.finish(); + } + + if !layer.text.is_empty() { + let render_span = debug::render(debug::Primitive::Image); + + for group in &layer.text { + for text in group.as_slice() { + self.engine.draw_text( + text, + Transformation::scale(scale_factor) + * group.transformation(), + pixels, + clip_mask, + layer_bounds, + ); + } + } + + render_span.finish(); + } + } + } + + self.engine.trim(); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn reset(&mut self, new_bounds: Rectangle) { + self.layers.reset(new_bounds); + } + + fn allocate_image( + &mut self, + _handle: &core::image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + #[cfg(feature = "image")] + #[allow(unsafe_code)] + // TODO: Concurrency + callback(self.engine.raster_pipeline.load(_handle)); + + #[cfg(not(feature = "image"))] + callback(Err(core::image::Error::Unsupported)); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + const ICED_LOGO: char = '\u{e801}'; + const SCROLL_UP_ICON: char = '\u{e802}'; + const SCROLL_DOWN_ICON: char = '\u{e803}'; + const SCROLL_LEFT_ICON: char = '\u{e804}'; + const SCROLL_RIGHT_ICON: char = '\u{e805}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); + } +} + +impl graphics::text::Renderer for Renderer { + fn fill_raw(&mut self, raw: graphics::text::Raw) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text_raw(raw, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, bounds: Rectangle) -> Self::Frame { + geometry::Frame::new(bounds) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { + primitives, + images, + text, + clip_bounds, + } => { + layer.draw_primitive_group( + primitives, + clip_bounds, + transformation, + ); + + for image in images { + layer.draw_image(image, transformation); + } + + layer.draw_text_group(text, clip_bounds, transformation); + } + Geometry::Cache(cache) => { + layer.draw_primitive_cache( + cache.primitives, + cache.clip_bounds, + transformation, + ); + + for image in cache.images.iter() { + layer.draw_image(image.clone(), transformation); + } + + layer.draw_text_cache( + cache.text, + cache.clip_bounds, + transformation, + ); + } + } + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, _mesh: graphics::Mesh) { + log::warn!("iced_tiny_skia does not support drawing meshes"); + } + + fn draw_mesh_cache(&mut self, _cache: iced_graphics::mesh::Cache) { + log::warn!("iced_tiny_skia does not support drawing meshes"); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + self.engine.raster_pipeline.load(handle) + } + + fn measure_image( + &self, + handle: &Self::Handle, + ) -> Option> { + self.engine.raster_pipeline.dimensions(handle) + } + + fn draw_image( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_raster(image, bounds, clip_bounds, transformation); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg( + &self, + handle: &core::svg::Handle, + ) -> crate::core::Size { + self.engine.vector_pipeline.viewport_dimensions(handle) + } + + fn draw_svg( + &mut self, + svg: core::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg(svg, bounds, clip_bounds, transformation); + } +} + +impl compositor::Default for Renderer { + type Compositor = window::Compositor; +} + +impl renderer::Headless for Renderer { + async fn new( + default_font: Font, + default_text_size: Pixels, + backend: Option<&str>, + ) -> Option { + if backend.is_some_and(|backend| { + !["tiny-skia", "tiny_skia"].contains(&backend) + }) { + return None; + } + + Some(Self::new(default_font, default_text_size)) + } + + fn name(&self) -> String { + "tiny-skia".to_owned() + } + + fn screenshot( + &mut self, + size: Size, + scale_factor: f32, + background_color: Color, + ) -> Vec { + let viewport = Viewport::with_physical_size(size, scale_factor); + + window::compositor::screenshot(self, &viewport, background_color) + } +} diff --git a/vendor/iced_tiny_skia/src/primitive.rs b/vendor/iced_tiny_skia/src/primitive.rs new file mode 100644 index 0000000..5de5104 --- /dev/null +++ b/vendor/iced_tiny_skia/src/primitive.rs @@ -0,0 +1,40 @@ +use crate::core::Rectangle; + +#[derive(Debug, Clone, PartialEq)] +pub enum Primitive { + /// A path filled with some paint. + Fill { + /// The path to fill. + path: tiny_skia::Path, + /// The paint to use. + paint: tiny_skia::Paint<'static>, + /// The fill rule to follow. + rule: tiny_skia::FillRule, + }, + /// A path stroked with some paint. + Stroke { + /// The path to stroke. + path: tiny_skia::Path, + /// The paint to use. + paint: tiny_skia::Paint<'static>, + /// The stroke settings. + stroke: tiny_skia::Stroke, + }, +} + +impl Primitive { + /// Returns the visible bounds of the [`Primitive`]. + pub fn visible_bounds(&self) -> Rectangle { + let bounds = match self { + Primitive::Fill { path, .. } => path.bounds(), + Primitive::Stroke { path, .. } => path.bounds(), + }; + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + } +} diff --git a/vendor/iced_tiny_skia/src/raster.rs b/vendor/iced_tiny_skia/src/raster.rs new file mode 100644 index 0000000..e580419 --- /dev/null +++ b/vendor/iced_tiny_skia/src/raster.rs @@ -0,0 +1,160 @@ +use crate::core::image as raster; +use crate::core::{Rectangle, Size}; +use crate::graphics; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; +use std::collections::hash_map; + +#[derive(Debug)] +pub struct Pipeline { + cache: RefCell, +} + +impl Pipeline { + pub fn new() -> Self { + Self { + cache: RefCell::new(Cache::default()), + } + } + + pub fn load( + &self, + handle: &raster::Handle, + ) -> Result { + let mut cache = self.cache.borrow_mut(); + let image = cache.allocate(handle)?; + + #[allow(unsafe_code)] + Ok(unsafe { + raster::allocate(handle, Size::new(image.width(), image.height())) + }) + } + + pub fn dimensions(&self, handle: &raster::Handle) -> Option> { + let mut cache = self.cache.borrow_mut(); + let image = cache.allocate(handle).ok()?; + + Some(Size::new(image.width(), image.height())) + } + + pub fn draw( + &mut self, + handle: &raster::Handle, + filter_method: raster::FilterMethod, + bounds: Rectangle, + opacity: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + transform: tiny_skia::Transform, + clip_mask: Option<&tiny_skia::Mask>, + ) { + let mut cache = self.cache.borrow_mut(); + + let Ok(image) = cache.allocate(handle) else { + return; + }; + + let width_scale = bounds.width / image.width() as f32; + let height_scale = bounds.height / image.height() as f32; + + let transform = transform.pre_scale(width_scale, height_scale); + + let quality = match filter_method { + raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, + raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, + }; + + pixels.draw_pixmap( + (bounds.x / width_scale) as i32, + (bounds.y / height_scale) as i32, + image, + &tiny_skia::PixmapPaint { + quality, + opacity, + ..Default::default() + }, + transform, + clip_mask, + ); + } + + pub fn trim_cache(&mut self) { + self.cache.borrow_mut().trim(); + } +} + +#[derive(Debug, Default)] +struct Cache { + entries: FxHashMap>, + hits: FxHashSet, +} + +impl Cache { + pub fn allocate( + &mut self, + handle: &raster::Handle, + ) -> Result, raster::Error> { + let id = handle.id(); + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { + let image = match graphics::image::load(handle) { + Ok(image) => image, + Err(error) => { + let _ = entry.insert(None); + + return Err(error); + } + }; + + if image.width() == 0 || image.height() == 0 { + return Err(raster::Error::Empty); + } + + let mut buffer = + vec![0u32; image.width() as usize * image.height() as usize]; + + for (i, pixel) in image.pixels().enumerate() { + let [r, g, b, a] = pixel.0; + + buffer[i] = bytemuck::cast( + tiny_skia::ColorU8::from_rgba(b, g, r, a).premultiply(), + ); + } + + let _ = entry.insert(Some(Entry { + width: image.width(), + height: image.height(), + pixels: buffer, + })); + } + + let _ = self.hits.insert(id); + + Ok(self + .entries + .get(&id) + .unwrap() + .as_ref() + .map(|entry| { + tiny_skia::PixmapRef::from_bytes( + bytemuck::cast_slice(&entry.pixels), + entry.width, + entry.height, + ) + .expect("Build pixmap from image bytes") + }) + .expect("Image should be allocated")) + } + + fn trim(&mut self) { + self.entries.retain(|key, _| self.hits.contains(key)); + self.hits.clear(); + } +} + +#[derive(Debug)] +struct Entry { + width: u32, + height: u32, + pixels: Vec, +} diff --git a/vendor/iced_tiny_skia/src/settings.rs b/vendor/iced_tiny_skia/src/settings.rs new file mode 100644 index 0000000..672c49f --- /dev/null +++ b/vendor/iced_tiny_skia/src/settings.rs @@ -0,0 +1,34 @@ +use crate::core::{Font, Pixels}; +use crate::graphics; + +/// The settings of a [`Compositor`]. +/// +/// [`Compositor`]: crate::window::Compositor +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: Pixels, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::default(), + default_text_size: Pixels(16.0), + } + } +} + +impl From for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + } + } +} diff --git a/vendor/iced_tiny_skia/src/text.rs b/vendor/iced_tiny_skia/src/text.rs new file mode 100644 index 0000000..add4dee --- /dev/null +++ b/vendor/iced_tiny_skia/src/text.rs @@ -0,0 +1,362 @@ +use crate::core::alignment; +use crate::core::text::{Alignment, Shaping}; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; +use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::editor; +use crate::graphics::text::font_system; +use crate::graphics::text::paragraph; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::hash_map; + +#[derive(Debug)] +pub struct Pipeline { + glyph_cache: GlyphCache, + cache: RefCell, +} + +impl Pipeline { + pub fn new() -> Self { + Pipeline { + glyph_cache: GlyphCache::new(), + cache: RefCell::new(Cache::new()), + } + } + + // TODO: Shared engine + #[allow(dead_code)] + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + font_system() + .write() + .expect("Write font system") + .load_font(bytes); + + self.cache = RefCell::new(Cache::new()); + } + + pub fn draw_paragraph( + &mut self, + paragraph: ¶graph::Weak, + position: Point, + color: Color, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + transformation: Transformation, + ) { + let Some(paragraph) = paragraph.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + paragraph.buffer(), + position, + color, + pixels, + clip_mask, + transformation, + ); + } + + pub fn draw_editor( + &mut self, + editor: &editor::Weak, + position: Point, + color: Color, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + transformation: Transformation, + ) { + let Some(editor) = editor.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + editor.buffer(), + position, + color, + pixels, + clip_mask, + transformation, + ); + } + + pub fn draw_cached( + &mut self, + content: &str, + bounds: Rectangle, + color: Color, + size: Pixels, + line_height: Pixels, + font: Font, + align_x: Alignment, + align_y: alignment::Vertical, + shaping: Shaping, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + transformation: Transformation, + ) { + let line_height = f32::from(line_height); + + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + let key = cache::Key { + bounds: bounds.size(), + content, + font, + size: size.into(), + line_height, + shaping, + align_x, + }; + + let (_, entry) = self.cache.get_mut().allocate(font_system, key); + + let width = entry.min_bounds.width; + let height = entry.min_bounds.height; + + let x = match align_x { + Alignment::Default | Alignment::Left | Alignment::Justified => { + bounds.x + } + Alignment::Center => bounds.x - width / 2.0, + Alignment::Right => bounds.x - width, + }; + + let y = match align_y { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - height / 2.0, + alignment::Vertical::Bottom => bounds.y - height, + }; + + draw( + font_system, + &mut self.glyph_cache, + &entry.buffer, + Point::new(x, y), + color, + pixels, + clip_mask, + transformation, + ); + } + + pub fn draw_raw( + &mut self, + buffer: &cosmic_text::Buffer, + position: Point, + color: Color, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + transformation: Transformation, + ) { + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + position, + color, + pixels, + clip_mask, + transformation, + ); + } + + pub fn trim_cache(&mut self) { + self.cache.get_mut().trim(); + self.glyph_cache.trim(); + } +} + +fn draw( + font_system: &mut cosmic_text::FontSystem, + glyph_cache: &mut GlyphCache, + buffer: &cosmic_text::Buffer, + position: Point, + color: Color, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + transformation: Transformation, +) { + let position = position * transformation; + + let mut swash = cosmic_text::SwashCache::new(); + + for run in buffer.layout_runs() { + for glyph in run.glyphs { + let physical_glyph = glyph.physical( + (position.x, position.y), + transformation.scale_factor(), + ); + + if let Some((buffer, placement)) = glyph_cache.allocate( + physical_glyph.cache_key, + glyph.color_opt.map(from_color).unwrap_or(color), + font_system, + &mut swash, + ) { + let pixmap = tiny_skia::PixmapRef::from_bytes( + buffer, + placement.width, + placement.height, + ) + .expect("Create glyph pixel map"); + + let opacity = color.a + * glyph + .color_opt + .map(|c| c.a() as f32 / 255.0) + .unwrap_or(1.0); + + pixels.draw_pixmap( + physical_glyph.x + placement.left, + physical_glyph.y - placement.top + + (run.line_y * transformation.scale_factor()).round() + as i32, + pixmap, + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, + tiny_skia::Transform::identity(), + clip_mask, + ); + } + } + } +} + +fn from_color(color: cosmic_text::Color) -> Color { + let [r, g, b, a] = color.as_rgba(); + + Color::from_rgba8(r, g, b, a as f32 / 255.0) +} + +#[derive(Debug, Clone, Default)] +struct GlyphCache { + entries: FxHashMap< + (cosmic_text::CacheKey, [u8; 3]), + (Vec, cosmic_text::Placement), + >, + recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>, + trim_count: usize, +} + +impl GlyphCache { + const TRIM_INTERVAL: usize = 300; + const CAPACITY_LIMIT: usize = 16 * 1024; + + fn new() -> Self { + GlyphCache::default() + } + + fn allocate( + &mut self, + cache_key: cosmic_text::CacheKey, + color: Color, + font_system: &mut cosmic_text::FontSystem, + swash: &mut cosmic_text::SwashCache, + ) -> Option<(&[u8], cosmic_text::Placement)> { + let [r, g, b, _a] = color.into_rgba8(); + let key = (cache_key, [r, g, b]); + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { + // TODO: Outline support + let image = swash.get_image_uncached(font_system, cache_key)?; + + let glyph_size = image.placement.width as usize + * image.placement.height as usize; + + if glyph_size == 0 { + return None; + } + + let mut buffer = vec![0u32; glyph_size]; + + match image.content { + cosmic_text::SwashContent::Mask => { + let mut i = 0; + + // TODO: Blend alpha + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + buffer[i] = bytemuck::cast( + tiny_skia::ColorU8::from_rgba( + b, + g, + r, + image.data[i], + ) + .premultiply(), + ); + + i += 1; + } + } + } + cosmic_text::SwashContent::Color => { + let mut i = 0; + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + // TODO: Blend alpha + buffer[i >> 2] = bytemuck::cast( + tiny_skia::ColorU8::from_rgba( + image.data[i + 2], + image.data[i + 1], + image.data[i], + image.data[i + 3], + ) + .premultiply(), + ); + + i += 4; + } + } + } + cosmic_text::SwashContent::SubpixelMask => { + // TODO + } + } + + let _ = entry.insert((buffer, image.placement)); + } + + let _ = self.recently_used.insert(key); + + self.entries.get(&key).map(|(buffer, placement)| { + (bytemuck::cast_slice(buffer.as_slice()), *placement) + }) + } + + pub fn trim(&mut self) { + if self.trim_count > Self::TRIM_INTERVAL + || self.recently_used.len() >= Self::CAPACITY_LIMIT + { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + + self.entries.shrink_to(Self::CAPACITY_LIMIT); + self.recently_used.shrink_to(Self::CAPACITY_LIMIT); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } + } +} diff --git a/vendor/iced_tiny_skia/src/vector.rs b/vendor/iced_tiny_skia/src/vector.rs new file mode 100644 index 0000000..5f157f1 --- /dev/null +++ b/vendor/iced_tiny_skia/src/vector.rs @@ -0,0 +1,235 @@ +use crate::core::svg::{Data, Handle}; +use crate::core::{Color, Rectangle, Size}; + +use resvg::usvg; +use rustc_hash::{FxHashMap, FxHashSet}; +use tiny_skia::Transform; + +use std::cell::RefCell; +use std::collections::hash_map; +use std::fs; +use std::panic; +use std::sync::Arc; + +#[derive(Debug)] +pub struct Pipeline { + cache: RefCell, +} + +impl Pipeline { + pub fn new() -> Self { + Self { + cache: RefCell::new(Cache::default()), + } + } + + pub fn viewport_dimensions(&self, handle: &Handle) -> Size { + self.cache + .borrow_mut() + .viewport_dimensions(handle) + .unwrap_or(Size::new(0, 0)) + } + + pub fn draw( + &mut self, + handle: &Handle, + color: Option, + bounds: Rectangle, + opacity: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + transform: Transform, + clip_mask: Option<&tiny_skia::Mask>, + ) { + if let Some(image) = self.cache.borrow_mut().draw( + handle, + color, + Size::new( + (bounds.width * transform.sx) as u32, + (bounds.height * transform.sy) as u32, + ), + ) { + pixels.draw_pixmap( + (bounds.x * transform.sx) as i32, + (bounds.y * transform.sy) as i32, + image, + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, + Transform::default(), + clip_mask, + ); + } + } + + pub fn trim_cache(&mut self) { + self.cache.borrow_mut().trim(); + } +} + +#[derive(Default)] +struct Cache { + trees: FxHashMap>, + tree_hits: FxHashSet, + rasters: FxHashMap, + raster_hits: FxHashSet, + fontdb: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct RasterKey { + id: u64, + color: Option<[u8; 4]>, + size: Size, +} + +impl Cache { + fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { + let id = handle.id(); + + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + + if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { + let svg = match handle.data() { + Data::Path(path) => { + fs::read_to_string(path).ok().and_then(|contents| { + usvg::Tree::from_str(&contents, &options).ok() + }) + } + Data::Bytes(bytes) => { + usvg::Tree::from_data(bytes, &options).ok() + } + }; + + let _ = entry.insert(svg); + } + + let _ = self.tree_hits.insert(id); + self.trees.get(&id).unwrap().as_ref() + } + + fn viewport_dimensions(&mut self, handle: &Handle) -> Option> { + let tree = self.load(handle)?; + let size = tree.size(); + + Some(Size::new(size.width() as u32, size.height() as u32)) + } + + fn draw( + &mut self, + handle: &Handle, + color: Option, + size: Size, + ) -> Option> { + if size.width == 0 || size.height == 0 { + return None; + } + + let key = RasterKey { + id: handle.id(), + color: color.map(Color::into_rgba8), + size, + }; + + #[allow(clippy::map_entry)] + if !self.rasters.contains_key(&key) { + let tree = self.load(handle)?; + + let mut image = tiny_skia::Pixmap::new(size.width, size.height)?; + + let tree_size = tree.size().to_int_size(); + + let target_size = if size.width > size.height { + tree_size.scale_to_width(size.width) + } else { + tree_size.scale_to_height(size.height) + }; + + let transform = if let Some(target_size) = target_size { + let tree_size = tree_size.to_size(); + let target_size = target_size.to_size(); + + tiny_skia::Transform::from_scale( + target_size.width() / tree_size.width(), + target_size.height() / tree_size.height(), + ) + } else { + tiny_skia::Transform::default() + }; + + // SVG rendering can panic on malformed or complex vectors. + // We catch panics to prevent crashes and continue gracefully. + let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { + resvg::render(tree, transform, &mut image.as_mut()); + })); + + if let Err(error) = render { + log::warn!("SVG rendering for {handle:?} panicked: {error:?}"); + } + + if let Some([r, g, b, _]) = key.color { + // Apply color filter + for pixel in + bytemuck::cast_slice_mut::(image.data_mut()) + { + *pixel = bytemuck::cast( + tiny_skia::ColorU8::from_rgba( + b, + g, + r, + (*pixel >> 24) as u8, + ) + .premultiply(), + ); + } + } else { + // Swap R and B channels for `softbuffer` presentation + for pixel in + bytemuck::cast_slice_mut::(image.data_mut()) + { + *pixel = *pixel & 0xFF00_FF00 + | ((0x0000_00FF & *pixel) << 16) + | ((0x00FF_0000 & *pixel) >> 16); + } + } + + let _ = self.rasters.insert(key, image); + } + + let _ = self.raster_hits.insert(key); + self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref) + } + + fn trim(&mut self) { + self.trees.retain(|key, _| self.tree_hits.contains(key)); + self.rasters.retain(|key, _| self.raster_hits.contains(key)); + + self.tree_hits.clear(); + self.raster_hits.clear(); + } +} + +impl std::fmt::Debug for Cache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cache") + .field("tree_hits", &self.tree_hits) + .field("rasters", &self.rasters) + .field("raster_hits", &self.raster_hits) + .finish_non_exhaustive() + } +} diff --git a/vendor/iced_tiny_skia/src/window.rs b/vendor/iced_tiny_skia/src/window.rs new file mode 100644 index 0000000..d8d9378 --- /dev/null +++ b/vendor/iced_tiny_skia/src/window.rs @@ -0,0 +1,3 @@ +pub mod compositor; + +pub use compositor::{Compositor, Surface}; diff --git a/vendor/iced_tiny_skia/src/window/compositor.rs b/vendor/iced_tiny_skia/src/window/compositor.rs new file mode 100644 index 0000000..404948f --- /dev/null +++ b/vendor/iced_tiny_skia/src/window/compositor.rs @@ -0,0 +1,268 @@ +use crate::core::{Color, Rectangle, Size}; +use crate::graphics::compositor::{self, Information}; +use crate::graphics::damage; +use crate::graphics::error::{self, Error}; +use crate::graphics::{self, Shell, Viewport}; +use crate::{Layer, Renderer, Settings}; + +use std::collections::VecDeque; +use std::num::NonZeroU32; + +pub struct Compositor { + context: softbuffer::Context>, + settings: Settings, +} + +pub struct Surface { + window: softbuffer::Surface< + Box, + Box, + >, + clip_mask: tiny_skia::Mask, + layer_stack: VecDeque>, + background_color: Color, + max_age: u8, +} + +impl crate::graphics::Compositor for Compositor { + type Renderer = Renderer; + type Surface = Surface; + + async fn with_backend( + settings: graphics::Settings, + display: impl compositor::Display, + _compatible_window: impl compositor::Window, + _shell: Shell, + backend: Option<&str>, + ) -> Result { + match backend { + None | Some("tiny-skia") | Some("tiny_skia") => { + Ok(new(settings.into(), display)) + } + Some(backend) => Err(Error::GraphicsAdapterNotFound { + backend: "tiny-skia", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } + } + + fn create_renderer(&self) -> Self::Renderer { + Renderer::new( + self.settings.default_font, + self.settings.default_text_size, + ) + } + + fn create_surface( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface { + let window = softbuffer::Surface::new( + &self.context, + Box::new(window.clone()) as _, + ) + .expect("Create softbuffer surface for window"); + + let mut surface = Surface { + window, + clip_mask: tiny_skia::Mask::new(1, 1).expect("Create clip mask"), + layer_stack: VecDeque::new(), + background_color: Color::BLACK, + max_age: 0, + }; + + if width > 0 && height > 0 { + self.configure_surface(&mut surface, width, height); + } + + surface + } + + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + surface + .window + .resize( + NonZeroU32::new(width).expect("Non-zero width"), + NonZeroU32::new(height).expect("Non-zero height"), + ) + .expect("Resize surface"); + + surface.clip_mask = + tiny_skia::Mask::new(width, height).expect("Create clip mask"); + surface.layer_stack.clear(); + } + + fn information(&self) -> Information { + Information { + adapter: String::from("CPU"), + backend: String::from("tiny-skia"), + } + } + + fn present( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + on_pre_present: impl FnOnce(), + ) -> Result<(), compositor::SurfaceError> { + present( + renderer, + surface, + viewport, + background_color, + on_pre_present, + ) + } + + fn screenshot( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + background_color: Color, + ) -> Vec { + screenshot(renderer, viewport, background_color) + } +} + +pub fn new( + settings: Settings, + display: impl compositor::Display, +) -> Compositor { + #[allow(unsafe_code)] + let context = softbuffer::Context::new(Box::new(display) as _) + .expect("Create softbuffer context"); + + Compositor { context, settings } +} + +pub fn present( + renderer: &mut Renderer, + surface: &mut Surface, + viewport: &Viewport, + background_color: Color, + on_pre_present: impl FnOnce(), +) -> Result<(), compositor::SurfaceError> { + let physical_size = viewport.physical_size(); + + let mut buffer = surface + .window + .buffer_mut() + .map_err(|_| compositor::SurfaceError::Lost)?; + + let last_layers = { + let age = buffer.age(); + + surface.max_age = surface.max_age.max(age); + surface.layer_stack.truncate(surface.max_age as usize); + + if age > 0 { + surface.layer_stack.get(age as usize - 1) + } else { + None + } + }; + + let damage = last_layers + .and_then(|last_layers| { + (surface.background_color == background_color).then(|| { + damage::diff( + last_layers, + renderer.layers(), + |layer| vec![layer.bounds], + Layer::damage, + ) + }) + }) + .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + + if damage.is_empty() { + if let Some(last_layers) = last_layers { + surface.layer_stack.push_front(last_layers.clone()); + } + } else { + surface.layer_stack.push_front(renderer.layers().to_vec()); + surface.background_color = background_color; + + let damage = damage::group( + damage, + Rectangle::with_size(viewport.logical_size()), + ); + + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + + renderer.draw( + &mut pixels, + &mut surface.clip_mask, + viewport, + &damage, + background_color, + ); + } + + on_pre_present(); + buffer.present().map_err(|_| compositor::SurfaceError::Lost) +} + +pub fn screenshot( + renderer: &mut Renderer, + viewport: &Viewport, + background_color: Color, +) -> Vec { + let size = viewport.physical_size(); + + let mut offscreen_buffer: Vec = + vec![0; size.width as usize * size.height as usize]; + + let mut clip_mask = tiny_skia::Mask::new(size.width, size.height) + .expect("Create clip mask"); + + renderer.draw( + &mut tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut offscreen_buffer), + size.width, + size.height, + ) + .expect("Create offscreen pixel map"), + &mut clip_mask, + viewport, + &[Rectangle::with_size(Size::new( + size.width as f32, + size.height as f32, + ))], + background_color, + ); + + offscreen_buffer.iter().fold( + Vec::with_capacity(offscreen_buffer.len() * 4), + |mut acc, pixel| { + const A_MASK: u32 = 0xFF_00_00_00; + const R_MASK: u32 = 0x00_FF_00_00; + const G_MASK: u32 = 0x00_00_FF_00; + const B_MASK: u32 = 0x00_00_00_FF; + + let a = ((A_MASK & pixel) >> 24) as u8; + let r = ((R_MASK & pixel) >> 16) as u8; + let g = ((G_MASK & pixel) >> 8) as u8; + let b = (B_MASK & pixel) as u8; + + acc.extend([r, g, b, a]); + acc + }, + ) +}