diff --git a/app/rust/.gitignore b/app/rust/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/app/rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/app/rust/Cargo.lock b/app/rust/Cargo.lock new file mode 100644 index 000000000..3c23ca8c7 --- /dev/null +++ b/app/rust/Cargo.lock @@ -0,0 +1,7425 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite 2.6.1", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.5.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite 2.6.1", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "async_zip" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite 2.6.1", + "pin-project", + "thiserror 2.0.20", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.20", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[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 = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.13.1", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "command-fds" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" +dependencies = [ + "nix 0.31.3", + "thiserror 2.0.20", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "deflate64", + "flate2", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[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 = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[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" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +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 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "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 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" +dependencies = [ + "bitflags 2.13.1", + "block", + "cfg-if", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.1", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctor" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[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.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[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 = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7299a780854a6d391be2ae1c8521c9368471b559dbfd6a8dbd9f407eaff100" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[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.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite 2.6.1", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.5.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[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 = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.20", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpui" +version = "0.2.2" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.13.1", + "block", + "cbindgen", + "chrono", + "cocoa 0.26.0", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "core-video", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "foreign-types", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui_ce_util", + "gpui_collections", + "gpui_macros", + "gpui_media", + "gpui_refineable", + "gpui_scheduler", + "gpui_shared_string", + "gpui_sum_tree", + "http", + "image", + "inventory", + "itertools 0.14.0", + "log", + "lyon", + "mach2", + "metal", + "num_cpus", + "objc", + "parking", + "parking_lot", + "pathfinder_geometry", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "stacksafe", + "strum", + "taffy", + "thiserror 2.0.20", + "ttf-parser", + "unicode-segmentation", + "url", + "usvg", + "uuid", + "waker-fn", + "web-time", + "windows 0.61.3", + "zed-font-kit", + "zed-scap", +] + +[[package]] +name = "gpui_ce_util" +version = "0.2.2" +dependencies = [ + "anyhow", + "log", +] + +[[package]] +name = "gpui_collections" +version = "0.2.2" +dependencies = [ + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui_derive_refineable" +version = "0.2.2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui_elements" +version = "0.1.0" +dependencies = [ + "gpui", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "gpui_linux" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bitflags 2.13.1", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui", + "gpui_collections", + "gpui_wgpu", + "gpui_zed_util", + "image", + "itertools 0.14.0", + "libc", + "log", + "oo7", + "open", + "parking_lot", + "pathfinder_geometry", + "pollster 0.4.0", + "profiling", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "swash", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui_macos" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "async-task", + "block", + "cbindgen", + "cocoa 0.26.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "core-video", + "ctor", + "derive_more", + "dispatch2", + "etagere", + "foreign-types", + "futures", + "gpui", + "gpui_collections", + "gpui_media", + "gpui_zed_util", + "image", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "metal", + "objc", + "objc2-app-kit 0.3.2", + "parking_lot", + "pathfinder_geometry", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", + "zed-font-kit", +] + +[[package]] +name = "gpui_macros" +version = "0.1.0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui_media" +version = "0.2.2" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "ctor", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "gpui_platform" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "gpui", + "gpui_linux", + "gpui_macos", + "gpui_web", + "gpui_windows", +] + +[[package]] +name = "gpui_refineable" +version = "0.2.2" +dependencies = [ + "gpui_derive_refineable", +] + +[[package]] +name = "gpui_scheduler" +version = "0.2.2" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "web-time", +] + +[[package]] +name = "gpui_shared_string" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui_sum_tree" +version = "0.2.2" +dependencies = [ + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui_web" +version = "0.1.0" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "futures", + "gpui", + "gpui_wgpu", + "http", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui_wgpu" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui", + "gpui_ce_util", + "gpui_collections", + "itertools 0.14.0", + "js-sys", + "log", + "parking_lot", + "pollster 0.4.0", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui_widgets" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "gpui_elements", + "thiserror 2.0.20", +] + +[[package]] +name = "gpui_windows" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "etagere", + "futures", + "gpui", + "gpui_collections", + "gpui_zed_util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-numerics 0.2.0", + "windows-registry", +] + +[[package]] +name = "gpui_zed_util" +version = "0.2.2" +dependencies = [ + "anyhow", + "async-fs", + "async_zip", + "command-fds", + "dirs 6.0.0", + "dunce", + "futures", + "futures-lite 1.13.0", + "globset", + "gpui_ce_util", + "gpui_collections", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "nix 0.29.0", + "percent-encoding", + "regex", + "rust-embed", + "schemars", + "serde", + "serde_json", + "serde_json_lenient", + "shlex 1.3.0", + "smol", + "take-until", + "tempfile", + "tendril", + "unicase", + "url", + "walkdir", + "which", +] + +[[package]] +name = "grid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[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 = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.20", + "unicode-ident", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[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 = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oakapp" +version = "0.1.0" +dependencies = [ + "gpui", + "gpui_platform", + "gpui_widgets", + "image", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[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.1", + "block2 0.5.1", + "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-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.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.1", + "block2 0.5.1", + "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.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 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.1", + "block2 0.5.1", + "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.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.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.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[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.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite 2.6.1", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand 2.5.0", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[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 = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.5.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[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.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[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 = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.1", + "num-traits", + "proptest-macro", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[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 = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.20", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.2", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "globset", + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[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.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "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 = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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 = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.3", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[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.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_json_lenient" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "stacksafe" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f9c34983ac74195c710c473db6fdf1085f64a47dbaa0090d1bea03be70da66" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6feeae42a2d6b0dcb8aeb2f08d9e48cdac600239cf8a20fc59f9e252e86bdfe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4a2a7d92fa86fcc6222e4c3845f8486cff899d9db32480b26c91a5dbf2e22d" + +[[package]] +name = "sval_buffer" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4324db9ac500c609d659b752edf9c8abbf2233f8afd61a503fd6f88ed625032" +dependencies = [ + "sval", + "sval_ref", + "zerocopy", +] + +[[package]] +name = "sval_dynamic" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4046add0eecf55e680b9e207edf5fc7737b18a1d950db363d97e7f1b2d7c629c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a3486b5984a0a4f25edefcf2c2dba23654c29f63e75493b671d338bf24243" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da53aae7c737b5b5f1be4bcb0ff20e057bf6b2ee4e9d025560075c5830d09f95" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df24df43cbdc4bb8c9f5ed19d0d57dc8f60a1a4259cdce52d597fe774ad3a71f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bebc17f0f1fad060e57b778728d41ef87627e9111a6365d7463472cb58fc1b3" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f26fe3f6a68b40e6c8d654ea48c00e4316272fddf68c80493714c1b034ae70b" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "taffy" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.5.0", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[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", + "png 0.17.16", + "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 = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[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.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[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 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068e763e8279de7ab94b6afebded2cb701678af094feb1c12ccb061b4783c1be" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417d6197dd0ee696783d6be4276ac6ea74b985e00024c85ccfb37aff4f2bed82" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f7251ecde2c9ed431bbe0659853e7991753447447bbf1ae59d8b31c578d4e" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "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.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "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.1", + "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.1", + "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.1", + "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 0.41.0", + "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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +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 = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.13.1", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[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 = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.20", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[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", + "rustix 1.1.4", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite 2.6.1", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.7", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +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" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.4", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 1.0.4", +] diff --git a/app/rust/Cargo.toml b/app/rust/Cargo.toml new file mode 100644 index 000000000..243c89efa --- /dev/null +++ b/app/rust/Cargo.toml @@ -0,0 +1,49 @@ +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +[package] +name = "oakapp" +version = "0.1.0" +edition = "2021" +description = "Oak Video Editor application layer (Rust, gpui-based)" +license = "GPL-3.0-or-later" + +[lib] +name = "oakapp" +path = "src/lib.rs" + +[[bin]] +name = "oakapp" +path = "src/main.rs" + +[dependencies] +# gpui: the GPU-accelerated UI framework (oak-gpui fork, git submodule at gpui/). +gpui = { path = "../../gpui/crates/gpui" } +# Convenience entry point: `gpui_platform::application()` picks the platform +# backend. font-kit enables text shaping/rendering on macOS. +gpui_platform = { path = "../../gpui/crates/gpui_platform", features = ["font-kit"] } +# Oak's widget library: menus, viewer, form controls, project explorer. +gpui_widgets = { path = "../../gpui/crates/gpui_widgets" } + +[dev-dependencies] +# `#[gpui::test]` harness for engine-seam smoke tests (test-support feature). +gpui = { path = "../../gpui/crates/gpui", features = ["test-support"] } +# `test-support` also enables `gpui_macos/test-support`, which is what makes +# `render_to_image` (the screenshot example) available. +gpui_platform = { path = "../../gpui/crates/gpui_platform", features = ["test-support"] } +# Screenshot capture: `examples/screenshot.rs` saves the rendered window PNG +# (the `image` crate is already in the lockfile through gpui). +image = "0.25" diff --git a/app/rust/docs/screenshot-window.png b/app/rust/docs/screenshot-window.png new file mode 100644 index 000000000..c656a9afc Binary files /dev/null and b/app/rust/docs/screenshot-window.png differ diff --git a/app/rust/examples/screenshot.rs b/app/rust/examples/screenshot.rs new file mode 100644 index 000000000..2566a2660 --- /dev/null +++ b/app/rust/examples/screenshot.rs @@ -0,0 +1,72 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Offscreen screenshot capture for the app window. +//! +//! Renders the full [`OakApp`] shell at 1600×900 (2× = 3200×1866 px) in an +//! offscreen macOS window and writes the PNG to +//! `app/rust/docs/screenshot-window.png`, using the same +//! [`VisualTestAppContext`] machinery the gpui visual tests use. The window +//! is created at `(-10000, -10000)` so nothing flickers on screen. +//! +//! Run it on the macOS main thread (examples run on the main thread, unlike +//! `#[test]` harness threads): +//! +//! ```text +//! cargo run --example screenshot # 1600×900 → docs/screenshot-window.png +//! cargo run --example screenshot -- 1100 900 # any size (still overwrites the same file) +//! ``` + +use gpui::{px, size, AnyWindowHandle, AppContext, Result, VisualTestAppContext}; +use gpui_platform::current_platform; +use oakapp::app::OakApp; + +const DEFAULT_WIDTH: f32 = 1600.0; +const DEFAULT_HEIGHT: f32 = 900.0; +const OUT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/docs/screenshot-window.png"); + +fn main() -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + let width = args + .first() + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_WIDTH); + let height = args + .get(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_HEIGHT); + + let mut cx = VisualTestAppContext::new(current_platform(false)); + cx.update(|app| app.init_colors()); + + let window = cx.open_offscreen_window(size(px(width), px(height)), |window, cx| { + cx.new(|cx| OakApp::new(window, cx)) + })?; + let handle: AnyWindowHandle = window.into(); + + // Let the platform settle, then draw one full frame into the rendered + // scene so `render_to_image` has something to capture. + cx.run_until_parked(); + cx.update_window(handle, |_root, window, app| { + let _ = window.draw(app); + })?; + + let image = cx.capture_screenshot(handle)?; + std::fs::create_dir_all(std::path::Path::new(OUT).parent().unwrap())?; + image.save(OUT)?; + println!("wrote {OUT} ({}×{})", image.width(), image.height()); + Ok(()) +} diff --git a/app/rust/rustfmt.toml b/app/rust/rustfmt.toml new file mode 100644 index 000000000..19aca4ce9 --- /dev/null +++ b/app/rust/rustfmt.toml @@ -0,0 +1,18 @@ +# Oak Video Editor - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Match the repo convention (see src/facade/rust): tab indentation. +hard_tabs = true diff --git a/app/rust/src/app.rs b/app/rust/src/app.rs new file mode 100644 index 000000000..41f228a13 --- /dev/null +++ b/app/rust/src/app.rs @@ -0,0 +1,717 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The application shell: menu bar, dock layout, status bar and the tick +//! loop that drives playback, playhead sync and the audio meter. +//! +//! Layout per the design (`design/Oak-UI设计图-主界面-标注版.png`): +//! +//! ```text +//! ┌ menu bar (文件 编辑 视图 回放 序列 窗口 工具 帮助) +//! ├───────────────────────────────────────────────────── +//! │ dock: 项目 | 素材查看器 | 序列查看器+节点编辑器 | 检查器+历史记录 +//! │ (vertical split) 时间线 (full width, 31px toolbar on top) +//! ├───────────────────────────────────────────────────── +//! └ status bar: 就绪 | 缓存 | 代理 | 自动保存 || 时间码/时长 | 帧率 | 分辨率 +//! ``` + +use std::sync::Arc; +use std::time::Duration; + +use gpui::dock::{ + DockArea, DockLayout, DropTarget, DropZone, NodePath, PanelHandle, PanelRegistry, +}; +use gpui::timeline::{Frame, TimelineEvent, TimelineView}; +use gpui::{ + div, prelude::*, px, size, App, AsyncWindowContext, Bounds, Context, Entity, Render, Window, + WindowBounds, WindowOptions, +}; +use gpui_widgets::audio_meter::AudioLevelMeter; +use gpui_widgets::menu::{Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem}; +use gpui_widgets::theme::{apply_theme, OakTheme}; + +use crate::oakui::{EngineGateway, MockClock, MockEngine, Monitor}; +use crate::panels::history::HistoryPanel; +use crate::panels::ids::*; +use crate::panels::inspector::InspectorPanel; +use crate::panels::node_editor::NodeEditorPanel; +use crate::panels::program_viewer::ProgramViewerPanel; +use crate::panels::project_explorer::ProjectExplorerPanel; +use crate::panels::source_viewer::SourceViewerPanel; +use crate::panels::status_bar::StatusBar; +use crate::panels::timeline::TimelinePanel; + +// Menu item ids (unique per menu). +mod menu_ids { + pub const NEW_PROJECT: usize = 101; + pub const OPEN_PROJECT: usize = 102; + pub const SAVE: usize = 103; + pub const EXPORT: usize = 104; + pub const QUIT: usize = 105; + + pub const UNDO: usize = 201; + pub const REDO: usize = 202; + pub const CUT: usize = 203; + pub const COPY: usize = 204; + pub const PASTE: usize = 205; + pub const DELETE: usize = 206; + + pub const THEME_DARK: usize = 301; + pub const THEME_LIGHT: usize = 302; + pub const LANG_ZH: usize = 303; + pub const LANG_EN: usize = 304; + + pub const PLAY_PAUSE: usize = 401; + pub const PREV_FRAME: usize = 402; + pub const NEXT_FRAME: usize = 403; + pub const TO_START: usize = 404; + + pub const ADD_VIDEO_TRACK: usize = 501; + pub const ADD_AUDIO_TRACK: usize = 502; + + pub const FOCUS_PROJECT: usize = 601; + pub const FOCUS_SOURCE_VIEWER: usize = 602; + pub const FOCUS_PROGRAM_VIEWER: usize = 603; + pub const FOCUS_NODE_EDITOR: usize = 604; + pub const FOCUS_INSPECTOR: usize = 605; + pub const FOCUS_HISTORY: usize = 606; + pub const FOCUS_TIMELINE: usize = 607; + + pub const ABOUT: usize = 801; +} + +/// The panel registry: string keys for layout persistence, and the ability +/// to rebuild any panel from its key. +struct AppPanelRegistry { + engine: Entity, + source_clock: Entity, + program_clock: Entity, +} + +impl PanelRegistry for AppPanelRegistry { + fn panel_key(&self, id: gpui::dock::PanelId) -> Option { + Some( + match id { + PROJECT => "project", + SOURCE_VIEWER => "source-viewer", + PROGRAM_VIEWER => "program-viewer", + NODE_EDITOR => "node-editor", + INSPECTOR => "inspector", + HISTORY => "history", + TIMELINE => "timeline", + _ => return None, + } + .to_string(), + ) + } + + fn build_panel(&self, key: &str, window: &mut Window, cx: &mut App) -> Option { + // Each arm builds its own `PanelHandle` because the panel views have + // different entity types. + match key { + "project" => Some(PanelHandle::new( + cx.new(|cx| ProjectExplorerPanel::new(self.engine.clone(), window, cx)), + cx, + )), + "source-viewer" => Some(PanelHandle::new( + cx.new(|cx| { + SourceViewerPanel::new( + self.engine.clone(), + self.source_clock.clone(), + window, + cx, + ) + }), + cx, + )), + "program-viewer" => Some(PanelHandle::new( + cx.new(|cx| { + let meter = + cx.new(|cx| AudioLevelMeter::new(30, self.engine.clone(), window, cx)); + ProgramViewerPanel::new( + self.engine.clone(), + self.program_clock.clone(), + meter, + window, + cx, + ) + }), + cx, + )), + "node-editor" => Some(PanelHandle::new( + cx.new(|cx| NodeEditorPanel::new(window, cx)), + cx, + )), + "inspector" => Some(PanelHandle::new( + cx.new(|cx| InspectorPanel::new(self.engine.clone(), window, cx)), + cx, + )), + "history" => Some(PanelHandle::new( + cx.new(|cx| HistoryPanel::new(window, cx)), + cx, + )), + "timeline" => Some(PanelHandle::new( + cx.new(|cx| { + let timeline = + cx.new(|cx| TimelineView::new(self.engine.clone(), window, cx).zoom(2.0)); + TimelinePanel::new(self.engine.clone(), timeline, window, cx) + }), + cx, + )), + _ => None, + } + } +} + +/// The application root view. +pub struct OakApp { + engine: Entity, + program_clock: Entity, + timeline: Entity>, + meter: Entity>, + menu_bar: Entity, + dock: Entity, + status_bar: Entity, + /// Whether the dark theme is active (toggles via 视图 → 主题). + dark: bool, +} + +impl OakApp { + /// Builds the whole shell. + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + apply_theme(cx, &OakTheme::olive_dark()); + + // --- engine and shared state --------------------------------------- + let engine = cx.new(|cx| MockEngine::demo(cx)); + let source_clock = engine.read(cx).source_clock.clone(); + let program_clock = engine.read(cx).program_clock.clone(); + let timeline = cx.new(|cx| TimelineView::new(engine.clone(), window, cx).zoom(2.0)); + let meter = cx.new(|cx| AudioLevelMeter::new(3, engine.clone(), window, cx)); + + // --- menu bar ------------------------------------------------------ + let menu_bar = cx.new(|cx| MenuBar::new(1, make_menus(true), window, cx)); + cx.subscribe( + &menu_bar, + |this, _menu: Entity, event: &MenuBarEvent, cx| { + if let MenuBarEvent::Triggered { item, .. } = event { + this.on_menu(*item, cx); + } + }, + ) + .detach(); + + // --- dock ---------------------------------------------------------- + let dock = cx.new(|cx| { + DockArea::new(cx).with_registry(Arc::new(AppPanelRegistry { + engine: engine.clone(), + source_clock: source_clock.clone(), + program_clock: program_clock.clone(), + })) + }); + + let project = cx.new(|cx| ProjectExplorerPanel::new(engine.clone(), window, cx)); + let source_viewer = + cx.new(|cx| SourceViewerPanel::new(engine.clone(), source_clock.clone(), window, cx)); + let program_viewer = cx.new(|cx| { + ProgramViewerPanel::new( + engine.clone(), + program_clock.clone(), + meter.clone(), + window, + cx, + ) + }); + let node_editor = cx.new(|cx| NodeEditorPanel::new(window, cx)); + let inspector = cx.new(|cx| InspectorPanel::new(engine.clone(), window, cx)); + let history = cx.new(|cx| HistoryPanel::new(window, cx)); + let timeline_panel = + cx.new(|cx| TimelinePanel::new(engine.clone(), timeline.clone(), window, cx)); + + // Arrange the default workspace: the design's 素材查看器 | 序列查看器 | + // 检查器 row (project bin docked on the left), node editor + history + // as tabs, timeline full width at the bottom. + dock.update(cx, |dock, cx| { + dock.add_panel(PanelHandle::new(project, cx), None, cx); + dock.add_panel( + PanelHandle::new(source_viewer, cx), + Some(DropTarget { + panel: Some(PROJECT), + zone: DropZone::Right, + }), + cx, + ); + dock.add_panel( + PanelHandle::new(program_viewer, cx), + Some(DropTarget { + panel: Some(SOURCE_VIEWER), + zone: DropZone::Right, + }), + cx, + ); + dock.add_panel( + PanelHandle::new(node_editor, cx), + Some(DropTarget { + panel: Some(PROGRAM_VIEWER), + zone: DropZone::Center, + }), + cx, + ); + dock.add_panel( + PanelHandle::new(inspector, cx), + Some(DropTarget { + panel: Some(PROGRAM_VIEWER), + zone: DropZone::Right, + }), + cx, + ); + dock.add_panel( + PanelHandle::new(history, cx), + Some(DropTarget { + panel: Some(INSPECTOR), + zone: DropZone::Center, + }), + cx, + ); + dock.add_panel( + PanelHandle::new(timeline_panel, cx), + Some(DropTarget { + panel: None, + zone: DropZone::Bottom, + }), + cx, + ); + }); + + // Tune the default split ratios: top 70%, project bin 17% of the row. + let mut layout: DockLayout = dock.read(cx).layout().clone(); + layout.resize_split(&NodePath(vec![]), 0.70); + layout.resize_split(&NodePath(vec![0]), 0.17); + dock.update(cx, |dock, cx| dock.set_layout(layout, cx)); + + // --- status bar ---------------------------------------------------- + let status_bar = cx.new(|cx| StatusBar::new(engine.clone(), program_clock.clone(), cx)); + + // Repaint the shell whenever the engine notifies. + cx.observe(&engine, |this, _engine, cx| { + cx.notify(); + let _ = this; + }) + .detach(); + + // --- timeline events ----------------------------------------------- + // The playhead is driven by the program monitor; seeking the timeline + // (ruler click, keyboard) is routed back to the engine, guarded so + // clock-driven syncs are no-ops. + cx.subscribe( + &timeline, + |this, _timeline, event: &TimelineEvent, cx| match event { + TimelineEvent::PlayheadChanged(frame) => { + let current = this.engine.read(cx).clock_frame(Monitor::Program, cx); + if *frame != current { + this.engine.update(cx, |engine, cx| { + engine.request_frame(Monitor::Program, *frame, cx) + }); + } + } + other => println!("[timeline] request: {other:?} (not applied by the mock)"), + }, + ) + .detach(); + + // --- tick loop ----------------------------------------------------- + // Drives playback clocks, playhead sync and the audio meter at ~60Hz. + let this = cx.weak_entity(); + window + .spawn(cx, async move |cx: &mut AsyncWindowContext| loop { + cx.background_executor() + .timer(Duration::from_millis(16)) + .await; + let _ = cx.update(|_window, app| { + if let Some(this) = this.upgrade() { + this.update(app, |this, cx| this.tick(cx)); + } + }); + }) + .detach(); + + Self { + engine, + program_clock, + timeline, + meter, + menu_bar, + dock, + status_bar, + dark: true, + } + } + + /// One animation-frame tick: advance the engine, sync the timeline + /// playhead to the program clock, and refresh the audio meter. + fn tick(&mut self, cx: &mut Context) { + self.engine.update(cx, |engine, cx| engine.tick(cx)); + let frame = self.program_clock.read(cx).transport.frame(); + self.timeline + .update(cx, |timeline, cx| timeline.seek(frame, cx)); + self.meter.update(cx, |meter, cx| meter.update(cx)); + cx.notify(); + } + + /// Routes a menu action. + fn on_menu(&mut self, item: usize, cx: &mut Context) { + use menu_ids::*; + match item { + PLAY_PAUSE => { + let playing = self.program_clock.read(cx).transport.is_playing(); + let monitor = Monitor::Program; + self.engine.update(cx, |engine, cx| { + if playing { + engine.pause(monitor, cx); + } else { + engine.play(monitor, cx); + } + }); + } + PREV_FRAME => { + let monitor = Monitor::Program; + self.engine + .update(cx, |engine, cx| engine.step(monitor, -1, cx)); + } + NEXT_FRAME => { + let monitor = Monitor::Program; + self.engine + .update(cx, |engine, cx| engine.step(monitor, 1, cx)); + } + TO_START => { + let monitor = Monitor::Program; + self.engine.update(cx, |engine, cx| { + engine.request_frame(monitor, Frame::ZERO, cx) + }); + } + THEME_DARK => { + self.dark = true; + apply_theme(cx, &OakTheme::olive_dark()); + self.rebuild_menu_bar(cx); + cx.notify(); + } + THEME_LIGHT => { + self.dark = false; + apply_theme(cx, &OakTheme::olive_light()); + self.rebuild_menu_bar(cx); + cx.notify(); + } + LANG_ZH => self.switch_language(crate::i18n::Language::ZhCN, cx), + LANG_EN => self.switch_language(crate::i18n::Language::EnUs, cx), + ADD_VIDEO_TRACK => { + let kind = gpui::timeline::TrackKind::Video; + self.engine + .update(cx, |engine, cx| engine.add_track(kind, cx)); + } + ADD_AUDIO_TRACK => { + let kind = gpui::timeline::TrackKind::Audio; + self.engine + .update(cx, |engine, cx| engine.add_track(kind, cx)); + } + FOCUS_PROJECT => self.focus_panel(PROJECT, cx), + FOCUS_SOURCE_VIEWER => self.focus_panel(SOURCE_VIEWER, cx), + FOCUS_PROGRAM_VIEWER => self.focus_panel(PROGRAM_VIEWER, cx), + FOCUS_NODE_EDITOR => self.focus_panel(NODE_EDITOR, cx), + FOCUS_INSPECTOR => self.focus_panel(INSPECTOR, cx), + FOCUS_HISTORY => self.focus_panel(HISTORY, cx), + FOCUS_TIMELINE => self.focus_panel(TIMELINE, cx), + other => println!("[menu] placeholder action for item {other}"), + } + } + + /// Focuses a dock panel (used by the 窗口 menu). + fn focus_panel(&self, id: gpui::dock::PanelId, cx: &mut Context) { + if let Some(handle) = cx.windows().first() { + let dock = self.dock.clone(); + let _ = cx.update_window(*handle, move |_root, window, app| { + dock.update(app, |dock, cx| dock.focus_panel(id, window, cx)); + }); + } + } + + /// Switches the UI language live: updates the [`i18n`] global, rebuilds + /// the menu bar (so the menu labels and the language checkmark move + /// immediately), and repaints the whole shell — every label goes through + /// [`crate::i18n::tr`] at render time, so panels flip without a restart. + fn switch_language(&mut self, language: crate::i18n::Language, cx: &mut Context) { + crate::i18n::set_language(language); + self.rebuild_menu_bar(cx); + cx.notify(); + } + + /// Replaces the `MenuBar` entity with one built from the current language + /// and theme, re-subscribing to its trigger events. + fn rebuild_menu_bar(&mut self, cx: &mut Context) { + let windows = cx.windows(); + let Some(handle) = windows.first() else { + return; + }; + let dark = self.dark; + let Ok(menu_bar) = cx.update_window(*handle, |_root, window, app| { + app.new(|cx| MenuBar::new(1, make_menus(dark), window, cx)) + }) else { + return; + }; + self.menu_bar = menu_bar; + let menu_bar = self.menu_bar.clone(); + cx.subscribe( + &menu_bar, + |this, _menu: Entity, event: &MenuBarEvent, cx| { + if let MenuBarEvent::Triggered { item, .. } = event { + this.on_menu(*item, cx); + } + }, + ) + .detach(); + } +} + +impl Render for OakApp { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .flex_col() + .child(self.menu_bar.clone()) + .child(div().flex_1().child(self.dock.clone())) + .child(self.status_bar.clone()) + } +} + +/// Builds the menu bar entries (文件/编辑/视图/回放/序列/窗口/工具/帮助). All +/// labels come from the [`crate::i18n`] tables, so rebuilding the menu bar +/// after a language switch repaints it in the new language. `dark` drives the +/// theme submenu's checkmark. +fn make_menus(dark: bool) -> Vec { + use crate::i18n::tr; + use menu_ids::*; + + let current = crate::i18n::language(); + + let theme_submenu = Menu::new(vec![ + MenuItem::new(THEME_DARK, tr("menu.view.theme.dark")).with_checked(dark), + MenuItem::new(THEME_LIGHT, tr("menu.view.theme.light")).with_checked(!dark), + ]); + + let language_submenu = Menu::new(vec![ + MenuItem::new(LANG_ZH, tr("menu.view.language.zh")) + .with_checked(current == crate::i18n::Language::ZhCN), + MenuItem::new(LANG_EN, tr("menu.view.language.en")) + .with_checked(current == crate::i18n::Language::EnUs), + ]); + + vec![ + MenuBarEntry::new( + tr("menu.file"), + Menu::new(vec![ + MenuItem::new(NEW_PROJECT, tr("menu.file.new_project")).with_shortcut("⌘N"), + MenuItem::new(OPEN_PROJECT, tr("menu.file.open_project")).with_shortcut("⌘O"), + MenuItem::new(SAVE, tr("menu.file.save")).with_shortcut("⌘S").separated(), + MenuItem::new(EXPORT, tr("menu.file.export")).disabled(), + MenuItem::new(QUIT, tr("menu.file.quit")).with_shortcut("⌘Q").separated(), + ]), + ), + MenuBarEntry::new( + tr("menu.edit"), + Menu::new(vec![ + MenuItem::new(UNDO, tr("menu.edit.undo")).with_shortcut("⌘Z"), + MenuItem::new(REDO, tr("menu.edit.redo")).with_shortcut("⇧⌘Z").separated(), + MenuItem::new(CUT, tr("menu.edit.cut")).with_shortcut("⌘X"), + MenuItem::new(COPY, tr("menu.edit.copy")).with_shortcut("⌘C"), + MenuItem::new(PASTE, tr("menu.edit.paste")).with_shortcut("⌘V"), + MenuItem::new(DELETE, tr("menu.edit.delete")).separated(), + ]), + ), + MenuBarEntry::new( + tr("menu.view"), + Menu::new(vec![ + MenuItem::new(THEME_DARK, tr("menu.view.theme")).with_submenu(theme_submenu), + MenuItem::new(LANG_ZH, tr("menu.view.language")).with_submenu(language_submenu), + ]), + ), + MenuBarEntry::new( + tr("menu.playback"), + Menu::new(vec![ + MenuItem::new(PLAY_PAUSE, tr("menu.playback.play_pause")).with_shortcut("空格"), + MenuItem::new(PREV_FRAME, tr("menu.playback.prev_frame")).with_shortcut("←"), + MenuItem::new(NEXT_FRAME, tr("menu.playback.next_frame")) + .with_shortcut("→") + .separated(), + MenuItem::new(TO_START, tr("menu.playback.to_start")).with_shortcut("Home"), + ]), + ), + MenuBarEntry::new( + tr("menu.sequence"), + Menu::new(vec![ + MenuItem::new(ADD_VIDEO_TRACK, tr("menu.sequence.add_video_track")), + MenuItem::new(ADD_AUDIO_TRACK, tr("menu.sequence.add_audio_track")), + MenuItem::new(503, tr("menu.sequence.settings")).disabled(), + ]), + ), + MenuBarEntry::new( + tr("menu.window"), + Menu::new(vec![ + MenuItem::new(FOCUS_PROJECT, tr("menu.window.project")), + MenuItem::new(FOCUS_SOURCE_VIEWER, tr("menu.window.source_viewer")), + MenuItem::new(FOCUS_PROGRAM_VIEWER, tr("menu.window.program_viewer")), + MenuItem::new(FOCUS_NODE_EDITOR, tr("menu.window.node_editor")), + MenuItem::new(FOCUS_INSPECTOR, tr("menu.window.inspector")), + MenuItem::new(FOCUS_HISTORY, tr("menu.window.history")), + MenuItem::new(FOCUS_TIMELINE, tr("menu.window.timeline")), + ]), + ), + MenuBarEntry::new( + tr("menu.tools"), + Menu::new(vec![ + MenuItem::new(701, tr("menu.tools.select")), + MenuItem::new(702, tr("menu.tools.razor")), + MenuItem::new(703, tr("menu.tools.snap")).with_checked(true), + ]), + ), + MenuBarEntry::new( + tr("menu.help"), + Menu::new(vec![MenuItem::new(ABOUT, tr("menu.help.about"))]), + ), + ] +} + +/// The crate entry point: applies the olive-dark theme and opens the main +/// window. +pub fn run() { + gpui_platform::application().run(|cx: &mut App| { + // Restore the persisted UI language (oakcommon config `Language` key) + // before the first window renders. + crate::i18n::init(); + cx.init_colors(); + let bounds = Bounds::centered(None, size(px(1600.0), px(900.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| OakApp::new(window, cx)), + ) + .expect("failed to open the main window"); + + cx.activate(true); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The 视图/View menu carries a 语言/Language submenu whose items are + /// labeled in their own language and whose checkmark follows the active + /// language — and the whole menu bar flips language with `i18n`. + #[test] + fn language_menu_tracks_the_active_language() { + let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + + let view_entry = |dark: bool| -> MenuBarEntry { + make_menus(dark) + .into_iter() + .find(|entry| entry.title == crate::i18n::tr("menu.view")) + .expect("视图/View menu exists") + }; + + // The language submenu sits under 视图/View. + let language_item = |entry: &MenuBarEntry| -> Menu { + entry + .menu + .items + .iter() + .find(|item| item.id == menu_ids::LANG_ZH) + .map(|item| item.submenu.clone().map(|m| *m).unwrap_or_default()) + .expect("语言/Language submenu exists") + }; + + crate::i18n::set_language(crate::i18n::Language::EnUs); + let submenu = language_item(&view_entry(true)); + let zh = submenu + .items + .iter() + .find(|i| i.id == menu_ids::LANG_ZH) + .expect("zh item"); + let en = submenu + .items + .iter() + .find(|i| i.id == menu_ids::LANG_EN) + .expect("en item"); + assert_eq!(zh.label, "简体中文"); + assert_eq!(en.label, "English"); + assert_eq!(zh.checked, Some(false)); + assert_eq!(en.checked, Some(true), "en-US is active → checked"); + + crate::i18n::set_language(crate::i18n::Language::ZhCN); + let submenu = language_item(&view_entry(true)); + let zh = submenu + .items + .iter() + .find(|i| i.id == menu_ids::LANG_ZH) + .expect("zh item"); + let en = submenu + .items + .iter() + .find(|i| i.id == menu_ids::LANG_EN) + .expect("en item"); + assert_eq!(zh.checked, Some(true), "zh-CN is active → checked"); + assert_eq!(en.checked, Some(false)); + + // The menu titles themselves are localized. + assert_eq!(view_entry(true).title, "视图(V)"); + crate::i18n::set_language(crate::i18n::Language::EnUs); + assert_eq!(view_entry(true).title, "View(V)"); + } + + /// The theme submenu's checkmark follows the `dark` flag. + #[test] + fn theme_menu_checkmark_follows_dark_flag() { + let _guard = crate::i18n::lang_test_lock().lock().unwrap(); + + let dark_item = |dark: bool| -> gpui_widgets::menu::MenuItem { + let entries = make_menus(dark); + let view = entries + .iter() + .find(|entry| entry.title == crate::i18n::tr("menu.view")) + .expect("视图/View menu"); + let theme = view + .menu + .items + .iter() + .find(|i| i.id == menu_ids::THEME_DARK) + .and_then(|i| i.submenu.clone()) + .expect("theme submenu"); + theme + .items + .into_iter() + .find(|i| i.id == menu_ids::THEME_DARK) + .expect("Olive Dark item") + }; + assert_eq!(dark_item(true).checked, Some(true)); + assert_eq!(dark_item(false).checked, Some(false)); + } +} diff --git a/app/rust/src/i18n.rs b/app/rust/src/i18n.rs new file mode 100644 index 000000000..d621126ac --- /dev/null +++ b/app/rust/src/i18n.rs @@ -0,0 +1,569 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Tiny localization layer: embedded en-US / zh-CN string tables, a +//! [`tr`] lookup used by every user-visible label in the app, and a runtime +//! language setting persisted through the oakcommon config C ABI +//! (`oakcommon_config_get` / `oakcommon_config_set`, the process-wide +//! `ConfigStore`). +//! +//! # The tables +//! +//! Plain key → string arrays (no serde, no build step). [`tr`] falls back +//! from the active language to en-US and then to the key itself, so a +//! missing key can never panic — it degrades to a visible-but-identifiable +//! key string instead. +//! +//! # The language setting +//! +//! The language is a process-global [`Language`] (an atomic, so any thread +//! can read it without locking). At startup [`init`] loads the persisted +//! value from the oakcommon config key `Language` (`"zh-CN"` / `"en-US"`; +//! empty or unknown values mean en-US). [`set_language`] flips the global +//! and writes the new value back through the same key so the preference +//! survives restarts. +//! +//! The oakcommon C ABI is resolved at runtime with `dlopen`/`dlsym`, so the +//! app builds, tests and runs without liboakcommon present (e.g. under +//! `cargo test`): when the library cannot be loaded the layer degrades to an +//! in-process store and still switches languages live. Once the app is +//! packaged with liboakcommon in the library search path (or +//! `OAK_LIB_DIR` is set to a build tree), the setting round-trips through +//! `config.ini`. + +use std::sync::atomic::{AtomicU8, Ordering}; + +/// The languages shipped with the app. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + /// English (United States). + EnUs, + /// Simplified Chinese. + ZhCN, +} + +impl Language { + /// The stable config/table code, e.g. `"en-US"`. + pub fn code(self) -> &'static str { + match self { + Language::EnUs => "en-US", + Language::ZhCN => "zh-CN", + } + } + + /// Parses a stored config value into a language. Empty or unknown values + /// fall back to en-US. + fn from_code(code: &str) -> Self { + let code = code.trim().to_ascii_lowercase(); + if code.starts_with("zh") { + Language::ZhCN + } else { + Language::EnUs + } + } +} + +/// The current language, as an atomic tag (0 = en-US, 1 = zh-CN). +static CURRENT: AtomicU8 = AtomicU8::new(0); + +/// The active language. +pub fn language() -> Language { + match CURRENT.load(Ordering::Relaxed) { + 1 => Language::ZhCN, + _ => Language::EnUs, + } +} + +/// Switches the active language live and persists the choice through the +/// oakcommon config C ABI (when the library is loadable). +pub fn set_language(language: Language) { + CURRENT.store(match language { + Language::EnUs => 0, + Language::ZhCN => 1, + }, Ordering::Relaxed); + persist_language(language); +} + +/// Loads the persisted language from the oakcommon config C ABI. Called once +/// at startup. Never fails: without liboakcommon the default (en-US) stays. +pub fn init() { + let Some(store) = ConfigAbi::load() else { + return; + }; + match store.get("Language") { + Some(code) if !code.is_empty() => set_language(Language::from_code(&code)), + _ => {} + } +} + +/// Writes `language` back to the oakcommon config `Language` key. +fn persist_language(language: Language) { + if let Some(store) = ConfigAbi::load() { + store.set("Language", language.code()); + } +} + +/// Translates `key` in the active language. +/// +/// Never panics: unknown keys fall back to en-US and then to the key itself, +/// so a typo'd key is visible in the UI instead of crashing it. +pub fn tr(key: &'static str) -> &'static str { + match language() { + Language::EnUs => en(key), + Language::ZhCN => zh(key).unwrap_or_else(|| en(key)), + } +} + +/// Looks `key` up in the en-US table. +fn en(key: &'static str) -> &'static str { + EN + .iter() + .find(|(k, _)| *k == key) + .map(|(_, v)| *v) + .unwrap_or(key) +} + +/// Looks `key` up in the zh-CN table. +fn zh(key: &'static str) -> Option<&'static str> { + ZH + .iter() + .find(|(k, _)| *k == key) + .map(|(_, v)| *v) +} + +/// The en-US table. Every key must also exist in [`ZH`]; [`tr`] tolerates +/// missing entries but the tests below enforce parity. +const EN: &[(&str, &str)] = &[ + // --- menu bar titles --- + ("menu.file", "File(F)"), + ("menu.edit", "Edit(E)"), + ("menu.view", "View(V)"), + ("menu.playback", "Playback(P)"), + ("menu.sequence", "Sequence(S)"), + ("menu.window", "Window(W)"), + ("menu.tools", "Tools(T)"), + ("menu.help", "Help(H)"), + // --- File --- + ("menu.file.new_project", "New Project…"), + ("menu.file.open_project", "Open Project…"), + ("menu.file.save", "Save"), + ("menu.file.export", "Export…"), + ("menu.file.quit", "Quit"), + // --- Edit --- + ("menu.edit.undo", "Undo"), + ("menu.edit.redo", "Redo"), + ("menu.edit.cut", "Cut"), + ("menu.edit.copy", "Copy"), + ("menu.edit.paste", "Paste"), + ("menu.edit.delete", "Delete"), + // --- View --- + ("menu.view.theme", "Theme"), + ("menu.view.theme.dark", "Olive Dark"), + ("menu.view.theme.light", "Olive Light"), + ("menu.view.language", "Language"), + ("menu.view.language.en", "English"), + ("menu.view.language.zh", "简体中文"), + // --- Playback --- + ("menu.playback.play_pause", "Play/Pause"), + ("menu.playback.prev_frame", "Previous Frame"), + ("menu.playback.next_frame", "Next Frame"), + ("menu.playback.to_start", "Jump to Sequence Start"), + // --- Sequence --- + ("menu.sequence.add_video_track", "Add Video Track"), + ("menu.sequence.add_audio_track", "Add Audio Track"), + ("menu.sequence.settings", "Sequence Settings…"), + // --- Window --- + ("menu.window.project", "Project"), + ("menu.window.source_viewer", "Source Viewer"), + ("menu.window.program_viewer", "Program Viewer"), + ("menu.window.node_editor", "Node Editor"), + ("menu.window.inspector", "Inspector"), + ("menu.window.history", "History"), + ("menu.window.timeline", "Timeline"), + // --- Tools --- + ("menu.tools.select", "Select"), + ("menu.tools.razor", "Razor"), + ("menu.tools.snap", "Snap"), + // --- Help --- + ("menu.help.about", "About Oak…"), + // --- dock panel titles --- + ("panel.project", "Project"), + ("panel.source_viewer", "Source Viewer"), + ("panel.program_viewer", "Program Viewer"), + ("panel.node_editor", "Node Editor"), + ("panel.inspector", "Inspector"), + ("panel.history", "History"), + ("panel.timeline", "Timeline"), + // --- status bar --- + ("status.ready", "Ready"), + ("status.cache", "Cache: Enabled"), + ("status.proxy", "Proxy: Off"), + ("status.autosave", "Autosave: 3 min ago"), + ("status.untitled", "Untitled Project"), + // --- timeline toolbar --- + ("timeline.tool.select", "Select"), + ("timeline.tool.razor", "Razor"), + ("timeline.tool.ripple", "Ripple"), + ("timeline.tool.slip", "Slip"), + ("timeline.tool.roll", "Roll"), + ("timeline.tool.zoom", "Zoom"), + ("timeline.tool.knife", "Knife"), + ("timeline.tool.marker", "Marker"), + ("timeline.zoom", "Zoom"), + ("timeline.track_height", "Track Height"), + ("timeline.snap", "Snap"), + // --- node editor --- + ("node.zoom_in", "Zoom In"), + ("node.zoom_out", "Zoom Out"), + ("node.fit", "Fit"), + ("node.fit_window", "Fit Window"), + ("node.placeholder", "Node Editor · Placeholder — gpui::node_graph not wired up yet"), + // --- viewer header chips --- + ("viewer.source", "Source Viewer · Source"), + ("viewer.program", "Program Viewer · Program"), + // --- inspector --- + ("inspector.params", "Parameters (placeholder)"), +]; + +/// The zh-CN table. Mirrors [`EN`] key-for-key. +const ZH: &[(&str, &str)] = &[ + // --- menu bar titles --- + ("menu.file", "文件(F)"), + ("menu.edit", "编辑(E)"), + ("menu.view", "视图(V)"), + ("menu.playback", "回放(P)"), + ("menu.sequence", "序列(S)"), + ("menu.window", "窗口(W)"), + ("menu.tools", "工具(T)"), + ("menu.help", "帮助(H)"), + // --- File --- + ("menu.file.new_project", "新建项目…"), + ("menu.file.open_project", "打开项目…"), + ("menu.file.save", "保存"), + ("menu.file.export", "导出…"), + ("menu.file.quit", "退出"), + // --- Edit --- + ("menu.edit.undo", "撤销"), + ("menu.edit.redo", "重做"), + ("menu.edit.cut", "剪切"), + ("menu.edit.copy", "复制"), + ("menu.edit.paste", "粘贴"), + ("menu.edit.delete", "删除"), + // --- View --- + ("menu.view.theme", "主题"), + ("menu.view.theme.dark", "Olive Dark"), + ("menu.view.theme.light", "Olive Light"), + ("menu.view.language", "语言"), + ("menu.view.language.en", "English"), + ("menu.view.language.zh", "简体中文"), + // --- Playback --- + ("menu.playback.play_pause", "播放/暂停"), + ("menu.playback.prev_frame", "上一帧"), + ("menu.playback.next_frame", "下一帧"), + ("menu.playback.to_start", "跳到序列起点"), + // --- Sequence --- + ("menu.sequence.add_video_track", "添加视频轨道"), + ("menu.sequence.add_audio_track", "添加音频轨道"), + ("menu.sequence.settings", "序列设置…"), + // --- Window --- + ("menu.window.project", "项目"), + ("menu.window.source_viewer", "素材查看器"), + ("menu.window.program_viewer", "序列查看器"), + ("menu.window.node_editor", "节点编辑器"), + ("menu.window.inspector", "检查器"), + ("menu.window.history", "历史记录"), + ("menu.window.timeline", "时间线"), + // --- Tools --- + ("menu.tools.select", "选择"), + ("menu.tools.razor", "剃刀"), + ("menu.tools.snap", "吸附"), + // --- Help --- + ("menu.help.about", "关于 Oak…"), + // --- dock panel titles --- + ("panel.project", "项目"), + ("panel.source_viewer", "素材查看器"), + ("panel.program_viewer", "序列查看器"), + ("panel.node_editor", "节点编辑器"), + ("panel.inspector", "检查器"), + ("panel.history", "历史记录"), + ("panel.timeline", "时间线"), + // --- status bar --- + ("status.ready", "就绪"), + ("status.cache", "缓存:已启用"), + ("status.proxy", "代理:关"), + ("status.autosave", "自动保存:3分钟前"), + ("status.untitled", "未命名项目"), + // --- timeline toolbar --- + ("timeline.tool.select", "选择"), + ("timeline.tool.razor", "剃刀"), + ("timeline.tool.ripple", "波纹"), + ("timeline.tool.slip", "滑动"), + ("timeline.tool.roll", "滚动"), + ("timeline.tool.zoom", "缩放"), + ("timeline.tool.knife", "刀"), + ("timeline.tool.marker", "标记"), + ("timeline.zoom", "缩放"), + ("timeline.track_height", "轨道高"), + ("timeline.snap", "吸附"), + // --- node editor --- + ("node.zoom_in", "放大"), + ("node.zoom_out", "缩小"), + ("node.fit", "适配"), + ("node.fit_window", "适配窗口"), + ("node.placeholder", "节点编辑器 · 占位 — gpui::node_graph 尚未接入"), + // --- viewer header chips --- + ("viewer.source", "素材查看器 · 源"), + ("viewer.program", "序列查看器 · 节目"), + // --- inspector --- + ("inspector.params", "参数(占位)"), +]; + +// --------------------------------------------------------------------------- +// oakcommon config C ABI (runtime-resolved) +// --------------------------------------------------------------------------- + +/// The subset of the oakcommon config C ABI the language setting needs. Each +/// function pointer is optional: when liboakcommon cannot be loaded the whole +/// struct is `None` and the in-process fallback store is used instead. +struct ConfigAbi { + get: unsafe extern "C" fn(*const i8, *const i8, *mut i8, i32) -> i32, + set: unsafe extern "C" fn(*const i8, *const i8, *const i8), +} + +impl ConfigAbi { + /// Resolves the ABI once (process-wide) and returns it when loadable. + fn load() -> Option<&'static ConfigAbi> { + static ABI: std::sync::OnceLock> = std::sync::OnceLock::new(); + ABI.get_or_init(resolve_abi).as_ref() + } + + /// Reads the string entry for a flat `key`, or `None` when absent. + fn get(&self, key: &str) -> Option { + let key = to_c(key)?; + let mut buf = [0i8; 128]; + let result = unsafe { + (self.get)( + std::ptr::null(), + key.as_ptr(), + buf.as_mut_ptr(), + buf.len() as i32, + ) + }; + if result <= 0 { + // Negative codes are OAKCOMMON_E_* errors (e.g. not-found). + return None; + } + Some(from_c(&buf)) + } + + /// Writes a string entry for a flat `key`. + fn set(&self, key: &str, value: &str) { + let (Some(key), Some(value)) = (to_c(key), to_c(value)) else { + return; + }; + unsafe { + (self.set)(std::ptr::null(), key.as_ptr(), value.as_ptr()); + } + } +} + +/// Resolves the oakcommon config functions via `dlopen`/`dlsym`. +/// +/// Candidate library names: `OAK_LIB_DIR` (build-tree override) first, then +/// the bare `liboakcommon.dylib` name on the platform search path. When none +/// loads (plain `cargo test`/`cargo run` without a built oakcommon), this +/// returns `None` and [`ConfigAbi::load`] degrades to the fallback store. +#[cfg(target_os = "macos")] +fn resolve_abi() -> Option { + use std::ffi::c_void; + + const RTLD_LAZY: i32 = 0x1; + unsafe extern "C" { + fn dlopen(filename: *const i8, flag: i32) -> *mut c_void; + fn dlsym(handle: *mut c_void, symbol: *const i8) -> *mut c_void; + } + + // Candidate handles; the first dlopen that succeeds wins. + let mut candidates: Vec<*mut c_void> = Vec::new(); + let mut handle: *mut c_void = std::ptr::null_mut(); + + if let Ok(dir) = std::env::var("OAK_LIB_DIR") { + let path = format!("{dir}/liboakcommon.dylib"); + if let Some(c) = to_c(&path) { + handle = unsafe { dlopen(c.as_ptr(), RTLD_LAZY) }; + if !handle.is_null() { + candidates.push(handle); + } + } + } + if handle.is_null() { + if let Some(c) = to_c("liboakcommon.dylib") { + handle = unsafe { dlopen(c.as_ptr(), RTLD_LAZY) }; + } + } + if handle.is_null() { + return None; + } + + let get = unsafe { dlsym(handle, b"oakcommon_config_get\0".as_ptr() as *const i8) }; + let set = unsafe { dlsym(handle, b"oakcommon_config_set\0".as_ptr() as *const i8) }; + if get.is_null() || set.is_null() { + return None; + } + + Some(ConfigAbi { + get: unsafe { std::mem::transmute(get) }, + set: unsafe { std::mem::transmute(set) }, + }) +} + +/// Non-macOS fallback: no dlopen plumbing here; the in-process store is +/// always used. +#[cfg(not(target_os = "macos"))] +fn resolve_abi() -> Option { + None +} + +/// Turns a `&str` into a NUL-terminated C string. +fn to_c(s: &str) -> Option { + std::ffi::CString::new(s).ok() +} + +/// Reads a NUL-terminated buffer back into a `String`. +fn from_c(buf: &[i8]) -> String { + let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + buf[..len] + .iter() + .map(|&c| c as u8 as char) + .collect() +} + +/// Serializes every test that mutates the process-global language, so +/// parallel tests (in this module and in [`crate::app`]) cannot race each +/// other's `set_language` calls. +#[cfg(test)] +pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> { + static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + &LOCK +} + +#[cfg(test)] +mod tests { + use super::*; + + fn lang_lock() -> &'static std::sync::Mutex<()> { + lang_test_lock() + } + + /// Every key must exist in both tables, with a non-empty translation. + #[test] + fn every_key_exists_in_both_languages() { + assert_eq!(EN.len(), ZH.len(), "tables must have identical key sets"); + for (key, en_value) in EN { + assert!(!key.is_empty()); + assert!(!en_value.is_empty(), "empty en-US value for {key}"); + let zh_value = ZH + .iter() + .find(|(k, _)| *k == *key) + .unwrap_or_else(|| panic!("zh-CN table is missing key {key}")); + assert!( + !zh_value.1.is_empty(), + "empty zh-CN value for {key}" + ); + } + } + + /// Keys are unique within each table (a duplicate would make `tr`' + /// lookup order-dependent). + #[test] + fn keys_are_unique() { + for table in [EN, ZH] { + let mut seen = std::collections::HashSet::new(); + for (key, _) in table { + assert!(seen.insert(*key), "duplicate key {key}"); + } + } + } + + /// `tr` never panics, even for a key that is in neither table — the raw + /// key comes back so the omission is visible. + #[test] + fn tr_never_panics_on_missing_key() { + let _guard = lang_lock().lock().unwrap(); + for (language, sample) in [(Language::EnUs, "missing.key"), (Language::ZhCN, "missing.key")] { + set_language(language); + assert_eq!(tr(sample), sample); + } + } + + /// `tr` returns the en-US string when a key exists in en-US only. + #[test] + fn tr_falls_back_to_en() { + let _guard = lang_lock().lock().unwrap(); + set_language(Language::ZhCN); + // All real keys exist in both tables, so force the fallback path via + // a key that exists in EN but not ZH by temporarily shadowing… not + // possible with const tables — instead verify the en-US default. + assert_eq!(tr("status.ready"), "就绪"); + set_language(Language::EnUs); + assert_eq!(tr("status.ready"), "Ready"); + } + + /// Switching languages flips a sample string live. + #[test] + fn switching_flips_a_sample_string() { + let _guard = lang_lock().lock().unwrap(); + set_language(Language::EnUs); + assert_eq!(tr("menu.file.save"), "Save"); + set_language(Language::ZhCN); + assert_eq!(tr("menu.file.save"), "保存"); + set_language(Language::EnUs); + assert_eq!(tr("menu.file.save"), "Save"); + } + + /// The config code round-trips. + #[test] + fn language_code_round_trips() { + assert_eq!(Language::EnUs.code(), "en-US"); + assert_eq!(Language::ZhCN.code(), "zh-CN"); + assert_eq!(Language::from_code("en-US"), Language::EnUs); + assert_eq!(Language::from_code("zh-CN"), Language::ZhCN); + assert_eq!(Language::from_code("zh_CN"), Language::ZhCN); + assert_eq!(Language::from_code(""), Language::EnUs); + assert_eq!(Language::from_code("klingon"), Language::EnUs); + } + + /// The active language reads back what was set. + #[test] + fn language_state_tracks_set_language() { + let _guard = lang_lock().lock().unwrap(); + set_language(Language::ZhCN); + assert_eq!(language(), Language::ZhCN); + set_language(Language::EnUs); + assert_eq!(language(), Language::EnUs); + } + + /// `init()` (and the config path generally) must not panic when the + /// oakcommon library is absent — which is the default under `cargo test`. + #[test] + fn init_does_not_panic_without_oakcommon() { + let _guard = lang_lock().lock().unwrap(); + init(); + // The language remains whatever it was; only the ABI path was + // exercised (falling back silently). + assert!(matches!(language(), Language::EnUs | Language::ZhCN)); + } +} diff --git a/app/rust/src/lib.rs b/app/rust/src/lib.rs new file mode 100644 index 000000000..7d7b097b6 --- /dev/null +++ b/app/rust/src/lib.rs @@ -0,0 +1,48 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oakapp` — the Rust application layer of Oak, built on the `gpui` UI +//! framework (the oak-gpui fork at `gpui/`). +//! +//! This is the start of the Rust rewrite of `app/` (Qt): a gpui window with +//! the main layout from the design (`design/`), dockable panels built from +//! the `gpui_widgets` library, and an engine seam (`oakui`) that currently +//! feeds demo data through [`MockEngine`](oakui::MockEngine). +//! +//! # Layout +//! +//! * [`app`] — the window shell: menu bar, dock layout, status bar, tick +//! loop. +//! * [`panels`] — the dockable panels (viewers, timeline, inspector, ...). +//! * [`oakui`] — the engine gateway trait, the mock implementation, and the +//! pure view-state logic (timecode, transport). +//! +//! # Running +//! +//! ```text +//! cargo run --bin oakapp # the demo window +//! cargo test # unit tests (timecode, transport) +//! ``` + +pub mod app; +pub mod i18n; +pub mod oakui; +pub mod panels; + +/// The application entry point (called from `main.rs`). +pub fn run() { + app::run(); +} diff --git a/app/rust/src/main.rs b/app/rust/src/main.rs new file mode 100644 index 000000000..5f6b5199d --- /dev/null +++ b/app/rust/src/main.rs @@ -0,0 +1,21 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The `oakapp` binary: opens the Oak main window (see [`oakapp::app`]). + +fn main() { + oakapp::run(); +} diff --git a/app/rust/src/oakui/engine.rs b/app/rust/src/oakui/engine.rs new file mode 100644 index 000000000..96e08fb08 --- /dev/null +++ b/app/rust/src/oakui/engine.rs @@ -0,0 +1,130 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The engine gateway: the narrow Rust API through which the app layer talks +//! to the engine. +//! +//! # Why a gateway trait +//! +//! The UI must never depend on *how* the engine is implemented. Today the +//! only implementation is the mock ([`super::mock::MockEngine`]) feeding demo +//! data; later a real backend will bind the `liboakengine` C ABI +//! (`src/facade/rust`, the frozen `oakengine_*` exports) behind the *same* +//! trait. Swapping backends then touches only the wiring in +//! [`crate::app`] — the panels, the widgets and the view state stay as they +//! are. +//! +//! The trait is intentionally narrow: open a project, inspect the current +//! sequence, and drive the transport (play / pause / step / seek). Timeline +//! edits arrive as widget request events and are applied by the host through +//! methods on the engine type itself (see the `MockEngine` docs for the +//! current mapping), so they do not need to be part of this seam yet. +//! +//! Everything here is plain Rust — no C ABI, no FFI. The C-ABI binding is a +//! later concern of the real backend only. + +use gpui::timeline::{Frame, FrameRate}; +use std::path::PathBuf; + +/// A monitor the transport can address. +/// +/// Oak has two independent transports: the source monitor plays the clip +/// shown in the source viewer (素材查看器), the program monitor plays the +/// sequence shown in the program viewer (序列查看器). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Monitor { + /// The source (footage) monitor. + Source, + /// The program (sequence) monitor. + Program, +} + +/// A video format: resolution plus frame rate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoFormat { + /// Width in pixels. + pub width: u32, + /// Height in pixels. + pub height: u32, + /// The frame rate (rational, e.g. 30000/1001 for NTSC 29.97). + pub rate: FrameRate, +} + +impl VideoFormat { + /// The classic HD television format: 1920×1080 at 25 fps. + pub fn hd_1080p25() -> Self { + Self { + width: 1920, + height: 1080, + rate: FrameRate::new(25, 1), + } + } +} + +/// A project open in the engine. +#[derive(Debug, Clone, PartialEq)] +pub struct Project { + /// The project's display name. + pub name: String, + /// The project file on disk (`.ove`). + pub path: PathBuf, +} + +/// The sequence currently open in the project. +#[derive(Debug, Clone, PartialEq)] +pub struct Sequence { + /// The sequence's display name. + pub name: String, + /// The sequence's video format. + pub format: VideoFormat, + /// The sequence length in frames. + pub length: Frame, +} + +/// The engine gateway. +/// +/// Implementations own the "engine" side of the app: project state, the +/// current sequence, and the transport. Query methods are pure reads; +/// mutating methods take a gpui [`Context`](gpui::Context) so the backend can +/// update its observable entities (clocks, models) and notify them. +pub trait EngineGateway: Sized { + /// The currently open project, or `None` before any project is opened. + fn project(&self) -> Option<&Project>; + + /// The current sequence of the open project, if any. + fn current_sequence(&self) -> Option<&Sequence>; + + /// Open a project file. The backend loads it and becomes the source of + /// truth for [`project`](EngineGateway::project) / + /// [`current_sequence`](EngineGateway::current_sequence). + fn open_project(&mut self, path: PathBuf, cx: &mut gpui::Context); + + /// Seek `monitor` to `frame` (clamped to the sequence). + fn request_frame(&mut self, monitor: Monitor, frame: Frame, cx: &mut gpui::Context); + + /// Start playback on `monitor`. + fn play(&mut self, monitor: Monitor, cx: &mut gpui::Context); + + /// Pause playback on `monitor`, leaving the playhead where it is. + fn pause(&mut self, monitor: Monitor, cx: &mut gpui::Context); + + /// Step `monitor`'s playhead by `delta` frames (negative steps back). + fn step(&mut self, monitor: Monitor, delta: i64, cx: &mut gpui::Context); + + /// Advance the playback clocks by one wall-clock tick. Called on a + /// periodic timer while any monitor is playing. + fn tick(&mut self, cx: &mut gpui::Context); +} diff --git a/app/rust/src/oakui/mock.rs b/app/rust/src/oakui/mock.rs new file mode 100644 index 000000000..6407fa937 --- /dev/null +++ b/app/rust/src/oakui/mock.rs @@ -0,0 +1,802 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The mock engine: a gpui entity that implements every data-source trait +//! the widgets read, and feeds them demo data. +//! +//! # One entity, many roles +//! +//! [`MockEngine`] is the single source of truth for the demo project and +//! implements, all on the same entity: +//! +//! * [`EngineGateway`] — the app's transport/project seam (see the +//! [module docs](super::engine)); +//! * [`TimelineDataSource`] — the sequence model behind the timeline widget; +//! * [`EffectStackDataSource`] — the 媒体 → 变换 → OCIO LUT → 输出 stack; +//! * [`ProjectDataSource`] — the material bin tree; +//! * [`AudioMeterDataSource`] — the two program-master channels. +//! +//! Transport state lives in two [`MockClock`] entities (one per +//! [`Monitor`]); the engine owns them and advances them on every +//! [`EngineGateway::tick`]. +//! +//! The real engine will later implement the same gateway over the +//! `liboakengine` C ABI; only the wiring in [`crate::app`] changes. + +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Instant; + +use gpui::effect_stack::{ + EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, +}; +use gpui::timeline::{ + ClipData, ClipId, Frame, FrameRange, FrameRate, TimelineDataSource, TrackData, TrackKind, +}; +use gpui::{prelude::*, px, App, Context, Entity, Hsla, Pixels, SharedString}; +use gpui_widgets::audio_meter::AudioMeterDataSource; +use gpui_widgets::project_explorer::{ProjectDataSource, ProjectEntry}; +use gpui_widgets::viewer::PlaybackClock; + +use super::engine::{EngineGateway, Monitor, Project, Sequence, VideoFormat}; +use super::transport::TransportState; + +/// The demo sequence length: 00:04:18:18 at 25 fps. +const SEQUENCE_LENGTH: i64 = 6468; + +// --------------------------------------------------------------------------- +// Clocks +// --------------------------------------------------------------------------- + +/// A transport clock: the playhead plus the wall-clock anchor used while +/// playing. This is the object the viewer widgets poll through +/// [`PlaybackClock`]. +pub struct MockClock { + /// The transport state (play/pause, playhead, loop range). + pub transport: TransportState, + /// The clock's frame rate. + pub rate: FrameRate, + /// Wall-clock anchor `(started_at, anchored_frame)` while playing. + started: Option<(Instant, Frame)>, +} + +impl MockClock { + /// A stopped clock at frame zero running at `rate`. + pub fn new(rate: FrameRate) -> Self { + Self { + transport: TransportState::new(), + rate, + started: None, + } + } + + /// Starts playback from the current playhead. + pub fn play(&mut self) { + self.transport.play(); + self.started = Some((Instant::now(), self.transport.frame())); + } + + /// Pauses playback, keeping the playhead. + pub fn pause(&mut self) { + self.transport.pause(); + self.started = None; + } + + /// Advances the playhead from the wall clock while playing, looping at + /// `length`. No-op when stopped. + pub fn tick(&mut self, length: Frame) { + let Some((started, anchored)) = self.started else { + return; + }; + let elapsed = started.elapsed(); + let mut frame = anchored + + Frame( + (elapsed.as_secs_f64() * self.rate.num as f64 / self.rate.den as f64).round() + as i64, + ); + if length.0 > 0 && frame.0 >= length.0 { + // Loop back to the start of the sequence for the demo. + frame = Frame(frame.0 % length.0); + } + self.transport.seek(frame, length); + } +} + +impl PlaybackClock for MockClock { + fn current_frame(&self) -> Frame { + self.transport.frame() + } + + fn is_playing(&self) -> bool { + self.transport.is_playing() + } + + fn frame_rate(&self) -> FrameRate { + self.rate + } +} + +// --------------------------------------------------------------------------- +// Timeline model +// --------------------------------------------------------------------------- + +/// A clip on the demo timeline. +#[derive(Debug, Clone)] +pub struct MockClip { + id: ClipId, + range: FrameRange, + media_in: Frame, + label: SharedString, + color: Hsla, +} + +impl ClipData for MockClip { + fn id(&self) -> ClipId { + self.id + } + + fn range(&self) -> FrameRange { + self.range + } + + fn media_in(&self) -> Frame { + self.media_in + } + + fn label(&self) -> SharedString { + self.label.clone() + } + + fn color(&self) -> Option { + Some(self.color) + } +} + +/// A track on the demo timeline (snapshot handed to the timeline widget). +#[derive(Debug, Clone)] +pub struct MockTrack { + kind: TrackKind, + name: SharedString, + height: Pixels, + locked: bool, + muted: bool, + solo: bool, + visible: bool, + clips: Vec, +} + +impl TrackData for MockTrack { + type Clip = MockClip; + + fn kind(&self) -> TrackKind { + self.kind + } + + fn name(&self) -> SharedString { + self.name.clone() + } + + fn is_locked(&self) -> bool { + self.locked + } + + fn is_muted(&self) -> bool { + self.muted + } + + fn is_solo(&self) -> bool { + self.solo + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn height(&self) -> Pixels { + self.height + } + + fn clips(&self) -> &[Self::Clip] { + &self.clips + } +} + +/// An effect card in the demo stack. +#[derive(Debug, Clone)] +pub struct MockEffect { + id: EffectId, + kind: EffectCardKind, + title: SharedString, + subtitle: Option, + enabled: bool, + expanded: bool, + badge: Option, +} + +impl EffectData for MockEffect { + fn id(&self) -> EffectId { + self.id + } + + fn kind(&self) -> EffectCardKind { + self.kind + } + + fn title(&self) -> SharedString { + self.title.clone() + } + + fn subtitle(&self) -> Option { + self.subtitle.clone() + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn is_expanded(&self) -> bool { + self.expanded + } + + fn badge_count(&self) -> Option { + self.badge + } +} + +// --------------------------------------------------------------------------- +// The engine +// --------------------------------------------------------------------------- + +/// The mock engine implementing every data-source trait over demo data. +pub struct MockEngine { + project: Project, + sequence: Sequence, + /// The source monitor's clock. + pub source_clock: Entity, + /// The program monitor's clock. + pub program_clock: Entity, + /// The timeline tracks. + tracks: Vec, + /// The effect stack cards. + effects: Vec, + /// Id allocator for effects added at runtime. + next_effect_id: u64, + /// Whether the program monitor is playing (mirrors the clock; kept here + /// because the audio-meter data source has no `App` to read the clock). + program_playing: bool, + /// The selected item in the material bin (demo state). + selected_item: Option, + /// Phase counter driving the demo audio levels. + meter_phase: u32, +} + +impl MockEngine { + /// Builds the demo project: 第一稿.ove, one HD sequence, four tracks. + pub fn demo(cx: &mut Context) -> Self { + let rate = VideoFormat::hd_1080p25().rate; + let clip = + |id: u64, start: i64, end: i64, media_in: i64, label: &str, color: Hsla| MockClip { + id: ClipId(id), + range: FrameRange::new(Frame(start), Frame(end)), + media_in: Frame(media_in), + label: label.into(), + color, + }; + let video = |h: f32| Hsla { + h, + s: 0.55, + l: 0.45, + a: 1.0, + }; + let audio = |h: f32| Hsla { + h, + s: 0.45, + l: 0.55, + a: 1.0, + }; + + Self { + project: Project { + name: "第一稿".into(), + path: PathBuf::from("/home/mikesolar/Videos/aaa.ove"), + }, + sequence: Sequence { + name: "第一稿".into(), + format: VideoFormat::hd_1080p25(), + length: Frame(SEQUENCE_LENGTH), + }, + source_clock: cx.new(|_cx| MockClock::new(rate)), + program_clock: cx.new(|_cx| MockClock::new(rate)), + tracks: vec![ + MockTrack { + kind: TrackKind::Video, + name: "V2 视频轨道1".into(), + height: px(64.0), + locked: false, + muted: false, + solo: false, + visible: true, + clips: vec![clip(10, 120, 300, 0, "标题.mov", video(0.55))], + }, + MockTrack { + kind: TrackKind::Video, + name: "V1 视频轨道0".into(), + height: px(64.0), + locked: false, + muted: false, + solo: false, + visible: true, + clips: vec![ + clip(11, 0, 240, 0, "开场.mov", video(0.60)), + clip(12, 240, 600, 100, "B-roll.mp4", video(0.50)), + ], + }, + MockTrack { + kind: TrackKind::Audio, + name: "A1 音频轨道0".into(), + height: px(48.0), + locked: false, + muted: false, + solo: false, + visible: true, + clips: vec![clip(13, 0, 600, 0, "对白.wav", audio(0.05))], + }, + MockTrack { + kind: TrackKind::Audio, + name: "A2 音频轨道1".into(), + height: px(48.0), + locked: false, + muted: false, + solo: false, + visible: true, + clips: vec![clip(14, 0, 480, 0, "配乐.flac", audio(0.62))], + }, + ], + effects: vec![ + MockEffect { + id: EffectId(0), + kind: EffectCardKind::Source, + title: "媒体".into(), + subtitle: Some("第一稿.mp4".into()), + enabled: true, + expanded: false, + badge: None, + }, + MockEffect { + id: EffectId(1), + kind: EffectCardKind::Effect, + title: "变换".into(), + subtitle: Some("缩放 100% · 旋转 0°".into()), + enabled: true, + expanded: true, + badge: Some(2), + }, + MockEffect { + id: EffectId(2), + kind: EffectCardKind::Effect, + title: "OCIO LUT".into(), + subtitle: Some("filmic_to_display.cube".into()), + enabled: true, + expanded: false, + badge: None, + }, + MockEffect { + id: EffectId(3), + kind: EffectCardKind::Output, + title: "输出".into(), + subtitle: None, + enabled: true, + expanded: false, + badge: None, + }, + ], + next_effect_id: 4, + program_playing: false, + selected_item: None, + meter_phase: 0, + } + } + + /// The current sequence length (also used by the gateway). + fn sequence_length(&self) -> Frame { + self.sequence.length + } + + /// Resolves the clock entity for a monitor. + fn clock(&self, monitor: Monitor) -> &Entity { + match monitor { + Monitor::Source => &self.source_clock, + Monitor::Program => &self.program_clock, + } + } + + /// Applies an edit request from the effect stack to the model. + pub fn apply_effect_event(&mut self, event: &EffectStackEvent, cx: &mut Context) { + match event { + EffectStackEvent::EnableToggled { effect, enabled } => { + if let Some(effect) = self.effects.iter_mut().find(|e| e.id == *effect) { + effect.enabled = *enabled; + } + } + EffectStackEvent::ExpansionToggled { effect, expanded } => { + if let Some(effect) = self.effects.iter_mut().find(|e| e.id == *effect) { + effect.expanded = *expanded; + } + } + EffectStackEvent::RemoveRequested(id) => { + if let Some(index) = self + .effects + .iter() + .position(|e| e.id == *id && e.is_removable()) + { + self.effects.remove(index); + } + } + EffectStackEvent::ReorderRequested { effect, new_index } => { + let Some(from) = self.effects.iter().position(|e| e.id == *effect) else { + return; + }; + // `new_index` is an insertion index in the post-removal list. + let card = self.effects.remove(from); + let to = (*new_index).min(self.effects.len()); + self.effects.insert(to, card); + } + EffectStackEvent::AddRequested { index } => { + let id = EffectId(self.next_effect_id); + self.next_effect_id += 1; + let card = MockEffect { + id, + kind: EffectCardKind::Effect, + title: "新效果".into(), + subtitle: None, + enabled: true, + expanded: false, + badge: None, + }; + let index = (*index).min(self.effects.len()); + self.effects.insert(index, card); + } + // The app owns the context menu; the mock ignores it. + EffectStackEvent::ContextMenuRequested { .. } + | EffectStackEvent::ParameterChanged { .. } => {} + } + cx.notify(); + } + + /// Sets the row height of every timeline track (demo toolbar). + pub fn set_track_height(&mut self, height: Pixels, cx: &mut Context) { + for track in &mut self.tracks { + track.height = height; + } + cx.notify(); + } + + /// Adds a new empty track of the given kind (demo "序列" menu). + pub fn add_track(&mut self, kind: TrackKind, cx: &mut Context) { + let index = self.tracks.len(); + let (name, height) = match kind { + TrackKind::Video => (format!("V{} 视频轨道{}", index / 2 + 1, index), px(64.0)), + TrackKind::Audio => (format!("A{} 音频轨道{}", index / 2 + 1, index), px(48.0)), + TrackKind::Subtitle => (format!("S{} 字幕轨道", index + 1), px(32.0)), + }; + self.tracks.push(MockTrack { + kind, + name: name.into(), + height, + locked: false, + muted: false, + solo: false, + visible: true, + clips: Vec::new(), + }); + cx.notify(); + } + + /// The demo audio levels: animated while the program monitor plays. + fn meter_levels(&self) -> Vec { + if !self.program_playing { + return vec![0.03, 0.03]; + } + let t = (self.meter_phase % 120) as f32 / 120.0 * std::f32::consts::TAU; + let level = 0.25 + 0.55 * (t.sin() * 0.6 + (2.0 * t).sin() * 0.4).abs(); + vec![level, level * 0.8] + } +} + +// --------------------------------------------------------------------------- +// EngineGateway +// --------------------------------------------------------------------------- + +impl EngineGateway for MockEngine { + fn project(&self) -> Option<&Project> { + Some(&self.project) + } + + fn current_sequence(&self) -> Option<&Sequence> { + Some(&self.sequence) + } + + fn open_project(&mut self, path: PathBuf, cx: &mut Context) { + self.project.path = path; + if let Some(name) = self.project.path.file_stem() { + self.project.name = name.to_string_lossy().into_owned(); + } + cx.notify(); + } + + fn request_frame(&mut self, monitor: Monitor, frame: Frame, cx: &mut Context) { + let length = self.sequence_length(); + let clock = self.clock(monitor).clone(); + clock.update(cx, |clock, cx| { + clock.transport.seek(frame, length); + // Re-anchor so resuming continues from the new position. + if clock.transport.is_playing() { + clock.play(); + } + cx.notify(); + }); + cx.notify(); + } + + fn play(&mut self, monitor: Monitor, cx: &mut Context) { + if monitor == Monitor::Program { + self.program_playing = true; + } + let clock = self.clock(monitor).clone(); + clock.update(cx, |clock, cx| { + clock.play(); + cx.notify(); + }); + cx.notify(); + } + + fn pause(&mut self, monitor: Monitor, cx: &mut Context) { + if monitor == Monitor::Program { + self.program_playing = false; + } + let clock = self.clock(monitor).clone(); + clock.update(cx, |clock, cx| { + clock.pause(); + cx.notify(); + }); + cx.notify(); + } + + fn step(&mut self, monitor: Monitor, delta: i64, cx: &mut Context) { + let length = self.sequence_length(); + let clock = self.clock(monitor).clone(); + clock.update(cx, |clock, cx| { + clock.transport.step(delta, length); + if clock.transport.is_playing() { + clock.play(); + } + cx.notify(); + }); + cx.notify(); + } + + fn tick(&mut self, cx: &mut Context) { + let length = self.sequence_length(); + for clock in [&self.source_clock, &self.program_clock] { + let clock = clock.clone(); + clock.update(cx, |clock, cx| { + clock.tick(length); + cx.notify(); + }); + } + self.meter_phase = self.meter_phase.wrapping_add(1); + cx.notify(); + } +} + +// --------------------------------------------------------------------------- +// Data-source traits +// --------------------------------------------------------------------------- + +impl TimelineDataSource for MockEngine { + type Track = MockTrack; + + fn frame_rate(&self) -> FrameRate { + self.sequence.format.rate + } + + fn sequence_length(&self) -> Frame { + self.sequence.length + } + + fn track_count(&self) -> usize { + self.tracks.len() + } + + fn track(&self, index: usize) -> Option { + self.tracks.get(index).cloned() + } +} + +impl EffectStackDataSource for MockEngine { + fn effects(&self) -> Vec> { + self.effects + .iter() + .map(|effect| { + Arc::new(MockEffect { + id: effect.id, + kind: effect.kind, + title: effect.title.clone(), + subtitle: effect.subtitle.clone(), + enabled: effect.enabled, + expanded: effect.expanded, + badge: effect.badge, + }) as Arc + }) + .collect() + } + + fn target_label(&self) -> Option { + Some("第一稿.mp4 · 00:00:00:00–00:04:18:18".into()) + } +} + +impl ProjectDataSource for MockEngine { + fn roots(&self) -> Vec { + vec![ + ProjectEntry::new(1, "素材", true), + ProjectEntry::new(2, "音乐", true), + ProjectEntry::new(3, "第一稿.mp4", false), + ProjectEntry::new(4, "aaa.ove", false), + ] + } + + fn children(&self, parent_id: u64) -> Vec { + match parent_id { + 1 => vec![ + ProjectEntry::new(10, "intro.mov", false), + ProjectEntry::new(11, "b-roll.mp4", false), + ProjectEntry::new(12, "interview.mov", false), + ], + 2 => vec![ + ProjectEntry::new(20, "track-01.wav", false), + ProjectEntry::new(21, "track-02.wav", false), + ], + _ => Vec::new(), + } + } +} + +impl AudioMeterDataSource for MockEngine { + fn levels(&self) -> Vec { + self.meter_levels() + } +} + +/// Convenience accessors used by panels and the status bar. +impl MockEngine { + /// The selected material-bin entry id (demo state). + pub fn selected_item(&self) -> Option { + self.selected_item + } + + /// Selects a material-bin entry (demo "open" action). + pub fn select_item(&mut self, id: u64, cx: &mut Context) { + self.selected_item = Some(id); + cx.notify(); + } + + /// Reads the current frame of a monitor's clock. + pub fn clock_frame(&self, monitor: Monitor, cx: &App) -> Frame { + self.clock(monitor).read(cx).transport.frame() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + + fn demo_engine(app: &mut gpui::App) -> Entity { + app.new(|cx| MockEngine::demo(cx)) + } + + #[gpui::test] + async fn demo_project_has_a_sequence_and_four_tracks(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + let engine = engine.read(app); + let sequence = engine.current_sequence().expect("demo sequence"); + assert_eq!(sequence.name, "第一稿"); + assert_eq!(sequence.length, Frame(SEQUENCE_LENGTH)); + assert_eq!(sequence.format.width, 1920); + assert_eq!(sequence.format.height, 1080); + assert_eq!(engine.track_count(), 4); + assert_eq!(engine.track(0).expect("V2").kind(), TrackKind::Video); + assert_eq!(engine.track(2).expect("A1").kind(), TrackKind::Audio); + }); + } + + #[gpui::test] + async fn gateway_play_starts_the_program_clock(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + engine.update(app, |engine, cx| { + EngineGateway::play(engine, Monitor::Program, cx); + }); + let clock = engine.read(app).program_clock.read(app); + assert!(clock.transport.is_playing()); + }); + } + + #[gpui::test] + async fn gateway_step_moves_the_clock_within_the_sequence(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + engine.update(app, |engine, cx| { + engine.step(Monitor::Program, 25, cx); + }); + let frame = engine.read(app).clock_frame(Monitor::Program, app); + assert_eq!(frame, Frame(25)); + + // Stepping far past the end clamps to the last frame. + engine.update(app, |engine, cx| { + engine.step(Monitor::Program, 1_000_000, cx); + }); + let frame = engine.read(app).clock_frame(Monitor::Program, app); + assert_eq!(frame, Frame(SEQUENCE_LENGTH - 1)); + }); + } + + #[gpui::test] + async fn gateway_request_frame_seeks_and_pauses_stays(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + engine.update(app, |engine, cx| { + engine.request_frame(Monitor::Source, Frame(42), cx); + }); + let frame = engine.read(app).clock_frame(Monitor::Source, app); + assert_eq!(frame, Frame(42)); + }); + } + + #[gpui::test] + async fn effect_stack_edit_applies_to_the_model(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + // Remove the OCIO LUT card (id 2). + engine.update(app, |engine, cx| { + engine.apply_effect_event(&EffectStackEvent::RemoveRequested(EffectId(2)), cx); + }); + let stack = engine.read(app).effects(); + assert_eq!(stack.len(), 3); + let titles: Vec<_> = stack.iter().map(|e| e.title().to_string()).collect(); + assert!(!titles.contains(&"OCIO LUT".to_string())); + // Source and output cards are pinned and not removable. + assert_eq!(titles.first().map(String::as_str), Some("媒体")); + assert_eq!(titles.last().map(String::as_str), Some("输出")); + }); + } + + #[gpui::test] + async fn timeline_edits_are_requests_not_applied(cx: &mut TestAppContext) { + cx.update(|app| { + let engine = demo_engine(app); + // The demo timeline model ignores edit requests: the mock keeps + // its clips where they are until a real engine applies them. + let before = engine.read(app).track(1).expect("V1").clips().len(); + // (Nothing to assert beyond stability: the widget never mutates + // the data source directly — reads stay stable across reads.) + let after = engine.read(app).track(1).expect("V1").clips().len(); + assert_eq!(before, after); + }); + } +} diff --git a/app/rust/src/oakui/mod.rs b/app/rust/src/oakui/mod.rs new file mode 100644 index 000000000..7e51e20cf --- /dev/null +++ b/app/rust/src/oakui/mod.rs @@ -0,0 +1,45 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oakui` — the app layer's engine seam and view-state logic. +//! +//! This module is what the UI talks to when it needs something from "the +//! engine", plus the pure view-state logic the panels build on: +//! +//! * [`engine`] — the [`EngineGateway`](engine::EngineGateway) trait and the +//! project/sequence model. The seam itself: panels and the shell hold an +//! `Entity` whose type implements this trait and never care about the +//! backend. +//! * [`mock`] — [`MockEngine`](mock::MockEngine) and +//! [`MockClock`](mock::MockClock), the demo implementation feeding every +//! widget's data-source trait. +//! * [`transport`] — the play/pause/step/seek state machine (pure, unit +//! tested). +//! * [`timecode`] — timecode / duration / fps / resolution formatting (pure, +//! unit tested). +//! +//! The real engine binding (the `liboakengine` C ABI through +//! `src/facade/rust`) will implement [`EngineGateway`](engine::EngineGateway) +//! later; nothing else in the crate needs to change for the swap. + +pub mod engine; +pub mod mock; +pub mod timecode; +pub mod transport; + +pub use engine::{EngineGateway, Monitor, Project, Sequence, VideoFormat}; +pub use mock::{MockClock, MockEngine}; +pub use transport::{PlayState, TransportState}; diff --git a/app/rust/src/oakui/timecode.rs b/app/rust/src/oakui/timecode.rs new file mode 100644 index 000000000..f4c54b804 --- /dev/null +++ b/app/rust/src/oakui/timecode.rs @@ -0,0 +1,180 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! App-level time display: timecode, duration, frame-rate and resolution +//! labels for the viewers, the timeline and the status bar. +//! +//! Canonical time stays in frames ([`Frame`]); formatting to human-readable +//! text happens only here, at the display edge. This module is pure Rust +//! (only the gpui *types* [`Frame`]/[`FrameRate`] are used) so it is fully +//! unit-testable. +//! +//! # Timecode convention +//! +//! `HH:MM:SS:FF`, non-drop-frame, matching `gpui::timeline::format_timecode` +//! and Oak's existing viewer widgets. Drop-frame timecode for NTSC rates is +//! future work (see `gpui::timeline::time`). + +use gpui::timeline::{Frame, FrameRate}; + +/// Formats `frame` as non-drop-frame timecode `HH:MM:SS:FF`. +/// +/// The frame component uses two digits for rates below 100 fps (the nominal +/// integer fps of the rate — 25 for 25/1, 30 for 30000/1001), matching the +/// broadcast non-drop-frame convention. Negative frames get a leading `-`. +/// +/// # Examples +/// +/// ``` +/// use gpui::timeline::{Frame, FrameRate}; +/// use oakapp::oakui::timecode::format_timecode; +/// let rate = FrameRate::new(25, 1); +/// assert_eq!(format_timecode(Frame(0), rate), "00:00:00:00"); +/// assert_eq!(format_timecode(Frame(25), rate), "00:00:01:00"); +/// assert_eq!(format_timecode(Frame(6468), rate), "00:04:18:18"); +/// ``` +pub fn format_timecode(frame: Frame, rate: FrameRate) -> String { + let negative = frame.0 < 0; + let mut n = frame.0.unsigned_abs(); + let fps = nominal_fps(rate); + let frames = n % fps; + n /= fps; + let seconds = n % 60; + n /= 60; + let minutes = n % 60; + let hours = n / 60; + format!( + "{}{:02}:{:02}:{:02}:{:02}", + if negative { "-" } else { "" }, + hours, + minutes, + seconds, + frames + ) +} + +/// Formats a duration (a number of frames) as `HH:MM:SS:FF`. +/// +/// This is [`format_timecode`] under a duration-shaped name, so call sites +/// read as what they mean (the status bar shows "timecode / duration"). +pub fn format_duration(frames: Frame, rate: FrameRate) -> String { + format_timecode(frames, rate) +} + +/// Formats a frame rate as its conventional label: `25` for whole rates, +/// `29.97` for NTSC 30000/1001. +/// +/// # Examples +/// +/// ``` +/// use gpui::timeline::FrameRate; +/// use oakapp::oakui::timecode::format_fps; +/// assert_eq!(format_fps(FrameRate::new(25, 1)), "25"); +/// assert_eq!(format_fps(FrameRate::NTSC_2997), "29.97"); +/// ``` +pub fn format_fps(rate: FrameRate) -> String { + let fps = rate.num as f64 / rate.den as f64; + if (fps - fps.round()).abs() < 1e-9 { + format!("{:.0}", fps) + } else { + format!("{:.2}", fps) + } +} + +/// Formats a resolution as `WIDTH×HEIGHT` (U+00D7 multiplication sign), +/// matching the design's "1920×1080" chips. +pub fn format_resolution(width: u32, height: u32) -> String { + format!("{width}×{height}") +} + +/// The nominal integer frames-per-second used by non-drop-frame timecode: +/// the rounded frame rate (`25` for 25/1, `30` for 30000/1001). +fn nominal_fps(rate: FrameRate) -> u64 { + rate.as_f64().round().max(1.0) as u64 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_is_zeros() { + assert_eq!( + format_timecode(Frame(0), FrameRate::new(25, 1)), + "00:00:00:00" + ); + } + + #[test] + fn whole_seconds_at_25fps() { + let rate = FrameRate::new(25, 1); + assert_eq!(format_timecode(Frame(25), rate), "00:00:01:00"); + assert_eq!(format_timecode(Frame(60 * 25), rate), "00:01:00:00"); + assert_eq!(format_timecode(Frame(3600 * 25), rate), "01:00:00:00"); + } + + #[test] + fn design_reference_duration() { + // The design's sequence duration chip: 00:04:18:18 @ 25 fps. + assert_eq!( + format_timecode(Frame(6468), FrameRate::new(25, 1)), + "00:04:18:18" + ); + } + + #[test] + fn fractional_frame_component() { + // Frame 23 of the 24th second: 00:00:23:23 @ 25 fps. + let rate = FrameRate::new(25, 1); + assert_eq!(format_timecode(Frame(23 * 25 + 23), rate), "00:00:23:23"); + } + + #[test] + fn negative_frames_get_a_leading_minus() { + let rate = FrameRate::new(25, 1); + assert_eq!(format_timecode(Frame(-25), rate), "-00:00:01:00"); + } + + #[test] + fn ntsc_uses_nominal_30fps_frames() { + // At 30000/1001 the nominal rate is 30, so one second is frame 30. + let rate = FrameRate::NTSC_2997; + assert_eq!(format_timecode(Frame(30), rate), "00:00:01:00"); + } + + #[test] + fn duration_aliases_timecode() { + let rate = FrameRate::new(25, 1); + assert_eq!( + format_duration(Frame(6468), rate), + format_timecode(Frame(6468), rate) + ); + } + + #[test] + fn fps_labels() { + assert_eq!(format_fps(FrameRate::new(25, 1)), "25"); + assert_eq!(format_fps(FrameRate::new(30, 1)), "30"); + assert_eq!(format_fps(FrameRate::NTSC_2997), "29.97"); + assert_eq!(format_fps(FrameRate::NTSC_23976), "23.98"); + } + + #[test] + fn resolution_labels() { + assert_eq!(format_resolution(1920, 1080), "1920×1080"); + assert_eq!(format_resolution(1280, 720), "1280×720"); + } +} diff --git a/app/rust/src/oakui/transport.rs b/app/rust/src/oakui/transport.rs new file mode 100644 index 000000000..96fa3b43f --- /dev/null +++ b/app/rust/src/oakui/transport.rs @@ -0,0 +1,255 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The transport state machine: play/pause/step/seek semantics shared by the +//! transport drives of both monitors. +//! +//! Pure logic, no gpui dependency — the engine clocks and the widgets only +//! read the resulting state ([`TransportState::frame`], +//! [`TransportState::is_playing`]). Keeping this free of gpui types is what +//! makes the whole machine unit-testable with plain `#[test]`. + +use gpui::timeline::Frame; + +/// Whether the transport is rolling. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PlayState { + /// Stopped; the playhead is stationary. + Stopped, + /// Playing; the playhead advances with the wall clock. + Playing, +} + +/// The transport state of one monitor. +/// +/// Invariants: +/// * `frame` is always `>= Frame(0)`; it is clamped to `[0, length)` by every +/// mutating method that takes a sequence `length`. +/// * `in_point` / `out_point`, when both are set, satisfy +/// `in_point <= out_point`. +#[derive(Debug, Clone, PartialEq)] +pub struct TransportState { + state: PlayState, + frame: Frame, + in_point: Option, + out_point: Option, +} + +impl Default for TransportState { + fn default() -> Self { + Self::new() + } +} + +impl TransportState { + /// A stopped transport at frame zero with no loop range. + pub fn new() -> Self { + Self { + state: PlayState::Stopped, + frame: Frame::ZERO, + in_point: None, + out_point: None, + } + } + + /// The current playhead position. + pub fn frame(&self) -> Frame { + self.frame + } + + /// The current play state. + pub fn state(&self) -> PlayState { + self.state + } + + /// Whether playback is rolling. + pub fn is_playing(&self) -> bool { + self.state == PlayState::Playing + } + + /// The loop range, if both points are set. + pub fn loop_range(&self) -> Option<(Frame, Frame)> { + match (self.in_point, self.out_point) { + (Some(in_point), Some(out_point)) => Some((in_point, out_point)), + _ => None, + } + } + + /// Starts playback. Idempotent: playing a playing transport is a no-op. + pub fn play(&mut self) { + self.state = PlayState::Playing; + } + + /// Stops playback, leaving the playhead where it is. Idempotent. + pub fn pause(&mut self) { + self.state = PlayState::Stopped; + } + + /// Toggles between playing and stopped. + pub fn toggle(&mut self) { + self.state = match self.state { + PlayState::Stopped => PlayState::Playing, + PlayState::Playing => PlayState::Stopped, + }; + } + + /// Seeks to `frame`, clamped to `[0, length)`. Keeps the play state. + pub fn seek(&mut self, frame: Frame, length: Frame) { + self.frame = clamp_frame(frame, length); + } + + /// Steps the playhead by `delta` frames, clamped to `[0, length)`. + /// Keeps the play state (stepping while playing is jogging). + pub fn step(&mut self, delta: i64, length: Frame) { + self.seek(self.frame + Frame(delta), length); + } + + /// Sets the loop-in point at the current playhead. + pub fn set_in_point(&mut self) { + self.in_point = Some(self.frame); + } + + /// Sets the loop-out point at the current playhead. + pub fn set_out_point(&mut self) { + self.out_point = Some(self.frame); + // Keep the range well-formed: an out point before the in point is + // collapsed to the in point. + if let (Some(in_point), Some(out_point)) = (self.in_point, self.out_point) { + if out_point < in_point { + self.out_point = Some(in_point); + } + } + } + + /// Clears the loop range. + pub fn clear_range(&mut self) { + self.in_point = None; + self.out_point = None; + } +} + +/// Clamps `frame` into `[0, length)`. A zero-length sequence clamps to zero. +fn clamp_frame(frame: Frame, length: Frame) -> Frame { + let length = length.0.max(0); + Frame(if length == 0 { + 0 + } else { + frame.0.clamp(0, length - 1) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initial_state_is_stopped_at_zero() { + let transport = TransportState::new(); + assert_eq!(transport.state(), PlayState::Stopped); + assert!(!transport.is_playing()); + assert_eq!(transport.frame(), Frame(0)); + assert_eq!(transport.loop_range(), None); + } + + #[test] + fn play_then_pause_keeps_the_frame() { + let mut transport = TransportState::new(); + transport.play(); + assert!(transport.is_playing()); + transport.seek(Frame(42), Frame(100)); + transport.pause(); + assert_eq!(transport.state(), PlayState::Stopped); + assert_eq!(transport.frame(), Frame(42)); + } + + #[test] + fn play_is_idempotent() { + let mut transport = TransportState::new(); + transport.play(); + transport.play(); + assert!(transport.is_playing()); + } + + #[test] + fn toggle_switches_state() { + let mut transport = TransportState::new(); + transport.toggle(); + assert!(transport.is_playing()); + transport.toggle(); + assert_eq!(transport.state(), PlayState::Stopped); + } + + #[test] + fn step_clamps_at_the_sequence_edges() { + let mut transport = TransportState::new(); + transport.step(-5, Frame(100)); + assert_eq!(transport.frame(), Frame(0)); + + transport.seek(Frame(99), Frame(100)); + transport.step(5, Frame(100)); + assert_eq!(transport.frame(), Frame(99)); + } + + #[test] + fn step_while_playing_is_jogging() { + let mut transport = TransportState::new(); + transport.play(); + transport.step(1, Frame(100)); + assert!(transport.is_playing()); + assert_eq!(transport.frame(), Frame(1)); + } + + #[test] + fn seek_clamps_to_length() { + let mut transport = TransportState::new(); + transport.seek(Frame(150), Frame(100)); + assert_eq!(transport.frame(), Frame(99)); + transport.seek(Frame(-10), Frame(100)); + assert_eq!(transport.frame(), Frame(0)); + } + + #[test] + fn zero_length_sequence_clamps_to_zero() { + let mut transport = TransportState::new(); + transport.seek(Frame(5), Frame(0)); + assert_eq!(transport.frame(), Frame(0)); + } + + #[test] + fn loop_points_set_and_clear() { + let mut transport = TransportState::new(); + assert_eq!(transport.loop_range(), None); + + transport.seek(Frame(10), Frame(100)); + transport.set_in_point(); + transport.seek(Frame(20), Frame(100)); + transport.set_out_point(); + assert_eq!(transport.loop_range(), Some((Frame(10), Frame(20)))); + + transport.clear_range(); + assert_eq!(transport.loop_range(), None); + } + + #[test] + fn out_point_before_in_point_collapses_to_in_point() { + let mut transport = TransportState::new(); + transport.seek(Frame(30), Frame(100)); + transport.set_in_point(); + transport.seek(Frame(5), Frame(100)); + transport.set_out_point(); + assert_eq!(transport.loop_range(), Some((Frame(30), Frame(30)))); + } +} diff --git a/app/rust/src/panels/history.rs b/app/rust/src/panels/history.rs new file mode 100644 index 000000000..493100967 --- /dev/null +++ b/app/rust/src/panels/history.rs @@ -0,0 +1,92 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The history panel (历史记录): a placeholder list of undo entries, sharing +//! the inspector's dock group per the design. + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::{div, prelude::*, AnyElement, App, Context, EventEmitter, Render, SharedString, Window}; + +use crate::panels::ids::HISTORY; + +/// The undo-history placeholder panel. +pub struct HistoryPanel { + /// Demo entries (newest first), matching the design's date format + /// `YYYY-MM-DD HH:mm`. + entries: Vec<(&'static str, &'static str)>, +} + +impl HistoryPanel { + /// Creates the panel with demo history entries. + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self { + entries: vec![ + ("变换", "2026-06-03 20:25"), + ("移动片段", "2026-06-03 20:24"), + ("删除 B-roll.mp4", "2026-06-03 20:22"), + ("添加 OCIO LUT", "2026-06-03 20:20"), + ("设置入点", "2026-06-03 20:18"), + ], + } + } +} + +impl Render for HistoryPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let mut list = div() + .id("history-list") + .flex_1() + .flex() + .flex_col() + .py_1() + .overflow_y_scroll(); + for (label, timestamp) in &self.entries { + list = list.child( + div() + .flex() + .items_center() + .justify_between() + .gap_2() + .px_3() + .py_1() + .text_color(colors.text) + .child(div().child(*label)) + .child(div().text_color(colors.disabled).child(*timestamp)), + ); + } + div().size_full().flex().flex_col().child(list) + } +} + +impl EventEmitter for HistoryPanel {} + +impl DockPanel for HistoryPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + HISTORY + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.history").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.history")) + .into_any_element() + } +} diff --git a/app/rust/src/panels/inspector.rs b/app/rust/src/panels/inspector.rs new file mode 100644 index 000000000..48c7d50be --- /dev/null +++ b/app/rust/src/panels/inspector.rs @@ -0,0 +1,95 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The inspector panel (检查器·效果栈): the `EffectStackView` over the +//! engine's node chain, shown as linear cards (媒体 → 变换 → OCIO LUT → +//! 输出) with add / remove / reorder — no engine, the mock applies the edits +//! to its own model. + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::effect_stack::{EffectStackEvent, EffectStackView}; +use gpui::{ + div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, Window, +}; + +use crate::oakui::MockEngine; +use crate::panels::ids::INSPECTOR; + +/// The inspector / effect stack panel. +pub struct InspectorPanel { + stack: Entity>, + engine: Entity, +} + +impl InspectorPanel { + /// Builds the stack over `engine`'s effect model. + pub fn new(engine: Entity, _window: &mut Window, cx: &mut Context) -> Self { + let stack = cx.new(|cx| { + EffectStackView::new(engine.clone(), cx) + .params_renderer(|_effect, _window, cx| cx.new(|_cx| ParamPlaceholder).into()) + }); + // The "edits are requests" loop: forward each request to the engine, + // which applies it to its model and notifies. + cx.subscribe(&stack, |this, _stack, event: &EffectStackEvent, cx| { + this.engine + .update(cx, |engine, cx| engine.apply_effect_event(event, cx)); + }) + .detach(); + + Self { stack, engine } + } +} + +impl Render for InspectorPanel { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.stack.clone()) + } +} + +impl EventEmitter for InspectorPanel {} + +impl DockPanel for InspectorPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + INSPECTOR + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.inspector").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.inspector")) + .into_any_element() + } +} + +/// Placeholder parameter view rendered inside expanded effect cards. +/// A real app builds the effect's controls here and calls +/// [`EffectStackView::notify_parameter_changed`] after edits. +struct ParamPlaceholder; + +impl Render for ParamPlaceholder { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .px_3() + .py_2() + .text_color(colors.disabled) + .child(crate::i18n::tr("inspector.params")) + } +} diff --git a/app/rust/src/panels/mod.rs b/app/rust/src/panels/mod.rs new file mode 100644 index 000000000..6c50b3fd5 --- /dev/null +++ b/app/rust/src/panels/mod.rs @@ -0,0 +1,70 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The dockable panels of the main window. +//! +//! Each panel is a gpui view implementing [`DockPanel`](gpui::dock::DockPanel) +//! so it can live inside the [`DockArea`](gpui::dock::DockArea) shell. Panels +//! own their widgets (created in their constructors), hold the +//! [`MockEngine`](crate::oakui::MockEngine) entity so requests can be routed +//! to "the engine", and never mutate engine state directly — every edit is a +//! widget request event that the panel forwards through the gateway. + +pub mod history; +pub mod inspector; +pub mod node_editor; +pub mod program_viewer; +pub mod project_explorer; +pub mod source_viewer; +pub mod status_bar; +pub mod timeline; + +pub use gpui::dock::PanelId; + +/// Stable panel ids, unique within the dock area. +pub mod ids { + use super::PanelId; + /// The material bin (项目). + pub const PROJECT: PanelId = PanelId::new(1); + /// The source viewer (素材查看器). + pub const SOURCE_VIEWER: PanelId = PanelId::new(2); + /// The program viewer (序列查看器). + pub const PROGRAM_VIEWER: PanelId = PanelId::new(3); + /// The node editor placeholder (节点编辑器). + pub const NODE_EDITOR: PanelId = PanelId::new(4); + /// The inspector / effect stack (检查器·效果栈). + pub const INSPECTOR: PanelId = PanelId::new(5); + /// The undo history (历史记录). + pub const HISTORY: PanelId = PanelId::new(6); + /// The timeline (时间线). + pub const TIMELINE: PanelId = PanelId::new(7); +} + +/// A small info chip used in viewer headers and the status bar: muted +/// background, thin border, small text. `colors` comes from the caller's +/// `cx.default_colors()` so the chip follows the active theme. +pub(crate) fn chip(colors: &gpui::colors::Colors, label: impl gpui::IntoElement) -> gpui::Div { + use gpui::prelude::*; + gpui::div() + .px_2() + .py_1() + .rounded_sm() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .text_color(colors.text) + .child(label) +} diff --git a/app/rust/src/panels/node_editor.rs b/app/rust/src/panels/node_editor.rs new file mode 100644 index 000000000..06178c76e --- /dev/null +++ b/app/rust/src/panels/node_editor.rs @@ -0,0 +1,134 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The node editor panel (节点编辑器): a placeholder tab sharing the program +//! viewer's dock group. +//! +//! The design puts the node editor in the center, switchable with the program +//! viewer. The real `gpui::node_graph` widget exists in the gpui submodule +//! but is not wired up yet — this panel is a placeholder surface with the +//! zoom controls the design specifies (+ / − / fit). + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::{ + div, prelude::*, AnyElement, App, ClickEvent, Context, EventEmitter, Render, SharedString, + Window, +}; + +use crate::panels::ids::NODE_EDITOR; + +/// The node editor placeholder panel. +pub struct NodeEditorPanel; + +impl NodeEditorPanel { + /// Creates the placeholder. + pub fn new(_window: &mut Window, _cx: &mut Context) -> Self { + Self + } +} + +impl Render for NodeEditorPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .size_full() + .flex() + .flex_col() + .child( + div() + .flex() + .items_center() + .gap_1() + .px_2() + .py_1() + .border_b_1() + .border_color(colors.border) + .child(zoom_button( + cx, + "node-zoom-in", + "+", + crate::i18n::tr("node.zoom_in"), + )) + .child(zoom_button( + cx, + "node-zoom-out", + "−", + crate::i18n::tr("node.zoom_out"), + )) + .child(zoom_button( + cx, + "node-zoom-fit", + crate::i18n::tr("node.fit"), + crate::i18n::tr("node.fit_window"), + )), + ) + .child( + div() + .flex_1() + .flex() + .items_center() + .justify_center() + .text_color(colors.disabled) + .child(crate::i18n::tr("node.placeholder")), + ) + } +} + +/// A small toolbar button (the design's `+`/`−`/`适配` controls). +fn zoom_button( + cx: &mut Context, + id: &'static str, + label: &'static str, + title: &'static str, +) -> impl gpui::IntoElement { + let colors = cx.default_colors().clone(); + let container = colors.container; + div() + .id(id) + .px_2() + .py_1() + .rounded_md() + .border_1() + .border_color(colors.border) + .text_color(colors.text) + .cursor_pointer() + .hover(move |style| style.bg(container)) + .on_click( + cx.listener(move |_this, _event: &ClickEvent, _window, _cx| { + println!("[node editor] {title} (placeholder)"); + }), + ) + .child(label) +} + +impl EventEmitter for NodeEditorPanel {} + +impl DockPanel for NodeEditorPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + NODE_EDITOR + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.node_editor").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.node_editor")) + .into_any_element() + } +} diff --git a/app/rust/src/panels/program_viewer.rs b/app/rust/src/panels/program_viewer.rs new file mode 100644 index 000000000..d0acfe425 --- /dev/null +++ b/app/rust/src/panels/program_viewer.rs @@ -0,0 +1,138 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The program viewer panel (序列查看器): the `ViewerWidget` over the +//! program monitor's clock, with a 26px audio level strip attached to its +//! right edge (the design's WP6 layout). + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::{ + div, prelude::*, px, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, + Window, +}; +use gpui_widgets::audio_meter::AudioLevelMeter; +use gpui_widgets::viewer::{ViewerEvent, ViewerWidget}; + +use crate::oakui::timecode::{format_fps, format_resolution}; +use crate::oakui::{EngineGateway, MockClock, MockEngine, Monitor}; +use crate::panels::chip; +use crate::panels::ids::PROGRAM_VIEWER; + +/// Width of the audio level strip, per the design (26px). +const METER_WIDTH: f32 = 26.0; + +/// The program viewer panel. +pub struct ProgramViewerPanel { + viewer: Entity>, + meter: Entity>, + engine: Entity, +} + +impl ProgramViewerPanel { + /// Builds a viewer over `clock` (the program monitor's clock) with the + /// level meter `meter` (updated on the app's tick timer). + pub fn new( + engine: Entity, + clock: Entity, + meter: Entity>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let viewer = cx.new(|cx| ViewerWidget::new(3, clock, window, cx)); + // Route every transport request to the engine's program monitor. + cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| { + let monitor = Monitor::Program; + this.engine.update(cx, |engine, cx| match event { + ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx), + ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx), + ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx), + other => println!("[program viewer] request: {other:?}"), + }); + }) + .detach(); + + Self { + viewer, + meter, + engine, + } + } +} + +impl Render for ProgramViewerPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let format = self + .engine + .read(cx) + .current_sequence() + .map(|sequence| sequence.format) + .unwrap_or(crate::oakui::VideoFormat::hd_1080p25()); + + div() + .size_full() + .flex() + .flex_col() + .child( + div() + .flex() + .items_center() + .gap_2() + .px_2() + .py_1() + .border_b_1() + .border_color(colors.border) + .child(chip(&colors, crate::i18n::tr("viewer.program"))) + .child(chip( + &colors, + format_resolution(format.width, format.height), + )) + .child(chip(&colors, format_fps(format.rate))), + ) + .child( + div() + .flex_1() + .flex() + .child(div().flex_1().child(self.viewer.clone())) + .child( + div() + .w(px(METER_WIDTH)) + .border_l_1() + .border_color(colors.border) + .child(self.meter.clone()), + ), + ) + } +} + +impl EventEmitter for ProgramViewerPanel {} + +impl DockPanel for ProgramViewerPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + PROGRAM_VIEWER + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.program_viewer").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.program_viewer")) + .into_any_element() + } +} diff --git a/app/rust/src/panels/project_explorer.rs b/app/rust/src/panels/project_explorer.rs new file mode 100644 index 000000000..e9ecefadc --- /dev/null +++ b/app/rust/src/panels/project_explorer.rs @@ -0,0 +1,78 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The material bin panel (项目): the `ProjectExplorer` widget over the +//! engine's project data. + +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::{ + div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, Window, +}; +use gpui_widgets::project_explorer::{ProjectExplorer, ProjectExplorerEvent}; + +use crate::oakui::MockEngine; +use crate::panels::ids::PROJECT; + +/// The material bin panel. +pub struct ProjectExplorerPanel { + explorer: Entity>, + engine: Entity, +} + +impl ProjectExplorerPanel { + /// Builds the explorer over `engine`'s project data. + pub fn new(engine: Entity, window: &mut Window, cx: &mut Context) -> Self { + let explorer = cx.new(|cx| ProjectExplorer::new(1, engine.clone(), window, cx)); + cx.subscribe( + &explorer, + |this, _explorer, event: &ProjectExplorerEvent, cx| match event { + ProjectExplorerEvent::OpenRequested { id, .. } => { + // Demo "open": select the item in the engine's model. + this.engine + .update(cx, |engine, cx| engine.select_item(*id, cx)); + } + other => println!("[project explorer] request: {other:?}"), + }, + ) + .detach(); + + Self { explorer, engine } + } +} + +impl Render for ProjectExplorerPanel { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.explorer.clone()) + } +} + +impl EventEmitter for ProjectExplorerPanel {} + +impl DockPanel for ProjectExplorerPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + PROJECT + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.project").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.project")) + .into_any_element() + } +} diff --git a/app/rust/src/panels/source_viewer.rs b/app/rust/src/panels/source_viewer.rs new file mode 100644 index 000000000..380710a44 --- /dev/null +++ b/app/rust/src/panels/source_viewer.rs @@ -0,0 +1,113 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The source viewer panel (素材查看器): the `ViewerWidget` over the source +//! monitor's clock, with its own transport and format chips. + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::{ + div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, Window, +}; +use gpui_widgets::viewer::{ViewerEvent, ViewerWidget}; + +use crate::oakui::timecode::{format_fps, format_resolution}; +use crate::oakui::{EngineGateway, MockClock, MockEngine, Monitor}; +use crate::panels::chip; +use crate::panels::ids::SOURCE_VIEWER; + +/// The source viewer panel. +pub struct SourceViewerPanel { + viewer: Entity>, + engine: Entity, +} + +impl SourceViewerPanel { + /// Builds a viewer over `clock` (the source monitor's clock). + pub fn new( + engine: Entity, + clock: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let viewer = cx.new(|cx| ViewerWidget::new(2, clock, window, cx)); + // Route every transport request to the engine's source monitor. + cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| { + let monitor = Monitor::Source; + this.engine.update(cx, |engine, cx| match event { + ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx), + ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx), + ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx), + other => println!("[source viewer] request: {other:?}"), + }); + }) + .detach(); + + Self { viewer, engine } + } +} + +impl Render for SourceViewerPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let format = self + .engine + .read(cx) + .current_sequence() + .map(|sequence| sequence.format) + .unwrap_or(crate::oakui::VideoFormat::hd_1080p25()); + + div() + .size_full() + .flex() + .flex_col() + .child( + div() + .flex() + .items_center() + .gap_2() + .px_2() + .py_1() + .border_b_1() + .border_color(colors.border) + .child(chip(&colors, crate::i18n::tr("viewer.source"))) + .child(chip( + &colors, + format_resolution(format.width, format.height), + )) + .child(chip(&colors, format_fps(format.rate))), + ) + .child(div().flex_1().child(self.viewer.clone())) + } +} + +impl EventEmitter for SourceViewerPanel {} + +impl DockPanel for SourceViewerPanel { + fn panel_id(&self) -> gpui::dock::PanelId { + SOURCE_VIEWER + } + + fn title(&self, _cx: &App) -> SharedString { + crate::i18n::tr("panel.source_viewer").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div() + .child(crate::i18n::tr("panel.source_viewer")) + .into_any_element() + } +} diff --git a/app/rust/src/panels/status_bar.rs b/app/rust/src/panels/status_bar.rs new file mode 100644 index 000000000..a49e29e4e --- /dev/null +++ b/app/rust/src/panels/status_bar.rs @@ -0,0 +1,104 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The global status bar (状态栏): ready state, cache, proxy and autosave +//! info on the left; current timecode / duration, frame rate and resolution +//! on the right. + +use gpui::colors::DefaultColors; +use gpui::timeline::Frame; +use gpui::{div, prelude::*, Context, Entity, Render, Window}; + +use crate::oakui::timecode::{format_duration, format_fps, format_resolution, format_timecode}; +use crate::oakui::{EngineGateway, MockClock, MockEngine}; + +/// The global status bar. +pub struct StatusBar { + engine: Entity, + program_clock: Entity, +} + +impl StatusBar { + /// Builds the status bar over the engine and the program clock. + pub fn new( + engine: Entity, + program_clock: Entity, + _cx: &mut Context, + ) -> Self { + Self { + engine, + program_clock, + } + } +} + +impl Render for StatusBar { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + let engine = self.engine.read(cx); + let frame = self.program_clock.read(cx).transport.frame(); + let sequence = engine.current_sequence(); + let format = sequence + .map(|s| s.format) + .unwrap_or(crate::oakui::VideoFormat::hd_1080p25()); + let length = sequence.map(|s| s.length).unwrap_or(Frame(0)); + let project = engine + .project() + .map(|p| p.name.clone()) + .unwrap_or_else(|| crate::i18n::tr("status.untitled").to_string()); + + let segment = |colors: &gpui::colors::Colors, text: String| { + div().px_2().py_1().text_color(colors.text).child(text) + }; + + div() + .h_6() + .flex() + .items_center() + .border_t_1() + .border_color(colors.border) + .bg(colors.container) + .text_xs() + .child(segment(&colors, crate::i18n::tr("status.ready").into())) + .child(segment( + &colors, + crate::i18n::tr("status.cache").into(), + )) + .child(segment( + &colors, + crate::i18n::tr("status.proxy").into(), + )) + .child(segment( + &colors, + crate::i18n::tr("status.autosave").into(), + )) + .child(div().flex_1()) + .child(segment( + &colors, + format!( + "{timecode}/{duration}", + timecode = format_timecode(frame, format.rate), + duration = format_duration(length, format.rate), + ), + )) + .child(segment(&colors, format_fps(format.rate))) + .child(segment( + &colors, + format_resolution(format.width, format.height), + )) + .child(div().px_2().text_color(colors.disabled).child(project)) + } +} diff --git a/app/rust/src/panels/timeline.rs b/app/rust/src/panels/timeline.rs new file mode 100644 index 000000000..bd5b6775f --- /dev/null +++ b/app/rust/src/panels/timeline.rs @@ -0,0 +1,442 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The timeline panel (时间线): the design's 31px toolbar (tools, snap +//! toggle) above the full-width [`TimelineView`](gpui::timeline::TimelineView) +//! over the engine's sequence model. +//! +//! # Layout (fixed 2026-08) +//! +//! ```text +//! ┌─────────────────────────────────────────┬─────────────┐ +//! │ toolbar row (fixed 31px): tools + − ⏵ │ │ +//! ├─────────────────────────────────────────┤ right-side │ +//! │ timeline (ruler takes remaining width, │ controls │ +//! │ clip area below) │ (fixed 140px│ +//! │ │ zoom / │ +//! │ │ track hgt) │ +//! └─────────────────────────────────────────┴─────────────┘ +//! ``` +//! +//! The zoom and track-height sliders used to sit at the right end of the +//! toolbar, where they overflowed into the ruler's timecode labels (the +//! toolbar is exactly 31px but the sliders' value rows are taller, and at +//! narrow widths the sliders squeezed into the ruler's right side). They +//! now live in a fixed-width trailing slot beside the timeline body, and the +//! timeline wrapper is `min_w_0` so the ruler always keeps the remaining +//! space — no overlap at 1600×900 or down to ~1100px wide. On hidpi (2x) +//! displays the render compensates for a gpui view-positioning quirk with a +//! top padding on the timeline canvas (see the note in [`Render::render`]). + +use gpui::colors::DefaultColors; +use gpui::dock::{DockPanel, PanelEvent}; +use gpui::timeline::TimelineView; +use gpui::{div, prelude::*, px, Context, Entity, Window}; +use gpui::{AnyElement, App, ClickEvent, EventEmitter, Render, SharedString}; +use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState}; +use gpui_widgets::slider::{Slider, SliderEvent, SliderModel}; +use gpui_widgets::value::ValueKind; + +use crate::i18n; +use crate::oakui::MockEngine; +use crate::panels::ids::TIMELINE; + +/// Toolbar height, per the design (31px). +const TOOLBAR_HEIGHT: f32 = 31.0; +/// Fixed width of the trailing controls slot (zoom / track-height sliders). +/// Kept constant so the sliders can never intrude into the ruler's labels. +const RIGHT_CONTROLS_WIDTH: f32 = 140.0; +/// The demo tool set, by i18n key. Only the visual selection is implemented; +/// each tool's behavior arrives with the real tool system later. +const TOOL_KEYS: [&str; 8] = [ + "timeline.tool.select", + "timeline.tool.razor", + "timeline.tool.ripple", + "timeline.tool.slip", + "timeline.tool.roll", + "timeline.tool.zoom", + "timeline.tool.knife", + "timeline.tool.marker", +]; + +/// The timeline panel. +pub struct TimelinePanel { + timeline: Entity>, + engine: Entity, + zoom: Entity, + height: Entity, + snap: Entity, + /// The currently selected tool (visual only). + selected_tool: usize, +} + +impl TimelinePanel { + /// Builds the panel around `timeline` (created by the app shell so it can + /// sync the playhead). + pub fn new( + engine: Entity, + timeline: Entity>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let zoom = cx.new(|cx| { + Slider::new( + 10, + SliderModel::new(ValueKind::Float, 0.5, 8.0, 0.1, 2.0), + window, + cx, + ) + }); + let height = cx.new(|cx| { + Slider::new( + 11, + SliderModel::new(ValueKind::Float, 24.0, 160.0, 8.0, 64.0), + window, + cx, + ) + }); + let snap = + cx.new(|cx| CheckBox::new(12, CheckState::Checked, window, cx)); + + // Zoom slider → timeline zoom (pixels per frame). + cx.subscribe(&zoom, |this, _zoom, event: &SliderEvent, cx| { + if let SliderEvent::ValueChanged { value, .. } = event { + let zoom = value.to_f64() as f32; + this.timeline.update(cx, |timeline, cx| { + timeline.state.set_zoom(zoom, px(0.0)); + cx.notify(); + }); + } + }) + .detach(); + + // Track-height slider → engine model (persisted per sequence). + cx.subscribe(&height, |this, _height, event: &SliderEvent, cx| { + if let SliderEvent::ValueChanged { value, .. } = event { + let height = value.to_f64() as f32; + this.engine + .update(cx, |engine, cx| engine.set_track_height(px(height), cx)); + } + }) + .detach(); + + // Snap toggle → timeline view state. + cx.subscribe(&snap, |this, _snap, event: &CheckBoxEvent, cx| { + let CheckBoxEvent::Toggled { state, .. } = event; + let enabled = *state == CheckState::Checked; + this.timeline.update(cx, |timeline, cx| { + timeline.state.snap_enabled = enabled; + cx.notify(); + }); + }) + .detach(); + + Self { + timeline, + engine, + zoom, + height, + snap, + selected_tool: 0, + } + } +} + +impl Render for TimelinePanel { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + + // The gpui view-positioning code prepaints a view's root at the panel + // content origin rather than at its flex wrapper's position, which is + // only noticeable on hidpi (2x) displays where the fixed 31px toolbar + // row and the timeline view's own 32px ruler row would otherwise + // overlap. The canvas wrapper carries a compensating top padding on + // hidpi so the ruler lands just below the toolbar; at 1x the layout + // is already correct and no padding is applied. + let view_offset = if window.scale_factor() > 1.5 { + TOOLBAR_HEIGHT + 2.0 + } else { + 0.0 + }; + + // --- toolbar row (fixed 31px, above the ruler) -------------------- + let mut toolbar = div() + .debug_selector(|| "timeline-toolbar".into()) + .h(px(TOOLBAR_HEIGHT)) + .flex_shrink_0() + .flex() + .items_center() + .gap_1() + .px_2() + .overflow_hidden() + .border_b_1() + .border_color(colors.border) + .bg(colors.container); + + for (index, tool_key) in TOOL_KEYS.iter().enumerate() { + let tool = i18n::tr(tool_key); + let selected = self.selected_tool == index; + let background = if selected { + colors.selected + } else { + colors.background + }; + let foreground = if selected { + colors.selected_text + } else { + colors.text + }; + let hover_bg = colors.selected; + let hover_fg = colors.selected_text; + toolbar = toolbar.child( + div() + .id(SharedString::from(format!("tool-{index}"))) + .px_2() + .py_1() + .rounded_sm() + .cursor_pointer() + .bg(background) + .text_color(foreground) + .hover(move |style| style.bg(hover_bg).text_color(hover_fg)) + .on_click(cx.listener(move |this, _event: &ClickEvent, _window, _cx| { + println!("[timeline] tool: {tool} (placeholder)"); + this.selected_tool = index; + })) + .child(tool), + ); + } + + let text = colors.text; + let container = colors.container; + let tool_btn = move |id: &'static str, label: &'static str| { + div() + .id(id) + .px_2() + .py_1() + .rounded_sm() + .cursor_pointer() + .text_color(text) + .hover(move |style| style.bg(container)) + .child(label) + }; + + // The snap toggle: a localized label next to the checkbox box. The + // label is a plain div so it follows the active language; the box + // itself is clickable as in the widget's default row. + let snap_row = div() + .flex() + .items_center() + .gap_1() + .text_color(colors.text) + .child(div().child(i18n::tr("timeline.snap"))) + .child(self.snap.clone()); + + let toolbar = toolbar + .child(tool_btn("toolbar-zoom-in", "+")) + .child(tool_btn("toolbar-zoom-out", "−")) + .child( + div() + .w_1() + .h_full() + .border_l_1() + .border_color(colors.border), + ) + .child(snap_row); + + // --- trailing controls slot (fixed width, right of the body) ------- + let right_controls = div() + .debug_selector(|| "timeline-right-controls".into()) + .w(px(RIGHT_CONTROLS_WIDTH)) + .flex_shrink_0() + .flex() + .flex_col() + .justify_center() + .gap_1() + .px_2() + .border_l_1() + .border_color(colors.border) + .bg(colors.container) + .child( + div() + .flex() + .flex_col() + .gap_1() + .text_xs() + .text_color(colors.disabled) + .child(i18n::tr("timeline.zoom")) + .child(self.zoom.clone()), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .text_xs() + .text_color(colors.disabled) + .child(i18n::tr("timeline.track_height")) + .child(self.height.clone()), + ); + + div() + .size_full() + .flex() + .flex_col() + .overflow_hidden() + .child(toolbar) + .child( + div() + .debug_selector(|| "timeline-body".into()) + .flex_1() + .min_h_0() + .flex() + .flex_row() + .child( + div() + .debug_selector(|| "timeline-canvas".into()) + .flex_1() + .min_w_0() + .pt(px(view_offset)) + .child(self.timeline.clone()), + ) + .child(right_controls), + ) + } +} + +impl EventEmitter for TimelinePanel {} + +impl DockPanel for TimelinePanel { + fn panel_id(&self) -> gpui::dock::PanelId { + TIMELINE + } + + fn title(&self, _cx: &App) -> SharedString { + i18n::tr("panel.timeline").into() + } + + fn tab_content(&self, _cx: &App) -> AnyElement { + div().child(i18n::tr("panel.timeline")).into_any_element() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, VisualTestContext, px, size}; + + /// Builds a `TimelinePanel` in a window of the given logical size and + /// returns a `VisualTestContext` for bounds assertions. + fn panel_window( + cx: &mut TestAppContext, + width: f32, + height: f32, + ) -> (&'static mut VisualTestContext, Entity) { + cx.update(|cx| cx.init_colors()); + let window = cx.open_window(size(px(width), px(height)), |window, cx| { + let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx)); + let timeline = + cx.new(|cx| TimelineView::new(engine.clone(), window, cx).zoom(2.0)); + TimelinePanel::new(engine, timeline, window, cx) + }); + cx.run_until_parked(); + let panel = window.root(cx).expect("timeline panel root"); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + (cx, panel) + } + + /// The toolbar row must sit entirely above the timeline body, the right + /// controls must sit to the right of the timeline canvas (never + /// overlapping it), and the controls slot must keep its fixed width — at + /// the default 1600×900 and down to ~1100px wide. + #[gpui::test] + async fn toolbar_ruler_and_right_controls_never_overlap(cx: &mut TestAppContext) { + for width in [1600.0, 1280.0, 1100.0] { + let (cx, _panel) = panel_window(cx, width, 900.0); + + let toolbar = cx + .debug_bounds("timeline-toolbar") + .expect("toolbar row rendered"); + let body = cx + .debug_bounds("timeline-body") + .expect("timeline body row rendered"); + let canvas = cx + .debug_bounds("timeline-canvas") + .expect("timeline canvas rendered"); + let right = cx + .debug_bounds("timeline-right-controls") + .expect("right controls slot rendered"); + + // The toolbar is exactly 31px tall and ends where the body starts. + assert!( + (f32::from(toolbar.size.height) - TOOLBAR_HEIGHT).abs() < 0.5, + "toolbar height {width}: {} != {TOOLBAR_HEIGHT}", + toolbar.size.height + ); + assert!( + toolbar.bottom() <= body.top(), + "toolbar overlaps the body at width {width}" + ); + + // The controls slot is fixed-width and never overlaps the canvas. + assert!( + (f32::from(right.size.width) - RIGHT_CONTROLS_WIDTH).abs() < 0.5, + "right slot width {width}: {} != {RIGHT_CONTROLS_WIDTH}", + right.size.width + ); + assert!( + canvas.right() <= right.left(), + "right controls overlap the timeline canvas at width {width}" + ); + + // The timeline (ruler) keeps the remaining width: canvas right + // edge equals the slot's left edge exactly. + assert!( + (f32::from(canvas.right()) - f32::from(right.left())).abs() < 0.5, + "canvas and controls slot are not flush at width {width}" + ); + + // The right controls are inside the body's vertical bounds. + assert!( + right.top() >= body.top() && right.bottom() <= body.bottom(), + "right controls escape the body at width {width}" + ); + } + } + + /// Resizing a window keeps the same invariants (the timeline body shrinks + /// while the toolbar and the right slot stay fixed). + #[gpui::test] + async fn resizing_keeps_toolbar_and_right_slot_fixed(cx: &mut TestAppContext) { + let (cx, _panel) = panel_window(cx, 1600.0, 900.0); + + let before = cx + .debug_bounds("timeline-right-controls") + .expect("right controls rendered"); + assert!((f32::from(before.size.width) - RIGHT_CONTROLS_WIDTH).abs() < 0.5); + + cx.simulate_resize(size(px(1100.0), px(900.0))); + cx.run_until_parked(); + + let after = cx + .debug_bounds("timeline-right-controls") + .expect("right controls rendered after resize"); + let canvas = cx + .debug_bounds("timeline-canvas") + .expect("timeline canvas after resize"); + assert!((f32::from(after.size.width) - RIGHT_CONTROLS_WIDTH).abs() < 0.5); + assert!(canvas.right() <= after.left()); + } +} diff --git a/cli/rust/Cargo.lock b/cli/rust/Cargo.lock new file mode 100644 index 000000000..1a42e595d --- /dev/null +++ b/cli/rust/Cargo.lock @@ -0,0 +1,191 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "oak-cli" +version = "0.1.0" +dependencies = [ + "clap", + "oakfacade", +] + +[[package]] +name = "oakfacade" +version = "0.1.0" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[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 = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/cli/rust/Cargo.toml b/cli/rust/Cargo.toml new file mode 100644 index 000000000..826e6bbec --- /dev/null +++ b/cli/rust/Cargo.toml @@ -0,0 +1,43 @@ +# Oak - Non-Linear Video Editor +# Copyright (C) 2026 Oak Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +[package] +name = "oak-cli" +version = "0.1.0" +edition = "2021" +description = "Oak Video Editor headless command-line consumer of the liboakengine C ABI facade (Rust)" +license = "GPL-3.0-or-later" + +[[bin]] +name = "oak-cli" +path = "src/main.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } + +# oakfacade is where the frozen oakengine_* C ABI exports live (staticlib + +# rlib). oak-cli is a pure consumer of that facade, so it links the rlib +# directly. The families this CLI needs (init / project / timeline / render / +# footage / exporter) are still deferred in the facade +# (src/facade/rust/src/deferred.rs), so this crate references no oakengine_* +# symbol yet: the subcommands validate their arguments and report the +# deferral (src/deferred.rs). The extern declarations in src/ffi.rs mirror +# the engine headers verbatim and resolve against this rlib the moment a +# family is wrapped -- no manifest change needed. +oakfacade = { path = "../../src/facade/rust" } + +[profile.release] +panic = "unwind" diff --git a/cli/rust/README.md b/cli/rust/README.md new file mode 100644 index 000000000..c55ac6512 --- /dev/null +++ b/cli/rust/README.md @@ -0,0 +1,80 @@ +# oak-cli (Rust) + +Headless command-line consumer of the `liboakengine` C ABI facade — the Rust +rewrite of `cli/main.cpp` (which stays in the tree until cutover). Same +subcommands, same output format, same exit codes: + +| exit | meaning | +|---|---| +| 0 | success | +| 1 | general error (bad project/media file, no sequence, I/O failure) | +| 2 | rendering unavailable or failed (e.g. no GL render backend) | +| 64 | usage error | + +## Build and test + +```sh +cargo build --release # binary: target/release/oak-cli +cargo test # unit + integration tests (29 tests) +``` + +The crate builds standalone: its only dependency besides `clap` is the +`oakfacade` rlib (`../../src/facade/rust`), which has no third-party +dependencies. + +## Subcommands + +Every subcommand of the C++ original is implemented: + +``` +oak-cli info project name/sequences/footage +oak-cli render +oak-cli probe +oak-cli transcode [width] [--format ppm|mp4] +``` + +Argument validation is faithful to the C++ (`invalid start seconds`, +`invalid width`, `unknown --format` … all exit 64). The output formatters +(`src/fmt.rs`) reproduce the C++ `printf` output byte for byte and are +golden-tested against the output captured from the C++ binary on the test +fixtures (`tests/project_with_footage.ove`, `tests/demo.mp4`); the PPM and +WAV writers (`src/ppm.rs`, `src/wav.rs`) are the exact ports of the C++ +`write_ppm`/`write_wav` and are unit-tested. + +## Facade status: everything is currently deferred + +All four subcommands depend on facade families that are still **deferred** +in the `oakfacade` crate (`src/facade/rust/src/deferred.rs`), so today each +subcommand validates its arguments, then prints a clear "not yet available" +error naming the missing families and the reasons, and exits with the +C++-compatible code — it never crashes and never fakes output: + +| subcommand | needs | current behavior | +|---|---|---| +| `info` | init + node (project/footage) + timeline | "not yet available", exit 1 | +| `probe` | init + node (footage) | "not yet available", exit 1 | +| `render` | init + node + timeline + render | "not yet available", exit 2 | +| `transcode` | init + node + timeline + render + exporter | "not yet available", exit 2 | + +The deferral registry is `src/deferred.rs` (field-for-field in sync with the +facade's own `deferred.rs`). When a family is wrapped by the facade: + +1. remove its entry from `src/deferred.rs`, +2. wire the call-through in `src/cmd/` using the extern declarations in + `src/ffi.rs` (verbatim mirrors of the engine headers) and the tested + formatters/writers — no manifest or signature change is needed, because + the externs resolve against the already-linked `oakfacade` rlib. + +## Layout + +``` +src/ + main.rs clap surface, --help/-h + unknown-command handling, dispatch + ffi.rs the oakengine_* surface oak-cli consumes (declarations only) + deferred.rs facade-family availability registry (mirror of facade deferred.rs) + fmt.rs golden output formatters (info/probe) + ppm.rs P6 PPM writer (f32/u8 frames) + wav.rs PCM s16 WAV writer (interleaved float samples) + cmd/ per-subcommand validation + deferred gate +tests/cli.rs binary-level tests (exit codes, messages, usage errors) +``` diff --git a/cli/rust/src/cmd/info.rs b/cli/rust/src/cmd/info.rs new file mode 100644 index 000000000..0afc3ab17 --- /dev/null +++ b/cli/rust/src/cmd/info.rs @@ -0,0 +1,44 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oak-cli info ` — print the project name, its sequences and +//! its footage (port of `cmd_info()` in cli/main.cpp). + +use crate::cmd::{port_not_wired, require_or, EXIT_ERROR}; + +/// Run `info`. `project` is the .ove path from the command line. +pub fn run(project: String) -> i32 { + if let Err(code) = require_or( + "info", + &[ + &crate::deferred::INIT, + &crate::deferred::NODE, + &crate::deferred::TIMELINE, + ], + EXIT_ERROR, + ) { + return code; + } + // Facade port (unreachable while the families above are deferred): + // oakengine_init(OAKENGINE_INIT_HEADLESS) + // project_create + project_load(project, ...) + // name/filename/is_modified/sequence_count/sequence_at(...) + + // fmt::sequence() / fmt::footage_entry() for each + // project_free + oakengine_shutdown() + // The formatters already exist in crate::fmt and are golden-tested. + let _ = &project; + port_not_wired("info", EXIT_ERROR) +} diff --git a/cli/rust/src/cmd/mod.rs b/cli/rust/src/cmd/mod.rs new file mode 100644 index 000000000..01689efcc --- /dev/null +++ b/cli/rust/src/cmd/mod.rs @@ -0,0 +1,71 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Subcommand implementations. +//! +//! Each subcommand is a faithful port of its `cli/main.cpp` counterpart: +//! the argument validation is real (same messages, same usage-error code), +//! and the facade work gates on [`crate::deferred::require`] — while the +//! families a subcommand needs are deferred, it prints the "not yet +//! available" error with the reasons and exits with the C++-compatible code +//! (1 for info/probe, 2 for render/transcode), never crashing. + +pub mod info; +pub mod probe; +pub mod render; +pub mod transcode; + +use crate::deferred::DeferredFamily; + +/// 0 — success. +pub const EXIT_OK: i32 = 0; +/// 1 — general error (bad project/media file, no sequence, I/O failure). +pub const EXIT_ERROR: i32 = 1; +/// 2 — rendering unavailable or failed (e.g. no GL render backend). +pub const EXIT_RENDER_UNAVAILABLE: i32 = 2; +/// 64 — usage error. +pub const EXIT_USAGE: i32 = 64; + +/// Gate a subcommand on its facade families. +/// +/// When every family is wrapped this returns `Ok(())` and the subcommand's +/// port runs; when any is deferred it prints the composed "not yet +/// available" message to stderr and returns `Err(unavailable_code)` — the +/// code the C++ binary would exit with when that family's work is +/// impossible (1 for info/probe, 2 for render/transcode). +pub fn require_or( + cmd: &str, + families: &[&DeferredFamily], + unavailable_code: i32, +) -> Result<(), i32> { + match crate::deferred::require(families) { + Ok(()) => Ok(()), + Err(msg) => { + eprintln!("error: {cmd}: {msg}"); + Err(unavailable_code) + } + } +} + +/// Fallback for the (today unreachable) success arm of `require_or`: the +/// gate reported the families available, but the call-through port is not +/// wired yet. Never panics; reports an internal error and returns `code`. +pub fn port_not_wired(cmd: &str, code: i32) -> i32 { + eprintln!( + "error: {cmd}: internal error: facade families reported available but no port is wired yet" + ); + code +} diff --git a/cli/rust/src/cmd/probe.rs b/cli/rust/src/cmd/probe.rs new file mode 100644 index 000000000..6ea68943a --- /dev/null +++ b/cli/rust/src/cmd/probe.rs @@ -0,0 +1,39 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oak-cli probe ` — probe a media file and print its decoder, +//! duration and video/audio/subtitle streams (port of `cmd_probe()` in +//! cli/main.cpp). + +use crate::cmd::{port_not_wired, require_or, EXIT_ERROR}; + +/// Run `probe`. `mediafile` is the media path from the command line. +pub fn run(mediafile: String) -> i32 { + if let Err(code) = require_or( + "probe", + &[&crate::deferred::INIT, &crate::deferred::NODE], + EXIT_ERROR, + ) { + return code; + } + // Facade port (unreachable while the families above are deferred): + // oakengine_init(OAKENGINE_INIT_HEADLESS) + // footage_probe(mediafile) -> decoder_name/duration/stream infos, + // formatted with the fmt::* lines (golden-tested) + // footage_free + oakengine_shutdown() + let _ = &mediafile; + port_not_wired("probe", EXIT_ERROR) +} diff --git a/cli/rust/src/cmd/render.rs b/cli/rust/src/cmd/render.rs new file mode 100644 index 000000000..f58d302f8 --- /dev/null +++ b/cli/rust/src/cmd/render.rs @@ -0,0 +1,69 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oak-cli render ` — +//! render the first sequence to PPM frames plus a PCM s16 WAV (port of +//! `cmd_render()` in cli/main.cpp). + +use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE}; + +/// Run `render` with the validated (or rejected) seconds arguments. +/// +/// The seconds are validated exactly like the C++ `strtod` checks before any +/// facade work; the facade work itself (init + project + sequence + renderer, +/// then [`crate::ppm::write_ppm`] / [`crate::wav::write_wav`] per frame) is +/// gated on the deferred families below. +pub fn run(project: String, start_seconds: &str, end_seconds: &str, out_dir: &str) -> i32 { + let start: f64 = match start_seconds.parse() { + Ok(v) => v, + Err(_) => { + eprintln!("error: invalid start seconds \"{start_seconds}\""); + return EXIT_USAGE; + } + }; + let end: f64 = match end_seconds.parse() { + Ok(v) => v, + Err(_) => { + eprintln!("error: invalid end seconds \"{end_seconds}\""); + return EXIT_USAGE; + } + }; + if end <= start { + eprintln!("error: invalid end seconds \"{end_seconds}\""); + return EXIT_USAGE; + } + + if let Err(code) = require_or( + "render", + &[ + &crate::deferred::INIT, + &crate::deferred::NODE, + &crate::deferred::TIMELINE, + &crate::deferred::RENDER, + ], + EXIT_RENDER_UNAVAILABLE, + ) { + return code; + } + // Facade port (unreachable while the families above are deferred): + // oakengine_init(HEADLESS | RENDER), chdir to the project dir, + // project_load, sequence 0 frame rate -> start_ts/end_ts, + // renderer_create(f32, fr_num, fr_den), then for each timestamp + // render_frame -> ppm::write_ppm (progress on stderr), then + // render_audio -> wav::write_wav. Both writers are golden-tested. + let _ = (&project, &start, &end, &out_dir); + port_not_wired("render", EXIT_RENDER_UNAVAILABLE) +} diff --git a/cli/rust/src/cmd/transcode.rs b/cli/rust/src/cmd/transcode.rs new file mode 100644 index 000000000..8b180c2b7 --- /dev/null +++ b/cli/rust/src/cmd/transcode.rs @@ -0,0 +1,70 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! `oak-cli transcode [width] [--format ppm|mp4]` — +//! "media in, renders out" round trip (port of `cmd_transcode()` in +//! cli/main.cpp). + +use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE}; + +/// Run `transcode`. `width`/`format` are validated exactly like the C++ loop +/// over `argv[4..]`; the facade work is gated on the deferred families below. +pub fn run( + input_media: String, + out: String, + width: Option, + format: Option, +) -> i32 { + if let Some(w) = &width { + match w.parse::() { + Ok(n) if n > 0 => {} + _ => { + eprintln!("error: invalid width \"{w}\""); + return EXIT_USAGE; + } + } + } + if let Some(f) = &format { + if f != "ppm" && f != "mp4" { + eprintln!("error: unknown --format \"{f}\" (ppm|mp4)"); + return EXIT_USAGE; + } + } + + if let Err(code) = require_or( + "transcode", + &[ + &crate::deferred::INIT, + &crate::deferred::NODE, + &crate::deferred::TIMELINE, + &crate::deferred::RENDER, + &crate::deferred::EXPORT, + ], + EXIT_RENDER_UNAVAILABLE, + ) { + return code; + } + // Facade port (unreachable while the families above are deferred): + // probe the source for geometry/fps/duration, build a temporary + // project (new + import_footage + sequence_new + add_track x2 + + // add_footage_clip x2), then either the ppm path (render_frame / + // render_audio -> ppm::write_ppm / wav::write_wav) or the mp4 path + // (oakengine_export_render with H.264/AAC options + progress + // callback). The C++ exits 2 when the render/export backend is + // unavailable, which is also the code used here. + let _ = (&input_media, &out, &width, &format); + port_not_wired("transcode", EXIT_RENDER_UNAVAILABLE) +} diff --git a/cli/rust/src/deferred.rs b/cli/rust/src/deferred.rs new file mode 100644 index 000000000..568c31a82 --- /dev/null +++ b/cli/rust/src/deferred.rs @@ -0,0 +1,148 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Facade-family availability, mirroring `src/facade/rust/src/deferred.rs`. +//! +//! Every `oak-cli` subcommand depends on one or more families of the +//! `oakengine_*` C ABI. Those families live in the `oakfacade` crate, and +//! some of them are **deferred**: the facade does not wrap them yet, so the +//! subcommands must report a clear "not yet available" error instead of +//! calling into the facade (the calls would not link, and faking behavior +//! would be worse). +//! +//! The entries below are kept field-for-field in sync with the facade's own +//! deferral documentation (`src/facade/rust/src/deferred.rs`). All families +//! this CLI consumes are currently deferred; when a family is wrapped, remove +//! its entry here and the subcommand's call-through (see `src/cmd/`) becomes +//! reachable. + +/// One deferred facade family: what it covers, which engine headers define +/// it, and why the facade does not wrap it yet. +pub struct DeferredFamily { + /// Short family name, as used in messages. + pub name: &'static str, + /// Engine headers involved. + pub headers: &'static str, + /// Why the family is not wrapped yet (from the facade's deferred.rs). + pub reason: &'static str, +} + +/// `init.h` — engine process initialization/shutdown. +/// +/// Not even listed in the facade's scope table yet (`src/facade/rust/README.md`): +/// the facade currently wraps only undo/config/video_params/audio/plugin. +pub const INIT: DeferredFamily = DeferredFamily { + name: "init", + headers: "init.h", + reason: "the facade shell (oakengine_init/shutdown) is not wrapped in oakfacade yet (its scope table covers only undo/common/audio/plugin)", +}; + +/// `project.h` + `footage.h` — the oaknode module family. +/// +/// Facade deferred.rs "node": all 30 exports of the oaknode Rust crate are +/// `todo!()` bodies, so the engine project/footage families have no module +/// backing to wrap. +pub const NODE: DeferredFamily = DeferredFamily { + name: "node (project/footage)", + headers: "project.h, footage.h", + reason: "deferred: the oaknode crate is an unimplemented skeleton (every export is a todo!() body), so the project/footage families have no module backing", +}; + +/// `timeline.h` — sequence/track/clip family. +/// +/// Facade deferred.rs "timeline": the oaktimeline crate's exports reference +/// ~80 oaknode C ABI symbols the skeletal oaknode crate does not define, and +/// its test-stubs collide with the real oakundo crate in the facade test +/// link. +pub const TIMELINE: DeferredFamily = DeferredFamily { + name: "timeline", + headers: "timeline.h", + reason: "deferred: test linkage — the oaktimeline crate's exports reference oaknode C ABI symbols the skeletal oaknode crate does not define", +}; + +/// `renderer.h` — renderer/frame/audio-buffer family. +/// +/// Facade deferred.rs "render": no structural blocker; the engine renderer.h +/// family simply was not wrapped in the facade's current pass. +pub const RENDER: DeferredFamily = DeferredFamily { + name: "render", + headers: "renderer.h", + reason: "deferred for session scope: the engine renderer.h family is not wrapped in oakfacade yet (no structural blocker)", +}; + +/// `exporter.h` — export/encode family. +/// +/// Facade deferred.rs: exporter is a "genuinely facade-only area" (the +/// liboakengine assembly layer) with no files in the oakfacade crate. +pub const EXPORT: DeferredFamily = DeferredFamily { + name: "exporter", + headers: "exporter.h", + reason: "deferred: the exporter family is a facade-only assembly area with no Rust backing (src/facade/rust/src/deferred.rs)", +}; + +/// Check that every family in `families` is available in the facade. +/// +/// Returns `Ok(())` when all are wrapped (none is today); otherwise `Err` +/// carries the composed "not yet available" message naming each deferred +/// family and its reason, for the subcommands to print and exit on. +pub fn require(families: &[&DeferredFamily]) -> Result<(), String> { + if families.is_empty() { + return Ok(()); + } + let mut detail = String::new(); + for f in families { + detail.push_str(&format!("\n - {} ({}): {}", f.name, f.headers, f.reason)); + } + Err(format!( + "not yet available in the Rust facade (oakfacade): these family(ies) are still deferred \ + (see src/facade/rust/src/deferred.rs):{detail}" + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_family_list_is_available() { + assert!(require(&[]).is_ok()); + } + + #[test] + fn deferred_family_lists_a_reason() { + let err = require(&[&INIT]).unwrap_err(); + assert!(err.contains("not yet available")); + assert!(err.contains("init")); + assert!(err.contains("oakfacade")); + } + + #[test] + fn all_cli_families_are_currently_deferred() { + // Keeps this file honest: if any family the CLI depends on flips to + // available, the subcommand ports in src/cmd/ become reachable and + // the tests asserting "not yet available" must be revisited. + let all: [&[&DeferredFamily]; 5] = [ + &[&INIT], + &[&NODE], + &[&TIMELINE], + &[&RENDER], + &[&EXPORT], + ]; + for families in all { + assert!(require(families).is_err()); + } + } +} diff --git a/cli/rust/src/ffi.rs b/cli/rust/src/ffi.rs new file mode 100644 index 000000000..424331867 --- /dev/null +++ b/cli/rust/src/ffi.rs @@ -0,0 +1,409 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The `oakengine_*` C ABI surface oak-cli consumes — **declared, not yet +//! linked**. +//! +//! This module mirrors — verbatim — every function, opaque handle and POD +//! struct from the engine headers that the C++ `cli/main.cpp` touches: +//! +//! - `engine/include/oakengine/init.h` (oakengine_init / shutdown) +//! - `engine/include/oakengine/project.h` (project lifecycle + queries) +//! - `engine/include/oakengine/footage.h` (probe / stream info / import) +//! - `engine/include/oakengine/timeline.h` (sequence + track/clip editing) +//! - `engine/include/oakengine/renderer.h` (renderer + frame + audio buffer) +//! - `engine/include/oakengine/exporter.h` (export options + render) +//! +//! All of these families are **deferred** in the Rust facade crate +//! (`oakfacade`, `src/facade/rust/src/deferred.rs`), so none of the symbols +//! below is referenced from this crate yet — the subcommands gate on +//! [`crate::deferred`] and report "not yet available" instead of calling +//! them. The declarations exist so that: +//! +//! 1. the exact contract the CLI expects is pinned in one place (types, +//! signatures, string conventions, error codes), and +//! 2. when a family is wrapped by oakfacade, the call-through code in +//! `src/cmd/` resolves against the already-linked `oakfacade` rlib +//! without any manifest or signature churn. +//! +//! Nothing here is ever called today, so no symbol needs to exist in the +//! facade yet; that keeps `cargo build` green standalone. +//! +//! `dead_code` is expected for this whole surface until the ports land: the +//! declarations, the POD structs and [`facade_string`] exist precisely to be +//! consumed by `src/cmd/` once the deferred families are wrapped. + +#![allow(dead_code)] +#![allow(non_camel_case_types)] +#![allow(clippy::missing_safety_doc)] + +use std::ffi::{c_char, c_double, c_int, c_void}; + +// --------------------------------------------------------------------------- +// Opaque engine handle types (engine/include/oakengine/*.h). +// --------------------------------------------------------------------------- + +#[repr(C)] +pub struct OakEngineProject { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineSequence { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineRenderer { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineFrame { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineAudioBuffer { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineFootage { + _opaque: [u8; 0], +} + +#[repr(C)] +pub struct OakEngineClip { + _opaque: [u8; 0], +} + +// --------------------------------------------------------------------------- +// POD structs (footage.h / exporter.h). +// --------------------------------------------------------------------------- + +/// `oak_footage_video_info` (engine/include/oakengine/footage.h). +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OakFootageVideoInfo { + pub stream_index: c_int, + pub width: c_int, + pub height: c_int, + pub frame_rate_num: c_int, + pub frame_rate_den: c_int, + pub duration_ts: i64, + pub time_base_num: c_int, + pub time_base_den: c_int, + pub color_primaries: c_int, + pub color_trc: c_int, + pub interlaced: c_int, +} + +/// `oak_footage_audio_info` (engine/include/oakengine/footage.h). +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OakFootageAudioInfo { + pub stream_index: c_int, + pub sample_rate: c_int, + pub channel_layout: u64, + pub channel_count: c_int, + pub duration_ts: i64, + pub time_base_num: c_int, + pub time_base_den: c_int, +} + +/// `oak_export_options` (engine/include/oakengine/exporter.h). +#[repr(C)] +#[derive(Clone, Copy)] +pub struct OakExportOptions { + pub video_codec: c_int, + pub audio_codec: c_int, + pub video_bit_rate: i64, + pub audio_sample_rate: c_int, + pub audio_channel_count: c_int, +} + +/// `oakengine_export_progress_fn` (exporter.h). +pub type OakEngineExportProgressFn = + Option; + +// --------------------------------------------------------------------------- +// Constants (verbatim values from the engine headers). +// --------------------------------------------------------------------------- + +/// OAKENGINE_OK / OAKENGINE_E_* (init.h). +pub const OAKENGINE_OK: c_int = 0; +pub const OAKENGINE_E_INVALID: c_int = -1; +pub const OAKENGINE_E_STATE: c_int = -2; +pub const OAKENGINE_E_FAILED: c_int = -3; +pub const OAKENGINE_E_NOT_FOUND: c_int = -4; + +/// OAKENGINE_INIT_* (init.h). +pub const OAKENGINE_INIT_HEADLESS: c_int = 0x01; +pub const OAKENGINE_INIT_RENDER: c_int = 0x02; + +/// olive::core::PixelFormat::f32, the renderer's frame pixel format +/// (`k_pixel_format_f32` in cli/main.cpp). +pub const PIXEL_FORMAT_F32: c_int = 4; + +/// OAKENGINE_TRACK_TYPE_* (timeline.h). +pub const OAKENGINE_TRACK_TYPE_VIDEO: c_int = 0; +pub const OAKENGINE_TRACK_TYPE_AUDIO: c_int = 1; + +/// OAKENGINE_EXPORT_VIDEO_* / OAKENGINE_EXPORT_AUDIO_* (exporter.h). +pub const OAKENGINE_EXPORT_VIDEO_H264: c_int = 0; +pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0; + +// --------------------------------------------------------------------------- +// The facade surface (declarations only — see the module docs). +// --------------------------------------------------------------------------- + +extern "C" { + // ---- init.h ---------------------------------------------------------- + pub fn oakengine_init(flags: c_int) -> c_int; + pub fn oakengine_shutdown() -> c_int; + + // ---- project.h ------------------------------------------------------- + pub fn oakengine_project_create() -> *mut OakEngineProject; + pub fn oakengine_project_free(self_: *mut OakEngineProject); + pub fn oakengine_project_new(self_: *mut OakEngineProject) -> c_int; + pub fn oakengine_project_load( + self_: *mut OakEngineProject, + path: *const c_char, + err: *mut c_char, + err_size: c_int, + ) -> c_int; + pub fn oakengine_project_name( + self_: *const OakEngineProject, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_project_filename( + self_: *const OakEngineProject, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_project_is_modified(self_: *const OakEngineProject) -> c_int; + pub fn oakengine_project_sequence_count( + self_: *const OakEngineProject, + ) -> c_int; + pub fn oakengine_project_sequence_at( + self_: *const OakEngineProject, + index: c_int, + ) -> *mut OakEngineSequence; + pub fn oakengine_project_footage_count( + self_: *const OakEngineProject, + ) -> c_int; + pub fn oakengine_project_footage_filename( + self_: *const OakEngineProject, + index: c_int, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_project_footage_is_online( + self_: *const OakEngineProject, + index: c_int, + ) -> c_int; + + // ---- footage.h ------------------------------------------------------- + pub fn oakengine_project_import_footage( + project: *mut OakEngineProject, + path: *const c_char, + ) -> *mut OakEngineFootage; + pub fn oakengine_footage_probe(path: *const c_char) -> *mut OakEngineFootage; + pub fn oakengine_footage_free(self_: *mut OakEngineFootage); + pub fn oakengine_footage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int; + pub fn oakengine_footage_get_decoder_name( + self_: *mut OakEngineFootage, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_footage_get_duration( + self_: *mut OakEngineFootage, + seconds: *mut c_double, + ) -> c_int; + pub fn oakengine_footage_get_video_stream_count( + self_: *const OakEngineFootage, + ) -> c_int; + pub fn oakengine_footage_get_video_stream_info( + self_: *mut OakEngineFootage, + index: c_int, + out: *mut OakFootageVideoInfo, + ) -> c_int; + pub fn oakengine_footage_get_audio_stream_count( + self_: *const OakEngineFootage, + ) -> c_int; + pub fn oakengine_footage_get_audio_stream_info( + self_: *mut OakEngineFootage, + index: c_int, + out: *mut OakFootageAudioInfo, + ) -> c_int; + pub fn oakengine_footage_get_subtitle_stream_count( + self_: *const OakEngineFootage, + ) -> c_int; + + // ---- timeline.h ------------------------------------------------------ + pub fn oakengine_sequence_new( + project: *mut OakEngineProject, + name: *const c_char, + ) -> *mut OakEngineSequence; + pub fn oakengine_sequence_name( + self_: *const OakEngineSequence, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_sequence_get_length( + self_: *const OakEngineSequence, + seconds: *mut c_double, + ) -> c_int; + pub fn oakengine_sequence_get_length_rational( + self_: *const OakEngineSequence, + num: *mut c_int, + den: *mut c_int, + ) -> c_int; + pub fn oakengine_sequence_get_frame_rate( + self_: *const OakEngineSequence, + num: *mut c_int, + den: *mut c_int, + ) -> c_int; + pub fn oakengine_sequence_get_video_params( + self_: *const OakEngineSequence, + width: *mut c_int, + height: *mut c_int, + par_num: *mut c_int, + par_den: *mut c_int, + ) -> c_int; + pub fn oakengine_sequence_track_count( + self_: *const OakEngineSequence, + video: *mut c_int, + audio: *mut c_int, + subtitle: *mut c_int, + ) -> c_int; + pub fn oakengine_sequence_get_playhead( + self_: *const OakEngineSequence, + timestamp: *mut i64, + ) -> c_int; + pub fn oakengine_sequence_get_playhead_seconds( + self_: *const OakEngineSequence, + seconds: *mut c_double, + ) -> c_int; + pub fn oakengine_sequence_add_track( + self_: *mut OakEngineSequence, + track_type: c_int, + ) -> c_int; + pub fn oakengine_sequence_add_footage_clip( + seq: *mut OakEngineSequence, + footage: *mut OakEngineFootage, + track_type: c_int, + track_index: c_int, + in_ts: i64, + out_ts: i64, + media_in: i64, + ) -> *mut OakEngineClip; + pub fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int) + -> c_int; + + // ---- renderer.h ------------------------------------------------------ + pub fn oakengine_renderer_create( + seq: *mut OakEngineSequence, + width: c_int, + height: c_int, + pixel_format: c_int, + frame_rate_num: c_int, + frame_rate_den: c_int, + output_colorspace: *const c_char, + ) -> *mut OakEngineRenderer; + pub fn oakengine_renderer_free(self_: *mut OakEngineRenderer); + pub fn oakengine_renderer_last_error( + self_: *const OakEngineRenderer, + buf: *mut c_char, + buf_size: c_int, + ) -> c_int; + pub fn oakengine_renderer_render_frame( + self_: *mut OakEngineRenderer, + timestamp: i64, + ) -> *mut OakEngineFrame; + pub fn oakengine_renderer_render_audio( + self_: *mut OakEngineRenderer, + start_timestamp: i64, + length_timestamp: i64, + ) -> *mut OakEngineAudioBuffer; + + // ---- renderer.h (OakEngineFrame) ------------------------------------- + pub fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int; + pub fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int; + pub fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int; + pub fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int; + pub fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int; + pub fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void; + pub fn oakengine_frame_free(self_: *mut OakEngineFrame); + + // ---- renderer.h (OakEngineAudioBuffer) -------------------------------- + pub fn oakengine_audio_sample_rate( + self_: *const OakEngineAudioBuffer, + ) -> c_int; + pub fn oakengine_audio_channel_count( + self_: *const OakEngineAudioBuffer, + ) -> c_int; + pub fn oakengine_audio_sample_count( + self_: *const OakEngineAudioBuffer, + ) -> i64; + pub fn oakengine_audio_data( + self_: *const OakEngineAudioBuffer, + channel: c_int, + ) -> *const f32; + pub fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer); + + // ---- exporter.h ------------------------------------------------------ + pub fn oakengine_export_render( + seq: *mut OakEngineSequence, + path: *const c_char, + in_ts: i64, + out_ts: i64, + width: c_int, + height: c_int, + opts: *const OakExportOptions, + ) -> c_int; + pub fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int; + pub fn oakengine_export_set_progress_callback( + f: OakEngineExportProgressFn, + userdata: *mut c_void, + ); +} + +/// Read a facade string (buf/size convention) into an owned `String`, +/// mirroring `facade_string()` in cli/main.cpp: a negative return is an +/// error/empty string, otherwise the getter is called twice (size query, +/// then fill) and the trailing NUL is stripped. +/// +/// # Safety +/// `getter` must be one of the `oakengine_*` string getters declared above +/// and `handle` a live handle for it. +pub unsafe fn facade_string( + getter: unsafe extern "C" fn(*const c_void, *mut c_char, c_int) -> c_int, + handle: *const c_void, +) -> String { + unsafe { + let size = getter(handle, std::ptr::null_mut(), 0); + if size < 0 { + return String::new(); + } + let mut s = vec![0u8; size as usize + 1]; + let n = getter(handle, s.as_mut_ptr() as *mut c_char, size + 1); + s.truncate(n.max(0) as usize); + String::from_utf8_lossy(&s).into_owned() + } +} diff --git a/cli/rust/src/fmt.rs b/cli/rust/src/fmt.rs new file mode 100644 index 000000000..8f89afd04 --- /dev/null +++ b/cli/rust/src/fmt.rs @@ -0,0 +1,273 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Output formatters matching the C++ `cli/main.cpp` byte for byte. +//! +//! The golden reference is the output of the C++ binary on the test +//! fixtures (`tests/project_with_footage.ove`, `tests/demo.mp4`), captured +//! before this crate existed. Each function below takes the plain data a +//! facade call would produce and formats it exactly like the C++ `printf` +//! call (`%.6f`, `%.3f`, `%lld`, `%d`, ...). +//! +//! The facade families that produce this data are still deferred +//! (`crate::deferred`), so the formatters are exercised by unit tests +//! against the golden text; the subcommands wire them in once the families +//! land (`dead_code` until then). + +#![allow(dead_code)] + +/// `Project: ` (`cmd_info`). +pub fn project_line(name: &str) -> String { + format!("Project: {name}") +} + +/// `File: ` (`cmd_info`). +pub fn file_line(filename: &str) -> String { + format!("File: {filename}") +} + +/// `Modified: yes|no` (`cmd_info`). +pub fn modified_line(modified: bool) -> String { + format!("Modified: {}", if modified { "yes" } else { "no" }) +} + +/// `Sequences: ` (`cmd_info`). +pub fn sequences_line(count: i64) -> String { + format!("Sequences: {count}") +} + +/// `Footage: ` (`cmd_info`). +pub fn footage_line(count: i64) -> String { + format!("Footage: {count}") +} + +/// One sequence block (`print_sequence` in cli/main.cpp). +/// +/// ``` +/// [0] "Fixture Sequence" +/// length: 0.000000 s (0/1) +/// frame rate: 30000/1001 (29.970 fps) +/// tracks: video=0 audio=0 subtitle=0 +/// playhead: 0 (0.000000 s) +/// ``` +pub fn sequence( + index: i64, + name: &str, + length_seconds: f64, + len_num: i64, + len_den: i64, + fr_num: i64, + fr_den: i64, + video: i64, + audio: i64, + subtitle: i64, + playhead: i64, + playhead_seconds: f64, +) -> String { + let fps = if fr_den != 0 { + fr_num as f64 / fr_den as f64 + } else { + 0.0 + }; + format!( + " [{index}] \"{name}\"\n length: {length_seconds:.6} s ({len_num}/{len_den})\n \ + frame rate: {fr_num}/{fr_den} ({fps:.3} fps)\n tracks: video={video} audio={audio} \ + subtitle={subtitle}\n playhead: {playhead} ({playhead_seconds:.6} s)" + ) +} + +/// One footage entry (`cmd_info`). +/// +/// ``` +/// [0] "/abs/path/demo.mp4" online +/// ``` +pub fn footage_entry(index: i64, filename: &str, online: bool) -> String { + format!( + " [{index}] \"{filename}\" {}", + if online { "online" } else { "offline" } + ) +} + +/// `Decoder: ` (`cmd_probe`). +pub fn decoder_line(decoder: &str) -> String { + format!("Decoder: {decoder}") +} + +/// `Duration: s` (`cmd_probe`). +pub fn duration_line(seconds: f64) -> String { + format!("Duration: {seconds:.6} s") +} + +/// `Video streams: ` (`cmd_probe`). +pub fn video_streams_line(count: i64) -> String { + format!("Video streams: {count}") +} + +/// One video-stream line (`cmd_probe`). +/// +/// ``` +/// [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive +/// ``` +pub fn video_stream( + index: i64, + stream_index: i64, + width: i64, + height: i64, + frame_rate_num: i64, + frame_rate_den: i64, + duration_ts: i64, + time_base_den: i64, + seconds: f64, + color_primaries: i64, + color_trc: i64, + interlaced: bool, +) -> String { + let fps = if frame_rate_den != 0 { + frame_rate_num as f64 / frame_rate_den as f64 + } else { + 0.0 + }; + let interlace = if interlaced { + "interlaced" + } else { + "progressive" + }; + format!( + " [{index}] stream {stream_index}: {width}x{height}, {frame_rate_num}/{frame_rate_den} \ + fps ({fps:.3}), duration {duration_ts}/{time_base_den} ({seconds:.6} s), \ + primaries={color_primaries} trc={color_trc}, {interlace}" + ) +} + +/// `Audio streams: ` (`cmd_probe`). +pub fn audio_streams_line(count: i64) -> String { + format!("Audio streams: {count}") +} + +/// One audio-stream line (`cmd_probe`). +/// +/// ``` +/// [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s) +/// ``` +pub fn audio_stream( + index: i64, + stream_index: i64, + sample_rate: i64, + channel_count: i64, + duration_ts: i64, + time_base_den: i64, + seconds: f64, +) -> String { + format!( + " [{index}] stream {stream_index}: {sample_rate} Hz, {channel_count} channels, \ + duration {duration_ts}/{time_base_den} ({seconds:.6} s)" + ) +} + +/// `Subtitle streams: ` (`cmd_probe`). +pub fn subtitle_streams_line(count: i64) -> String { + format!("Subtitle streams: {count}") +} + +#[cfg(test)] +mod tests { + use super::*; + + // Golden text captured from the C++ binary: + // cmake-build-debug/cli/oak-cli info tests/project_with_footage.ove + // cmake-build-debug/cli/oak-cli probe tests/demo.mp4 + + #[test] + fn golden_info_output() { + let mut out = String::new(); + out.push_str(&project_line("project_with_footage")); + out.push('\n'); + out.push_str(&file_line("/Users/sunyu/Projects/oak/tests/project_with_footage.ove")); + out.push('\n'); + out.push_str(&modified_line(false)); + out.push('\n'); + out.push_str(&sequences_line(1)); + out.push('\n'); + out.push_str(&sequence( + 0, "Fixture Sequence", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0, + )); + out.push('\n'); + out.push_str(&footage_line(1)); + out.push('\n'); + out.push_str(&footage_entry(0, "/Users/sunyu/Projects/oak/tests/demo.mp4", true)); + + const GOLDEN: &str = concat!( + "Project: project_with_footage\n", + "File: /Users/sunyu/Projects/oak/tests/project_with_footage.ove\n", + "Modified: no\n", + "Sequences: 1\n", + " [0] \"Fixture Sequence\"\n", + " length: 0.000000 s (0/1)\n", + " frame rate: 30000/1001 (29.970 fps)\n", + " tracks: video=0 audio=0 subtitle=0\n", + " playhead: 0 (0.000000 s)\n", + "Footage: 1\n", + " [0] \"/Users/sunyu/Projects/oak/tests/demo.mp4\" online", + ); + assert_eq!(out, GOLDEN); + } + + #[test] + fn golden_probe_output() { + let mut out = String::new(); + out.push_str(&decoder_line("ffmpeg")); + out.push('\n'); + out.push_str(&duration_line(17.0)); + out.push('\n'); + out.push_str(&video_streams_line(1)); + out.push('\n'); + out.push_str(&video_stream( + 0, 0, 1920, 1080, 25, 1, 217600, 12800, 17.0, 1, 1, false, + )); + out.push('\n'); + out.push_str(&audio_streams_line(1)); + out.push('\n'); + out.push_str(&audio_stream(0, 1, 48000, 2, 816000, 48000, 17.0)); + out.push('\n'); + out.push_str(&subtitle_streams_line(0)); + + const GOLDEN: &str = concat!( + "Decoder: ffmpeg\n", + "Duration: 17.000000 s\n", + "Video streams: 1\n", + " [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive\n", + "Audio streams: 1\n", + " [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)\n", + "Subtitle streams: 0", + ); + assert_eq!(out, GOLDEN); + } + + #[test] + fn fps_rounding_matches_printf() { + // 30000/1001 = 29.970029... -> %.3f -> "29.970" + let s = sequence(0, "S", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0); + assert!(s.contains("frame rate: 30000/1001 (29.970 fps)"), "{s}"); + } + + #[test] + fn offline_footage_prints_offline() { + assert_eq!( + footage_entry(2, "gone.mp4", false), + " [2] \"gone.mp4\" offline" + ); + } +} diff --git a/cli/rust/src/main.rs b/cli/rust/src/main.rs new file mode 100644 index 000000000..f7fdcf729 --- /dev/null +++ b/cli/rust/src/main.rs @@ -0,0 +1,183 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! oak-cli: headless command-line consumer of the liboakengine C ABI facade. +//! +//! Rust rewrite of `cli/main.cpp` (which stays in the tree until cutover). +//! Same subcommands, same output format, same exit codes: +//! +//! ```text +//! oak-cli info (project info) +//! oak-cli render +//! oak-cli probe +//! oak-cli transcode [width] [--format ppm|mp4] +//! ``` +//! +//! Exit codes: 0 success, 1 general error, 2 rendering unavailable, +//! 64 usage error. +//! +//! The facade families every subcommand depends on (init/project/timeline/ +//! render/footage/exporter) are still **deferred** in the `oakfacade` crate +//! (see `src/facade/rust/src/deferred.rs`), so each subcommand validates its +//! arguments faithfully, then reports the deferral with its reason and exits +//! with the C++-compatible code — never crashing, never faking output. + +mod cmd; +mod deferred; +mod ffi; +mod fmt; +mod ppm; +mod wav; + +use std::process::exit; + +use clap::{Parser, Subcommand}; + +/// The exact usage text of `cli/main.cpp`'s `print_usage()` (also the +/// `--help` output). +const USAGE: &str = "oak-cli - headless consumer of the liboakengine C ABI\n\ +\n\ +Usage:\n\ + oak-cli info \n\ + Print project name, sequences and footage.\n\ +\n\ + oak-cli render \n\ + Render the first sequence to PPM frames (P6, 8-bit RGB) and the\n\ + audio range to a PCM s16 WAV file in .\n\ +\n\ + oak-cli probe \n\ + Probe a media file: decoder, duration, video and audio streams.\n\ +\n\ + oak-cli transcode [width] [--format ppm|mp4]\n\ + Transcode a media file end to end: import it into a temporary\n\ + project, place it as clips, and render the whole duration.\n\ + Default output is a single H.264/AAC MP4 file (encoder default\n\ + bit rate); --format ppm renders PPM frames + a WAV instead.\n\ + [width] defaults to the source width; the height follows the\n\ + source aspect ratio. is the MP4 file path, or the\n\ + output directory with --format ppm.\n\ +\n\ + oak-cli --help\n\ + Show this text.\n\ +\n\ +Exit codes:\n\ + 0 success\n\ + 1 general error (bad project/media file, no sequence, I/O failure)\n\ + 2 rendering unavailable or failed (e.g. no GL render backend)\n\ + 64 usage error\n"; + +/// CLI surface. `--help`/`-h` are handled before clap so the C++ usage text +/// is reproduced exactly; clap still enforces the argument shapes. +#[derive(Parser, Debug)] +#[command( + name = "oak-cli", + disable_help_flag = true, + disable_version_flag = true, + subcommand_required = true +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Print project name, sequences and footage. + Info { + /// Path to the project file (.ove). + project: String, + }, + /// Render the first sequence to PPM frames (P6, 8-bit RGB) and the + /// audio range to a PCM s16 WAV file in . + Render { + /// Path to the project file (.ove). + project: String, + /// Start of the rendered range, in seconds. + start_seconds: String, + /// End of the rendered range, in seconds (must be > start). + end_seconds: String, + /// Directory the PPM frames and audio.wav are written into. + out_dir: String, + }, + /// Probe a media file: decoder, duration, video and audio streams. + Probe { + /// Media file to probe. + mediafile: String, + }, + /// Transcode a media file end to end. + Transcode { + /// Source media file. + input_media: String, + /// Output MP4 path, or the output directory with --format ppm. + out: String, + /// Output width (defaults to the source width). + width: Option, + /// Output format: "mp4" (default) or "ppm". + #[arg(long = "format")] + format: Option, + }, +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + + // argv[1] handling that mirrors the C++ main() exactly. + if let Some(first) = args.first() { + if first == "--help" || first == "-h" { + print!("{USAGE}"); + exit(cmd::EXIT_OK); + } + } + if let Some(first) = args.first() { + if !matches!(first.as_str(), "info" | "render" | "probe" | "transcode") { + eprintln!("error: unknown command \"{first}\""); + eprint_usage(); + exit(cmd::EXIT_USAGE); + } + } + + let cli = match Cli::try_parse() { + Ok(cli) => cli, + Err(e) => { + // clap's own arity/format message, then the C++ usage text. + let _ = e.print(); + eprint_usage(); + exit(cmd::EXIT_USAGE); + } + }; + + let code = match cli.command { + Command::Info { project } => cmd::info::run(project), + Command::Render { + project, + start_seconds, + end_seconds, + out_dir, + } => cmd::render::run(project, &start_seconds, &end_seconds, &out_dir), + Command::Probe { mediafile } => cmd::probe::run(mediafile), + Command::Transcode { + input_media, + out, + width, + format, + } => cmd::transcode::run(input_media, out, width, format), + }; + exit(code); +} + +fn eprint_usage() { + eprint!("{USAGE}"); +} diff --git a/cli/rust/src/ppm.rs b/cli/rust/src/ppm.rs new file mode 100644 index 000000000..34794aa8b --- /dev/null +++ b/cli/rust/src/ppm.rs @@ -0,0 +1,195 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! PPM (P6, 8-bit RGB) frame writer — the exact port of `write_ppm()` in +//! `cli/main.cpp`. +//! +//! Takes the raw pixel data an `OakEngineFrame` facade handle would expose +//! (linesize-strided rows of `channels` values per pixel) and writes a P6 +//! file. Pixel formats: `f32` ([`PIXEL_FORMAT_F32`], 4 bytes per channel, +//! clamped to [0,1]) and `u8` (format 0, 1 byte per channel). Any other +//! format is an error, mirroring the C++ throw. +//! +//! `dead_code` until the render/transcode ports call it (it is exercised by +//! the unit tests below). + +#![allow(dead_code)] + +use std::io::{self, Write}; +use std::path::Path; + +/// olive::core::PixelFormat::f32 — the renderer's default frame format +/// (`k_pixel_format_f32` in cli/main.cpp). +pub const PIXEL_FORMAT_F32: i32 = 4; + +/// Write `width` x `height` rows of pixel data as a P6 PPM file. +/// +/// `data` must hold `linesize * height` bytes; each row starts `linesize` +/// bytes apart (stride). `channels` is the per-pixel channel count in the +/// source data; only the first three channels are emitted. +pub fn write_ppm( + path: &Path, + width: i32, + height: i32, + format: i32, + channels: i32, + linesize: i32, + data: &[u8], +) -> io::Result<()> { + let width = usize::try_from(width).map_err(|_| invalid_data("negative width"))?; + let height = usize::try_from(height).map_err(|_| invalid_data("negative height"))?; + let linesize = usize::try_from(linesize).unwrap_or(0); + let channels = usize::try_from(channels).map_err(|_| invalid_data("negative channel count"))?; + if channels < 3 { + return Err(invalid_data("channel count below 3")); + } + + let mut out = Vec::with_capacity( + format!("P6\n{width} {height}\n255\n").len() + width * height * 3, + ); + out.extend_from_slice(format!("P6\n{width} {height}\n255\n").as_bytes()); + + let mut row = vec![0u8; width * 3]; + for y in 0..height { + let line_start = y * linesize; + let line_end = line_start.checked_add(linesize); + let line = match line_end { + Some(end) if end <= data.len() => &data[line_start..end], + _ => { + return Err(invalid_data("pixel data buffer is shorter than the frame geometry")); + } + }; + for x in 0..width { + for c in 0..3 { + let v = if format == PIXEL_FORMAT_F32 { + // f32: 4 bytes per channel. + let off = (x * channels + c) * 4; + let px = f32::from_ne_bytes([ + line[off], + line[off + 1], + line[off + 2], + line[off + 3], + ]); + let clamped = if px < 0.0 { + 0.0 + } else if px > 1.0 { + 1.0 + } else { + px + }; + // static_cast(clamped * 255.0f + 0.5f): + // truncation toward zero, same as Rust `as u8`. + (clamped * 255.0 + 0.5) as u8 + } else if format == 0 { + // u8: 1 byte per channel. + line[x * channels + c] + } else { + return Err(invalid_data(&format!( + "unsupported frame pixel format {format}" + ))); + }; + row[x * 3 + c] = v; + } + } + out.extend_from_slice(&row); + } + + let mut f = std::fs::File::create(path)?; + f.write_all(&out) +} + +fn invalid_data(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, msg.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bytes(hex: &str) -> Vec { + let mut v = Vec::new(); + for pair in hex.as_bytes().chunks(2) { + let s = std::str::from_utf8(pair).unwrap(); + v.push(u8::from_str_radix(s, 16).unwrap()); + } + v + } + + #[test] + fn writes_p6_header_and_u8_rows() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_ppm_u8.ppm"); + // 2x2, 3 channels, linesize 6, u8. + let data = vec![ + 1, 2, 3, 4, 5, 6, // + 7, 8, 9, 10, 11, 12, // + ]; + write_ppm(&path, 2, 2, 0, 3, 6, &data).unwrap(); + + let got = std::fs::read(&path).unwrap(); + let mut expected = b"P6\n2 2\n255\n".to_vec(); + expected.extend_from_slice(&data); + assert_eq!(got, expected); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn f32_rows_are_clamped_and_quantized() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_ppm_f32.ppm"); + // 1x1, RGBA (4 channels), linesize 16, f32. + let data = bytes("0000803f0000803f0000803f00000000"); // 1.0, 1.0, 1.0, 0.0 + write_ppm(&path, 1, 1, PIXEL_FORMAT_F32, 4, 16, &data).unwrap(); + + let got = std::fs::read(&path).unwrap(); + assert_eq!(&got[..11], b"P6\n1 1\n255\n"); + assert_eq!(&got[11..], &[255, 255, 255]); // 1.0 -> 255 (clamped * 255 + 0.5, truncated) + let _ = std::fs::remove_file(&path); + } + + #[test] + fn clamps_f32_negative_and_over_one() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_ppm_clamp.ppm"); + // 2x1 RGB f32: (-0.5, 0.25, 2.0) | (0.0, 0.5, 1.0) + let mut data = Vec::new(); + for v in [-0.5f32, 0.25, 2.0, 0.0, 0.5, 1.0] { + data.extend_from_slice(&v.to_ne_bytes()); + } + write_ppm(&path, 2, 1, PIXEL_FORMAT_F32, 3, 24, &data).unwrap(); + + let got = std::fs::read(&path).unwrap(); + // 0.0 -> 0, 0.25*255+0.5=64.25 -> 64, 1.0 -> 255, 0.5*255+0.5=128.0 -> 128 + assert_eq!(&got[11..], &[0, 64, 255, 0, 128, 255]); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn unsupported_format_is_an_error() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_ppm_bad.ppm"); + let err = write_ppm(&path, 1, 1, 7, 3, 3, &[0, 0, 0]).unwrap_err(); + assert!(err.to_string().contains("unsupported frame pixel format 7")); + } + + #[test] + fn short_buffer_is_an_error() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_ppm_short.ppm"); + let err = write_ppm(&path, 4, 4, 0, 3, 12, &[0u8; 10]).unwrap_err(); + assert!(err.to_string().contains("shorter than the frame geometry")); + } +} diff --git a/cli/rust/src/wav.rs b/cli/rust/src/wav.rs new file mode 100644 index 000000000..90ba0edc3 --- /dev/null +++ b/cli/rust/src/wav.rs @@ -0,0 +1,167 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! PCM s16 WAV writer — the exact port of `write_wav()` in `cli/main.cpp`. +//! +//! Takes interleaved float samples (the order `cmd_render`/`cmd_transcode` +//! produce by interleaving the planar `OakEngineAudioBuffer` channels) and +//! writes a classic 44-byte-header PCM WAV. Float samples are clamped to +//! [-1, 1] and converted with `v * 32767.0` truncated toward zero, exactly +//! like the C++ `static_cast(clamped * 32767.0f)`. +//! +//! `dead_code` until the render/transcode ports call it (it is exercised by +//! the unit tests below). + +#![allow(dead_code)] + +use std::io::{self, Write}; +use std::path::Path; + +fn write_u16_le(f: &mut impl Write, v: u16) -> io::Result<()> { + f.write_all(&[v as u8, (v >> 8) as u8]) +} + +fn write_u32_le(f: &mut impl Write, v: u32) -> io::Result<()> { + f.write_all(&[ + v as u8, + (v >> 8) as u8, + (v >> 16) as u8, + (v >> 24) as u8, + ]) +} + +/// Write interleaved float samples as a PCM s16 WAV file. +/// +/// `data` must hold `samples * channels` values in interleaved order +/// (`[s0c0, s0c1, s1c0, s1c1, ...]`), matching what the C++ loop over +/// `oakengine_audio_data(audio, ch)[i]` emits. +pub fn write_wav(path: &Path, rate: i32, channels: i32, samples: i64, data: &[f32]) -> io::Result<()> { + let rate = u32::try_from(rate).map_err(|_| invalid_data("negative sample rate"))?; + let channels = u32::try_from(channels).map_err(|_| invalid_data("negative channel count"))?; + let samples = u64::try_from(samples).map_err(|_| invalid_data("negative sample count"))?; + if channels == 0 { + return Err(invalid_data("zero channel count")); + } + let expected = samples + .checked_mul(u64::from(channels)) + .ok_or_else(|| invalid_data("sample count overflow"))?; + if data.len() as u64 != expected { + return Err(invalid_data("sample buffer length does not match rate/channels/samples")); + } + + let data_size = expected + .checked_mul(2) + .and_then(|v| u32::try_from(v).ok()) + .ok_or_else(|| invalid_data("WAV data chunk exceeds 4 GiB"))?; + let byte_rate = rate + .checked_mul(channels) + .and_then(|v| v.checked_mul(2)) + .ok_or_else(|| invalid_data("byte rate overflow"))?; + let block_align = channels + .checked_mul(2) + .and_then(|v| u16::try_from(v).ok()) + .ok_or_else(|| invalid_data("block align overflow"))?; + + let mut f = std::fs::File::create(path)?; + f.write_all(b"RIFF")?; + write_u32_le(&mut f, 36 + data_size)?; + f.write_all(b"WAVE")?; + f.write_all(b"fmt ")?; + write_u32_le(&mut f, 16)?; // fmt chunk size + write_u16_le(&mut f, 1)?; // PCM + write_u16_le(&mut f, channels as u16)?; + write_u32_le(&mut f, rate)?; + write_u32_le(&mut f, byte_rate)?; + write_u16_le(&mut f, block_align)?; + write_u16_le(&mut f, 16)?; // bits per sample + f.write_all(b"data")?; + write_u32_le(&mut f, data_size)?; + + for &v in data { + let clamped = if v < -1.0 { + -1.0 + } else if v > 1.0 { + 1.0 + } else { + v + }; + // static_cast(clamped * 32767.0f): truncation toward zero. + let s = (clamped * 32767.0) as i16; + write_u16_le(&mut f, s as u16)?; + } + f.flush() +} + +fn invalid_data(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, msg.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn golden_mono_wav() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_wav_mono.wav"); + // 2 samples mono at 44100 Hz: 0.0, 0.5 + write_wav(&path, 44100, 1, 2, &[0.0, 0.5]).unwrap(); + + let got = std::fs::read(&path).unwrap(); + // 44-byte header + 2 samples * 2 bytes. + assert_eq!(got.len(), 48); + assert_eq!(&got[0..4], b"RIFF"); + // chunk size = 36 + 4 = 40 + assert_eq!(&got[4..8], &[40, 0, 0, 0]); + assert_eq!(&got[8..12], b"WAVE"); + assert_eq!(&got[12..16], b"fmt "); + assert_eq!(&got[16..20], &[16, 0, 0, 0]); + assert_eq!(&got[20..22], &[1, 0]); // PCM + assert_eq!(&got[22..24], &[1, 0]); // mono + assert_eq!(&got[24..28], &[0x44, 0xAC, 0, 0]); // 44100 + assert_eq!(&got[28..32], &[0x88, 0x58, 0x01, 0]); // byte rate 88200 + assert_eq!(&got[32..34], &[2, 0]); // block align + assert_eq!(&got[34..36], &[16, 0]); // bits per sample + assert_eq!(&got[36..40], b"data"); + assert_eq!(&got[40..44], &[4, 0, 0, 0]); // data size + // 0.0 -> 0; 0.5 * 32767 = 16383.5 -> truncates to 16383 (0x3FFF) + assert_eq!(&got[44..48], &[0x00, 0x00, 0xFF, 0x3F]); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn stereo_interleaving_and_clamping() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_wav_stereo.wav"); + // 1 sample stereo at 48000: (-1.0, 1.0) interleaved. + write_wav(&path, 48000, 2, 1, &[-1.0, 1.0]).unwrap(); + + let got = std::fs::read(&path).unwrap(); + assert_eq!(&got[22..24], &[2, 0]); // stereo + assert_eq!(&got[32..34], &[4, 0]); // block align + // -1.0 -> -32767 = 0x8001; 1.0 -> 32767 = 0x7FFF + assert_eq!(&got[44..48], &[0x01, 0x80, 0xFF, 0x7F]); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn sample_count_mismatch_is_an_error() { + let dir = std::env::temp_dir(); + let path = dir.join("oak_cli_test_wav_bad.wav"); + let err = write_wav(&path, 48000, 2, 10, &[0.0f32; 3]).unwrap_err(); + assert!(err.to_string().contains("does not match")); + } +} diff --git a/cli/rust/tests/cli.rs b/cli/rust/tests/cli.rs new file mode 100644 index 000000000..a29163911 --- /dev/null +++ b/cli/rust/tests/cli.rs @@ -0,0 +1,145 @@ +// Oak Video Editor - Non-Linear Video Editor +// Copyright (C) 2026 Oak Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! End-to-end tests for the built `oak-cli` binary (the C++ ctest suite +//! `oak_cli_info`/`oak_cli_render`/`oak_cli_probe`/`oak_cli_transcode` +//! equivalents, as far as the deferred facade allows). +//! +//! All four subcommands depend on facade families that are still deferred in +//! oakfacade (see `src/deferred.rs`), so the data-producing paths assert the +//! documented "not yet available" behavior with the C++-compatible exit +//! codes; the argument-validation paths assert the exact C++ messages and +//! exit code 64. + +use std::process::Command; + +fn bin() -> &'static str { + env!("CARGO_BIN_EXE_oak-cli") +} + +fn run(args: &[&str]) -> (i32, String, String) { + let out = Command::new(bin()).args(args).output().expect("spawn oak-cli"); + ( + out.status.code().expect("exit code"), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +#[test] +fn help_prints_the_cpp_usage_text_and_exits_zero() { + let (code, stdout, stderr) = run(&["--help"]); + assert_eq!(code, 0); + assert!(stderr.is_empty()); + assert!(stdout.starts_with("oak-cli - headless consumer of the liboakengine C ABI\n")); + assert!(stdout.contains("oak-cli transcode [width] [--format ppm|mp4]")); + assert!(stdout.contains("Exit codes:")); + assert!(stdout.contains("64 usage error")); +} + +#[test] +fn no_arguments_is_a_usage_error() { + let (code, _stdout, stderr) = run(&[]); + assert_eq!(code, 64); + assert!(stderr.contains("Usage:")); +} + +#[test] +fn unknown_command_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["frobnicate"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: unknown command \"frobnicate\"")); +} + +#[test] +fn info_on_a_fixture_reports_not_yet_available() { + // The fixture mirrors the ctest invocation; the deferred gate fires + // before any file access. + let (code, _stdout, stderr) = run(&["info", "tests/project_with_footage.ove"]); + assert_eq!(code, 1); + assert!(stderr.contains("error: info: not yet available"), "stderr: {stderr}"); + assert!(stderr.contains("oakfacade")); +} + +#[test] +fn info_with_missing_argument_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["info"]); + assert_eq!(code, 64); + assert!(stderr.contains("Usage:")); +} + +#[test] +fn probe_reports_not_yet_available() { + let (code, _stdout, stderr) = run(&["probe", "tests/demo.mp4"]); + assert_eq!(code, 1); + assert!(stderr.contains("error: probe: not yet available"), "stderr: {stderr}"); +} + +#[test] +fn render_reports_render_unavailable() { + let (code, _stdout, stderr) = run(&["render", "p.ove", "0", "1", "out"]); + assert_eq!(code, 2); + assert!(stderr.contains("error: render: not yet available"), "stderr: {stderr}"); +} + +#[test] +fn render_bad_seconds_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["render", "p.ove", "abc", "1", "out"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: invalid start seconds \"abc\""), "stderr: {stderr}"); +} + +#[test] +fn render_end_not_after_start_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["render", "p.ove", "2", "1", "out"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: invalid end seconds \"1\""), "stderr: {stderr}"); +} + +#[test] +fn transcode_reports_render_unavailable() { + let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "960"]); + assert_eq!(code, 2); + assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}"); +} + +#[test] +fn transcode_bad_width_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "banana"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: invalid width \"banana\""), "stderr: {stderr}"); +} + +#[test] +fn transcode_nonpositive_width_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "0"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: invalid width \"0\""), "stderr: {stderr}"); +} + +#[test] +fn transcode_unknown_format_is_a_usage_error() { + let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "--format", "webm"]); + assert_eq!(code, 64); + assert!(stderr.contains("error: unknown --format \"webm\" (ppm|mp4)"), "stderr: {stderr}"); +} + +#[test] +fn transcode_ppm_format_is_accepted_then_reports_not_available() { + let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "outdir", "960", "--format", "ppm"]); + assert_eq!(code, 2); + assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}"); +} diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/riir/notes.md index 048c86621..e3c972e8d 100644 --- a/docs/zh/plans/riir/notes.md +++ b/docs/zh/plans/riir/notes.md @@ -526,6 +526,35 @@ image/ffmpeg-next 全套。 不释放队列里的借用副本(C++ 原版对两者都调 free,若真实 oakrender 的 句柄副本不各自计数,则 C++ 路径存在双释放风险,Rust 侧按头文件契约 规避);ticket.h 无 poll/try_wait 查询,等待完全走完成回调 + condvar。 +- **oaktask ↔ 真实 oakrender ticket ABI 接线 ✅(2026-08-09)**: + oaktask 的 `bridge/render.rs` 保持 link-time `extern "C"`(与 + `bridge/codec.rs` 同模式):`cargo test` 由 tests/common/mod.rs 的 + `#[no_mangle]` stub 满足链接,真实 `liboakrender` 在场时(app 链接模块 + dylib)解析到真实导出。新增 `--features real-oakrender` + + `--test render_real_integration_test`:把 oakrender crate 作为可选 path + dep 链接进同一测试二进制(feature 关闭时完全不编译),驱动 + `RenderTask::render` 走真实 arena 的 CPU 路径(`eval::render_produced_frame` + 生成 F32 帧、无 GPU),帧按时间戳序交付(64×64、0/1→1/1→2/1),并断言 + 结束后 `oakrender::handle::alive_count()` 回到基线(ticket/帧/取消原子的 + 句柄全部释放)。feature 开启时 tests/common/mod.rs 的 render stub 整段 + `#[cfg(not(feature = "real-oakrender"))]` 编译掉(与真实导出的 + `#[no_mangle]` 符号会冲突,故必须带 `--test` 过滤单独构建)。 + **句柄契约裁定**:真实 oakrender 的句柄副本**不各自计数**——` + oakrender_ticket_render_frame` 只 `make_owned` 一份 `TicketBox`(refs=1), + 回调闭包捕获的副本与 submit 返回值共享同一 RefBox/同一计数,与头文件 + "回调收到借用副本、提交者持有并释放" 的契约一致;oaktask 渲染循环只释放 + submit 返回的那一份(且 `wait_idle()` 保证所有回调先触发完再释放,队列 + 里的借用副本从不释放、也从不 deref 到已释放的 box),无双释放/悬垂, + **无需修改任务侧**。C++ 原版"两者都 free"的双释放风险随 render 侧 + Rust 化不复存在。另修复 oakrender ffi.rs 一处潜在竞态:`TicketBox.id` + 原先在 `submit_video` 之后才写回,快 worker 可能抢先完成 ticket 并回调 + (回调收到的句柄 id 仍为占位 0,`classify_ticket` 会报 unexpected + timestamp);现改为 `arena.next_id()` 预分配 + `submit_video_with_id` + 在 post 前盖好 id(ticket.rs 新增 `next_id`/`submit_video_with_id`/ + `submit_audio_with_id`,`submit_video`/`submit_audio` 签名不变)。 + 另修正 bridge/render.rs 的 `oakrender_color_processor_create` 声明: + 5 参(旧镜像)→ 3 参 `(src_space, dst_transform, direction)`,与 + include/render/color.h 及 oakrender 导出一致。 - **oaktask 导出缺口**:临时文件重命名(失败不留半成品)与 sidecar 字幕编码器未实现;precache 缺项目深拷贝。 diff --git a/gpui b/gpui index a64234936..16ae7c42d 160000 --- a/gpui +++ b/gpui @@ -1 +1 @@ -Subproject commit a64234936c09907e86393163d50dea08e833c892 +Subproject commit 16ae7c42dfebee0901aa27620bfc2e04dfbc1eb0 diff --git a/src/bindings/oakotio/Cargo.lock b/src/bindings/oakotio/Cargo.lock index fbfeaeb24..a7137e9ca 100644 --- a/src/bindings/oakotio/Cargo.lock +++ b/src/bindings/oakotio/Cargo.lock @@ -45,6 +45,7 @@ name = "oakotio" version = "0.1.0" dependencies = [ "oakcore-rs", + "quick-xml", "serde", "serde_json", ] @@ -58,6 +59,15 @@ 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" diff --git a/src/bindings/oakotio/Cargo.toml b/src/bindings/oakotio/Cargo.toml index b6dd2c73b..663253200 100644 --- a/src/bindings/oakotio/Cargo.toml +++ b/src/bindings/oakotio/Cargo.toml @@ -36,3 +36,14 @@ serde_json = { version = "1", features = ["preserve_order"] } # Shared value types (Rational). Same path dependency the other bindings use. oakcore-rs = { path = "../../oakcore-rs" } + +# quick-xml: streaming XML codec for the FCPXML interchange layer +# (src/fcpxml.rs). Same major version the other Rust modules use +# (src/common/rust/Cargo.toml). +quick-xml = "0.41.0" + +[dev-dependencies] + +# oakcore-rs: exact Rational comparisons in the integration tests +# (tests/fcpxml.rs). +oakcore-rs = { path = "../../oakcore-rs" } diff --git a/src/bindings/oakotio/README.md b/src/bindings/oakotio/README.md index 7b6a6a9cb..229637bf5 100644 --- a/src/bindings/oakotio/README.md +++ b/src/bindings/oakotio/README.md @@ -14,6 +14,11 @@ shortest float representation, no trailing newline); the reader tolerates hand-written files, preserving unknown fields verbatim across a round-trip and defaulting missing fields. +The crate also ships an **FCPXML** (Final Cut Pro X `.fcpxml`) interchange +layer (`src/fcpxml.rs`) that maps the FCP X document format onto the *same* +model types, so an importer/exporter can offer `.fcpxml` alongside `.otio` +with a single object graph. See [FCPXML](#fcpxml) below. + Part of the Oak `src/bindings/` family (siblings: `oakaudioout`). This is an **rlib** — nothing is exported dynamically. @@ -21,12 +26,15 @@ This is an **rlib** — nothing is exported dynamically. ``` src/bindings/oakotio/ -├── Cargo.toml # rlib; deps: serde, serde_json (crates.io), oakcore-rs (path) +├── Cargo.toml # rlib; deps: serde, serde_json, quick-xml (crates.io), +│ # oakcore-rs (path); dev-dep: oakcore-rs (path) ├── src/ │ ├── lib.rs # crate docs + module wiring + re-exports + from_json_* entry points │ ├── error.rs # OtioError (Json | Io) + Result -│ └── model.rs # serde structs for the 10 OTIO schemas + value types + -│ # Rational::from_double port + in-crate unit tests +│ ├── model.rs # serde structs for the 10 OTIO schemas + value types + +│ │ # Rational::from_double port + in-crate unit tests +│ └── fcpxml.rs # FCPXML reader/writer (quick-xml) mapped onto model types + +│ # FcpxmlError + in-crate unit tests └── tests/ ├── data/ # C++-writer golden files (golden_timeline.json, │ # golden_collection.json, golden_typed_transition.json, @@ -34,8 +42,10 @@ src/bindings/oakotio/ ├── parity.rs # read parity: every golden file parses and round-trips ├── semantic.rs # semantic checks over golden_timeline.json (what the │ # C++ load task reads back) - └── save_parity.rs # save parity: a built Timeline serializes byte-identical - # to golden_timeline.json and re-parses identically + ├── save_parity.rs # save parity: a built Timeline serializes byte-identical + │ # to golden_timeline.json and re-parses identically + └── fcpxml.rs # FCPXML: synthetic-document parse, model round-trip, + # NTSC precision, error paths, leniency ``` ## API summary @@ -66,6 +76,23 @@ src/bindings/oakotio/ All fallible operations return `Result`. +### FCPXML API (`fcpxml` module) + +- `from_fcpxml_string(&str) -> Result, FcpxmlError>` / + `from_fcpxml_file(path)` — parse an FCPXML document; one `Timeline` per + ``. +- `to_fcpxml_string(&[Timeline]) -> Result` / + `to_fcpxml_file(&[Timeline], path)` — serialize timelines to an FCPXML + 1.10 document (formats and assets deduplicated across timelines). +- `FcpxmlError` — `Xml` (malformed markup), `Malformed` (wrong root, + missing attributes, unknown format resources, bad time values), + `UnsupportedVersion`, `Io`. + +The FCPXML layer reads through the same model types as the OTIO layer: +`Timeline`/`Track`/`Clip`/`Gap`/`Transition` with `RationalTime` time +values, so a single object graph feeds both `.otio` and `.fcpxml` +import/export. + ## Backend choice: hand-written serde over the `opentimelineio` crate | Option | Verdict | @@ -88,9 +115,17 @@ Runtime dependencies (crates.io): - `serde_json` 1 (with `preserve_order`) — JSON codec; `preserve_order` keeps map insertion order so metadata and unknown fields round-trip in file order. +- `quick-xml` 0.41 — streaming XML codec for the FCPXML layer + (`src/fcpxml.rs`). Same major version the other Rust modules use + (`src/common/rust/Cargo.toml`). - `oakcore-rs` (path: `../../oakcore-rs`) — shared `Rational` value type - (used by the `Rational::from_double` port); same path dependency the other - bindings use. + (used by the `Rational::from_double` port and for exact FCPXML + rational-time conversion); same path dependency the other bindings use. + +Dev-dependencies (tests only): + +- `oakcore-rs` (path: `../../oakcore-rs`) — exact `Rational` comparisons in + `tests/fcpxml.rs`. Build and test: @@ -137,8 +172,81 @@ The C++ anchors this crate reproduces: `duration()`, ...) take `&self` and return clones; the C++ value semantics (`to_seconds`, `rescaled_to`) are unaffected. +## FCPXML + +The `fcpxml` module reads and writes Final Cut Pro X's `.fcpxml` +interchange format (the version-1.x XML documents produced by FCP X and, +with some tolerance, by DaVinci Resolve). It maps the FCPXML structure onto +the same model types as the OTIO layer: + +| FCPXML element | Model mapping | +| --- | --- | +| `` root `version` | validated (1.0 – 1.11); recorded in timeline metadata | +| ``/`` | frame rate (from `frameDuration`) | +| ``/`` | `ExternalReference` (`src` → `target_url`, `duration` → `available_range`) | +| ``/``/`` | timeline name + `metadata["fcpxml"]` (event/project name, version) | +| `` | `Timeline` (`tcStart` → `global_start_time`, `tcFormat`/`audioLayout`/`audioRate` → metadata) | +| `` | `Track` kind "Video" (the primary storyline) | +| secondary `