From e563b340ac4c81941d82d95c5daaf06f700be5d8 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 10 Aug 2026 08:12:08 +0800 Subject: [PATCH] feat: oakengine facade, oaknode/oakrender impls, worker+CLI, app skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oaknode Rust crate: full implementation (core engine, sequence/ track/block/footage, traverser, serializer, 43 node behaviors; 493 tests green) - oakrender Rust crate: full implementation incl. wgpu backend skeleton, ticket arena, worker pool (136 tests green; fixed lost-wakeup and ticket ordering races) - src/facade/rust (oakfacade): 222 oakengine_* exports over the module C ABIs (61 tests green); worker_main + real POSIX shm frame-slot transport (SpscRingBuffer/FrameSlotPool, wire-compatible with engine/render/ipc) - cli/rust + worker/rust binaries (29 + 29 tests green) - oakotio: FCPXML import/export (49 tests green) - oaktask: OTIO/FCPXML format dispatch (90 tests green) - app/rust: gpui app skeleton — dock panels (viewers/timeline/ explorer/inspector/node editor), transport, olive themes, i18n (en/zh), 37 tests green - gpui submodule: menu checkmarks, dock ratios, vertical meter, CPU-frame viewer surface, drop-frame timecode --- app/rust/.gitignore | 1 + app/rust/Cargo.lock | 7425 +++++++++++++ app/rust/Cargo.toml | 49 + app/rust/docs/screenshot-window.png | Bin 0 -> 467939 bytes app/rust/examples/screenshot.rs | 72 + app/rust/rustfmt.toml | 18 + app/rust/src/app.rs | 717 ++ app/rust/src/i18n.rs | 569 + app/rust/src/lib.rs | 48 + app/rust/src/main.rs | 21 + app/rust/src/oakui/engine.rs | 130 + app/rust/src/oakui/mock.rs | 802 ++ app/rust/src/oakui/mod.rs | 45 + app/rust/src/oakui/timecode.rs | 180 + app/rust/src/oakui/transport.rs | 255 + app/rust/src/panels/history.rs | 92 + app/rust/src/panels/inspector.rs | 95 + app/rust/src/panels/mod.rs | 70 + app/rust/src/panels/node_editor.rs | 134 + app/rust/src/panels/program_viewer.rs | 138 + app/rust/src/panels/project_explorer.rs | 78 + app/rust/src/panels/source_viewer.rs | 113 + app/rust/src/panels/status_bar.rs | 104 + app/rust/src/panels/timeline.rs | 442 + cli/rust/Cargo.lock | 191 + cli/rust/Cargo.toml | 43 + cli/rust/README.md | 80 + cli/rust/src/cmd/info.rs | 44 + cli/rust/src/cmd/mod.rs | 71 + cli/rust/src/cmd/probe.rs | 39 + cli/rust/src/cmd/render.rs | 69 + cli/rust/src/cmd/transcode.rs | 70 + cli/rust/src/deferred.rs | 148 + cli/rust/src/ffi.rs | 409 + cli/rust/src/fmt.rs | 273 + cli/rust/src/main.rs | 183 + cli/rust/src/ppm.rs | 195 + cli/rust/src/wav.rs | 167 + cli/rust/tests/cli.rs | 145 + docs/zh/plans/riir/notes.md | 29 + gpui | 2 +- src/bindings/oakotio/Cargo.lock | 10 + src/bindings/oakotio/Cargo.toml | 11 + src/bindings/oakotio/README.md | 128 +- src/bindings/oakotio/src/fcpxml.rs | 1562 +++ src/bindings/oakotio/src/lib.rs | 9 + src/bindings/oakotio/src/model.rs | 28 + src/bindings/oakotio/tests/fcpxml.rs | 495 + src/facade/rust/Cargo.lock | 1725 +++ src/facade/rust/Cargo.toml | 55 + src/facade/rust/README.md | 129 + src/facade/rust/src/audio.rs | 577 + src/facade/rust/src/bridge/audio.rs | 210 + src/facade/rust/src/bridge/codec.rs | 192 + src/facade/rust/src/bridge/common.rs | 342 + src/facade/rust/src/bridge/mod.rs | 43 + src/facade/rust/src/bridge/node.rs | 986 ++ src/facade/rust/src/bridge/plugin.rs | 34 + src/facade/rust/src/bridge/render.rs | 216 + src/facade/rust/src/bridge/task.rs | 121 + src/facade/rust/src/bridge/timeline.rs | 220 + src/facade/rust/src/bridge/undo.rs | 102 + src/facade/rust/src/codec.rs | 1084 ++ src/facade/rust/src/common.rs | 611 ++ src/facade/rust/src/deferred.rs | 91 + src/facade/rust/src/error.rs | 87 + src/facade/rust/src/handle.rs | 305 + src/facade/rust/src/ipc.rs | 1765 +++ src/facade/rust/src/lib.rs | 89 + src/facade/rust/src/node.rs | 7544 +++++++++++++ src/facade/rust/src/plugin.rs | 122 + src/facade/rust/src/render.rs | 958 ++ src/facade/rust/src/task.rs | 822 ++ src/facade/rust/src/timeline.rs | 5802 ++++++++++ src/facade/rust/src/undo.rs | 402 + src/facade/rust/src/worker.rs | 1181 ++ src/facade/rust/tests/audio.rs | 150 + src/facade/rust/tests/codec.rs | 221 + src/facade/rust/tests/common.rs | 188 + src/facade/rust/tests/common/mod.rs | 432 + src/facade/rust/tests/linkage.rs | 70 + src/facade/rust/tests/node.rs | 377 + src/facade/rust/tests/plugin.rs | 70 + src/facade/rust/tests/render.rs | 176 + src/facade/rust/tests/task.rs | 288 + src/facade/rust/tests/timeline.rs | 586 + src/facade/rust/tests/undo.rs | 246 + src/node/rust/Cargo.toml | 6 + src/node/rust/README.md | 21 +- src/node/rust/src/block.rs | 315 + src/node/rust/src/bridge/codec.rs | 18 +- src/node/rust/src/bridge/common.rs | 1657 ++- src/node/rust/src/bridge/core.rs | 161 + src/node/rust/src/bridge/mod.rs | 56 +- src/node/rust/src/bridge/render.rs | 148 +- src/node/rust/src/bridge/timeline.rs | 52 +- src/node/rust/src/bridge/undo.rs | 408 +- src/node/rust/src/colormanager.rs | 112 +- src/node/rust/src/error.rs | 2 +- src/node/rust/src/ffi.rs | 9596 ++++++++++++++++- src/node/rust/src/folder.rs | 98 +- src/node/rust/src/footage.rs | 202 +- src/node/rust/src/graph.rs | 550 +- src/node/rust/src/handle.rs | 32 + src/node/rust/src/id.rs | 24 + src/node/rust/src/input.rs | 31 + src/node/rust/src/keyframe.rs | 278 +- src/node/rust/src/node.rs | 337 +- src/node/rust/src/nodes/blur.rs | 339 +- src/node/rust/src/nodes/chromakey.rs | 289 +- src/node/rust/src/nodes/colordifferencekey.rs | 162 +- .../rust/src/nodes/cornerpindistortnode.rs | 323 +- src/node/rust/src/nodes/cropdistortnode.rs | 290 +- src/node/rust/src/nodes/despill.rs | 132 +- src/node/rust/src/nodes/displaytransform.rs | 237 +- src/node/rust/src/nodes/dropshadowfilter.rs | 174 +- src/node/rust/src/nodes/flipdistortnode.rs | 168 +- src/node/rust/src/nodes/generatorwithmerge.rs | 60 +- src/node/rust/src/nodes/group.rs | 268 +- src/node/rust/src/nodes/mask.rs | 206 +- src/node/rust/src/nodes/math.rs | 266 +- src/node/rust/src/nodes/mathbase.rs | 1548 ++- src/node/rust/src/nodes/matrix.rs | 434 +- src/node/rust/src/nodes/merge.rs | 153 +- src/node/rust/src/nodes/mod.rs | 22 +- src/node/rust/src/nodes/mosaicfilternode.rs | 135 +- src/node/rust/src/nodes/multicamnode.rs | 340 +- src/node/rust/src/nodes/noise.rs | 130 +- src/node/rust/src/nodes/ociobase.rs | 116 +- .../src/nodes/ociogradingtransformlinear.rs | 492 +- .../rust/src/nodes/ociogradingtransformlog.rs | 453 +- src/node/rust/src/nodes/ociolut.rs | 461 +- src/node/rust/src/nodes/opacity.rs | 215 +- src/node/rust/src/nodes/pan.rs | 242 +- src/node/rust/src/nodes/plugin.rs | 347 +- src/node/rust/src/nodes/polygon.rs | 192 +- src/node/rust/src/nodes/rippledistortnode.rs | 212 +- src/node/rust/src/nodes/shapenode.rs | 200 +- src/node/rust/src/nodes/shapenodebase.rs | 59 +- src/node/rust/src/nodes/solid.rs | 85 +- src/node/rust/src/nodes/stroke.rs | 200 +- src/node/rust/src/nodes/swirldistortnode.rs | 216 +- src/node/rust/src/nodes/textbackend.rs | 92 +- src/node/rust/src/nodes/textv1.rs | 329 +- src/node/rust/src/nodes/textv2.rs | 433 +- src/node/rust/src/nodes/textv3.rs | 651 +- src/node/rust/src/nodes/threewaycolor.rs | 170 +- src/node/rust/src/nodes/tiledistortnode.rs | 215 +- src/node/rust/src/nodes/timeformat.rs | 414 +- src/node/rust/src/nodes/timeinput.rs | 67 +- src/node/rust/src/nodes/timeoffsetnode.rs | 293 +- src/node/rust/src/nodes/timeremap.rs | 243 +- .../rust/src/nodes/transformdistortnode.rs | 580 +- src/node/rust/src/nodes/trigonometry.rs | 219 +- src/node/rust/src/nodes/valuenode.rs | 115 +- src/node/rust/src/nodes/volume.rs | 233 +- src/node/rust/src/nodes/wavedistortnode.rs | 171 +- src/node/rust/src/nodes/whitebalance.rs | 230 +- src/node/rust/src/ops.rs | 228 +- src/node/rust/src/project.rs | 318 +- src/node/rust/src/sequence.rs | 115 +- src/node/rust/src/serializer.rs | 758 +- src/node/rust/src/track.rs | 322 + src/node/rust/src/traverser.rs | 117 +- src/node/rust/src/value.rs | 636 +- src/node/rust/tests/dbg2_test.rs | 7 + src/node/rust/tests/engine_units_test.rs | 854 ++ src/node/rust/tests/ffi_contract_test.rs | 854 +- src/node/rust/tests/ffi_timeline_test.rs | 1898 ++++ src/node/rust/tests/graph_test.rs | 177 +- src/node/rust/tests/phase2_units_test.rs | 728 ++ src/node/rust/tests/serializer_test.rs | 112 +- src/node/rust/tests/structure_test.rs | 384 +- src/node/rust/tests/traverser_test.rs | 179 +- src/node/rust/tests/value_keyframe_test.rs | 246 +- src/render/rust/COVERAGE.md | 146 + src/render/rust/Cargo.lock | 1299 +++ src/render/rust/Cargo.toml | 26 + src/render/rust/README.md | 124 + src/render/rust/src/autocacher.rs | 448 + src/render/rust/src/backend.rs | 1093 ++ src/render/rust/src/bridge/codec.rs | 217 + src/render/rust/src/bridge/common.rs | 200 + src/render/rust/src/bridge/mod.rs | 68 + src/render/rust/src/bridge/node.rs | 175 + src/render/rust/src/cache.rs | 888 ++ src/render/rust/src/cancelatom.rs | 94 + src/render/rust/src/color.rs | 629 ++ src/render/rust/src/copier.rs | 192 + src/render/rust/src/error.rs | 62 + src/render/rust/src/eval.rs | 333 + src/render/rust/src/ffi.rs | 2285 ++++ src/render/rust/src/frame.rs | 146 + src/render/rust/src/handle.rs | 326 + src/render/rust/src/lib.rs | 56 + src/render/rust/src/manager.rs | 262 + src/render/rust/src/texture.rs | 367 + src/render/rust/src/ticket.rs | 592 + src/render/rust/src/worker.rs | 539 + src/render/rust/tests/cache_test.rs | 397 + src/render/rust/tests/common/mod.rs | 117 + src/render/rust/tests/copier_test.rs | 270 + src/render/rust/tests/ffi_contract_test.rs | 334 + src/render/rust/tests/pipeline_test.rs | 424 + src/render/rust/tests/ticket_worker_test.rs | 422 + src/task/rust/COVERAGE.md | 26 +- src/task/rust/Cargo.lock | 1220 ++- src/task/rust/Cargo.toml | 21 + src/task/rust/README.md | 49 +- src/task/rust/src/bridge/render.rs | 22 +- src/task/rust/src/ffi/project.rs | 10 +- src/task/rust/src/project.rs | 1 + src/task/rust/src/project/format.rs | 65 + src/task/rust/src/project/loadotio.rs | 102 +- src/task/rust/src/project/saveotio.rs | 68 +- src/task/rust/tarpaulin-report.html | 4 +- src/task/rust/tests/common/mod.rs | 81 +- src/task/rust/tests/otio_test.rs | 269 + .../tests/render_real_integration_test.rs | 164 + worker/rust/Cargo.lock | 1470 +++ worker/rust/Cargo.toml | 50 + worker/rust/README.md | 106 + worker/rust/src/ipc.rs | 257 + worker/rust/src/main.rs | 103 + worker/rust/src/session.rs | 487 + worker/rust/src/transport.rs | 120 + worker/rust/tests/worker.rs | 55 + 227 files changed, 96585 insertions(+), 972 deletions(-) create mode 100644 app/rust/.gitignore create mode 100644 app/rust/Cargo.lock create mode 100644 app/rust/Cargo.toml create mode 100644 app/rust/docs/screenshot-window.png create mode 100644 app/rust/examples/screenshot.rs create mode 100644 app/rust/rustfmt.toml create mode 100644 app/rust/src/app.rs create mode 100644 app/rust/src/i18n.rs create mode 100644 app/rust/src/lib.rs create mode 100644 app/rust/src/main.rs create mode 100644 app/rust/src/oakui/engine.rs create mode 100644 app/rust/src/oakui/mock.rs create mode 100644 app/rust/src/oakui/mod.rs create mode 100644 app/rust/src/oakui/timecode.rs create mode 100644 app/rust/src/oakui/transport.rs create mode 100644 app/rust/src/panels/history.rs create mode 100644 app/rust/src/panels/inspector.rs create mode 100644 app/rust/src/panels/mod.rs create mode 100644 app/rust/src/panels/node_editor.rs create mode 100644 app/rust/src/panels/program_viewer.rs create mode 100644 app/rust/src/panels/project_explorer.rs create mode 100644 app/rust/src/panels/source_viewer.rs create mode 100644 app/rust/src/panels/status_bar.rs create mode 100644 app/rust/src/panels/timeline.rs create mode 100644 cli/rust/Cargo.lock create mode 100644 cli/rust/Cargo.toml create mode 100644 cli/rust/README.md create mode 100644 cli/rust/src/cmd/info.rs create mode 100644 cli/rust/src/cmd/mod.rs create mode 100644 cli/rust/src/cmd/probe.rs create mode 100644 cli/rust/src/cmd/render.rs create mode 100644 cli/rust/src/cmd/transcode.rs create mode 100644 cli/rust/src/deferred.rs create mode 100644 cli/rust/src/ffi.rs create mode 100644 cli/rust/src/fmt.rs create mode 100644 cli/rust/src/main.rs create mode 100644 cli/rust/src/ppm.rs create mode 100644 cli/rust/src/wav.rs create mode 100644 cli/rust/tests/cli.rs create mode 100644 src/bindings/oakotio/src/fcpxml.rs create mode 100644 src/bindings/oakotio/tests/fcpxml.rs create mode 100644 src/facade/rust/Cargo.lock create mode 100644 src/facade/rust/Cargo.toml create mode 100644 src/facade/rust/README.md create mode 100644 src/facade/rust/src/audio.rs create mode 100644 src/facade/rust/src/bridge/audio.rs create mode 100644 src/facade/rust/src/bridge/codec.rs create mode 100644 src/facade/rust/src/bridge/common.rs create mode 100644 src/facade/rust/src/bridge/mod.rs create mode 100644 src/facade/rust/src/bridge/node.rs create mode 100644 src/facade/rust/src/bridge/plugin.rs create mode 100644 src/facade/rust/src/bridge/render.rs create mode 100644 src/facade/rust/src/bridge/task.rs create mode 100644 src/facade/rust/src/bridge/timeline.rs create mode 100644 src/facade/rust/src/bridge/undo.rs create mode 100644 src/facade/rust/src/codec.rs create mode 100644 src/facade/rust/src/common.rs create mode 100644 src/facade/rust/src/deferred.rs create mode 100644 src/facade/rust/src/error.rs create mode 100644 src/facade/rust/src/handle.rs create mode 100644 src/facade/rust/src/ipc.rs create mode 100644 src/facade/rust/src/lib.rs create mode 100644 src/facade/rust/src/node.rs create mode 100644 src/facade/rust/src/plugin.rs create mode 100644 src/facade/rust/src/render.rs create mode 100644 src/facade/rust/src/task.rs create mode 100644 src/facade/rust/src/timeline.rs create mode 100644 src/facade/rust/src/undo.rs create mode 100644 src/facade/rust/src/worker.rs create mode 100644 src/facade/rust/tests/audio.rs create mode 100644 src/facade/rust/tests/codec.rs create mode 100644 src/facade/rust/tests/common.rs create mode 100644 src/facade/rust/tests/common/mod.rs create mode 100644 src/facade/rust/tests/linkage.rs create mode 100644 src/facade/rust/tests/node.rs create mode 100644 src/facade/rust/tests/plugin.rs create mode 100644 src/facade/rust/tests/render.rs create mode 100644 src/facade/rust/tests/task.rs create mode 100644 src/facade/rust/tests/timeline.rs create mode 100644 src/facade/rust/tests/undo.rs create mode 100644 src/node/rust/src/bridge/core.rs create mode 100644 src/node/rust/tests/dbg2_test.rs create mode 100644 src/node/rust/tests/engine_units_test.rs create mode 100644 src/node/rust/tests/ffi_timeline_test.rs create mode 100644 src/node/rust/tests/phase2_units_test.rs create mode 100644 src/render/rust/COVERAGE.md create mode 100644 src/render/rust/Cargo.lock create mode 100644 src/render/rust/Cargo.toml create mode 100644 src/render/rust/README.md create mode 100644 src/render/rust/src/autocacher.rs create mode 100644 src/render/rust/src/backend.rs create mode 100644 src/render/rust/src/bridge/codec.rs create mode 100644 src/render/rust/src/bridge/common.rs create mode 100644 src/render/rust/src/bridge/mod.rs create mode 100644 src/render/rust/src/bridge/node.rs create mode 100644 src/render/rust/src/cache.rs create mode 100644 src/render/rust/src/cancelatom.rs create mode 100644 src/render/rust/src/color.rs create mode 100644 src/render/rust/src/copier.rs create mode 100644 src/render/rust/src/error.rs create mode 100644 src/render/rust/src/eval.rs create mode 100644 src/render/rust/src/ffi.rs create mode 100644 src/render/rust/src/frame.rs create mode 100644 src/render/rust/src/handle.rs create mode 100644 src/render/rust/src/lib.rs create mode 100644 src/render/rust/src/manager.rs create mode 100644 src/render/rust/src/texture.rs create mode 100644 src/render/rust/src/ticket.rs create mode 100644 src/render/rust/src/worker.rs create mode 100644 src/render/rust/tests/cache_test.rs create mode 100644 src/render/rust/tests/common/mod.rs create mode 100644 src/render/rust/tests/copier_test.rs create mode 100644 src/render/rust/tests/ffi_contract_test.rs create mode 100644 src/render/rust/tests/pipeline_test.rs create mode 100644 src/render/rust/tests/ticket_worker_test.rs create mode 100644 src/task/rust/src/project/format.rs create mode 100644 src/task/rust/tests/render_real_integration_test.rs create mode 100644 worker/rust/Cargo.lock create mode 100644 worker/rust/Cargo.toml create mode 100644 worker/rust/README.md create mode 100644 worker/rust/src/ipc.rs create mode 100644 worker/rust/src/main.rs create mode 100644 worker/rust/src/session.rs create mode 100644 worker/rust/src/transport.rs create mode 100644 worker/rust/tests/worker.rs 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 0000000000000000000000000000000000000000..c656a9afc5429dc2e84200c0eee74606491e25b6 GIT binary patch literal 467939 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 literal 0 HcmV?d00001 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 `