commit 2f4c8f0fe2e715d4b2520c01c80ce0e238e09ccd Author: Codex Bot Date: Sun Oct 4 19:32:04 2026 +0800 feat: bootstrap namespace-driven Tessera renderer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91213f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.DS_Store +*.swp +*.swo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f38feb4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2037 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "ctor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" +dependencies = [ + "dtor", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.5", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "drm", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.5", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tessera" +version = "0.1.0" +dependencies = [ + "anyhow", + "font8x8", + "serde", + "serde_json", + "softbuffer", + "tiny_http", + "winit", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0304719 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tessera" +version = "0.1.0" +edition = "2024" +description = "Cross-platform desktop rendering client" +license = "MIT OR Apache-2.0" + +[dependencies] +anyhow = "1.0" +font8x8 = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +softbuffer = "0.4.8" +tiny_http = "0.12" +winit = "0.30.13" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..2740d32 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Tessera + +Tessera is a clean desktop rendering host. It starts with an empty page. Coding agents submit source code to a namespace through a local CRUD API; Tessera parses that code, builds its architecture tree, compiles it to drawing commands, and renders the result. + +## Start + +```bash +cargo run --release +``` + +Tessera listens only on `127.0.0.1:4737`. + +Start every agent integration by reading the live Markdown guide: + +```bash +curl http://127.0.0.1:4737/agent +``` + +## Namespace code API + +The initial active namespace is `app.main`. Tessera has no file concept. + +```text +GET /v1/namespaces/app.main/code +PUT /v1/namespaces/app.main/code +DELETE /v1/namespaces/app.main/code +``` + +Submit code as plain text: + +```bash +curl \ + --request PUT \ + --header 'Content-Type: text/plain; charset=utf-8' \ + --data-binary @- \ + http://127.0.0.1:4737/v1/namespaces/app.main/code <<'TESSERA' +namespace app.main + +use tessera.ui::{Column, Text} + +export scene Main { + render { + Column(gap: 12, padding: 24) { + Text("Hello", role: .title) + Text("Rendered by Tessera") + } + } +} +TESSERA +``` + +Updates are atomic. Invalid code returns diagnostics with HTTP 422 and preserves the last successfully rendered revision. + +## Developer tools + +The developer sidebar is closed by default. + +- macOS: `Command-Option-I` or `F12` +- Windows/Linux: `Control-Shift-I` or `F12` + +The upper section displays the compiler-produced architecture tree. Selecting a node shows the corresponding source range below it. + +## Current syntax experiment + +The first parser intentionally supports only enough syntax to test the architecture: + +- `namespace` +- namespace-only `use` +- one `export scene` +- one `render` root +- `Column`, `Row`, `Spacer`, and `Text` +- `gap`, `padding`, and `.title` + +There are no files, HTML, CSS, JavaScript, DOM, or resource-network concepts in the runtime. diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..83cd3bf --- /dev/null +++ b/src/api.rs @@ -0,0 +1,306 @@ +use std::{ + io::Read, + sync::{Arc, RwLock}, + thread, +}; + +use anyhow::{Context as _, Result}; +use tiny_http::{Header, Method, Request, Response, Server, StatusCode}; +use winit::event_loop::EventLoopProxy; + +use crate::{ + app::AppEvent, + compiler, + document::{NamespaceState, RenderDocument}, +}; + +pub const API_ADDRESS: &str = "127.0.0.1:4737"; +const MAX_CODE_SIZE: u64 = 1_048_576; + +pub fn start( + namespace: Arc>, + proxy: EventLoopProxy, +) -> Result<()> { + let server = Server::http(API_ADDRESS) + .map_err(|error| anyhow::anyhow!(error)) + .with_context(|| format!("bind Tessera API to {API_ADDRESS}"))?; + thread::Builder::new() + .name("tessera-agent-api".to_owned()) + .spawn(move || { + for request in server.incoming_requests() { + handle(request, &namespace, &proxy); + } + }) + .context("start Tessera agent API thread")?; + Ok(()) +} + +fn handle( + mut request: Request, + namespace: &Arc>, + proxy: &EventLoopProxy, +) { + let method = request.method().clone(); + let path = request + .url() + .split('?') + .next() + .unwrap_or(request.url()) + .to_owned(); + + let response = if method == Method::Get && path == "/agent" { + match namespace.read() { + Ok(namespace) => markdown(StatusCode(200), agent_guide(&namespace)), + Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"), + } + } else if let Some(addressed_namespace) = namespace_from_path(&path).map(str::to_owned) { + code_request(&mut request, method, &addressed_namespace, namespace, proxy) + } else if method == Method::Options { + text(StatusCode(204), "", "text/plain") + } else { + error_response(StatusCode(404), "route not found; start with GET /agent") + }; + + let _ = request.respond(response); +} + +fn code_request( + request: &mut Request, + method: Method, + addressed_namespace: &str, + namespace: &Arc>, + proxy: &EventLoopProxy, +) -> Response>> { + let Ok(current) = namespace.read() else { + return error_response(StatusCode(500), "namespace lock is poisoned"); + }; + if addressed_namespace != current.name { + return error_response( + StatusCode(404), + format!( + "namespace `{addressed_namespace}` is not active; current namespace is `{}`", + current.name + ), + ); + } + + if method == Method::Get { + let mut response = text( + StatusCode(200), + ¤t.document.source, + "text/plain; charset=utf-8", + ); + add_header( + &mut response, + "X-Tessera-Revision", + ¤t.revision.to_string(), + ); + return response; + } + drop(current); + + match method { + Method::Put => match read_code(request) { + Ok(code) => match compiler::compile(&code, addressed_namespace) { + Ok(document) => { + if let Err(error) = document.validate() { + return compile_error(error.to_string()); + } + match namespace.write() { + Ok(mut current) => { + current.revision += 1; + current.document = document; + let revision = current.revision; + drop(current); + let _ = proxy.send_event(AppEvent::DocumentUpdated); + json( + StatusCode(200), + serde_json::json!({ + "ok": true, + "namespace": addressed_namespace, + "revision": revision, + "diagnostics": [] + }), + ) + } + Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"), + } + } + Err(error) => compile_error(error.to_string()), + }, + Err(error) => error_response(StatusCode(400), error.to_string()), + }, + Method::Delete => match namespace.write() { + Ok(mut current) => { + current.revision += 1; + current.document = RenderDocument::default(); + let revision = current.revision; + drop(current); + let _ = proxy.send_event(AppEvent::DocumentUpdated); + json( + StatusCode(200), + serde_json::json!({ + "ok": true, + "namespace": addressed_namespace, + "revision": revision + }), + ) + } + Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"), + }, + Method::Options => text(StatusCode(204), "", "text/plain"), + _ => error_response(StatusCode(405), "code supports GET, PUT, and DELETE"), + } +} + +fn namespace_from_path(path: &str) -> Option<&str> { + path.strip_prefix("/v1/namespaces/")? + .strip_suffix("/code") + .filter(|namespace| !namespace.is_empty() && !namespace.contains('/')) +} + +fn read_code(request: &mut Request) -> Result { + if request + .body_length() + .is_some_and(|length| length as u64 > MAX_CODE_SIZE) + { + anyhow::bail!("code exceeds the 1 MiB limit"); + } + let mut code = String::new(); + request + .as_reader() + .take(MAX_CODE_SIZE + 1) + .read_to_string(&mut code) + .context("read code")?; + if code.len() as u64 > MAX_CODE_SIZE { + anyhow::bail!("code exceeds the 1 MiB limit"); + } + Ok(code) +} + +fn agent_guide(namespace: &NamespaceState) -> String { + format!( + r#"# Tessera Agent API + +Current namespace: `{name}` +Current revision: `{revision}` + +Tessera stores code by namespace. It has no files. Submit code only; Tessera parses, +checks, compiles, and renders it. + +## Code CRUD + +- Read: `GET http://{address}/v1/namespaces/{name}/code` +- Create or update: `PUT http://{address}/v1/namespaces/{name}/code` +- Delete: `DELETE http://{address}/v1/namespaces/{name}/code` + +Send PUT bodies as `text/plain; charset=utf-8`. + +## Minimal syntax + +```tessera +namespace {name} + +use tessera.ui::{{Column, Row, Spacer, Text}} + +export scene Main {{ + render {{ + Column(gap: 12, padding: 24) {{ + Text("Hello", role: .title) + Text("Rendered by Tessera") + }} + }} +}} +``` + +Supported by this syntax experiment: one exported scene, one render root, +`Column`, `Row`, `Spacer`, `Text`, `gap`, `padding`, and `.title`. +Compilation errors reject the update and preserve the last rendered revision. +"#, + name = namespace.name, + revision = namespace.revision, + address = API_ADDRESS + ) +} + +fn compile_error(message: String) -> Response>> { + json( + StatusCode(422), + serde_json::json!({ + "ok": false, + "diagnostics": [{ + "severity": "error", + "message": message + }] + }), + ) +} + +fn error_response( + status: StatusCode, + message: impl Into, +) -> Response>> { + json(status, serde_json::json!({ "error": message.into() })) +} + +fn json(status: StatusCode, value: serde_json::Value) -> Response>> { + text( + status, + &value.to_string(), + "application/json; charset=utf-8", + ) +} + +fn markdown(status: StatusCode, value: String) -> Response>> { + text(status, &value, "text/markdown; charset=utf-8") +} + +fn text(status: StatusCode, value: &str, content_type: &str) -> Response>> { + let mut response = Response::from_string(value).with_status_code(status); + add_header(&mut response, "Content-Type", content_type); + add_header( + &mut response, + "Access-Control-Allow-Origin", + "http://127.0.0.1", + ); + add_header( + &mut response, + "Access-Control-Allow-Methods", + "GET, PUT, DELETE, OPTIONS", + ); + add_header( + &mut response, + "Access-Control-Allow-Headers", + "Content-Type", + ); + response +} + +fn add_header(response: &mut Response>>, name: &str, value: &str) { + if let Ok(header) = Header::from_bytes(name, value) { + response.add_header(header); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extracts_namespace_only_from_code_routes() { + assert_eq!( + namespace_from_path("/v1/namespaces/app.main/code"), + Some("app.main") + ); + assert_eq!(namespace_from_path("/v1/namespaces/app.main"), None); + assert_eq!(namespace_from_path("/v1/namespaces/a/b/code"), None); + } + + #[test] + fn guide_contains_only_code_crud_routes() { + let guide = agent_guide(&NamespaceState::default()); + assert!(guide.contains("Code CRUD")); + assert!(guide.contains("PUT http://127.0.0.1:4737")); + assert!(!guide.contains("display_list")); + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..3002b1d --- /dev/null +++ b/src/app.rs @@ -0,0 +1,193 @@ +use std::{ + num::NonZeroU32, + sync::{Arc, RwLock}, +}; + +use softbuffer::{Context, Surface}; +use winit::{ + application::ApplicationHandler, + dpi::{LogicalSize, PhysicalPosition}, + event::{ElementState, MouseButton, WindowEvent}, + event_loop::ActiveEventLoop, + keyboard::{Key, ModifiersState, NamedKey}, + window::{Window, WindowAttributes, WindowId}, +}; + +use crate::{ + developer_tools::DeveloperToolsState, + document::NamespaceState, + geometry::Point, + scene::{self, SceneLayout}, + software_renderer::SoftwareRenderer, +}; + +#[derive(Clone, Debug)] +pub enum AppEvent { + DocumentUpdated, +} + +pub struct TesseraApp { + window: Option>, + context: Option>>, + surface: Option, Arc>>, + namespace: Arc>, + developer_tools: DeveloperToolsState, + layout: SceneLayout, + pointer: Point, + modifiers: ModifiersState, +} + +impl TesseraApp { + pub fn new(namespace: Arc>) -> Self { + Self { + window: None, + context: None, + surface: None, + namespace, + developer_tools: DeveloperToolsState::default(), + layout: SceneLayout::default(), + pointer: Point::default(), + modifiers: ModifiersState::default(), + } + } + + fn request_redraw(&self) { + if let Some(window) = &self.window { + window.request_redraw(); + } + } + + fn toggle_developer_tools(&mut self) { + if let Ok(namespace) = self.namespace.read() { + self.developer_tools.toggle(&namespace.document); + } + self.request_redraw(); + } + + fn document_updated(&mut self) { + if let Ok(namespace) = self.namespace.read() { + self.developer_tools.document_updated(&namespace.document); + } + self.request_redraw(); + } + + fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) { + if event.state != ElementState::Pressed || event.repeat { + return; + } + let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12)); + let is_i = + matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i")); + let mac_shortcut = self.modifiers.super_key() && self.modifiers.alt_key() && is_i; + let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i; + if is_f12 || mac_shortcut || other_shortcut { + self.toggle_developer_tools(); + } + } + + fn handle_click(&mut self) { + let Some(node_id) = self.layout.developer_tools.node_at(self.pointer) else { + return; + }; + self.developer_tools.selected_node_id = Some(node_id.to_owned()); + self.request_redraw(); + } + + fn redraw(&mut self) { + let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else { + return; + }; + let size = window.inner_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + let Ok(namespace) = self.namespace.read() else { + return; + }; + let scene = scene::build_scene( + size.width, + size.height, + window.scale_factor() as f32, + &namespace.document, + &self.developer_tools, + ); + drop(namespace); + self.layout = scene.layout; + + surface + .resize(width, height) + .expect("resize software surface"); + let mut buffer = surface.buffer_mut().expect("acquire software buffer"); + SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height); + buffer.present().expect("present software-rendered frame"); + } +} + +impl ApplicationHandler for TesseraApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + if self.window.is_some() { + return; + } + + let attributes = WindowAttributes::default() + .with_title("Tessera") + .with_inner_size(LogicalSize::new(1_280.0, 800.0)) + .with_min_inner_size(LogicalSize::new(720.0, 480.0)); + let window = Arc::new( + event_loop + .create_window(attributes) + .expect("create Tessera window"), + ); + let context = Context::new(window.clone()).expect("create software-rendering context"); + let surface = + Surface::new(&context, window.clone()).expect("create software-rendering surface"); + + self.window = Some(window.clone()); + self.context = Some(context); + self.surface = Some(surface); + window.request_redraw(); + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) { + match event { + AppEvent::DocumentUpdated => self.document_updated(), + } + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let Some(window) = self.window.as_ref() else { + return; + }; + if window.id() != window_id { + return; + } + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => { + window.request_redraw() + } + WindowEvent::CursorMoved { + position: PhysicalPosition { x, y }, + .. + } => self.pointer = Point::new(x as f32, y as f32), + WindowEvent::MouseInput { + state: ElementState::Released, + button: MouseButton::Left, + .. + } => self.handle_click(), + WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(), + WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event), + WindowEvent::RedrawRequested => self.redraw(), + _ => {} + } + } +} diff --git a/src/compiler.rs b/src/compiler.rs new file mode 100644 index 0000000..679d74b --- /dev/null +++ b/src/compiler.rs @@ -0,0 +1,625 @@ +use anyhow::{Result, bail}; + +use crate::document::{CodeNode, Frame, RenderCommand, RenderDocument}; + +pub fn compile(source: &str, expected_namespace: &str) -> Result { + let tokens = Lexer::new(source).tokenize()?; + let syntax = Parser::new(tokens).parse()?; + if syntax.namespace != expected_namespace { + bail!( + "namespace mismatch: API addresses `{expected_namespace}`, code declares `{}`", + syntax.namespace + ); + } + + let mut commands = Vec::new(); + layout_node(&syntax.root, 0.0, 0.0, &mut commands); + Ok(RenderDocument { + source: source.to_owned(), + root: Some(architecture(source, &syntax)), + frame: Frame { + background: "#F7F7F5".to_owned(), + commands, + }, + }) +} + +#[derive(Clone, Debug)] +struct SyntaxDocument { + namespace: String, + scene_name: String, + scene_span: Span, + render_span: Span, + root: SyntaxNode, +} + +#[derive(Clone, Debug)] +struct SyntaxNode { + name: String, + arguments: Vec, + children: Vec, + span: Span, +} + +#[derive(Clone, Debug)] +enum Argument { + Positional(Value), + Named(String, Value), +} + +#[derive(Clone, Debug)] +enum Value { + Text(String), + Number(f32), + Symbol(String), +} + +#[derive(Clone, Copy, Debug)] +struct Span { + start: usize, + end: usize, +} + +#[derive(Clone, Debug, PartialEq)] +enum TokenKind { + Identifier(String), + String(String), + Number(f32), + LeftBrace, + RightBrace, + LeftParen, + RightParen, + Colon, + Comma, + Dot, + End, +} + +#[derive(Clone, Debug)] +struct Token { + kind: TokenKind, + span: Span, +} + +struct Lexer<'a> { + source: &'a str, + offset: usize, +} + +impl<'a> Lexer<'a> { + fn new(source: &'a str) -> Self { + Self { source, offset: 0 } + } + + fn tokenize(mut self) -> Result> { + let mut tokens = Vec::new(); + while self.offset < self.source.len() { + self.skip_whitespace_and_comments(); + if self.offset >= self.source.len() { + break; + } + let start = self.offset; + let character = self.current_char().expect("offset is in bounds"); + let kind = match character { + '{' => self.single(TokenKind::LeftBrace), + '}' => self.single(TokenKind::RightBrace), + '(' => self.single(TokenKind::LeftParen), + ')' => self.single(TokenKind::RightParen), + ':' => self.single(TokenKind::Colon), + ',' => self.single(TokenKind::Comma), + '.' => self.single(TokenKind::Dot), + '"' => self.string()?, + value if value.is_ascii_digit() || value == '-' => self.number()?, + value if is_identifier_start(value) => self.identifier(), + _ => bail!("unexpected character `{character}` at byte {start}"), + }; + tokens.push(Token { + kind, + span: Span { + start, + end: self.offset, + }, + }); + } + tokens.push(Token { + kind: TokenKind::End, + span: Span { + start: self.source.len(), + end: self.source.len(), + }, + }); + Ok(tokens) + } + + fn current_char(&self) -> Option { + self.source[self.offset..].chars().next() + } + + fn advance(&mut self) -> Option { + let character = self.current_char()?; + self.offset += character.len_utf8(); + Some(character) + } + + fn single(&mut self, kind: TokenKind) -> TokenKind { + self.advance(); + kind + } + + fn skip_whitespace_and_comments(&mut self) { + loop { + while self.current_char().is_some_and(char::is_whitespace) { + self.advance(); + } + if self.source[self.offset..].starts_with("//") { + while self.current_char().is_some_and(|value| value != '\n') { + self.advance(); + } + } else { + break; + } + } + } + + fn identifier(&mut self) -> TokenKind { + let start = self.offset; + while self.current_char().is_some_and(is_identifier_continue) { + self.advance(); + } + TokenKind::Identifier(self.source[start..self.offset].to_owned()) + } + + fn number(&mut self) -> Result { + let start = self.offset; + if self.current_char() == Some('-') { + self.advance(); + } + while self + .current_char() + .is_some_and(|value| value.is_ascii_digit()) + { + self.advance(); + } + if self.current_char() == Some('.') { + self.advance(); + while self + .current_char() + .is_some_and(|value| value.is_ascii_digit()) + { + self.advance(); + } + } + Ok(TokenKind::Number(self.source[start..self.offset].parse()?)) + } + + fn string(&mut self) -> Result { + self.advance(); + let mut value = String::new(); + while let Some(character) = self.advance() { + match character { + '"' => return Ok(TokenKind::String(value)), + '\\' => match self.advance() { + Some('n') => value.push('\n'), + Some('t') => value.push('\t'), + Some('"') => value.push('"'), + Some('\\') => value.push('\\'), + Some(other) => bail!("unsupported string escape `\\{other}`"), + None => bail!("unterminated string escape"), + }, + other => value.push(other), + } + } + bail!("unterminated string literal") + } +} + +fn is_identifier_start(value: char) -> bool { + value == '_' || value.is_alphabetic() +} + +fn is_identifier_continue(value: char) -> bool { + value == '_' || value.is_alphanumeric() +} + +struct Parser { + tokens: Vec, + current: usize, +} + +impl Parser { + fn new(tokens: Vec) -> Self { + Self { tokens, current: 0 } + } + + fn parse(&mut self) -> Result { + self.expect_word("namespace")?; + let namespace = self.parse_path()?; + + while self.check_word("use") { + self.parse_use()?; + } + + self.expect_word("export")?; + let scene_start = self.peek().span.start; + self.expect_word("scene")?; + let scene_name = self.identifier()?; + self.expect(TokenKind::LeftBrace)?; + let render_start = self.peek().span.start; + self.expect_word("render")?; + self.expect(TokenKind::LeftBrace)?; + let root = self.parse_node()?; + let render_end = self.expect(TokenKind::RightBrace)?.span.end; + let scene_end = self.expect(TokenKind::RightBrace)?.span.end; + self.expect(TokenKind::End)?; + + Ok(SyntaxDocument { + namespace, + scene_name, + scene_span: Span { + start: scene_start, + end: scene_end, + }, + render_span: Span { + start: render_start, + end: render_end, + }, + root, + }) + } + + fn parse_path(&mut self) -> Result { + let mut path = self.identifier()?; + while self.matches(&TokenKind::Dot) { + path.push('.'); + path.push_str(&self.identifier()?); + } + Ok(path) + } + + fn parse_use(&mut self) -> Result<()> { + self.expect_word("use")?; + self.identifier()?; + while self.matches(&TokenKind::Dot) { + self.identifier()?; + } + if self.matches(&TokenKind::Colon) { + self.expect(TokenKind::Colon)?; + self.expect(TokenKind::LeftBrace)?; + if !self.check(&TokenKind::RightBrace) { + loop { + self.identifier()?; + if !self.matches(&TokenKind::Comma) { + break; + } + } + } + self.expect(TokenKind::RightBrace)?; + } + Ok(()) + } + + fn parse_node(&mut self) -> Result { + let start = self.peek().span.start; + let name = self.identifier()?; + let mut arguments = Vec::new(); + if self.matches(&TokenKind::LeftParen) { + if !self.check(&TokenKind::RightParen) { + loop { + if let TokenKind::Identifier(name) = &self.peek().kind + && self.peek_next().kind == TokenKind::Colon + { + let name = name.clone(); + self.advance(); + self.advance(); + arguments.push(Argument::Named(name, self.value()?)); + } else { + arguments.push(Argument::Positional(self.value()?)); + } + if !self.matches(&TokenKind::Comma) { + break; + } + } + } + self.expect(TokenKind::RightParen)?; + } + + let mut children = Vec::new(); + if self.matches(&TokenKind::LeftBrace) { + while !self.check(&TokenKind::RightBrace) { + children.push(self.parse_node()?); + } + self.expect(TokenKind::RightBrace)?; + } + let end = self.previous().span.end; + Ok(SyntaxNode { + name, + arguments, + children, + span: Span { start, end }, + }) + } + + fn value(&mut self) -> Result { + let token = self.advance().clone(); + match token.kind { + TokenKind::String(value) => Ok(Value::Text(value)), + TokenKind::Number(value) => Ok(Value::Number(value)), + TokenKind::Identifier(value) => Ok(Value::Symbol(value)), + TokenKind::Dot => Ok(Value::Symbol(format!(".{}", self.identifier()?))), + _ => bail!("expected a value at byte {}", token.span.start), + } + } + + fn identifier(&mut self) -> Result { + let token = self.advance().clone(); + match token.kind { + TokenKind::Identifier(value) => Ok(value), + _ => bail!("expected an identifier at byte {}", token.span.start), + } + } + + fn expect_word(&mut self, expected: &str) -> Result<()> { + let actual = self.identifier()?; + if actual != expected { + bail!("expected `{expected}`, found `{actual}`"); + } + Ok(()) + } + + fn expect(&mut self, expected: TokenKind) -> Result { + if self.check(&expected) { + return Ok(self.advance().clone()); + } + bail!( + "expected {}, found {} at byte {}", + token_name(&expected), + token_name(&self.peek().kind), + self.peek().span.start + ) + } + + fn matches(&mut self, expected: &TokenKind) -> bool { + if self.check(expected) { + self.advance(); + true + } else { + false + } + } + + fn check(&self, expected: &TokenKind) -> bool { + std::mem::discriminant(&self.peek().kind) == std::mem::discriminant(expected) + } + + fn check_word(&self, expected: &str) -> bool { + matches!(&self.peek().kind, TokenKind::Identifier(value) if value == expected) + } + + fn peek(&self) -> &Token { + &self.tokens[self.current] + } + + fn peek_next(&self) -> &Token { + &self.tokens[(self.current + 1).min(self.tokens.len() - 1)] + } + + fn previous(&self) -> &Token { + &self.tokens[self.current.saturating_sub(1)] + } + + fn advance(&mut self) -> &Token { + let index = self.current; + if self.current < self.tokens.len() - 1 { + self.current += 1; + } + &self.tokens[index] + } +} + +fn token_name(token: &TokenKind) -> &'static str { + match token { + TokenKind::Identifier(_) => "identifier", + TokenKind::String(_) => "string", + TokenKind::Number(_) => "number", + TokenKind::LeftBrace => "`{`", + TokenKind::RightBrace => "`}`", + TokenKind::LeftParen => "`(`", + TokenKind::RightParen => "`)`", + TokenKind::Colon => "`:`", + TokenKind::Comma => "`,`", + TokenKind::Dot => "`.`", + TokenKind::End => "end of code", + } +} + +fn architecture(source: &str, syntax: &SyntaxDocument) -> CodeNode { + CodeNode { + id: format!("{}::{}", syntax.namespace, syntax.scene_name), + name: syntax.scene_name.clone(), + kind: "scene".to_owned(), + start_line: line_number(source, syntax.scene_span.start), + end_line: line_number(source, syntax.scene_span.end), + children: vec![CodeNode { + id: format!("{}::{}::render", syntax.namespace, syntax.scene_name), + name: "render".to_owned(), + kind: "render".to_owned(), + start_line: line_number(source, syntax.render_span.start), + end_line: line_number(source, syntax.render_span.end), + children: vec![node_architecture( + source, + &syntax.namespace, + &syntax.scene_name, + &syntax.root, + "0", + )], + }], + } +} + +fn node_architecture( + source: &str, + namespace: &str, + scene: &str, + node: &SyntaxNode, + path: &str, +) -> CodeNode { + CodeNode { + id: format!("{namespace}::{scene}::render::{path}"), + name: node.name.clone(), + kind: node.name.to_lowercase(), + start_line: line_number(source, node.span.start), + end_line: line_number(source, node.span.end), + children: node + .children + .iter() + .enumerate() + .map(|(index, child)| { + node_architecture(source, namespace, scene, child, &format!("{path}.{index}")) + }) + .collect(), + } +} + +fn line_number(source: &str, offset: usize) -> usize { + source[..offset.min(source.len())] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1 +} + +#[derive(Clone, Copy, Default)] +struct Size { + width: f32, + height: f32, +} + +fn layout_node(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec) -> Size { + match node.name.as_str() { + "Text" => layout_text(node, x, y, commands), + "Row" => layout_row(node, x, y, commands), + "Spacer" => Size { + width: number_argument(node, "size").unwrap_or(0.0), + height: number_argument(node, "size").unwrap_or(0.0), + }, + _ => layout_column(node, x, y, commands), + } +} + +fn layout_text(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec) -> Size { + let value = node.arguments.iter().find_map(|argument| match argument { + Argument::Positional(Value::Text(value)) => Some(value.clone()), + _ => None, + }); + let value = value.unwrap_or_default(); + let size = match named_argument(node, "role") { + Some(Value::Symbol(value)) if value == ".title" => 2, + _ => 1, + }; + commands.push(RenderCommand::Text { + x, + y, + value: value.clone(), + size, + color: "#242422".to_owned(), + }); + Size { + width: value.chars().count() as f32 * 8.0 * size as f32, + height: 8.0 * size as f32, + } +} + +fn layout_column(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec) -> Size { + let padding = number_argument(node, "padding").unwrap_or(0.0); + let gap = number_argument(node, "gap").unwrap_or(0.0); + let mut cursor_y = y + padding; + let mut maximum_width: f32 = 0.0; + for (index, child) in node.children.iter().enumerate() { + if index > 0 { + cursor_y += gap; + } + let size = layout_node(child, x + padding, cursor_y, commands); + cursor_y += size.height; + maximum_width = maximum_width.max(size.width); + } + Size { + width: maximum_width + padding * 2.0, + height: cursor_y - y + padding, + } +} + +fn layout_row(node: &SyntaxNode, x: f32, y: f32, commands: &mut Vec) -> Size { + let padding = number_argument(node, "padding").unwrap_or(0.0); + let gap = number_argument(node, "gap").unwrap_or(0.0); + let mut cursor_x = x + padding; + let mut maximum_height: f32 = 0.0; + for (index, child) in node.children.iter().enumerate() { + if index > 0 { + cursor_x += gap; + } + let size = layout_node(child, cursor_x, y + padding, commands); + cursor_x += size.width; + maximum_height = maximum_height.max(size.height); + } + Size { + width: cursor_x - x + padding, + height: maximum_height + padding * 2.0, + } +} + +fn number_argument(node: &SyntaxNode, name: &str) -> Option { + match named_argument(node, name) { + Some(Value::Number(value)) => Some(*value), + _ => None, + } +} + +fn named_argument<'a>(node: &'a SyntaxNode, name: &str) -> Option<&'a Value> { + node.arguments.iter().find_map(|argument| match argument { + Argument::Named(argument_name, value) if argument_name == name => Some(value), + _ => None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const EXAMPLE: &str = r#"namespace app.main + +use tessera.ui::{Column, Text} + +export scene Main { + render { + Column(gap: 12, padding: 24) { + Text("Hello", role: .title) + Text("Rendered by Tessera") + } + } +}"#; + + #[test] + fn compiles_baseline_render_syntax() { + let document = compile(EXAMPLE, "app.main").unwrap(); + assert_eq!(document.frame.commands.len(), 2); + assert_eq!(document.root.as_ref().unwrap().name, "Main"); + assert_eq!( + document.root.as_ref().unwrap().children[0].children[0] + .children + .len(), + 2 + ); + } + + #[test] + fn rejects_namespace_mismatch() { + let error = compile(EXAMPLE, "app.other").unwrap_err(); + assert!(error.to_string().contains("namespace mismatch")); + } + + #[test] + fn reports_incomplete_syntax() { + let error = compile("namespace app.main", "app.main").unwrap_err(); + assert!(error.to_string().contains("expected")); + } +} diff --git a/src/developer_tools.rs b/src/developer_tools.rs new file mode 100644 index 0000000..c47e022 --- /dev/null +++ b/src/developer_tools.rs @@ -0,0 +1,298 @@ +use crate::{ + display_list::{Color, DisplayList, DrawCommand}, + document::{CodeNode, RenderDocument}, + geometry::{Point, Rect}, +}; + +const PANEL: Color = Color::rgb(247, 247, 245); +const PANEL_ALT: Color = Color::rgb(238, 238, 235); +const BORDER: Color = Color::rgb(214, 214, 209); +const TEXT: Color = Color::rgb(36, 36, 34); +const MUTED: Color = Color::rgb(112, 112, 107); +const SELECTED: Color = Color::rgb(224, 229, 247); + +#[derive(Debug, Default)] +pub struct DeveloperToolsState { + pub open: bool, + pub selected_node_id: Option, +} + +impl DeveloperToolsState { + pub fn toggle(&mut self, document: &RenderDocument) { + self.open = !self.open; + self.ensure_selection(document); + } + + pub fn document_updated(&mut self, document: &RenderDocument) { + self.ensure_selection(document); + } + + fn ensure_selection(&mut self, document: &RenderDocument) { + let selection_exists = self + .selected_node_id + .as_deref() + .is_some_and(|id| document.node(id).is_some()); + if !selection_exists { + self.selected_node_id = document.root.as_ref().map(|node| node.id.clone()); + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct DeveloperToolsLayout { + rows: Vec, +} + +impl DeveloperToolsLayout { + pub fn node_at(&self, point: Point) -> Option<&str> { + self.rows + .iter() + .find(|row| row.rect.contains(point)) + .map(|row| row.node_id.as_str()) + } +} + +#[derive(Clone, Debug)] +struct TreeRowHit { + rect: Rect, + node_id: String, +} + +pub fn build( + width: u32, + height: u32, + scale: f32, + document: &RenderDocument, + state: &DeveloperToolsState, +) -> (DisplayList, DeveloperToolsLayout) { + if !state.open { + return (DisplayList::default(), DeveloperToolsLayout::default()); + } + + let mut list = DisplayList::default(); + let width = width as f32; + let height = height as f32; + let panel_width = (430.0 * scale).min(width * 0.52); + let panel_x = width - panel_width; + let header_height = 44.0 * scale; + let tree_height = ((height - header_height) * 0.42).max(150.0 * scale); + let code_y = header_height + tree_height; + + fill( + &mut list, + Rect::new(panel_x, 0.0, panel_width, height), + 0.0, + PANEL, + ); + fill( + &mut list, + Rect::new(panel_x, 0.0, 1.0 * scale, height), + 0.0, + BORDER, + ); + fill( + &mut list, + Rect::new(panel_x, header_height - scale, panel_width, scale), + 0.0, + BORDER, + ); + fill( + &mut list, + Rect::new(panel_x, code_y, panel_width, height - code_y), + 0.0, + PANEL_ALT, + ); + fill( + &mut list, + Rect::new(panel_x, code_y, panel_width, scale), + 0.0, + BORDER, + ); + + text( + &mut list, + panel_x + 14.0 * scale, + 15.0 * scale, + "DEVELOPER", + scale, + TEXT, + ); + text( + &mut list, + width - 116.0 * scale, + 15.0 * scale, + "F12 TOGGLE", + scale, + MUTED, + ); + + text( + &mut list, + panel_x + 14.0 * scale, + header_height + 13.0 * scale, + "ARCHITECTURE", + scale, + MUTED, + ); + + let mut layout = DeveloperToolsLayout::default(); + if let Some(root) = &document.root { + let mut flattened = Vec::new(); + flatten(root, 0, &mut flattened); + let row_height = 24.0 * scale; + let first_y = header_height + 34.0 * scale; + let maximum_rows = ((tree_height - 40.0 * scale) / row_height).max(0.0) as usize; + + for (index, (depth, node)) in flattened.into_iter().take(maximum_rows).enumerate() { + let y = first_y + index as f32 * row_height; + let row = Rect::new( + panel_x + 7.0 * scale, + y, + panel_width - 14.0 * scale, + row_height, + ); + if state.selected_node_id.as_deref() == Some(node.id.as_str()) { + fill(&mut list, row, 5.0 * scale, SELECTED); + } + text( + &mut list, + row.x + (9.0 + depth as f32 * 14.0) * scale, + row.y + 8.0 * scale, + &fit(&format!("{} {}", node.kind, node.name), 44), + scale, + TEXT, + ); + layout.rows.push(TreeRowHit { + rect: row, + node_id: node.id.clone(), + }); + } + } else { + text( + &mut list, + panel_x + 14.0 * scale, + header_height + 48.0 * scale, + "NO RENDER TREE", + scale, + MUTED, + ); + } + + text( + &mut list, + panel_x + 14.0 * scale, + code_y + 16.0 * scale, + "SOURCE", + scale, + MUTED, + ); + draw_source( + &mut list, + Rect::new(panel_x, code_y, panel_width, height - code_y), + scale, + document, + state.selected_node_id.as_deref(), + ); + + (list, layout) +} + +fn draw_source( + list: &mut DisplayList, + area: Rect, + scale: f32, + document: &RenderDocument, + selected_id: Option<&str>, +) { + if document.source.is_empty() { + text( + list, + area.x + 14.0 * scale, + area.y + 50.0 * scale, + "NO SOURCE LOADED", + scale, + MUTED, + ); + return; + } + + let all_lines: Vec<_> = document.source.lines().collect(); + let selected = selected_id.and_then(|id| document.node(id)); + let start = selected + .map(|node| node.start_line.saturating_sub(1)) + .unwrap_or(0) + .min(all_lines.len()); + let declared_end = selected + .map(|node| node.end_line) + .unwrap_or(all_lines.len()); + let end = if declared_end == 0 { + all_lines.len() + } else { + declared_end.min(all_lines.len()) + }; + let line_height = 18.0 * scale; + let first_y = area.y + 42.0 * scale; + let maximum_lines = + ((area.y + area.height - first_y - 10.0 * scale) / line_height).max(0.0) as usize; + let max_chars = ((area.width / (8.0 * scale)) - 9.0).max(8.0) as usize; + + for (offset, source_line) in all_lines[start..end].iter().take(maximum_lines).enumerate() { + let line_number = start + offset + 1; + let y = first_y + offset as f32 * line_height; + text( + list, + area.x + 12.0 * scale, + y, + &format!("{:>4}", line_number), + scale, + MUTED, + ); + text( + list, + area.x + 55.0 * scale, + y, + &fit(&source_line.replace('\t', " "), max_chars), + scale, + TEXT, + ); + } +} + +fn flatten<'a>(node: &'a CodeNode, depth: usize, output: &mut Vec<(usize, &'a CodeNode)>) { + output.push((depth, node)); + for child in &node.children { + flatten(child, depth + 1, output); + } +} + +fn fit(value: &str, maximum: usize) -> String { + let characters: Vec<_> = value.chars().collect(); + if characters.len() <= maximum { + return value.to_owned(); + } + if maximum < 3 { + return characters.into_iter().take(maximum).collect(); + } + characters + .into_iter() + .take(maximum - 3) + .chain(['.', '.', '.']) + .collect() +} + +fn fill(list: &mut DisplayList, rect: Rect, radius: f32, color: Color) { + list.push(DrawCommand::FillRoundedRect { + rect, + radius, + color, + }); +} + +fn text(list: &mut DisplayList, x: f32, y: f32, value: &str, scale: f32, color: Color) { + list.push(DrawCommand::Text { + origin: Point::new(x, y), + value: value.to_owned(), + scale: scale.round().max(1.0) as u32, + color, + }); +} diff --git a/src/display_list.rs b/src/display_list.rs new file mode 100644 index 0000000..17e3c08 --- /dev/null +++ b/src/display_list.rs @@ -0,0 +1,70 @@ +use crate::geometry::{Point, Rect}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +impl Color { + pub const fn rgb(red: u8, green: u8, blue: u8) -> Self { + Self { + red, + green, + blue, + alpha: 255, + } + } + + pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self { + Self { + red, + green, + blue, + alpha, + } + } +} + +#[derive(Clone, Debug)] +pub enum DrawCommand { + Clear(Color), + FillRoundedRect { + rect: Rect, + radius: f32, + color: Color, + }, + StrokeRoundedRect { + rect: Rect, + radius: f32, + width: f32, + color: Color, + }, + Text { + origin: Point, + value: String, + scale: u32, + color: Color, + }, +} + +#[derive(Clone, Debug, Default)] +pub struct DisplayList { + commands: Vec, +} + +impl DisplayList { + pub fn push(&mut self, command: DrawCommand) { + self.commands.push(command); + } + + pub fn commands(&self) -> &[DrawCommand] { + &self.commands + } + + pub fn extend(&mut self, other: DisplayList) { + self.commands.extend(other.commands); + } +} diff --git a/src/document.rs b/src/document.rs new file mode 100644 index 0000000..bd932fd --- /dev/null +++ b/src/document.rs @@ -0,0 +1,326 @@ +use std::collections::HashSet; + +use anyhow::{Result, bail}; +use serde::{Deserialize, Serialize}; + +use crate::{ + display_list::{Color, DisplayList, DrawCommand}, + geometry::{Point, Rect}, +}; + +pub const DEFAULT_NAMESPACE: &str = "app.main"; + +#[derive(Clone, Debug)] +pub struct NamespaceState { + pub name: String, + pub revision: u64, + pub document: RenderDocument, +} + +impl Default for NamespaceState { + fn default() -> Self { + Self { + name: DEFAULT_NAMESPACE.to_owned(), + revision: 0, + document: RenderDocument::default(), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct RenderDocument { + #[serde(default)] + pub source: String, + #[serde(default)] + pub root: Option, + #[serde(default)] + pub frame: Frame, +} + +impl RenderDocument { + pub fn validate(&self) -> Result<()> { + if self.source.len() > 1_048_576 { + bail!("source exceeds the 1 MiB limit"); + } + if self.frame.commands.len() > 10_000 { + bail!("frame exceeds the 10,000 command limit"); + } + + parse_color(&self.frame.background)?; + for command in &self.frame.commands { + command.validate()?; + } + + let mut ids = HashSet::new(); + if let Some(root) = &self.root { + root.validate(0, &mut ids)?; + } + Ok(()) + } + + pub fn display_list(&self, scale: f32) -> Result { + let mut list = DisplayList::default(); + list.push(DrawCommand::Clear(parse_color(&self.frame.background)?)); + for command in &self.frame.commands { + list.push(command.to_draw_command(scale)?); + } + Ok(list) + } + + pub fn node(&self, id: &str) -> Option<&CodeNode> { + self.root.as_ref()?.find(id) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CodeNode { + pub id: String, + pub name: String, + pub kind: String, + #[serde(default)] + pub start_line: usize, + #[serde(default)] + pub end_line: usize, + #[serde(default)] + pub children: Vec, +} + +impl CodeNode { + fn validate<'a>(&'a self, depth: usize, ids: &mut HashSet<&'a str>) -> Result<()> { + if depth > 64 { + bail!("architecture tree exceeds 64 levels"); + } + if self.id.is_empty() { + bail!("architecture node id cannot be empty"); + } + if !ids.insert(&self.id) { + bail!("duplicate architecture node id: {}", self.id); + } + if self.start_line > 0 && self.end_line > 0 && self.start_line > self.end_line { + bail!("invalid source range for node {}", self.id); + } + for child in &self.children { + child.validate(depth + 1, ids)?; + } + Ok(()) + } + + fn find(&self, id: &str) -> Option<&CodeNode> { + if self.id == id { + return Some(self); + } + self.children.iter().find_map(|child| child.find(id)) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Frame { + #[serde(default = "default_background")] + pub background: String, + #[serde(default)] + pub commands: Vec, +} + +impl Default for Frame { + fn default() -> Self { + Self { + background: default_background(), + commands: Vec::new(), + } + } +} + +fn default_background() -> String { + "#F7F7F5".to_owned() +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum RenderCommand { + RoundedRect { + x: f32, + y: f32, + width: f32, + height: f32, + #[serde(default)] + radius: f32, + color: String, + }, + StrokeRoundedRect { + x: f32, + y: f32, + width: f32, + height: f32, + #[serde(default)] + radius: f32, + #[serde(default = "default_stroke_width")] + stroke_width: f32, + color: String, + }, + Text { + x: f32, + y: f32, + value: String, + #[serde(default = "default_text_size")] + size: u32, + color: String, + }, +} + +impl RenderCommand { + fn validate(&self) -> Result<()> { + match self { + Self::RoundedRect { + x, + y, + width, + height, + radius, + color, + } => { + validate_rect(*x, *y, *width, *height, *radius)?; + parse_color(color)?; + } + Self::StrokeRoundedRect { + x, + y, + width, + height, + radius, + stroke_width, + color, + } => { + validate_rect(*x, *y, *width, *height, *radius)?; + if !stroke_width.is_finite() || *stroke_width <= 0.0 { + bail!("stroke_width must be a positive finite number"); + } + parse_color(color)?; + } + Self::Text { + x, + y, + value, + size, + color, + } => { + if !x.is_finite() || !y.is_finite() { + bail!("text coordinates must be finite"); + } + if value.len() > 16_384 { + bail!("text command exceeds 16 KiB"); + } + if !(1..=8).contains(size) { + bail!("text size must be between 1 and 8"); + } + parse_color(color)?; + } + } + Ok(()) + } + + fn to_draw_command(&self, scale: f32) -> Result { + Ok(match self { + Self::RoundedRect { + x, + y, + width, + height, + radius, + color, + } => DrawCommand::FillRoundedRect { + rect: scaled_rect(*x, *y, *width, *height, scale), + radius: radius * scale, + color: parse_color(color)?, + }, + Self::StrokeRoundedRect { + x, + y, + width, + height, + radius, + stroke_width, + color, + } => DrawCommand::StrokeRoundedRect { + rect: scaled_rect(*x, *y, *width, *height, scale), + radius: radius * scale, + width: stroke_width * scale, + color: parse_color(color)?, + }, + Self::Text { + x, + y, + value, + size, + color, + } => DrawCommand::Text { + origin: Point::new(x * scale, y * scale), + value: value.clone(), + scale: ((*size as f32 * scale).round() as u32).max(1), + color: parse_color(color)?, + }, + }) + } +} + +fn default_stroke_width() -> f32 { + 1.0 +} + +fn default_text_size() -> u32 { + 1 +} + +fn validate_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Result<()> { + if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() { + bail!("rectangle values must be finite"); + } + if width < 0.0 || height < 0.0 || !radius.is_finite() || radius < 0.0 { + bail!("rectangle dimensions and radius cannot be negative"); + } + Ok(()) +} + +fn scaled_rect(x: f32, y: f32, width: f32, height: f32, scale: f32) -> Rect { + Rect::new(x * scale, y * scale, width * scale, height * scale) +} + +pub fn parse_color(value: &str) -> Result { + let hex = value + .strip_prefix('#') + .ok_or_else(|| anyhow::anyhow!("color must begin with #"))?; + if hex.len() != 6 && hex.len() != 8 { + bail!("color must use #RRGGBB or #RRGGBBAA"); + } + let parse = |range| u8::from_str_radix(&hex[range], 16); + Ok(Color::rgba( + parse(0..2)?, + parse(2..4)?, + parse(4..6)?, + if hex.len() == 8 { parse(6..8)? } else { 255 }, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_document_is_valid_and_blank() { + let document = RenderDocument::default(); + document.validate().unwrap(); + assert!(document.root.is_none()); + assert!(document.frame.commands.is_empty()); + } + + #[test] + fn parses_rgb_and_rgba_colors() { + assert_eq!( + parse_color("#102030").unwrap(), + Color::rgba(16, 32, 48, 255) + ); + assert_eq!( + parse_color("#10203080").unwrap(), + Color::rgba(16, 32, 48, 128) + ); + } +} diff --git a/src/geometry.rs b/src/geometry.rs new file mode 100644 index 0000000..baeda4f --- /dev/null +++ b/src/geometry.rs @@ -0,0 +1,50 @@ +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Point { + pub x: f32, + pub y: f32, +} + +impl Point { + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Rect { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, +} + +impl Rect { + pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self { + Self { + x, + y, + width, + height, + } + } + + pub fn contains(self, point: Point) -> bool { + point.x >= self.x + && point.x <= self.x + self.width + && point.y >= self.y + && point.y <= self.y + self.height + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rectangle_contains_edges_but_not_outside_points() { + let rect = Rect::new(10.0, 20.0, 30.0, 40.0); + assert!(rect.contains(Point::new(10.0, 20.0))); + assert!(rect.contains(Point::new(40.0, 60.0))); + assert!(!rect.contains(Point::new(9.9, 20.0))); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a9db696 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +mod api; +mod app; +mod compiler; +mod developer_tools; +mod display_list; +mod document; +mod geometry; +mod scene; +mod software_renderer; + +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use app::{AppEvent, TesseraApp}; +use document::NamespaceState; +use winit::event_loop::{ControlFlow, EventLoop}; + +fn main() -> Result<()> { + let event_loop = EventLoop::::with_user_event().build()?; + event_loop.set_control_flow(ControlFlow::Wait); + let namespace = Arc::new(RwLock::new(NamespaceState::default())); + api::start(namespace.clone(), event_loop.create_proxy())?; + event_loop.run_app(&mut TesseraApp::new(namespace))?; + Ok(()) +} diff --git a/src/scene.rs b/src/scene.rs new file mode 100644 index 0000000..40fefab --- /dev/null +++ b/src/scene.rs @@ -0,0 +1,39 @@ +use crate::{ + developer_tools::{self, DeveloperToolsLayout, DeveloperToolsState}, + display_list::DisplayList, + document::RenderDocument, +}; + +#[derive(Clone, Debug, Default)] +pub struct SceneLayout { + pub developer_tools: DeveloperToolsLayout, +} + +pub fn build_scene( + width: u32, + height: u32, + scale: f32, + document: &RenderDocument, + developer_tools_state: &DeveloperToolsState, +) -> DisplayScene { + let mut display_list = document.display_list(scale).unwrap_or_else(|_| { + RenderDocument::default() + .display_list(scale) + .expect("default document is valid") + }); + let (developer_list, developer_layout) = + developer_tools::build(width, height, scale, document, developer_tools_state); + display_list.extend(developer_list); + + DisplayScene { + display_list, + layout: SceneLayout { + developer_tools: developer_layout, + }, + } +} + +pub struct DisplayScene { + pub display_list: DisplayList, + pub layout: SceneLayout, +} diff --git a/src/software_renderer.rs b/src/software_renderer.rs new file mode 100644 index 0000000..21a58c4 --- /dev/null +++ b/src/software_renderer.rs @@ -0,0 +1,195 @@ +use font8x8::{BASIC_FONTS, UnicodeFonts}; + +use crate::{ + display_list::{Color, DisplayList, DrawCommand}, + geometry::{Point, Rect}, +}; + +pub struct SoftwareRenderer; + +impl SoftwareRenderer { + pub fn render(list: &DisplayList, target: &mut [u32], width: u32, height: u32) { + let mut canvas = Canvas { + pixels: target, + width, + height, + }; + + for command in list.commands() { + match command { + DrawCommand::Clear(color) => canvas.clear(*color), + DrawCommand::FillRoundedRect { + rect, + radius, + color, + } => canvas.fill_rounded_rect(*rect, *radius, *color), + DrawCommand::StrokeRoundedRect { + rect, + radius, + width, + color, + } => canvas.stroke_rounded_rect(*rect, *radius, *width, *color), + DrawCommand::Text { + origin, + value, + scale, + color, + } => canvas.draw_text(*origin, value, *scale, *color), + } + } + } +} + +struct Canvas<'a> { + pixels: &'a mut [u32], + width: u32, + height: u32, +} + +impl Canvas<'_> { + fn clear(&mut self, color: Color) { + self.pixels.fill(pack(color)); + } + + fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) { + let bounds = self.clamped_bounds(rect); + let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0); + for y in bounds.1..bounds.3 { + for x in bounds.0..bounds.2 { + if rounded_rect_contains(rect, radius, x as f32 + 0.5, y as f32 + 0.5) { + self.blend_pixel(x, y, color); + } + } + } + } + + fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) { + let bounds = self.clamped_bounds(rect); + let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0); + let outer_radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0); + let inner = Rect::new( + rect.x + width, + rect.y + width, + (rect.width - width * 2.0).max(0.0), + (rect.height - width * 2.0).max(0.0), + ); + let inner_radius = (outer_radius - width).max(0.0); + + for y in bounds.1..bounds.3 { + for x in bounds.0..bounds.2 { + let px = x as f32 + 0.5; + let py = y as f32 + 0.5; + if rounded_rect_contains(rect, outer_radius, px, py) + && !rounded_rect_contains(inner, inner_radius, px, py) + { + self.blend_pixel(x, y, color); + } + } + } + } + + fn draw_text(&mut self, origin: Point, value: &str, scale: u32, color: Color) { + let scale = scale.max(1) as i32; + let mut cursor_x = origin.x.round() as i32; + let baseline_y = origin.y.round() as i32; + + for character in value.chars() { + if let Some(glyph) = BASIC_FONTS.get(character) { + for (row, byte) in glyph.iter().enumerate() { + for column in 0..8 { + if byte & (1 << column) != 0 { + self.fill_pixels( + cursor_x + column * scale, + baseline_y + row as i32 * scale, + scale, + scale, + color, + ); + } + } + } + } + cursor_x += 8 * scale; + } + } + + fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) { + ( + rect.x.max(0.0).floor() as i32, + rect.y.max(0.0).floor() as i32, + (rect.x + rect.width).min(self.width as f32).ceil() as i32, + (rect.y + rect.height).min(self.height as f32).ceil() as i32, + ) + } + + fn fill_pixels(&mut self, x: i32, y: i32, width: i32, height: i32, color: Color) { + for py in y.max(0)..(y + height).min(self.height as i32) { + for px in x.max(0)..(x + width).min(self.width as i32) { + self.blend_pixel(px, py, color); + } + } + } + + fn blend_pixel(&mut self, x: i32, y: i32, source: Color) { + if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 || source.alpha == 0 + { + return; + } + + let index = y as usize * self.width as usize + x as usize; + if source.alpha == 255 { + self.pixels[index] = pack(source); + return; + } + + let destination = self.pixels[index]; + let dst_red = (destination >> 16) & 0xff; + let dst_green = (destination >> 8) & 0xff; + let dst_blue = destination & 0xff; + let alpha = source.alpha as u32; + let inverse = 255 - alpha; + let red = (source.red as u32 * alpha + dst_red * inverse) / 255; + let green = (source.green as u32 * alpha + dst_green * inverse) / 255; + let blue = (source.blue as u32 * alpha + dst_blue * inverse) / 255; + self.pixels[index] = (red << 16) | (green << 8) | blue; + } +} + +fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool { + if rect.width <= 0.0 || rect.height <= 0.0 { + return false; + } + if radius <= 0.0 { + return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height; + } + + let closest_x = x.clamp(rect.x + radius, rect.x + rect.width - radius); + let closest_y = y.clamp(rect.y + radius, rect.y + rect.height - radius); + let dx = x - closest_x; + let dy = y - closest_y; + dx * dx + dy * dy <= radius * radius +} + +fn pack(color: Color) -> u32 { + (color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn renderer_changes_pixels_inside_a_rectangle() { + let mut list = DisplayList::default(); + list.push(DrawCommand::Clear(Color::rgb(0, 0, 0))); + list.push(DrawCommand::FillRoundedRect { + rect: Rect::new(1.0, 1.0, 2.0, 2.0), + radius: 0.0, + color: Color::rgb(255, 0, 0), + }); + let mut pixels = vec![0; 16]; + SoftwareRenderer::render(&list, &mut pixels, 4, 4); + assert_eq!(pixels[5], 0x00ff0000); + assert_eq!(pixels[0], 0); + } +}