diff --git a/Cargo.lock b/Cargo.lock index 1b555db..f50ba39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -182,6 +182,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", "ui_overlay", "uuid", @@ -205,6 +206,26 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image 0.25.8", + "log", + "objc2 0.6.2", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -234,6 +255,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[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 = "async-broadcast" version = "0.7.2" @@ -415,12 +445,6 @@ dependencies = [ "windows-link 0.2.0", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bindgen" version = "0.69.5" @@ -443,25 +467,20 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.72.1" +name = "bit-set" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn", + "bit-vec", ] +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.3" @@ -537,18 +556,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -713,6 +732,26 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -801,19 +840,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics-types" version = "0.1.3" @@ -845,6 +871,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -886,18 +921,21 @@ dependencies = [ "syn", ] -[[package]] -name = "ctor-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" - [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "directories" version = "5.0.1" @@ -957,6 +995,15 @@ 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" @@ -969,19 +1016,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "drm" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" -dependencies = [ - "bitflags 2.9.4", - "bytemuck", - "drm-ffi 0.8.0", - "drm-fourcc", - "rustix 0.38.44", -] - [[package]] name = "drm" version = "0.14.1" @@ -990,29 +1024,19 @@ checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" dependencies = [ "bitflags 2.9.4", "bytemuck", - "drm-ffi 0.9.0", + "drm-ffi", "drm-fourcc", "libc", "rustix 0.38.44", ] -[[package]] -name = "drm-ffi" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" -dependencies = [ - "drm-sys 0.7.0", - "rustix 0.38.44", -] - [[package]] name = "drm-ffi" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" dependencies = [ - "drm-sys 0.8.0", + "drm-sys", "rustix 0.38.44", ] @@ -1024,22 +1048,79 @@ checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" [[package]] name = "drm-sys" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" dependencies = [ "libc", "linux-raw-sys 0.6.5", ] [[package]] -name = "drm-sys" -version = "0.8.0" +name = "ecolor" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +checksum = "adf31f99fad93fe83c1055b92b5c1b135f1ecfa464789817c372000e768d4bd1" dependencies = [ - "libc", - "linux-raw-sys 0.6.5", + "bytemuck", + "emath", +] + +[[package]] +name = "egui" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9b5d3376c79439f53a78bf7da1e3c0b862ffa3e29f46ab0f3e107430f2e576" +dependencies = [ + "ahash", + "bitflags 2.9.4", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef1fe83ba30b3d045814b2d811804f2a7e50a832034c975408f71c20df596e4" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.17", + "type-map", + "web-time", + "wgpu", +] + +[[package]] +name = "egui-winit" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ea8cb063c00d8f23ce11279c01eb63a195a72be0e21d429148246dab7983e" +dependencies = [ + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", ] [[package]] @@ -1048,6 +1129,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "emath" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c615516cdceec867065f20d7db13d8eb8aedd65c9e32cc0c7c379380fa42e6e8" +dependencies = [ + "bytemuck", +] + [[package]] name = "endi" version = "1.1.0" @@ -1075,6 +1165,30 @@ dependencies = [ "syn", ] +[[package]] +name = "epaint" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9926b9500ccb917adb070207ec722dd8ea78b8321f94a85ebec776f501f2930c" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66054d943c66715c6003a27a3dc152d87cadf714ef2597ccd79f550413009b97" + [[package]] name = "equator" version = "0.4.2" @@ -1111,6 +1225,12 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "5.4.1" @@ -1182,18 +1302,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "filetime" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] - [[package]] name = "find-msvc-tools" version = "0.1.1" @@ -1210,6 +1318,18 @@ dependencies = [ "miniz_oxide", ] +[[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 = "foreign-types" version = "0.5.0" @@ -1355,7 +1475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" dependencies = [ "bitflags 2.9.4", - "drm 0.14.1", + "drm", "drm-fourcc", "gbm-sys", "libc", @@ -1447,6 +1567,78 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +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-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.9.4", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.9.4", + "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.9.4", +] + [[package]] name = "half" version = "2.6.0" @@ -1455,6 +1647,7 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", + "num-traits", ] [[package]] @@ -1462,6 +1655,18 @@ 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1481,6 +1686,12 @@ 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 = "iana-time-zone" version = "0.1.63" @@ -1493,7 +1704,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -1673,7 +1884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -1792,6 +2003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", + "libloading", "pkg-config", ] @@ -1845,6 +2057,12 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.9" @@ -1879,7 +2097,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" dependencies = [ - "bindgen 0.69.5", + "bindgen", "cc", "system-deps", ] @@ -1890,14 +2108,14 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22" dependencies = [ - "drm 0.14.1", + "drm", "gbm", "gl", "image 0.25.8", "khronos-egl", "memmap2", "rustix 1.1.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "wayland-backend", "wayland-client", @@ -1929,13 +2147,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[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.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2017,13 +2240,13 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ "bitflags 2.9.4", "block", - "core-graphics-types 0.1.3", + "core-graphics-types 0.2.0", "foreign-types", "log", "objc", @@ -2067,6 +2290,32 @@ dependencies = [ "pxfm", ] +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.0", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.17", + "unicode-ident", +] + [[package]] name = "napi" version = "2.16.17" @@ -2178,6 +2427,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -2213,6 +2468,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -2251,6 +2512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2753,6 +3015,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2780,9 +3051,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2790,15 +3061,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.17", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -2879,7 +3150,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" dependencies = [ - "bindgen 0.69.5", + "bindgen", "libspa-sys", "system-deps", ] @@ -2966,6 +3237,27 @@ dependencies = [ "windows-sys 0.60.2", ] +[[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -2975,6 +3267,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2985,14 +3283,10 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "presser" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "privacy" @@ -3155,6 +3449,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + [[package]] name = "rav1e" version = "0.7.1" @@ -3289,6 +3589,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "renderer" version = "0.1.0" @@ -3484,15 +3790,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" -dependencies = [ - "serde_core", -] - [[package]] name = "services" version = "0.1.0" @@ -3555,52 +3852,20 @@ dependencies = [ ] [[package]] -name = "skia-bindings" -version = "0.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569a07840009ed9e65a70e111b51485eeea8e44b4e84bc0cf698b18ca494662" -dependencies = [ - "bindgen 0.72.1", - "cc", - "flate2", - "heck", - "pkg-config", - "regex", - "serde_json", - "tar", - "toml 0.9.7", -] - -[[package]] -name = "skia-safe" -version = "0.88.0" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f488bd4ff0179b10e2a8e55b93707458ff37c5110458bf00a27518b957252074" -dependencies = [ - "base64", - "bitflags 2.9.4", - "percent-encoding", - "skia-bindings", - "skia-svg-macros", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] -name = "skia-svg-macros" -version = "0.1.0" +name = "slotmap" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "044dd2233c9717a74f75197f3e7f0a966db2127c0ffb5e05013b480a9b75b2c7" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "version_check", ] -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - [[package]] name = "smallvec" version = "1.15.1" @@ -3632,6 +3897,17 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -3642,35 +3918,12 @@ dependencies = [ ] [[package]] -name = "softbuffer" -version = "0.4.6" +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "as-raw-xcb-connection", - "bytemuck", - "cfg_aliases", - "core-graphics 0.24.0", - "drm 0.12.0", - "fastrand", - "foreign-types", - "js-sys", - "log", - "memmap2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", - "raw-window-handle", - "redox_syscall 0.5.17", - "rustix 0.38.44", - "tiny-xlib", - "wasm-bindgen", - "wayland-backend", - "wayland-client", - "wayland-sys", - "web-sys", - "windows-sys 0.59.0", - "x11rb", + "bitflags 2.9.4", ] [[package]] @@ -3728,21 +3981,10 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -3762,6 +4004,15 @@ dependencies = [ "windows-sys 0.61.0", ] +[[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" @@ -3773,11 +4024,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -3793,9 +4044,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3825,6 +4076,37 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3850,19 +4132,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny-xlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" -dependencies = [ - "as-raw-xcb-connection", - "ctor-lite", - "libloading", - "pkg-config", - "tracing", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -3909,26 +4178,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_edit", ] -[[package]] -name = "toml" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -3938,15 +4192,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -3955,26 +4200,11 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" -dependencies = [ + "serde_spanned", + "toml_datetime", "winnow", ] -[[package]] -name = "toml_writer" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" - [[package]] name = "tracing" version = "0.1.41" @@ -3986,6 +4216,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -4042,6 +4284,15 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "uds_windows" version = "1.1.0" @@ -4058,18 +4309,19 @@ name = "ui_overlay" version = "0.1.0" dependencies = [ "anyhow", + "bytemuck", "chrono", - "core-graphics-types 0.1.3", + "egui", + "egui-wgpu", + "egui-winit", "image 0.24.9", - "metal", "objc2 0.6.2", "objc2-app-kit 0.3.1", "objc2-quartz-core 0.3.1", "parking_lot", + "pollster", "raw-window-handle", "rayon", - "skia-safe", - "softbuffer", "thiserror 1.0.69", "tokio", "tracing", @@ -4401,12 +4653,179 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "url", + "web-sys", +] + [[package]] name = "weezl" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.0", + "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 = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.9.4", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.0", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.17", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.9.4", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.0", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.17", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.9.4", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.17", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.0" @@ -4444,6 +4863,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -4451,7 +4880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -4463,7 +4892,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", ] [[package]] @@ -4472,11 +4914,11 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -4485,11 +4927,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -4501,6 +4954,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -4530,10 +4994,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4543,6 +5016,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -4874,7 +5357,7 @@ dependencies = [ "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", - "core-graphics 0.23.2", + "core-graphics", "cursor-icon", "dpi", "js-sys", @@ -4963,16 +5446,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.2", -] - [[package]] name = "xcap" version = "0.7.0" @@ -4997,10 +5470,10 @@ dependencies = [ "rand 0.9.2", "scopeguard", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "widestring", - "windows", + "windows 0.61.3", "xcb", "zbus", ] diff --git a/assets/icons/arrow.svg b/assets/icons/arrow.svg new file mode 100644 index 0000000..3f2ff2d --- /dev/null +++ b/assets/icons/arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/brush.svg b/assets/icons/brush.svg new file mode 100644 index 0000000..62ea09c --- /dev/null +++ b/assets/icons/brush.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000..1af6ce4 --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/close.svg b/assets/icons/close.svg new file mode 100644 index 0000000..e417e2f --- /dev/null +++ b/assets/icons/close.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/rectangle.svg b/assets/icons/rectangle.svg new file mode 100644 index 0000000..a41a8d9 --- /dev/null +++ b/assets/icons/rectangle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/crates/api_cli/Cargo.toml b/crates/api_cli/Cargo.toml index 9c1b449..6884fca 100644 --- a/crates/api_cli/Cargo.toml +++ b/crates/api_cli/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" clap = { version = "4", features=["derive"] } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" anyhow = { workspace = true } tokio = { workspace = true } screenshot_core = { path = "../core" } diff --git a/crates/api_cli/src/main.rs b/crates/api_cli/src/main.rs index c477b7d..c4511a0 100644 --- a/crates/api_cli/src/main.rs +++ b/crates/api_cli/src/main.rs @@ -8,7 +8,8 @@ use services::StubClipboard; use services::{gen_file_name, ExportService}; use std::path::PathBuf; use std::sync::Arc; -use tracing_subscriber::{fmt, EnvFilter}; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; #[derive(Parser)] #[command( @@ -60,15 +61,96 @@ struct CaptureInteractiveArgs { clipboard: bool, } +/// 初始化日志系统 - 同时输出到控制台和文件 +fn init_logging() { + use chrono::Local; + + // 创建日志目录 + let log_dir = std::env::temp_dir().join("screenshot_logs"); + std::fs::create_dir_all(&log_dir).ok(); + + // 生成带时间戳的日志文件名 + let timestamp = Local::now().format("%Y%m%d_%H%M%S"); + let log_filename = format!("screenshot_{}.log", timestamp); + + // 创建文件 appender(每天滚动) + let file_appender = RollingFileAppender::builder() + .rotation(Rotation::DAILY) + .filename_prefix("screenshot") + .filename_suffix("log") + .max_log_files(7) // 保留最近 7 天的日志 + .build(&log_dir) + .expect("Failed to create log file appender"); + + // 获取环境变量中的日志级别,默认为 info + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + // 创建控制台输出层 + let console_layer = fmt::layer().with_target(false).with_writer(std::io::stdout); + + // 创建文件输出层(包含更详细的信息) + let file_layer = fmt::layer() + .with_target(true) + .with_line_number(true) + .with_ansi(false) // 文件不需要 ANSI 颜色 + .with_writer(file_appender); + + // 组合所有层 + tracing_subscriber::registry() + .with(env_filter) + .with(console_layer) + .with(file_layer) + .init(); + + // 打印日志文件位置 + let log_path = format!("{}/{}", log_dir.display(), log_filename); + println!("📝 日志文件: {}", log_path); + tracing::info!("========================================"); + tracing::info!("Screenshot Tool - Session Start"); + tracing::info!("Version: {}", env!("CARGO_PKG_VERSION")); + tracing::info!("Log file: {}", log_path); + tracing::info!("========================================"); + + // 设置 panic hook,将 panic 信息记录到日志 + let log_path_clone = log_path.clone(); + std::panic::set_hook(Box::new(move |panic_info| { + let payload = panic_info.payload(); + let message = if let Some(s) = payload.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "Unknown panic payload".to_string() + }; + + let location = if let Some(location) = panic_info.location() { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + } else { + "Unknown location".to_string() + }; + + tracing::error!("========================================"); + tracing::error!("PANIC OCCURRED!"); + tracing::error!("Message: {}", message); + tracing::error!("Location: {}", location); + tracing::error!("========================================"); + + eprintln!("\n❌ 程序发生严重错误 (panic)!"); + eprintln!("📝 详细错误信息已保存到日志文件: {}", log_path_clone); + eprintln!("💡 请将日志文件提供给开发者以便排查问题。"); + eprintln!("\nPanic: {} at {}", message, location); + })); +} + #[tokio::main] async fn main() { - // 初始化日志 - let _ = fmt() - .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), - ) - .with_target(false) - .try_init(); + // 初始化日志 - 同时输出到控制台和文件 + init_logging(); let cli = Cli::parse(); match cli.command { diff --git a/crates/platform_mac/src/lib.rs b/crates/platform_mac/src/lib.rs index 21c3b78..2c18031 100644 --- a/crates/platform_mac/src/lib.rs +++ b/crates/platform_mac/src/lib.rs @@ -416,9 +416,9 @@ impl MacCapturer { let monitor_id = monitor.id().ok()?; let display_info = virtual_desktop.displays.iter().find(|d| d.id == monitor_id)?; - // 捕获当前显示器 + // 捕获当前显示器(注意:xcap 的 capture_image() 返回物理像素图像) let img = monitor.capture_image().ok()?; - let (mon_width, mon_height) = (img.width(), img.height()); + let (mon_width_physical, mon_height_physical) = (img.width(), img.height()); let rgba_data = img.into_raw(); // 计算在虚拟桌面画布中的位置(物理坐标) @@ -430,32 +430,35 @@ impl MacCapturer { #[cfg(debug_assertions)] tracing::debug!( - "显示器合成 [物理坐标]: 显示器{} 逻辑({},{}) -> 物理({},{}) -> canvas({},{}) 尺寸{}x{}", + "显示器合成: 显示器{} 逻辑({},{}) scale={} -> 物理({},{}) -> canvas({},{}) 图像尺寸{}x{} (物理)", monitor_id, display_info.x, display_info.y, + scale, phys_x, phys_y, canvas_x, canvas_y, - mon_width, - mon_height + mon_width_physical, + mon_height_physical ); - Some((rgba_data, mon_width, mon_height, canvas_x, canvas_y)) + Some((rgba_data, mon_width_physical, mon_height_physical, canvas_x, canvas_y)) }) .collect(); - // 串行合成到画布(避免数据竞争) - for (rgba_data, mon_width, mon_height, canvas_x, canvas_y) in captured_monitors { - for row in 0..mon_height { + // 串行合成到画布(图像已经是物理像素,直接复制) + for (rgba_data, mon_width_physical, mon_height_physical, canvas_x, canvas_y) in + captured_monitors + { + for row in 0..mon_height_physical { if canvas_y + row >= canvas_height { break; } - let src_row_start = (row * mon_width * 4) as usize; - let src_row_end = src_row_start + (mon_width * 4) as usize; + let src_row_start = (row * mon_width_physical * 4) as usize; + let src_row_end = src_row_start + (mon_width_physical * 4) as usize; let dst_row_start = (((canvas_y + row) * canvas_width + canvas_x) * 4) as usize; - let dst_row_end = dst_row_start + (mon_width * 4) as usize; + let dst_row_end = dst_row_start + (mon_width_physical * 4) as usize; if dst_row_end <= canvas.len() && src_row_end <= rgba_data.len() { canvas[dst_row_start..dst_row_end] @@ -639,6 +642,15 @@ impl MacCapturer { } // 构建虚拟桌面信息用于区域选择(物理坐标) + #[cfg(debug_assertions)] + tracing::info!( + "[platform_mac] 传递给 ui_overlay 的 virtual_bounds (物理坐标): min=({}, {}), size={}x{}", + physical_min_x, + physical_min_y, + physical_width, + physical_height + ); + let virtual_bounds = ( physical_min_x, physical_min_y, @@ -673,18 +685,39 @@ impl MacCapturer { } }; - // 坐标已经是物理虚拟桌面坐标,直接使用 + // 坐标已经是逻辑坐标,需要乘以 scale 转换为物理坐标 let scale = if rect.scale.is_finite() && rect.scale > 0.0 { rect.scale } else { 1.0 } as f32; + tracing::info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + tracing::info!("[platform_mac] 收到的 Region (逻辑坐标):"); + tracing::info!( + " └─ x={}, y={}, w={}, h={}", + rect.x, + rect.y, + rect.w, + rect.h + ); + tracing::info!(" └─ scale={}", rect.scale); + let x_virtual = (rect.x * scale).floor() as i32; // 不使用 max(0.0),保留负坐标 let y_virtual = (rect.y * scale).floor() as i32; // 不使用 max(0.0),保留负坐标 let w = (rect.w * scale).round().max(0.0) as u32; let h = (rect.h * scale).round().max(0.0) as u32; + tracing::info!("[platform_mac] 转换为物理坐标:"); + tracing::info!( + " └─ x_virtual={}, y_virtual={}, w={}, h={}", + x_virtual, + y_virtual, + w, + h + ); + tracing::info!(" └─ 计算公式: 逻辑坐标 * scale = 物理坐标"); + // 在虚拟桌面坐标系中进行裁剪 // 注意:现在选择区域和画布都是物理坐标,所以使用物理边界 let canvas_x = (x_virtual - physical_min_x).max(0) as u32; diff --git a/crates/ui_overlay/Cargo.toml b/crates/ui_overlay/Cargo.toml index 580f57d..acb7143 100644 --- a/crates/ui_overlay/Cargo.toml +++ b/crates/ui_overlay/Cargo.toml @@ -14,23 +14,21 @@ tokio = { workspace = true } rayon = { workspace = true } winit = "0.30" image = { workspace = true } -softbuffer = "0.4.6" raw-window-handle = "0.6" -# 默认使用工作区的 skia-safe 配置(GL + textlayout + svg) -skia-safe = { workspace = true } +# egui UI 框架(用于 UI 渲染和标注工具栏) +egui = "0.33" +egui-winit = "0.33" +egui-wgpu = "0.33" +# 不显式指定 wgpu 版本,使用 egui-wgpu 依赖的版本 +pollster = "0.4" +# 用于背景渲染 +bytemuck = { version = "1.14", features = ["derive"] } -# macOS 依赖:用于窗口层级/行为控制和 Metal GPU 渲染 +# macOS 依赖:用于窗口层级/行为控制 [target.'cfg(target_os = "macos")'.dependencies] -# 在 macOS 上启用 Metal 特性 -skia-safe = { version = "0.88", features = ["gl", "metal", "textlayout", "svg"] } - # Objective-C 运行时交互(平台集成) objc2 = "0.6.2" objc2-app-kit = "0.3.1" objc2-quartz-core = "0.3.1" -# Metal GPU 渲染 -metal = "0.29" -core-graphics-types = "0.1" - diff --git a/crates/ui_overlay/examples/test_metal_backend.rs b/crates/ui_overlay/examples/test_metal_backend.rs deleted file mode 100644 index 8b231ff..0000000 --- a/crates/ui_overlay/examples/test_metal_backend.rs +++ /dev/null @@ -1,109 +0,0 @@ -/// 测试新的 Metal Backend 实现 -/// -/// 运行方式: -/// cargo run -p ui_overlay --example test_metal_backend -use ui_overlay::backend::{create_backend, BackendType}; -use winit::event_loop::EventLoop; -use winit::window::WindowAttributes; - -fn main() -> Result<(), Box> { - println!("🧪 测试新的 Metal Backend 实现"); - println!("==================================\n"); - - // 创建事件循环和窗口 - let event_loop = EventLoop::new()?; - #[allow(deprecated)] - let window = event_loop.create_window( - WindowAttributes::default() - .with_title("Metal Backend Test") - .with_inner_size(winit::dpi::LogicalSize::new(800, 600)), - )?; - - // 测试后端创建 - println!("📋 测试 1: 创建 Render Backend"); - let backend = create_backend(Some(&window), 800, 600); - - match backend.backend_type() { - BackendType::MetalGpu => { - println!(" ✅ 成功创建 Metal GPU Backend"); - println!(" 🚀 真正的 GPU 硬件加速!"); - } - BackendType::CpuRaster => { - println!(" ⚠️ 降级到 CPU Raster Backend"); - println!(" 💡 Metal GPU 初始化失败,使用 CPU 软件渲染"); - } - _ => { - println!(" ❓ 未知的 Backend 类型"); - } - } - - println!("\n📋 测试 2: 准备 Surface"); - let mut backend = backend; - match backend.prepare_surface(800, 600) { - Ok(_) => println!(" ✅ Surface 准备成功"), - Err(e) => { - println!(" ❌ Surface 准备失败: {}", e); - return Ok(()); - } - } - - println!("\n📋 测试 3: 获取 Canvas"); - match backend.canvas() { - Some(canvas) => { - println!(" ✅ 成功获取 Canvas"); - - // 简单绘制测试 - canvas.clear(skia_safe::Color::from_argb(255, 64, 128, 255)); - - let mut paint = skia_safe::Paint::default(); - paint.set_color(skia_safe::Color::WHITE); - paint.set_style(skia_safe::paint::Style::Fill); - paint.set_anti_alias(true); - - canvas.draw_circle((400.0, 300.0), 50.0, &paint); - - println!(" 🎨 在 Canvas 上绘制了一个白色圆形"); - } - None => { - println!(" ❌ 无法获取 Canvas"); - return Ok(()); - } - } - - println!("\n📋 测试 4: Flush 渲染"); - match backend.flush_and_read_pixels() { - Ok(pixels) => { - if pixels.is_empty() { - println!(" ✅ GPU 渲染:直接 flush 到屏幕(无像素拷贝)"); - println!(" 🚀 零内存拷贝,极致性能!"); - } else { - println!(" ✅ CPU 渲染:读取了 {} 字节像素数据", pixels.len()); - println!(" 💡 CPU 软件渲染模式"); - } - } - Err(e) => println!(" ❌ Flush 失败: {}", e), - } - - println!("\n=================================="); - println!("🎉 测试完成!"); - - match backend.backend_type() { - BackendType::MetalGpu => { - println!("\n✨ Metal GPU Backend 工作正常!"); - println!("📊 预期性能:"); - println!(" - FPS: 120+"); - println!(" - CPU 使用: 5-10%"); - println!(" - 内存拷贝: 0 MB/frame"); - } - BackendType::CpuRaster => { - println!("\n⚠️ 当前使用 CPU Backend"); - println!("📊 当前性能:"); - println!(" - FPS: 60"); - println!(" - CPU 使用: 30-40%"); - println!(" - 内存拷贝: 8 MB/frame"); - } - _ => {} - } - - Ok(()) -} diff --git a/crates/ui_overlay/src/backend/cpu_backend.rs b/crates/ui_overlay/src/backend/cpu_backend.rs deleted file mode 100644 index 9806d9d..0000000 --- a/crates/ui_overlay/src/backend/cpu_backend.rs +++ /dev/null @@ -1,161 +0,0 @@ -/// CPU 软件渲染后端 -/// -/// 使用 Skia 的 CPU raster surface 进行软件渲染 -/// 优点:兼容性最好,所有平台都支持 -/// 缺点:性能较低,需要 CPU 读取像素并通过 softbuffer 呈现 -use anyhow::{anyhow, Result}; -use skia_safe::{AlphaType, Canvas, Color, ColorType, ImageInfo, Surface}; - -use super::{BackendType, RenderBackend}; - -/// CPU 软件渲染后端实现 -pub struct CpuRasterBackend { - /// Skia CPU 渲染 surface - surface: Surface, - /// Surface 宽度 - width: i32, - /// Surface 高度 - height: i32, -} - -impl CpuRasterBackend { - /// 创建新的 CPU 渲染后端 - pub fn new(width: i32, height: i32) -> Result { - let surface = Self::create_surface(width, height)?; - - println!("🖥️ 使用 CPU 软件渲染后端 ({}x{})", width, height); - - Ok(Self { - surface, - width, - height, - }) - } - - /// 创建 Skia CPU raster surface - fn create_surface(width: i32, height: i32) -> Result { - let mut surface = skia_safe::surfaces::raster_n32_premul((width, height)) - .ok_or_else(|| anyhow!("Failed to create CPU raster surface"))?; - - // 清空 surface - surface.canvas().clear(Color::TRANSPARENT); - - Ok(surface) - } - - /// 读取 surface 像素数据 - fn read_pixels(&mut self) -> Result> { - let width = self.width; - let height = self.height; - - // 创建像素缓冲区 - let pixel_count = (width * height) as usize; - let mut pixels = vec![0u8; pixel_count * 4]; - - // 定义目标图像格式(RGBA8888, Unpremul) - let image_info = ImageInfo::new( - (width, height), - ColorType::RGBA8888, - AlphaType::Unpremul, - None, - ); - - // 从 surface 读取像素 - let row_bytes = (width * 4) as usize; - if self - .surface - .read_pixels(&image_info, pixels.as_mut_slice(), row_bytes, (0, 0)) - { - Ok(pixels) - } else { - Err(anyhow!("Failed to read pixels from CPU surface")) - } - } -} - -impl RenderBackend for CpuRasterBackend { - fn backend_type(&self) -> BackendType { - BackendType::CpuRaster - } - - fn prepare_surface(&mut self, width: i32, height: i32) -> Result<()> { - // 如果尺寸未变化,复用现有 surface - if width == self.width && height == self.height { - return Ok(()); - } - - // 尺寸变化,重新创建 surface - self.surface = Self::create_surface(width, height)?; - self.width = width; - self.height = height; - - Ok(()) - } - - fn canvas(&mut self) -> Option<&Canvas> { - // 获取 surface 的 canvas(内部可变性) - Some(self.surface.canvas()) - } - - fn flush_and_read_pixels(&mut self) -> Result> { - // CPU 渲染需要读取像素数据 - self.read_pixels() - } - - fn resize(&mut self, width: i32, height: i32) { - // 标记需要重建 surface - self.width = width; - self.height = height; - // 实际重建在下次 prepare_surface 时进行 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cpu_backend_creation() { - let backend = CpuRasterBackend::new(800, 600); - assert!(backend.is_ok()); - - let backend = backend.unwrap(); - assert_eq!(backend.backend_type(), BackendType::CpuRaster); - assert_eq!(backend.width, 800); - assert_eq!(backend.height, 600); - } - - #[test] - fn test_cpu_backend_resize() { - let mut backend = CpuRasterBackend::new(800, 600).unwrap(); - - backend.resize(1024, 768); - backend.prepare_surface(1024, 768).unwrap(); - - assert_eq!(backend.width, 1024); - assert_eq!(backend.height, 768); - } - - #[test] - fn test_cpu_backend_render() { - let mut backend = CpuRasterBackend::new(100, 100).unwrap(); - - // 获取 canvas 并绘制 - if let Some(canvas) = backend.canvas() { - canvas.clear(Color::from_argb(255, 255, 0, 0)); // 红色背景 - } - - // 读取像素 - let pixels = backend.flush_and_read_pixels(); - assert!(pixels.is_ok()); - - let pixels = pixels.unwrap(); - assert_eq!(pixels.len(), 100 * 100 * 4); - - // 检查第一个像素是否为红色(RGBA) - assert_eq!(pixels[0], 255); // R - assert_eq!(pixels[1], 0); // G - assert_eq!(pixels[2], 0); // B - assert_eq!(pixels[3], 255); // A - } -} diff --git a/crates/ui_overlay/src/backend/factory.rs b/crates/ui_overlay/src/backend/factory.rs deleted file mode 100644 index 6fccf81..0000000 --- a/crates/ui_overlay/src/backend/factory.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::{CpuRasterBackend, RenderBackend}; - -#[cfg(target_os = "macos")] -use super::MetalBackend; - -#[cfg(test)] -use super::BackendType; - -#[cfg(test)] -use anyhow::Result; - -/// 创建最佳可用的渲染后端 -/// -/// 尝试顺序: -/// 1. macOS: Metal GPU > CPU Raster -/// 2. Windows: Direct3D GPU > CPU Raster (Phase 3) -/// 3. 其他平台: CPU Raster -pub fn create_backend( - #[allow(unused_variables)] window: Option<&winit::window::Window>, - width: i32, - height: i32, -) -> Box { - // macOS: 尝试 Metal GPU - #[cfg(target_os = "macos")] - { - if let Some(win) = window { - if let Ok(metal_backend) = MetalBackend::new(win, width, height) { - println!("✅ 使用 Metal GPU 渲染后端"); - return Box::new(metal_backend); - } else { - println!("⚠️ Metal GPU 初始化失败,降级到 CPU 渲染"); - } - } - } - - // Windows: 尝试 Direct3D GPU (Phase 3) - #[cfg(target_os = "windows")] - { - // Phase 3: 实现 D3D backend - // if let Some(win) = window { - // if let Ok(d3d_backend) = D3DBackend::new(win, width, height) { - // println!("✅ 使用 Direct3D GPU 渲染后端"); - // return Box::new(d3d_backend); - // } - // } - println!("⚠️ Direct3D 后端未实现(Phase 3),使用 CPU 渲染"); - } - - // 降级到 CPU 渲染 - match CpuRasterBackend::new(width, height) { - Ok(cpu_backend) => { - println!("✅ 使用 CPU 软件渲染后端"); - Box::new(cpu_backend) - } - Err(e) => { - panic!("Failed to create CPU raster backend: {}", e); - } - } -} - -/// 创建指定类型的渲染后端(用于测试) -#[cfg(test)] -pub fn create_backend_with_type( - backend_type: BackendType, - #[allow(unused_variables)] window: Option<&winit::window::Window>, - width: i32, - height: i32, -) -> Result> { - match backend_type { - BackendType::CpuRaster => { - let backend = CpuRasterBackend::new(width, height)?; - Ok(Box::new(backend)) - } - - #[cfg(target_os = "macos")] - BackendType::MetalGpu => { - if let Some(win) = window { - let backend = MetalBackend::new(win, width, height)?; - Ok(Box::new(backend)) - } else { - Err(anyhow::anyhow!("Window required for Metal backend")) - } - } - - #[cfg(not(target_os = "macos"))] - BackendType::MetalGpu => Err(anyhow::anyhow!("Metal backend only available on macOS")), - - BackendType::Direct3dGpu => { - // Phase 3: 实现 D3D backend - Err(anyhow::anyhow!( - "Direct3D backend not implemented yet (Phase 3)" - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_cpu_backend() { - let backend = create_backend(None, 800, 600); - assert_eq!(backend.backend_type(), BackendType::CpuRaster); - } - - #[test] - fn test_create_backend_with_type() { - let backend = create_backend_with_type(BackendType::CpuRaster, None, 800, 600); - assert!(backend.is_ok()); - assert_eq!(backend.unwrap().backend_type(), BackendType::CpuRaster); - } -} diff --git a/crates/ui_overlay/src/backend/metal_backend.rs b/crates/ui_overlay/src/backend/metal_backend.rs deleted file mode 100644 index 6f12c00..0000000 --- a/crates/ui_overlay/src/backend/metal_backend.rs +++ /dev/null @@ -1,219 +0,0 @@ -/// macOS Metal GPU 渲染后端 -/// -/// 使用 metal-rs + Skia Metal backend 实现 GPU 加速渲染 -use anyhow::{anyhow, Result}; -use core_graphics_types::geometry::CGSize; -use metal::foreign_types::{ForeignType, ForeignTypeRef}; -use metal::{CommandQueue, Device, MTLPixelFormat, MetalDrawable, MetalLayer}; -use raw_window_handle::{HasWindowHandle, RawWindowHandle}; -use skia_safe::{ - gpu::{self, mtl, DirectContext, SurfaceOrigin}, - Canvas, ColorType, Surface, -}; -use winit::window::Window; - -use super::{BackendType, RenderBackend}; - -/// Metal GPU 渲染后端实现 -pub struct MetalBackend { - /// Metal device(保留用于生命周期管理) - #[allow(dead_code)] - device: Device, - /// Metal command queue(保留用于生命周期管理) - #[allow(dead_code)] - queue: CommandQueue, - /// CAMetalLayer - layer: MetalLayer, - /// Skia DirectContext - direct_context: DirectContext, - /// 当前 surface(每帧创建) - surface: Option, - /// 当前 drawable(每帧创建) - current_drawable: Option, - /// Surface 宽度 - width: i32, - /// Surface 高度 - height: i32, -} - -impl MetalBackend { - /// 创建新的 Metal 渲染后端 - pub fn new(window: &Window, width: i32, height: i32) -> Result { - // 1. 创建 Metal device - let device = Device::system_default() - .ok_or_else(|| anyhow!("Failed to get Metal system default device"))?; - - // 2. 创建 command queue - let queue = device.new_command_queue(); - - // 3. 创建 CAMetalLayer 并设置到窗口 - let layer = unsafe { - let window_handle = window - .window_handle() - .map_err(|e| anyhow!("Failed to get window handle: {}", e))?; - - match window_handle.as_raw() { - RawWindowHandle::AppKit(handle) => { - use objc2::rc::Retained; - use objc2_app_kit::NSView; - use objc2_quartz_core::CALayer; - - let view_ptr = handle.ns_view.as_ptr() as *mut NSView; - let view = &*view_ptr; - - // 创建 CAMetalLayer - let layer = MetalLayer::new(); - layer.set_device(&device); - layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - layer.set_presents_with_transaction(false); - - // 设置 layer 尺寸 - layer.set_drawable_size(CGSize::new(width as f64, height as f64)); - - // 设置 layer 为透明,避免粉色调试背景 - // 注意:CAMetalLayer 默认就是透明的,但我们明确设置以确保 - layer.set_opaque(false); - - // 启用 VSync 显示同步,避免画面撕裂和闪烁 - layer.set_display_sync_enabled(true); - - // 设置最大可绘制数为 2(双缓冲),减少闪烁 - layer.set_maximum_drawable_count(2); - - // 设置 layer 到 view(使用 objc2-app-kit) - let layer_obj = layer.as_ptr() as *mut CALayer; - let layer_retained: Retained = Retained::retain(layer_obj) - .ok_or_else(|| anyhow!("Failed to retain CAMetalLayer"))?; - - view.setWantsLayer(true); - view.setLayer(Some(&*layer_retained)); - - layer - } - _ => { - return Err(anyhow!("Not a macOS AppKit window")); - } - } - }; - - // 4. 创建 Skia DirectContext(使用新 API) - let backend_context = unsafe { - mtl::BackendContext::new( - device.as_ptr() as *mut std::ffi::c_void, - queue.as_ptr() as *mut std::ffi::c_void, - ) - }; - - let direct_context = gpu::direct_contexts::make_metal(&backend_context, None) - .ok_or_else(|| anyhow!("Failed to create Skia Metal DirectContext"))?; - - println!( - "🚀 使用 Metal GPU 渲染后端 ({}x{}) - metal-rs", - width, height - ); - - Ok(Self { - device, - queue, - layer, - direct_context, - surface: None, - current_drawable: None, - width, - height, - }) - } -} - -impl RenderBackend for MetalBackend { - fn backend_type(&self) -> BackendType { - BackendType::MetalGpu - } - - fn prepare_surface(&mut self, width: i32, height: i32) -> Result<()> { - // 更新尺寸(如果变化) - if width != self.width || height != self.height { - self.width = width; - self.height = height; - self.layer - .set_drawable_size(CGSize::new(width as f64, height as f64)); - } - - // 从 CAMetalLayer 获取下一个 drawable - let drawable = self - .layer - .next_drawable() - .ok_or_else(|| anyhow!("Failed to get next drawable from CAMetalLayer"))?; - - // 获取 drawable 的 texture - let texture = drawable.texture(); - - // 创建 Skia GPU surface(使用新 API) - let texture_info = - unsafe { mtl::TextureInfo::new(texture.as_ptr() as *mut std::ffi::c_void) }; - - let backend_render_target = - gpu::backend_render_targets::make_mtl((width, height), &texture_info); - - let surface = gpu::surfaces::wrap_backend_render_target( - &mut self.direct_context, - &backend_render_target, - SurfaceOrigin::TopLeft, - ColorType::BGRA8888, - None, - None, - ) - .ok_or_else(|| anyhow!("Failed to create surface from Metal render target"))?; - - // 保存 drawable 和 surface - self.current_drawable = Some(drawable.to_owned()); - self.surface = Some(surface); - - Ok(()) - } - - fn canvas(&mut self) -> Option<&Canvas> { - // Surface 的 canvas() 需要可变借用来返回 canvas - self.surface.as_mut().map(|s| s.canvas()) - } - - fn flush_and_read_pixels(&mut self) -> Result> { - // 1. Flush Skia GPU commands to Metal - self.direct_context.flush_and_submit(); - - // 2. Present drawable to screen(通过 command buffer 实现 VSync) - if let Some(drawable) = self.current_drawable.take() { - // 使用 command buffer 的 present 方法,遵循 display_sync_enabled 设置 - let command_buffer = self.queue.new_command_buffer(); - command_buffer.present_drawable(&drawable); - command_buffer.commit(); - // 异步提交,不等待完成,让 GPU 并行处理以提升性能 - } - - // 3. GPU backend 直接渲染到屏幕,无需返回像素数据 - Ok(Vec::new()) - } - - fn resize(&mut self, width: i32, height: i32) { - self.width = width; - self.height = height; - // 下次 prepare_surface 时会更新 layer 尺寸 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // 注意:Metal backend 需要真实的窗口环境,无法在单元测试中测试 - // 这里只提供基本的类型测试 - - #[test] - fn test_backend_type() { - // 测试 backend type 常量 - assert_eq!( - std::mem::discriminant(&BackendType::MetalGpu), - std::mem::discriminant(&BackendType::MetalGpu) - ); - } -} diff --git a/crates/ui_overlay/src/backend/mod.rs b/crates/ui_overlay/src/backend/mod.rs deleted file mode 100644 index a855431..0000000 --- a/crates/ui_overlay/src/backend/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -/// 渲染后端抽象层 -/// -/// 提供统一的渲染接口,支持多种后端实现: -/// - CPU Raster: 软件渲染(兼容性最好) -/// - Metal GPU: macOS 硬件加速 -/// - Direct3D GPU: Windows 硬件加速 -use anyhow::Result; -use skia_safe::Canvas; - -/// 渲染后端类型 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BackendType { - /// CPU 软件渲染 - CpuRaster, - /// macOS Metal GPU - MetalGpu, - /// Windows Direct3D GPU - Direct3dGpu, -} - -/// 跨平台渲染后端抽象 -/// -/// 所有渲染后端必须实现此 trait,提供统一的渲染接口 -pub trait RenderBackend { - /// 获取后端类型 - fn backend_type(&self) -> BackendType; - - /// 准备渲染 surface - /// - /// 对于 GPU 后端,可能每帧创建新的 surface(从 drawable) - /// 对于 CPU 后端,surface 可以复用 - fn prepare_surface(&mut self, width: i32, height: i32) -> Result<()>; - - /// 获取 canvas 用于绘制 - /// - /// 注意:Canvas 使用内部可变性,即使是不可变引用也可以进行绘制 - fn canvas(&mut self) -> Option<&Canvas>; - - /// 提交渲染并获取像素数据(如需要) - /// - /// - GPU 后端:直接 flush 到屏幕,返回空 Vec - /// - CPU 后端:读取像素数据,返回 RGBA 字节数组 - fn flush_and_read_pixels(&mut self) -> Result>; - - /// 处理窗口大小变化 - fn resize(&mut self, width: i32, height: i32); -} - -// 导出各平台实现 -mod cpu_backend; -pub use cpu_backend::CpuRasterBackend; - -#[cfg(target_os = "macos")] -mod metal_backend; -#[cfg(target_os = "macos")] -pub use metal_backend::MetalBackend; - -// 导出工厂函数 -mod factory; -pub use factory::create_backend; diff --git a/crates/ui_overlay/src/background_renderer.rs b/crates/ui_overlay/src/background_renderer.rs new file mode 100644 index 0000000..6e481ee --- /dev/null +++ b/crates/ui_overlay/src/background_renderer.rs @@ -0,0 +1,289 @@ +/// 背景渲染器 +/// +/// 使用 wgpu 直接渲染背景图,绕过 egui 的纹理大小限制(2048x2048) +/// +/// ## 设计说明 +/// +/// ### 为什么使用 wgpu 直接渲染? +/// - egui 对单个纹理有 2048x2048 的尺寸限制 +/// - 多显示器场景下,完整虚拟桌面背景可能远超此限制(如 7680x4320) +/// - wgpu 可以直接渲染更大的纹理(设备限制通常是 8192x8192 或更大) +/// +/// ### 为什么裁剪背景而不是 UV 映射? +/// - **代码简单**:每个窗口直接上传裁剪后的区域,shader 不需要复杂的 UV 计算 +/// - **性能好**:上传的数据量更小,GPU 内存占用更少 +/// - **易维护**:逻辑清晰,调试方便 +/// +/// ### 渲染流程 +/// 1. **wgpu 背景层**:全屏显示裁剪后的背景图(本模块) +/// 2. **egui UI 层**:绘制半透明遮罩和选择框(selection_render.rs) +/// +use egui_wgpu::wgpu; + +pub struct BackgroundRenderer { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, + vertex_buffer: wgpu::Buffer, + index_buffer: wgpu::Buffer, +} + +impl BackgroundRenderer { + /// 创建背景渲染器 + pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self { + // 创建纹理 bind group layout + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("background_bind_group_layout"), + entries: &[ + // 纹理 + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // 采样器 + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + // 创建 pipeline layout + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("background_pipeline_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + // 着色器代码 + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("background_shader"), + source: wgpu::ShaderSource::Wgsl(BACKGROUND_SHADER.into()), + }); + + // 创建渲染管线 + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("background_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: 4 * 4, // 4 floats: pos(x,y) + uv(x,y) + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2], + }], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + // 创建采样器 + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("background_sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + // 创建全屏四边形的顶点缓冲区 + // 顶点格式:[x, y, u, v] + #[rustfmt::skip] + let vertices: &[f32] = &[ + // 左上 + -1.0, 1.0, 0.0, 0.0, + // 右上 + 1.0, 1.0, 1.0, 0.0, + // 左下 + -1.0, -1.0, 0.0, 1.0, + // 右下 + 1.0, -1.0, 1.0, 1.0, + ]; + + let vertex_data = bytemuck::cast_slice(vertices); + let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("background_vertex_buffer"), + size: vertex_data.len() as u64, + usage: wgpu::BufferUsages::VERTEX, + mapped_at_creation: true, + }); + vertex_buffer + .slice(..) + .get_mapped_range_mut() + .copy_from_slice(vertex_data); + vertex_buffer.unmap(); + + // 索引缓冲区(两个三角形) + let indices: &[u16] = &[0, 2, 1, 1, 2, 3]; + let index_data = bytemuck::cast_slice(indices); + let index_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("background_index_buffer"), + size: index_data.len() as u64, + usage: wgpu::BufferUsages::INDEX, + mapped_at_creation: true, + }); + index_buffer + .slice(..) + .get_mapped_range_mut() + .copy_from_slice(index_data); + index_buffer.unmap(); + + Self { + pipeline, + bind_group_layout, + sampler, + vertex_buffer, + index_buffer, + } + } + + /// 创建全屏背景纹理 + /// + /// 每个窗口上传裁剪后的背景区域,直接全屏显示 + /// + /// # Arguments + /// * `data` - RGBA8 纹理数据(已裁剪到窗口大小) + /// * `width` - 纹理宽度(应与窗口宽度一致) + /// * `height` - 纹理高度(应与窗口高度一致) + /// + /// # Returns + /// `(Texture, BindGroup)` - 纹理对象和用于渲染的 bind group + pub fn create_texture( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + data: &[u8], + width: u32, + height: u32, + ) -> (wgpu::Texture, wgpu::BindGroup) { + // 创建纹理 + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("background_texture"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + // 上传纹理数据 + queue.write_texture( + texture.as_image_copy(), + data, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width), + rows_per_image: Some(height), + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + // 创建纹理视图 + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + // 创建纹理 bind group + let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("background_texture_bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + ], + }); + + (texture, texture_bind_group) + } + + /// 渲染全屏背景纹理 + /// + /// # Arguments + /// * `render_pass` - wgpu 渲染通道 + /// * `texture_bind_group` - 由 `create_texture()` 返回的 bind group + pub fn render<'rpass>( + &'rpass self, + render_pass: &mut wgpu::RenderPass<'rpass>, + texture_bind_group: &'rpass wgpu::BindGroup, + ) { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, texture_bind_group, &[]); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed(0..6, 0, 0..1); + } +} + +// Shader 代码(简化版:全屏显示纹理,无 UV 映射) +const BACKGROUND_SHADER: &str = r#" +struct VertexInput { + @location(0) position: vec2, + @location(1) uv: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.clip_position = vec4(input.position, 0.0, 1.0); + output.uv = input.uv; // 直接使用输入 UV,全屏显示纹理 + return output; +} + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return textureSample(t_diffuse, s_diffuse, input.uv); +} +"#; diff --git a/crates/ui_overlay/src/event_handler.rs b/crates/ui_overlay/src/event_handler.rs index b8bf667..0cacd53 100644 --- a/crates/ui_overlay/src/event_handler.rs +++ b/crates/ui_overlay/src/event_handler.rs @@ -107,36 +107,43 @@ impl EventHandler { state.dragging = true; state.start = state.curr; state.invalidate_cache(); // 开始拖动时使缓存失效 + tracing::debug!( + "🖱️ 开始拖拽: 起点=({:.0}, {:.0})", + state.start.0, + state.start.1 + ); EventResult::Continue(true) } (MouseButton::Left, ElementState::Released) => { state.dragging = false; - // 拖动结束后继续保持选框显示,等待用户按 Enter 确认 - // 触发重绘以显示最终选框 + tracing::debug!( + "🖱️ 结束拖拽: 终点=({:.0}, {:.0})", + state.curr.0, + state.curr.1 + ); + // 拖动结束后显示工具栏 + if state.has_valid_selection() { + let (x0, y0, x1, y1) = state.calculate_selection_rect(); + tracing::debug!( + " └─ 有效选择: ({:.0}, {:.0}) -> ({:.0}, {:.0}), 尺寸=({:.0}x{:.0})", + x0, + y0, + x1, + y1, + (x1 - x0).abs(), + (y1 - y0).abs() + ); + state.toolbar_visible = true; + } else { + tracing::debug!(" └─ 选择无效(太小)"); + } + // 触发重绘以显示最终选框和工具栏 EventResult::Continue(state.has_valid_selection()) } _ => EventResult::Continue(false), } } - /// 转换鼠标位置坐标 - pub fn convert_cursor_position( - position: winit::dpi::PhysicalPosition, - virtual_x: i32, - virtual_y: i32, - virtual_bounds: Option<(i32, i32, u32, u32)>, - scale: f64, - ) -> (f64, f64) { - if virtual_bounds.is_some() { - // 虚拟桌面模式:使用物理像素,叠加窗口在虚拟桌面的偏移 - (virtual_x as f64 + position.x, virtual_y as f64 + position.y) - } else { - // 非虚拟模式:转换为逻辑坐标 - let logical_pos = position.to_logical::(scale); - (logical_pos.x, logical_pos.y) - } - } - /// 检查选择区域是否与窗口有交集 pub fn selection_intersects_window( state: &mut SelectionState, @@ -212,16 +219,6 @@ mod tests { assert!(matches!(result, EventResult::Continue(true))); } - #[test] - fn test_convert_cursor_position_virtual() { - let pos = winit::dpi::PhysicalPosition::new(50.0, 60.0); - let virtual_bounds = Some((0, 0, 1920, 1080)); - - let (x, y) = EventHandler::convert_cursor_position(pos, 100, 200, virtual_bounds, 2.0); - - assert_eq!((x, y), (150.0, 260.0)); - } - #[test] fn test_handle_cursor_moved_with_threshold() { let mut state = SelectionState::new(None); diff --git a/crates/ui_overlay/src/image_cache.rs b/crates/ui_overlay/src/image_cache.rs deleted file mode 100644 index 485fed6..0000000 --- a/crates/ui_overlay/src/image_cache.rs +++ /dev/null @@ -1,94 +0,0 @@ -/// 图像缓存模块 -/// -/// 缓存 Skia Image 对象,避免每帧重新创建 -use skia_safe::Image; -use std::sync::Arc; - -/// 图像缓存 -/// -/// 性能优化:背景图片在整个选择过程中不会变化, -/// 因此只需在初始化时创建一次,后续直接复用 -pub struct ImageCache { - /// 暗化后的背景图像 - tinted_image: Option>, - /// 原始背景图像 - original_image: Option>, - /// 是否已初始化 - initialized: bool, -} - -impl ImageCache { - pub fn new() -> Self { - Self { - tinted_image: None, - original_image: None, - initialized: false, - } - } - - /// 一次性初始化所有图像缓存 - /// - /// 背景图片在整个选择过程中不会变化,因此只需初始化一次 - pub fn ensure_images_cached( - &mut self, - original_bg: &[u8], - tinted_bg: &[u8], - width: u32, - height: u32, - ) { - if self.initialized { - return; - } - - self.original_image = Self::create_image(original_bg, width, height); - self.tinted_image = Self::create_image(tinted_bg, width, height); - self.initialized = true; - } - - /// 获取暗化背景图像(返回 Arc 引用,零拷贝) - pub fn get_tinted_image(&self) -> Option> { - self.tinted_image.clone() - } - - /// 获取原始背景图像(返回 Arc 引用,零拷贝) - pub fn get_original_image(&self) -> Option> { - self.original_image.clone() - } - - /// 创建 Skia Image - fn create_image(data: &[u8], width: u32, height: u32) -> Option> { - let info = skia_safe::ImageInfo::new( - (width as i32, height as i32), - skia_safe::ColorType::RGBA8888, - skia_safe::AlphaType::Premul, - None, - ); - - let image = skia_safe::images::raster_from_data( - &info, - skia_safe::Data::new_copy(data), - width as usize * 4, - )?; - - Some(Arc::new(image)) - } -} - -impl Default for ImageCache { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_image_cache_initialization() { - let cache = ImageCache::new(); - assert!(!cache.initialized); - assert!(cache.tinted_image.is_none()); - assert!(cache.original_image.is_none()); - } -} diff --git a/crates/ui_overlay/src/lib.rs b/crates/ui_overlay/src/lib.rs index b720d11..bf2f51a 100644 --- a/crates/ui_overlay/src/lib.rs +++ b/crates/ui_overlay/src/lib.rs @@ -116,16 +116,16 @@ impl RegionSelector for MockSelector { } } -pub mod backend; +mod background_renderer; mod coordinate_utils; mod event_handler; mod frame_timer; -mod image_cache; pub mod platform; mod selection_app; mod selection_render; mod selection_state; mod selector; +pub mod toolbar; mod window_info; mod window_manager; diff --git a/crates/ui_overlay/src/platform/macos.rs b/crates/ui_overlay/src/platform/macos.rs index b2d3707..e719136 100644 --- a/crates/ui_overlay/src/platform/macos.rs +++ b/crates/ui_overlay/src/platform/macos.rs @@ -6,6 +6,26 @@ use winit::window::Window; pub type GuardInner = NSApplicationPresentationOptions; +/// macOS 窗口层级常量 +/// +/// 主菜单窗口层级 +#[allow(dead_code)] +const NS_MAIN_MENU_WINDOW_LEVEL: i64 = 24; +/// 屏幕保护程序窗口层级(覆盖所有内容包括菜单栏) +const NS_SCREEN_SAVER_WINDOW_LEVEL: i64 = 1000; +/// 工具栏窗口层级(高于主覆盖窗口)- 预留给未来的工具栏使用 +#[allow(dead_code)] +const TOOLBAR_WINDOW_LEVEL: i64 = 1100; + +/// macOS 窗口集合行为标志 +/// +/// 窗口可以加入所有空间 +const NS_WINDOW_COLLECTION_BEHAVIOR_CAN_JOIN_ALL_SPACES: u64 = 1 << 0; +/// 全屏主窗口 +const NS_WINDOW_COLLECTION_BEHAVIOR_FULLSCREEN_PRIMARY: u64 = 1 << 7; +/// 固定窗口(不随空间切换移动) +const NS_WINDOW_COLLECTION_BEHAVIOR_STATIONARY: u64 = 1 << 4; + pub fn start_presentation() -> Option { // NSApplication *app = [NSApplication sharedApplication]; activate and hide menu/dock let app: *mut AnyObject = unsafe { msg_send![class!(NSApplication), sharedApplication] }; @@ -69,18 +89,13 @@ pub fn apply_overlay_window_appearance(window: &Window, color: [u8; 4]) { let _: () = msg_send![ns_window, setBackgroundColor: color_obj]; let _: () = msg_send![ns_window, setAlphaValue: 1.0]; - // 设置窗口层级(高于菜单栏) - // NSMainMenuWindowLevel = 24 - // NSScreenSaverWindowLevel = 1000 - // 使用 ScreenSaver 层级确保完全覆盖菜单栏 - let level: i64 = 1000; - let _: () = msg_send![ns_window, setLevel: level]; - - // 设置窗口集合行为 - // NSWindowCollectionBehaviorCanJoinAllSpaces (1 << 0) = 1 - // NSWindowCollectionBehaviorFullScreenPrimary (1 << 7) = 128 - // NSWindowCollectionBehaviorStationary (1 << 4) = 16 - let behavior: u64 = (1 << 0) | (1 << 7) | (1 << 4); + // 设置窗口层级(使用屏幕保护程序级别,覆盖菜单栏) + let _: () = msg_send![ns_window, setLevel: NS_SCREEN_SAVER_WINDOW_LEVEL]; + + // 设置窗口集合行为:可加入所有空间 + 全屏主窗口 + 固定 + let behavior = NS_WINDOW_COLLECTION_BEHAVIOR_CAN_JOIN_ALL_SPACES + | NS_WINDOW_COLLECTION_BEHAVIOR_FULLSCREEN_PRIMARY + | NS_WINDOW_COLLECTION_BEHAVIOR_STATIONARY; let _: () = msg_send![ns_window, setCollectionBehavior: behavior]; // 确保窗口接受鼠标事件(不忽略鼠标事件) @@ -91,18 +106,15 @@ pub fn apply_overlay_window_appearance(window: &Window, color: [u8; 4]) { let current_level: i64 = msg_send![ns_window, level]; let current_behavior: u64 = msg_send![ns_window, collectionBehavior]; tracing::debug!( - "[macOS Window] 窗口层级 level={}, 集合行为 collectionBehavior={:#b} ({})", + "[macOS Window] 窗口层级 level={}, 集合行为 behavior={:#b}", current_level, - current_behavior, current_behavior ); - tracing::debug!( - "[macOS Window] 层级说明: NSMainMenuWindowLevel=24, NSScreenSaverWindowLevel=1000" - ); - if current_level < 24 { + if current_level < NS_MAIN_MENU_WINDOW_LEVEL { tracing::warn!( - "[macOS Window] ⚠️ 窗口层级 {} 低于菜单栏层级 24,菜单栏可能显示!", - current_level + "[macOS Window] ⚠️ 窗口层级 {} 低于菜单栏层级 {},菜单栏可能显示!", + current_level, + NS_MAIN_MENU_WINDOW_LEVEL ); } } diff --git a/crates/ui_overlay/src/selection_app.rs b/crates/ui_overlay/src/selection_app.rs index b368fe0..85d29db 100644 --- a/crates/ui_overlay/src/selection_app.rs +++ b/crates/ui_overlay/src/selection_app.rs @@ -1,11 +1,10 @@ /// 选择器应用程序模块 /// -/// 包含 SelectionApp 的核心逻辑和事件处理 +/// 包含 SelectionApp 的核心逻辑和事件处理,使用 egui 进行渲染 use crate::event_handler::{EventHandler, EventResult, SelectionState}; use crate::frame_timer::FrameTimer; -use crate::image_cache::ImageCache; use crate::platform; -use crate::selection_render::{BackgroundProcessor, WindowRenderer}; +use crate::selection_render::{EguiSelectionRenderer, WindowRenderer}; use crate::window_manager::WindowManager; use crate::Region; use winit::application::ApplicationHandler; @@ -13,7 +12,6 @@ use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use winit::window::WindowAttributes; -const OVERLAY_BG_COLOR: [u8; 4] = [0, 0, 0, 128]; const TARGET_FPS: u32 = 60; // 目标帧率:60 FPS /// 选择器应用程序 @@ -24,11 +22,7 @@ pub struct SelectionApp { pub bg: Option>, pub bg_w: u32, pub bg_h: u32, - pub bg_tinted: Option>, - pub overlay_color: [u8; 4], pub state: SelectionState, - /// 图像缓存,避免每帧重新创建 Skia Image - image_cache: ImageCache, /// 帧率控制器,限制到 60 FPS frame_timer: FrameTimer, } @@ -55,10 +49,7 @@ impl SelectionApp { bg: bg_rgba, bg_w, bg_h, - bg_tinted: None, - overlay_color: OVERLAY_BG_COLOR, state: SelectionState::new(virtual_bounds), - image_cache: ImageCache::new(), frame_timer: FrameTimer::new(TARGET_FPS), } } @@ -68,14 +59,15 @@ impl SelectionApp { return; } - // 初始化渲染后端 - if let Err(e) = self.window_manager.windows[window_index].init_backend() { - eprintln!("⚠️ Failed to init render backend: {}", e); + // 初始化 WGPU 渲染器 + if let Err(e) = self.window_manager.windows[window_index].init_wgpu() { + eprintln!("⚠️ Failed to init WGPU: {}", e); return; } - // 确保背景和图像缓存已初始化 - self.ensure_backgrounds_and_cache(); + // 确保背景和图像缓存已初始化(需要 egui context) + let egui_ctx = self.window_manager.windows[window_index].egui_ctx.clone(); + self.ensure_backgrounds_and_cache(&egui_ctx, window_index); let render_data = self.prepare_render_data(window_index); @@ -88,9 +80,18 @@ impl SelectionApp { fn prepare_render_data(&mut self, window_index: usize) -> RenderData { let (x0c, y0c, x1c, y1c) = self.state.calculate_selection_rect(); // 只要有有效的选择尺寸,就应该渲染选择框 - // 无论是否正在拖动,这样在拖动完成后选择框依然显示 let selection_exists = (x1c - x0c).abs() > 1.0 && (y1c - y0c).abs() > 1.0; + if selection_exists { + tracing::debug!( + "🔍 窗口 {} 准备渲染数据: 选择框=({:.0}, {:.0}) -> ({:.0}, {:.0}), 尺寸=({:.0}x{:.0})", + window_index, + x0c, y0c, x1c, y1c, + (x1c - x0c).abs(), + (y1c - y0c).abs() + ); + } + let window_info = &self.window_manager.windows[window_index]; let window_needs_selection = WindowRenderer::should_render_selection( selection_exists, @@ -101,6 +102,18 @@ impl SelectionApp { window_info.size_px.height, ); + if selection_exists { + tracing::debug!( + " └─ 窗口 {} 虚拟位置=({}, {}), 尺寸={}x{}, 需要渲染={}", + window_index, + window_info.virtual_x, + window_info.virtual_y, + window_info.size_px.width, + window_info.size_px.height, + window_needs_selection + ); + } + RenderData { selection_rect: (x0c as i32, y0c as i32, x1c as i32, y1c as i32), selection_exists, @@ -113,19 +126,21 @@ impl SelectionApp { // 获取窗口信息 let window_virtual_x = self.window_manager.windows[window_index].virtual_x; let window_virtual_y = self.window_manager.windows[window_index].virtual_y; + let window_size_px = self.window_manager.windows[window_index].size_px; + let window_scale = self.window_manager.windows[window_index].scale; #[cfg(debug_assertions)] if render_data.selection_exists && window_index == 0 { // 只在第一个窗口打印,避免刷屏 - let window_info = &self.window_manager.windows[window_index]; let (x0, y0, x1, y1) = render_data.selection_rect; tracing::debug!("=== 渲染坐标转换 [Window {}] ===", window_index); tracing::debug!("选择框虚拟坐标: ({}, {}) 到 ({}, {})", x0, y0, x1, y1); tracing::debug!("窗口虚拟位置: ({}, {})", window_virtual_x, window_virtual_y); tracing::debug!( - "窗口物理尺寸: {}x{}", - window_info.size_px.width, - window_info.size_px.height + "窗口物理尺寸: {}x{}, scale: {}", + window_size_px.width, + window_size_px.height, + window_scale ); if let Some((vx, vy, vw, vh)) = self.state.virtual_bounds { @@ -136,76 +151,31 @@ impl SelectionApp { } } - // 获取缓存的图像(不再需要每次创建) - let bg_tinted_image = self.image_cache.get_tinted_image(); - let original_image = self.image_cache.get_original_image(); + // 注意:背景现在由 wgpu 直接渲染,egui 层只绘制 UI 元素 - let window_info = &mut self.window_manager.windows[window_index]; let virtual_bounds = self.state.virtual_bounds; - // 执行渲染 - window_info.render(|canvas| { - use skia_safe::Color; - - // 1. 清空背景 - canvas.clear(Color::from_argb(0, 0, 0, 0)); - - // 2. 渲染暗化背景 - 使用旧的渲染器(已有正确的坐标转换) - if let Some(ref tinted_image) = bg_tinted_image { - // 手动实现 render_virtual_cached 的逻辑 - if let Some((vx, vy, _, _)) = virtual_bounds { - let offset_x = -(window_virtual_x - vx) as f32; - let offset_y = -(window_virtual_y - vy) as f32; - canvas.draw_image(tinted_image, (offset_x, offset_y), None); - } else { - canvas.draw_image(tinted_image, (0, 0), None); - } - } - - // 3. 渲染选择区域 - 只在 window_needs_selection 为 true 时渲染 + // 计算逻辑坐标下的选择框(用于 egui 渲染) + let selection_rect_logical = if render_data.selection_exists && render_data.window_needs_selection { let (x0, y0, x1, y1) = render_data.selection_rect; - // 渲染选择区域的原始背景(未暗化) - if let Some(ref image) = original_image { - // 手动实现 render_original_background_cached 的逻辑 - canvas.save(); - - // 计算窗口坐标(虚拟桌面坐标 -> 窗口本地坐标) - let (win_x0, win_y0, win_x1, win_y1) = if virtual_bounds.is_some() { - ( - x0 - window_virtual_x, - y0 - window_virtual_y, - x1 - window_virtual_x, - y1 - window_virtual_y, - ) - } else { - (x0, y0, x1, y1) - }; - - // 裁剪到选择区域 - let clip_rect = skia_safe::Rect::from_ltrb( - win_x0 as f32, - win_y0 as f32, - win_x1 as f32, - win_y1 as f32, - ); - canvas.clip_rect(clip_rect, skia_safe::ClipOp::Intersect, true); - - // 绘制原始图像 - if let Some((vx, vy, _, _)) = virtual_bounds { - let offset_x = -(window_virtual_x - vx) as f32; - let offset_y = -(window_virtual_y - vy) as f32; - canvas.draw_image(image, (offset_x, offset_y), None); - } else { - canvas.draw_image(image, (0, 0), None); - } - - canvas.restore(); - } + tracing::debug!( + "🎯 窗口 {} 选择框转换: 虚拟坐标=({}, {}) -> ({}, {})", + window_index, + x0, + y0, + x1, + y1 + ); + tracing::debug!( + " └─ 窗口虚拟位置=({}, {}), scale={}", + window_virtual_x, + window_virtual_y, + window_scale + ); - // 手动实现 render_border 的逻辑 - let (x0, y0, x1, y1) = render_data.selection_rect; + // 将虚拟坐标转换为窗口本地坐标 let (win_x0, win_y0, win_x1, win_y1) = if virtual_bounds.is_some() { ( x0 - window_virtual_x, @@ -217,55 +187,138 @@ impl SelectionApp { (x0, y0, x1, y1) }; - let mut paint = skia_safe::Paint::default(); - paint.set_color(skia_safe::Color::WHITE); - paint.set_style(skia_safe::paint::Style::Stroke); - paint.set_stroke_width(2.0); - paint.set_anti_alias(true); - - let rect = skia_safe::Rect::from_ltrb( - win_x0 as f32, - win_y0 as f32, - win_x1 as f32, - win_y1 as f32, + tracing::debug!( + " └─ 窗口本地坐标=({}, {}) -> ({}, {})", + win_x0, + win_y0, + win_x1, + win_y1 ); - canvas.draw_rect(rect, &paint); - } + + // 转换为逻辑坐标(egui 使用逻辑坐标) + let scale = window_scale as f32; + let logical_rect = ( + win_x0 as f32 / scale, + win_y0 as f32 / scale, + (win_x1 - win_x0) as f32 / scale, + (win_y1 - win_y0) as f32 / scale, + ); + + tracing::debug!( + " └─ egui逻辑坐标=({:.1}, {:.1}), 尺寸=({:.1}, {:.1})", + logical_rect.0, + logical_rect.1, + logical_rect.2, + logical_rect.3 + ); + + Some(logical_rect) + } else { + if render_data.selection_exists { + tracing::debug!("窗口 {} 无需绘制选择框(不在范围内)", window_index); + } + None + }; + + // 窗口逻辑尺寸 + let window_logical_size = ( + window_size_px.width as f32 / window_scale as f32, + window_size_px.height as f32 / window_scale as f32, + ); + + let window_info = &mut self.window_manager.windows[window_index]; + + // 执行 egui 渲染(只绘制 UI 层:选择框、文本等) + // 背景已经由 wgpu 在底层渲染 + tracing::trace!( + "渲染窗口 {} UI 层,选择框={:?}", + window_index, + selection_rect_logical + ); + + window_info.render(|ctx| { + // 使用全屏 CentralPanel,透明背景 + egui::CentralPanel::default() + .frame(egui::Frame::NONE) + .show(ctx, |ui| { + let painter = ui.painter(); + + // 只绘制选择框和 UI 元素(不绘制背景) + EguiSelectionRenderer::render_selection( + painter, + selection_rect_logical, + window_logical_size, + ); + }); }) } /// 确保背景暗化和图像缓存都已初始化 /// /// 性能优化:只在首次调用时处理,后续调用直接返回 - fn ensure_backgrounds_and_cache(&mut self) { - // 生成暗化背景(如果尚未生成) - if self.bg_tinted.is_none() { - if let Some(bg) = &self.bg { - if !bg.is_empty() && self.bg_w > 0 && self.bg_h > 0 { - self.bg_tinted = - Some(BackgroundProcessor::tint_background(bg, self.overlay_color)); - } - } + fn ensure_backgrounds_and_cache(&mut self, _ctx: &egui::Context, window_index: usize) { + // 检查这个窗口是否已上传背景 + if self.window_manager.windows[window_index].has_background_texture() { + tracing::debug!("窗口 {} 背景已上传,跳过", window_index); + return; } - // 一次性初始化 Skia Image 缓存 - if let (Some(ref bg), Some(ref bg_tinted)) = (&self.bg, &self.bg_tinted) { - self.image_cache - .ensure_images_cached(bg, bg_tinted, self.bg_w, self.bg_h); + // 为这个窗口上传原始背景(不暗化) + // 暗化效果将由 egui 层的遮罩实现 + if let Some(ref bg) = self.bg { + if !bg.is_empty() && self.bg_w > 0 && self.bg_h > 0 { + // 获取虚拟坐标系统的最小值 + let (virtual_min_x, virtual_min_y) = self + .state + .virtual_bounds + .map(|(min_x, min_y, _, _)| (min_x, min_y)) + .unwrap_or((0, 0)); + + let window_info = &self.window_manager.windows[window_index]; + tracing::info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + tracing::info!("[背景上传] 窗口 {} 准备上传背景:", window_index); + tracing::info!(" └─ 完整背景: {}x{} (RGBA)", self.bg_w, self.bg_h); + tracing::info!( + " └─ 虚拟桌面最小值: ({}, {})", + virtual_min_x, + virtual_min_y + ); + tracing::info!( + " └─ 窗口虚拟坐标 (物理): ({}, {})", + window_info.virtual_x, + window_info.virtual_y + ); + tracing::info!( + " └─ 窗口物理尺寸: {}x{}", + window_info.size_px.width, + window_info.size_px.height + ); + tracing::info!(" └─ 窗口 scale: {}", window_info.scale); + + self.window_manager.windows[window_index].upload_background_texture( + bg, + self.bg_w, + self.bg_h, + virtual_min_x, + virtual_min_y, + ); + } } } pub fn request_redraw_all(&mut self) { // 在请求重绘时检查帧率限制 - // 如果不应该渲染,就不发起重绘请求 if !self.frame_timer.should_render() { + tracing::trace!("🚫 帧率限制:跳过重绘请求"); return; } if self.state.should_throttle_redraw() { + tracing::trace!("🚫 重绘节流:跳过重绘请求"); return; } + tracing::debug!("✅ 请求所有窗口重绘"); self.state.mark_redraw_requested(); self.window_manager.request_redraw_all(); } @@ -281,6 +334,17 @@ impl SelectionApp { None => return, }; + // 首先让 egui 处理事件 + let window_info = &mut self.window_manager.windows[window_index]; + let response = window_info + .egui_state + .on_window_event(&window_info.window, &event); + + // 如果 egui 消费了事件,可能需要重绘 + if response.consumed { + self.request_redraw_all(); + } + match event { WindowEvent::CloseRequested => { self.handle_close_requested(event_loop); @@ -299,6 +363,18 @@ impl SelectionApp { WindowEvent::Resized(new_size) => { self.handle_resized(window_index, new_size); } + WindowEvent::Moved(new_position) => { + // 忽略 Moved 事件,因为 macOS 报告的位置是错误的 + // 窗口位置在创建时已经正确设置,不应该改变 + tracing::debug!( + "[窗口 {}] 忽略 Moved 事件: 报告位置=({}, {}),保持原位置=({}, {})", + window_index, + new_position.x, + new_position.y, + self.window_manager.windows[window_index].virtual_x, + self.window_manager.windows[window_index].virtual_y + ); + } WindowEvent::RedrawRequested => { self.handle_redraw_requested(window_index); } @@ -349,31 +425,73 @@ impl SelectionApp { position: winit::dpi::PhysicalPosition, ) { let window_info = &self.window_manager.windows[window_index]; - let new_pos = EventHandler::convert_cursor_position( - position, - window_info.virtual_x, - window_info.virtual_y, - self.state.virtual_bounds, - window_info.scale, - ); + let (vx, vy) = (window_info.virtual_x, window_info.virtual_y); - if EventHandler::handle_cursor_moved(&mut self.state, new_pos) { - self.request_redraw_all(); - } + // 将窗口本地坐标转换为虚拟桌面坐标 + let virtual_x = position.x + vx as f64; + let virtual_y = position.y + vy as f64; + + // 更新状态,渲染由 about_to_wait 触发 + EventHandler::handle_cursor_moved(&mut self.state, (virtual_x, virtual_y)); } fn handle_scale_factor_changed(&mut self, window_index: usize, scale_factor: f64) { - self.window_manager.windows[window_index].update_scale(scale_factor); - self.request_redraw_all(); + // 从 window 获取实际的 scale,而不是完全信任事件参数 + let window_info = &self.window_manager.windows[window_index]; + let actual_scale = window_info.window.scale_factor(); + + tracing::debug!( + "[窗口 {}] ScaleFactorChanged 事件: 事件scale={}, 实际window.scale={}", + window_index, + scale_factor, + actual_scale + ); + + self.window_manager.windows[window_index].update_scale(actual_scale); } fn handle_resized(&mut self, window_index: usize, new_size: winit::dpi::PhysicalSize) { - self.window_manager.windows[window_index].update_size(new_size); - self.request_redraw_all(); + // 重要:winit 在 macOS 上经常发送错误的尺寸和 scale + // 我们检测异常并使用创建时保存的正确值 + let window_info = &self.window_manager.windows[window_index]; + let reported_size = window_info.window.inner_size(); + let reported_scale = window_info.window.scale_factor(); + let initial_size = window_info.initial_size_px; + let initial_scale = window_info.initial_scale; + + tracing::debug!( + "[窗口 {}] Resized 事件: 事件={}x{}, window报告={}x{} scale={}, 初始={}x{} scale={}", + window_index, + new_size.width, + new_size.height, + reported_size.width, + reported_size.height, + reported_scale, + initial_size.width, + initial_size.height, + initial_scale + ); + + // 检测是否是 winit 的bug:报告的尺寸是初始尺寸的一半(逻辑尺寸) + let is_bug = (reported_size.width as f64) < (initial_size.width as f64 * 0.6) + || (reported_size.height as f64) < (initial_size.height as f64 * 0.6) + || reported_scale < initial_scale * 0.6; + + if is_bug { + tracing::warn!( + "[窗口 {}] ⚠️ 检测到 winit bug!忽略错误的 Resized 事件,保持初始值", + window_index + ); + // 不更新,保持原值 + } else { + // 正常更新 + self.window_manager.windows[window_index].update_size(reported_size); + self.window_manager.windows[window_index].update_scale(reported_scale); + } } fn handle_redraw_requested(&mut self, window_index: usize) { - self.state.clear_redraw_pending(); + tracing::trace!("🖼️ 窗口 {} 收到重绘请求", window_index); self.render_window_by_index(window_index); } @@ -385,84 +503,185 @@ impl SelectionApp { button_state: winit::event::ElementState, ) { match EventHandler::handle_mouse_input(&mut self.state, button, button_state) { - EventResult::Continue(need_redraw) => { - if need_redraw { - self.request_redraw_all(); - } + EventResult::Continue(_need_redraw) => { + // 渲染由 about_to_wait 触发,不再直接渲染 } - EventResult::Exit => event_loop.exit(), EventResult::Finish => { if let Some(region) = self.create_region(window_index) { self.state.result = Some(region); event_loop.exit(); } } + EventResult::Exit => { + self.state.result = None; + event_loop.exit(); + } } } fn create_region(&mut self, window_index: usize) -> Option { - let scale_out = if self.state.virtual_bounds.is_some() { - 1.0 - } else { - self.window_manager.windows[window_index].scale as f32 - }; + // calculate_selection_rect 返回的是虚拟桌面物理坐标(像素) + let (x0_phys, y0_phys, x1_phys, y1_phys) = self.state.calculate_selection_rect(); + if (x1_phys - x0_phys).abs() < 1.0 || (y1_phys - y0_phys).abs() < 1.0 { + return None; + } - self.state.build_region(scale_out) + // 计算选择区域中心点(物理坐标),找到对应的显示器 scale + let center_x_phys = (x0_phys + x1_phys) / 2.0; + let center_y_phys = (y0_phys + y1_phys) / 2.0; + + // 查找中心点所在的窗口(显示器) + tracing::info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + tracing::info!( + "[创建 Region] 选择框物理坐标: ({:.1}, {:.1}) -> ({:.1}, {:.1})", + x0_phys, + y0_phys, + x1_phys, + y1_phys + ); + tracing::info!( + "[创建 Region] 中心点物理坐标: ({:.1}, {:.1})", + center_x_phys, + center_y_phys + ); + + tracing::info!("[创建 Region] 查找中心点所在的显示器:"); + for (idx, w) in self.window_manager.windows.iter().enumerate() { + let wx = w.virtual_x as f64; + let wy = w.virtual_y as f64; + let ww = w.size_px.width as f64; + let wh = w.size_px.height as f64; + let contains = center_x_phys >= wx + && center_x_phys < wx + ww + && center_y_phys >= wy + && center_y_phys < wy + wh; + tracing::info!( + " 窗口 {}: virtual=({}, {}) size={}x{} scale={} contains={}", + idx, + wx, + wy, + ww, + wh, + w.scale, + contains + ); + } + + let scale = self + .window_manager + .windows + .iter() + .find(|w| { + let wx = w.virtual_x as f64; + let wy = w.virtual_y as f64; + let ww = w.size_px.width as f64; + let wh = w.size_px.height as f64; + center_x_phys >= wx + && center_x_phys < wx + ww + && center_y_phys >= wy + && center_y_phys < wy + wh + }) + .map(|w| w.scale) + .unwrap_or_else(|| { + // 如果中心点不在任何窗口内(理论上不应该),使用触发窗口的 scale + tracing::warn!( + "选择区域中心点 ({:.1}, {:.1}) 不在任何窗口内,使用窗口 {} 的 scale", + center_x_phys, + center_y_phys, + window_index + ); + self.window_manager.windows[window_index].scale + }); + + tracing::info!("[创建 Region] 选定的 scale: {}", scale); + + // 将物理坐标转换为逻辑坐标(Region 应该保存逻辑坐标) + let x_phys = x0_phys.min(x1_phys); + let y_phys = y0_phys.min(y1_phys); + let w_phys = (x1_phys - x0_phys).abs(); + let h_phys = (y1_phys - y0_phys).abs(); + + let x_logical = (x_phys / scale) as f32; + let y_logical = (y_phys / scale) as f32; + let w_logical = (w_phys / scale) as f32; + let h_logical = (h_phys / scale) as f32; + let scale_f32 = scale as f32; + + tracing::info!("[创建 Region] 坐标转换:"); + tracing::info!( + " └─ 物理: ({:.1}, {:.1}) 尺寸 {:.1}x{:.1}", + x_phys, + y_phys, + w_phys, + h_phys + ); + tracing::info!( + " └─ 逻辑: ({:.1}, {:.1}) 尺寸 {:.1}x{:.1} (除以 scale={})", + x_logical, + y_logical, + w_logical, + h_logical, + scale + ); + tracing::info!( + " └─ Region 保存: x={:.1}, y={:.1}, w={:.1}, h={:.1}, scale={}", + x_logical, + y_logical, + w_logical, + h_logical, + scale_f32 + ); + + Some(Region::new( + x_logical, y_logical, w_logical, h_logical, scale_f32, + )) } } impl ApplicationHandler for SelectionApp { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - if !self.window_manager.windows.is_empty() { - return; - } - - // 使用 winit 的显示器信息创建窗口 self.window_manager .initialize_windows(event_loop, &self.attrs); - #[cfg(debug_assertions)] + #[cfg(target_os = "macos")] { - tracing::debug!("=== SelectionApp 窗口信息 ==="); - tracing::debug!("创建了 {} 个窗口", self.window_manager.windows.len()); - for (i, window_info) in self.window_manager.windows.iter().enumerate() { - tracing::debug!( - "[Window {}] virtual_pos=({}, {}), size_px={}x{}, scale={}", - i, - window_info.virtual_x, - window_info.virtual_y, - window_info.size_px.width, - window_info.size_px.height, - window_info.scale - ); + if let Some(first_win) = self.window_manager.windows.first() { + platform::apply_overlay_window_appearance(&first_win.window, [0, 0, 0, 0]); } } - if !self.window_manager.windows.is_empty() { - self.pres_guard = platform::start_presentation(); - - // 一次性初始化背景和图像缓存 - self.ensure_backgrounds_and_cache(); - - #[cfg(target_os = "macos")] - { - let color = self.overlay_color; - for window_info in &self.window_manager.windows { - platform::apply_overlay_window_appearance(window_info.window.as_ref(), color); - } + // 对所有窗口应用平台特定的外观 + #[cfg(target_os = "macos")] + { + for window_info in &self.window_manager.windows { + platform::apply_overlay_window_appearance(&window_info.window, [0, 0, 0, 0]); } + } - // 先渲染所有窗口的首帧,避免显示时出现粉色背景 - for i in 0..self.window_manager.windows.len() { - self.render_window_by_index(i); + // 显示窗口前进行预热渲染 + for i in 0..self.window_manager.windows.len() { + if let Err(e) = self.window_manager.windows[i].init_wgpu() { + eprintln!("⚠️ Failed to init WGPU for window {}: {}", i, e); + continue; } - // 首帧渲染完成后再显示窗口 - for i in 0..self.window_manager.windows.len() { - self.window_manager.windows[i].window.set_visible(true); - self.window_manager.windows[i].window.request_redraw(); + // 预热渲染(确保缓存初始化) + let egui_ctx = self.window_manager.windows[i].egui_ctx.clone(); + self.ensure_backgrounds_and_cache(&egui_ctx, i); + + let render_data = self.prepare_render_data(i); + if let Err(e) = self.render(i, &render_data) { + eprintln!("⚠️ Warmup render error for window {}: {}", i, e); } } + + // 现在显示所有窗口 + for window_info in &self.window_manager.windows { + window_info.window.set_visible(true); + window_info.window.request_redraw(); + } + + self.pres_guard = platform::start_presentation(); } fn window_event( @@ -475,6 +694,10 @@ impl ApplicationHandler for SelectionApp { } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - // 空闲时不强制重绘 + // Winit 推荐的渲染模式:在事件循环空闲时请求重绘 + // 拖拽时或有选择框时,持续请求重绘所有窗口 + if self.state.dragging { + self.window_manager.request_redraw_all(); + } } } diff --git a/crates/ui_overlay/src/selection_render.rs b/crates/ui_overlay/src/selection_render.rs index dcddb8f..bd2a73b 100644 --- a/crates/ui_overlay/src/selection_render.rs +++ b/crates/ui_overlay/src/selection_render.rs @@ -1,53 +1,106 @@ /// 选择器渲染逻辑模块 /// -/// 提供选择框的渲染管理功能 +/// 提供选择框的渲染管理功能,使用 egui painter 进行绘制 use crate::event_handler::EventHandler; -/// 背景处理工具 -pub struct BackgroundProcessor; +/// egui 选择框渲染器 +pub struct EguiSelectionRenderer; -impl BackgroundProcessor { - /// 将背景与叠加颜色混合(暗化效果) +impl EguiSelectionRenderer { + /// 使用 egui painter 渲染选择框 /// - /// 性能优化:使用 rayon 并行处理像素 - /// - 小图像(< 256KB)使用单线程避免线程开销 - /// - 大图像使用多线程并行处理 - pub fn tint_background(bg: &[u8], overlay_color: [u8; 4]) -> Vec { - use rayon::prelude::*; - - let overlay_alpha = overlay_color[3] as u16; - let inv_alpha = 255u16.saturating_sub(overlay_alpha); - let tint_r = overlay_color[0] as u16; - let tint_g = overlay_color[1] as u16; - let tint_b = overlay_color[2] as u16; - - let mut tinted = vec![0u8; bg.len()]; - - // 对于小图像,使用单线程避免并行开销 - const PARALLEL_THRESHOLD: usize = 256 * 1024; // 256KB - - if bg.len() < PARALLEL_THRESHOLD { - // 单线程处理小图像 - for (src, dst) in bg.chunks_exact(4).zip(tinted.chunks_exact_mut(4)) { - dst[0] = (((src[0] as u16) * inv_alpha + tint_r * overlay_alpha) / 255) as u8; - dst[1] = (((src[1] as u16) * inv_alpha + tint_g * overlay_alpha) / 255) as u8; - dst[2] = (((src[2] as u16) * inv_alpha + tint_b * overlay_alpha) / 255) as u8; - dst[3] = 255; + /// # Arguments + /// * `painter` - egui painter + /// * `selection_rect` - 选择框矩形 (x, y, w, h),逻辑坐标 + /// * `window_size` - 窗口尺寸 (width, height),逻辑坐标 + pub fn render_selection( + painter: &egui::Painter, + selection_rect: Option<(f32, f32, f32, f32)>, + window_size: (f32, f32), + ) { + let (win_w, win_h) = window_size; + + // 暗化遮罩颜色 (半透明黑色) + let darken_color = egui::Color32::from_rgba_unmultiplied(0, 0, 0, 128); + + // 如果有选择框,绘制选择框外的暗化遮罩 + if let Some((x, y, w, h)) = selection_rect { + tracing::trace!( + "📐 绘制选择框和遮罩: pos=({:.1}, {:.1}), size=({:.1}x{:.1})", + x, + y, + w, + h + ); + + let sel_rect = egui::Rect::from_min_size(egui::pos2(x, y), egui::vec2(w, h)); + + // 绘制4个矩形遮罩覆盖选择框外的区域 + // 1. 上方遮罩 + if y > 0.0 { + painter.rect_filled( + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(win_w, y)), + 0.0, + darken_color, + ); } + + // 2. 下方遮罩 + let bottom = y + h; + if bottom < win_h { + painter.rect_filled( + egui::Rect::from_min_max(egui::pos2(0.0, bottom), egui::pos2(win_w, win_h)), + 0.0, + darken_color, + ); + } + + // 3. 左侧遮罩 + if x > 0.0 { + painter.rect_filled( + egui::Rect::from_min_max(egui::pos2(0.0, y), egui::pos2(x, bottom)), + 0.0, + darken_color, + ); + } + + // 4. 右侧遮罩 + let right = x + w; + if right < win_w { + painter.rect_filled( + egui::Rect::from_min_max(egui::pos2(right, y), egui::pos2(win_w, bottom)), + 0.0, + darken_color, + ); + } + + // 绘制白色边框(2像素宽) + painter.rect_stroke( + sel_rect, + 0.0, + egui::Stroke::new(2.0, egui::Color32::WHITE), + egui::StrokeKind::Outside, + ); + + // 绘制尺寸信息 + let size_text = format!("{}x{}", w as i32, h as i32); + let text_pos = egui::pos2(x + w / 2.0, (y - 20.0_f32).max(5.0)); + painter.text( + text_pos, + egui::Align2::CENTER_CENTER, + size_text, + egui::FontId::proportional(14.0), + egui::Color32::WHITE, + ); } else { - // 并行处理大图像(按像素行分块) - tinted - .par_chunks_mut(4) - .zip(bg.par_chunks(4)) - .for_each(|(dst, src)| { - dst[0] = (((src[0] as u16) * inv_alpha + tint_r * overlay_alpha) / 255) as u8; - dst[1] = (((src[1] as u16) * inv_alpha + tint_g * overlay_alpha) / 255) as u8; - dst[2] = (((src[2] as u16) * inv_alpha + tint_b * overlay_alpha) / 255) as u8; - dst[3] = 255; - }); + // 没有选择框时,整个窗口显示暗化遮罩 + tracing::trace!("📐 绘制全屏暗化遮罩"); + painter.rect_filled( + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(win_w, win_h)), + 0.0, + darken_color, + ); } - - tinted } } @@ -86,17 +139,10 @@ impl WindowRenderer { #[cfg(test)] mod tests { - use super::*; #[test] fn test_tint_background() { - let bg = vec![255, 128, 64, 255, 200, 100, 50, 255]; - let overlay_color = [0, 0, 0, 128]; // 半透明黑色 - let tinted = BackgroundProcessor::tint_background(&bg, overlay_color); - - assert_eq!(tinted.len(), bg.len()); - // 验证颜色被暗化 - assert!(tinted[0] < bg[0]); - assert!(tinted[4] < bg[4]); + // This test was for BackgroundProcessor, which is now removed. + // So this test case is also removed. } } diff --git a/crates/ui_overlay/src/selection_state.rs b/crates/ui_overlay/src/selection_state.rs index 142817f..b74791f 100644 --- a/crates/ui_overlay/src/selection_state.rs +++ b/crates/ui_overlay/src/selection_state.rs @@ -3,6 +3,16 @@ /// 管理选择框的状态、修饰键、重绘控制等 use crate::Region; +/// 应用模式 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum AppMode { + /// 选择区域模式 + SelectingRegion, + /// 标注编辑模式 + EditingAnnotations, +} + /// 选择器应用的状态 /// /// 包含选择框的位置、大小、修饰键状态以及性能优化相关的缓存 @@ -31,6 +41,11 @@ pub struct SelectionState { cached_rect: Option<(f64, f64, f64, f64)>, /// 缓存是否有效 cache_valid: bool, + /// 当前应用模式 + #[allow(dead_code)] + pub app_mode: AppMode, + /// 工具栏是否可见 + pub toolbar_visible: bool, } impl SelectionState { @@ -49,6 +64,8 @@ impl SelectionState { last_redraw_time: std::time::Instant::now(), cached_rect: None, cache_valid: false, + app_mode: AppMode::SelectingRegion, + toolbar_visible: false, } } @@ -115,22 +132,6 @@ impl SelectionState { sx != ex && sy != ey } - /// 构建当前选择的 Region 对象 - pub fn build_region(&mut self, scale: f32) -> Option { - if !self.has_valid_selection() { - return None; - } - - let (sx, sy, ex, ey) = self.calculate_selection_rect(); - Some(Region { - x: sx.round() as f32, - y: sy.round() as f32, - w: (ex - sx).abs().round() as f32, - h: (ey - sy).abs().round() as f32, - scale, - }) - } - /// 重绘频率控制 /// /// 限制到 60 FPS (16ms 间隔) 以优化性能 @@ -150,11 +151,6 @@ impl SelectionState { self.redraw_pending = true; self.last_redraw_time = std::time::Instant::now(); } - - /// 清除重绘标记 - pub fn clear_redraw_pending(&mut self) { - self.redraw_pending = false; - } } #[cfg(test)] diff --git a/crates/ui_overlay/src/toolbar/icons.rs b/crates/ui_overlay/src/toolbar/icons.rs new file mode 100644 index 0000000..5f0fb69 --- /dev/null +++ b/crates/ui_overlay/src/toolbar/icons.rs @@ -0,0 +1,93 @@ +/// 图标加载和管理模块 +/// +/// 负责加载 SVG 图标并转换为 egui 可用的纹理 +use std::collections::HashMap; + +/// SVG 图标内容(编译时嵌入) +pub struct SvgIcons { + pub rectangle: &'static str, + pub arrow: &'static str, + pub brush: &'static str, + pub check: &'static str, + pub close: &'static str, +} + +impl SvgIcons { + /// 获取内嵌的 SVG 图标 + pub fn load() -> Self { + Self { + rectangle: include_str!("../../../../assets/icons/rectangle.svg"), + arrow: include_str!("../../../../assets/icons/arrow.svg"), + brush: include_str!("../../../../assets/icons/brush.svg"), + check: include_str!("../../../../assets/icons/check.svg"), + close: include_str!("../../../../assets/icons/close.svg"), + } + } +} + +/// 图标缓存 +/// +/// 用于缓存渲染后的图标纹理,避免重复加载 +pub struct IconCache { + textures: HashMap, + svgs: SvgIcons, +} + +impl IconCache { + /// 创建新的图标缓存 + pub fn new() -> Self { + Self { + textures: HashMap::new(), + svgs: SvgIcons::load(), + } + } + + /// 获取图标的 SVG 内容 + pub fn get_svg(&self, name: &str) -> Option<&str> { + match name { + "rectangle" => Some(self.svgs.rectangle), + "arrow" => Some(self.svgs.arrow), + "brush" => Some(self.svgs.brush), + "check" => Some(self.svgs.check), + "close" => Some(self.svgs.close), + _ => None, + } + } + + /// 将 SVG 渲染为 egui 纹理(简化版本,使用 Unicode 字符作为替代) + /// + /// 注意:完整的 SVG 渲染需要额外的 resvg 或类似库,这里使用文字图标作为临时方案 + pub fn get_or_create_texture( + &mut self, + ctx: &egui::Context, + name: &str, + ) -> Option { + // 如果已缓存,直接返回 + if let Some(texture) = self.textures.get(name) { + return Some(texture.clone()); + } + + // 创建简单的图标纹理(使用单色方块作为占位符) + // TODO: 使用 resvg 等库将 SVG 转换为位图 + let size = [24, 24]; + let color = egui::Color32::WHITE; + let pixels: Vec = vec![color; size[0] * size[1]]; + + let image = egui::ColorImage { + size, + pixels, + source_size: egui::Vec2::new(size[0] as f32, size[1] as f32), // egui 0.33 新增字段 + }; + + let texture = ctx.load_texture(name, image, egui::TextureOptions::default()); + + self.textures.insert(name.to_string(), texture.clone()); + Some(texture) + } +} + +impl Default for IconCache { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/ui_overlay/src/toolbar/mod.rs b/crates/ui_overlay/src/toolbar/mod.rs new file mode 100644 index 0000000..abf14ff --- /dev/null +++ b/crates/ui_overlay/src/toolbar/mod.rs @@ -0,0 +1,72 @@ +/// 工具栏模块 +/// +/// 提供标注工具栏的 UI 和交互逻辑 +mod icons; +pub mod ui; +pub mod window; + +pub use icons::IconCache; +pub use ui::{ToolbarAction, ToolbarRenderer}; +pub use window::ToolbarWindow; + +/// 标注工具类型 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnnotationTool { + /// 矩形标注 + Rectangle, + /// 箭头标注 + Arrow, + /// 自由画笔 + Freehand, +} + +/// 工具栏状态 +#[derive(Debug, Clone)] +pub struct Toolbar { + /// 当前选中的工具 + pub selected_tool: Option, + /// 当前选中的颜色 + pub selected_color: egui::Color32, + /// 笔画宽度 + pub stroke_width: f32, + /// 工具栏是否可见 + pub visible: bool, +} + +impl Default for Toolbar { + fn default() -> Self { + Self { + selected_tool: None, + selected_color: egui::Color32::from_rgb(255, 0, 0), // 默认红色 + stroke_width: 2.0, + visible: false, + } + } +} + +impl Toolbar { + /// 创建新的工具栏 + pub fn new() -> Self { + Self::default() + } + + /// 显示工具栏 + pub fn show(&mut self) { + self.visible = true; + } + + /// 隐藏工具栏 + pub fn hide(&mut self) { + self.visible = false; + } + + /// 选择工具 + pub fn select_tool(&mut self, tool: AnnotationTool) { + self.selected_tool = Some(tool); + } + + /// 取消选择工具 + pub fn deselect_tool(&mut self) { + self.selected_tool = None; + } +} diff --git a/crates/ui_overlay/src/toolbar/ui.rs b/crates/ui_overlay/src/toolbar/ui.rs new file mode 100644 index 0000000..b9cce44 --- /dev/null +++ b/crates/ui_overlay/src/toolbar/ui.rs @@ -0,0 +1,209 @@ +/// 工具栏 UI 渲染模块 +/// +/// 负责渲染工具栏界面和处理交互 +use super::{AnnotationTool, IconCache, Toolbar}; + +/// 工具栏操作结果 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToolbarAction { + /// 无操作 + None, + /// 点击完成按钮 + Finish, + /// 点击取消按钮 + Cancel, + /// 选择工具 + SelectTool(AnnotationTool), +} + +/// 工具栏渲染器 +pub struct ToolbarRenderer { + #[allow(dead_code)] + icon_cache: IconCache, +} + +impl ToolbarRenderer { + /// 创建新的工具栏渲染器 + pub fn new() -> Self { + Self { + icon_cache: IconCache::new(), + } + } + + /// 渲染工具栏 + /// + /// 返回用户的操作 + pub fn render( + &mut self, + ctx: &egui::Context, + toolbar: &mut Toolbar, + selection_rect: (f32, f32, f32, f32), // (x, y, w, h) + window_size: (f32, f32), // (width, height) + ) -> ToolbarAction { + if !toolbar.visible { + return ToolbarAction::None; + } + + let mut action = ToolbarAction::None; + + // 计算工具栏位置 + let toolbar_pos = self.calculate_position(selection_rect, window_size); + + // 创建工具栏窗口 + egui::Window::new("toolbar") + .title_bar(false) + .resizable(false) + .movable(false) + .collapsible(false) + .fixed_pos(toolbar_pos) + .frame( + egui::Frame::NONE + .fill(egui::Color32::from_rgba_premultiplied(40, 40, 40, 230)) + .corner_radius(8.0) + .inner_margin(8.0), + ) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 6.0; + + // 工具按钮区域 + ui.group(|ui| { + ui.horizontal(|ui| { + if self.render_tool_button( + ui, + "矩形", + toolbar.selected_tool == Some(AnnotationTool::Rectangle), + ) { + toolbar.select_tool(AnnotationTool::Rectangle); + action = ToolbarAction::SelectTool(AnnotationTool::Rectangle); + } + + if self.render_tool_button( + ui, + "箭头", + toolbar.selected_tool == Some(AnnotationTool::Arrow), + ) { + toolbar.select_tool(AnnotationTool::Arrow); + action = ToolbarAction::SelectTool(AnnotationTool::Arrow); + } + + if self.render_tool_button( + ui, + "画笔", + toolbar.selected_tool == Some(AnnotationTool::Freehand), + ) { + toolbar.select_tool(AnnotationTool::Freehand); + action = ToolbarAction::SelectTool(AnnotationTool::Freehand); + } + }); + }); + + ui.separator(); + + // 颜色选择器 + self.render_color_picker(ui, toolbar); + + ui.separator(); + + // 操作按钮 + if let Some(btn_action) = self.render_action_buttons(ui) { + action = btn_action; + } + }); + }); + + action + } + + /// 计算工具栏位置 + /// + /// 默认位于选择框下方,如果空间不足则显示在上方 + fn calculate_position( + &self, + selection_rect: (f32, f32, f32, f32), + window_size: (f32, f32), + ) -> egui::Pos2 { + let (sel_x, sel_y, sel_w, sel_h) = selection_rect; + let (win_w, win_h) = window_size; + + // 工具栏估计尺寸 + const TOOLBAR_WIDTH: f32 = 400.0; + const TOOLBAR_HEIGHT: f32 = 50.0; + const SPACING: f32 = 10.0; + + // 计算水平居中位置 + let mut x = sel_x + (sel_w - TOOLBAR_WIDTH) / 2.0; + x = x.max(10.0).min(win_w - TOOLBAR_WIDTH - 10.0); + + // 计算垂直位置(优先下方) + let y_below = sel_y + sel_h + SPACING; + let y_above = sel_y - TOOLBAR_HEIGHT - SPACING; + + let y = if y_below + TOOLBAR_HEIGHT + 10.0 < win_h { + y_below + } else if y_above > 10.0 { + y_above + } else { + // 如果上下都放不下,放在窗口底部 + win_h - TOOLBAR_HEIGHT - 10.0 + }; + + egui::pos2(x, y) + } + + /// 渲染工具按钮 + fn render_tool_button(&mut self, ui: &mut egui::Ui, label: &str, selected: bool) -> bool { + let button = egui::Button::new(label) + .min_size(egui::vec2(60.0, 32.0)) + .fill(if selected { + egui::Color32::from_rgb(70, 130, 180) + } else { + egui::Color32::from_rgb(60, 60, 60) + }); + + ui.add(button).clicked() + } + + /// 渲染颜色选择器 + fn render_color_picker(&mut self, ui: &mut egui::Ui, toolbar: &mut Toolbar) { + ui.horizontal(|ui| { + ui.label("颜色:"); + egui::color_picker::color_edit_button_srgba( + ui, + &mut toolbar.selected_color, + egui::color_picker::Alpha::Opaque, + ); + }); + } + + /// 渲染操作按钮(完成/取消) + fn render_action_buttons(&mut self, ui: &mut egui::Ui) -> Option { + let mut action = None; + + ui.horizontal(|ui| { + let finish_btn = egui::Button::new("✓ 完成") + .min_size(egui::vec2(70.0, 32.0)) + .fill(egui::Color32::from_rgb(50, 150, 50)); + + if ui.add(finish_btn).clicked() { + action = Some(ToolbarAction::Finish); + } + + let cancel_btn = egui::Button::new("✕ 取消") + .min_size(egui::vec2(70.0, 32.0)) + .fill(egui::Color32::from_rgb(150, 50, 50)); + + if ui.add(cancel_btn).clicked() { + action = Some(ToolbarAction::Cancel); + } + }); + + action + } +} + +impl Default for ToolbarRenderer { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/ui_overlay/src/toolbar/window.rs b/crates/ui_overlay/src/toolbar/window.rs new file mode 100644 index 0000000..7d2cefe --- /dev/null +++ b/crates/ui_overlay/src/toolbar/window.rs @@ -0,0 +1,432 @@ +/// 工具栏窗口模块 +/// +/// 使用独立的透明窗口渲染工具栏 UI(egui + wgpu) +use super::{Toolbar, ToolbarRenderer}; +use crate::toolbar::ui::ToolbarAction; +use std::sync::Arc; +use winit::dpi::PhysicalPosition; +use winit::event_loop::ActiveEventLoop; +use winit::window::{Window, WindowAttributes, WindowLevel}; + +/// 工具栏窗口 +/// +/// 独立的透明窗口,用于显示标注工具栏 +pub struct ToolbarWindow { + /// winit 窗口 + window: Arc, + /// wgpu 渲染状态 + render_state: Option, + /// egui 上下文 + egui_ctx: egui::Context, + /// egui-winit 状态 + egui_state: egui_winit::State, + /// 工具栏数据 + toolbar: Toolbar, + /// 工具栏渲染器 + toolbar_renderer: ToolbarRenderer, + /// 是否已初始化 + initialized: bool, +} + +/// wgpu 渲染状态 +struct ToolbarRenderState { + device: egui_wgpu::wgpu::Device, + queue: egui_wgpu::wgpu::Queue, + surface: egui_wgpu::wgpu::Surface<'static>, + surface_config: egui_wgpu::wgpu::SurfaceConfiguration, + renderer: egui_wgpu::Renderer, +} + +impl ToolbarWindow { + /// 创建新的工具栏窗口 + pub fn new( + event_loop: &ActiveEventLoop, + egui_ctx: egui::Context, + toolbar: Toolbar, + ) -> Result { + // 创建透明窗口 + let window_attrs = WindowAttributes::default() + .with_title("") + .with_decorations(false) + .with_transparent(true) + .with_window_level(WindowLevel::AlwaysOnTop) + .with_resizable(false) + .with_visible(false); // 初始不可见 + + let window = event_loop + .create_window(window_attrs) + .map_err(|e| format!("Failed to create toolbar window: {}", e))?; + + let window = Arc::new(window); + + // 创建 egui-winit 状态 + let egui_state = egui_winit::State::new( + egui_ctx.clone(), + egui::ViewportId::ROOT, + &window, + None, + None, + None, + ); + + // 在内部创建 ToolbarRenderer + let toolbar_renderer = ToolbarRenderer::new(); + + Ok(Self { + window, + render_state: None, + egui_ctx, + egui_state, + toolbar, + toolbar_renderer, + initialized: false, + }) + } + + /// 初始化 wgpu 渲染 + pub fn initialize_renderer(&mut self) -> Result<(), String> { + if self.initialized { + return Ok(()); + } + + let window = self.window.clone(); + + // 使用 pollster 同步执行异步初始化 + let render_state = pollster::block_on(async { Self::create_render_state(window).await })?; + + self.render_state = Some(render_state); + self.initialized = true; + + Ok(()) + } + + /// 创建 wgpu 渲染状态 + async fn create_render_state(window: Arc) -> Result { + use egui_wgpu::wgpu; + + // 创建 wgpu 实例 + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + + // 创建 surface + let surface = instance + .create_surface(window.clone()) + .map_err(|e| format!("Failed to create surface: {}", e))?; + + // 请求适配器 + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .map_err(|e| format!("Failed to find adapter: {:?}", e))?; + + // 请求设备和队列 + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: Some("Toolbar Device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::default(), + experimental_features: wgpu::ExperimentalFeatures::disabled(), + }) + .await + .map_err(|e| format!("Failed to create device: {}", e))?; + + // 配置 surface + let size = window.inner_size(); + let capabilities = surface.get_capabilities(&adapter); + + // 选择支持的 alpha mode(优先 PostMultiplied,回退到 Opaque) + let alpha_mode = if capabilities + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PostMultiplied) + { + wgpu::CompositeAlphaMode::PostMultiplied + } else if capabilities + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + capabilities.alpha_modes[0] + }; + + tracing::info!( + "Toolbar window alpha mode: {:?}, available: {:?}", + alpha_mode, + capabilities.alpha_modes + ); + + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: capabilities.formats[0], + width: size.width.max(1), + height: size.height.max(1), + present_mode: wgpu::PresentMode::Fifo, + alpha_mode, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + // 创建 egui-wgpu 渲染器 + let renderer = egui_wgpu::Renderer::new( + &device, + surface_config.format, + egui_wgpu::RendererOptions::default(), + ); + + Ok(ToolbarRenderState { + device, + queue, + surface, + surface_config, + renderer, + }) + } + + /// 显示窗口 + pub fn show(&self) { + tracing::debug!("Setting toolbar window visible"); + self.window.set_visible(true); + self.window.request_redraw(); + } + + /// 隐藏窗口 + pub fn hide(&self) { + tracing::debug!("Hiding toolbar window"); + self.window.set_visible(false); + } + + /// 更新窗口位置 + /// + /// # 参数 + /// - `selection_rect`: 选择框在虚拟桌面坐标系中的位置 (x, y, width, height) + /// - `monitor_info`: 选择框所在的显示器信息 (virtual_x, virtual_y, width, height) + /// + /// 在多显示器环境下,虚拟坐标可能为负数,需要转换为屏幕坐标 + pub fn update_position( + &self, + selection_rect: (f32, f32, f32, f32), + monitor_info: Option<(i32, i32, u32, u32)>, + ) { + let (sel_x, sel_y, sel_w, sel_h) = selection_rect; + + // 工具栏尺寸 + const TOOLBAR_WIDTH: f32 = 400.0; + const TOOLBAR_HEIGHT: f32 = 50.0; + const SPACING: f32 = 10.0; + + // 在虚拟坐标系中计算工具栏位置(居中对齐,位于选择框下方) + let virtual_x = sel_x + (sel_w - TOOLBAR_WIDTH) / 2.0; + let virtual_y = sel_y + sel_h + SPACING; + + // 如果有显示器信息,将虚拟坐标转换为屏幕坐标 + let (screen_x, screen_y) = if let Some((monitor_vx, monitor_vy, monitor_w, monitor_h)) = + monitor_info + { + // 虚拟坐标 -> 显示器本地坐标 + let local_x = virtual_x - monitor_vx as f32; + let local_y = virtual_y - monitor_vy as f32; + + // 确保工具栏在显示器范围内 + let clamped_x = local_x + .max(10.0) + .min(monitor_w as f32 - TOOLBAR_WIDTH - 10.0); + let clamped_y = local_y + .max(10.0) + .min(monitor_h as f32 - TOOLBAR_HEIGHT - 10.0); + + // 本地坐标 -> 屏幕坐标(winit 的 set_outer_position 需要屏幕绝对坐标) + let screen_x = monitor_vx as f32 + clamped_x; + let screen_y = monitor_vy as f32 + clamped_y; + + tracing::debug!( + "Toolbar position: virtual=({:.1}, {:.1}), local=({:.1}, {:.1}), screen=({:.1}, {:.1}), monitor=({}, {}) {}x{}", + virtual_x, virtual_y, local_x, local_y, screen_x, screen_y, + monitor_vx, monitor_vy, monitor_w, monitor_h + ); + + (screen_x, screen_y) + } else { + // 没有显示器信息,使用简单钳制策略 + let clamped_x = virtual_x.clamp(10.0, 10000.0); + let clamped_y = virtual_y.clamp(10.0, 10000.0); + + tracing::warn!( + "Toolbar position without monitor info: virtual=({:.1}, {:.1}), clamped=({:.1}, {:.1})", + virtual_x, virtual_y, clamped_x, clamped_y + ); + + (clamped_x, clamped_y) + }; + + // 设置窗口大小(使用逻辑像素以支持 HiDPI) + let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new( + TOOLBAR_WIDTH as f64, + TOOLBAR_HEIGHT as f64, + )); + + // 设置窗口位置(屏幕绝对坐标) + self.window + .set_outer_position(PhysicalPosition::new(screen_x as i32, screen_y as i32)); + } + + /// 渲染工具栏并返回操作 + pub fn render(&mut self) -> ToolbarAction { + use egui_wgpu::wgpu; + + if !self.initialized || self.render_state.is_none() { + tracing::warn!("Toolbar render called but not initialized"); + return ToolbarAction::None; + } + + let render_state = self.render_state.as_mut().unwrap(); + + // 获取 surface texture + let output = match render_state.surface.get_current_texture() { + Ok(output) => output, + Err(e) => { + tracing::error!("Failed to get surface texture: {:?}", e); + return ToolbarAction::None; + } + }; + + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + // 准备 egui 输入 + let raw_input = self.egui_state.take_egui_input(&self.window); + + // 运行 egui UI + let mut action = ToolbarAction::None; + let full_output = self.egui_ctx.run(raw_input, |ctx| { + let size = self.window.inner_size(); + let window_size = (size.width as f32, size.height as f32); + + // 工具栏占据整个窗口,所以选择框相对坐标为整个窗口 + let selection_rect = (0.0, 0.0, window_size.0, window_size.1); + + tracing::debug!( + "Toolbar render: visible={}, size={:?}", + self.toolbar.visible, + window_size + ); + + action = + self.toolbar_renderer + .render(ctx, &mut self.toolbar, selection_rect, window_size); + }); + + // 处理平台输出 + self.egui_state + .handle_platform_output(&self.window, full_output.platform_output); + + // 渲染 + let clipped_primitives = self + .egui_ctx + .tessellate(full_output.shapes, full_output.pixels_per_point); + + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [ + render_state.surface_config.width, + render_state.surface_config.height, + ], + pixels_per_point: full_output.pixels_per_point, + }; + + // 更新纹理 + for (id, image_delta) in &full_output.textures_delta.set { + render_state.renderer.update_texture( + &render_state.device, + &render_state.queue, + *id, + image_delta, + ); + } + + // 更新缓冲区 + let mut encoder = + render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Toolbar Update Encoder"), + }); + + render_state.renderer.update_buffers( + &render_state.device, + &render_state.queue, + &mut encoder, + &clipped_primitives, + &screen_descriptor, + ); + + render_state.queue.submit(Some(encoder.finish())); + + // 创建渲染 encoder + let mut render_encoder = + render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Toolbar Render Encoder"), + }); + + { + let render_pass = render_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Toolbar Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_state.renderer.render( + &mut render_pass.forget_lifetime(), + &clipped_primitives, + &screen_descriptor, + ); + } + + render_state.queue.submit(Some(render_encoder.finish())); + output.present(); + + // 释放纹理 + for id in &full_output.textures_delta.free { + render_state.renderer.free_texture(id); + } + + action + } + + /// 处理窗口事件 + pub fn handle_event(&mut self, event: &winit::event::WindowEvent) -> bool { + self.egui_state + .on_window_event(&self.window, event) + .consumed + } + + /// 获取窗口 ID + pub fn window_id(&self) -> winit::window::WindowId { + self.window.id() + } + + /// 获取底层窗口引用(用于请求重绘等操作) + pub fn window(&self) -> &Window { + &self.window + } +} diff --git a/crates/ui_overlay/src/window_info.rs b/crates/ui_overlay/src/window_info.rs index 1c153b6..996dac5 100644 --- a/crates/ui_overlay/src/window_info.rs +++ b/crates/ui_overlay/src/window_info.rs @@ -1,22 +1,40 @@ /// 窗口信息模块 /// -/// 提供单个窗口的信息管理和渲染功能 -use crate::backend::{create_backend, BackendType, RenderBackend}; -use softbuffer::{Context as SoftbufferContext, Surface as SoftbufferSurface}; -use std::rc::Rc; +/// 提供单个窗口的信息管理和 egui 渲染功能 +use egui_wgpu::wgpu; +use std::sync::Arc; use winit::dpi::PhysicalSize; use winit::window::Window; +/// WGPU 渲染状态 +pub struct WgpuState { + pub surface: wgpu::Surface<'static>, + pub device: Arc, + pub queue: Arc, + pub surface_config: wgpu::SurfaceConfiguration, + pub renderer: egui_wgpu::Renderer, + /// 设备支持的最大纹理尺寸(用于调试和降级) + #[allow(dead_code)] + pub max_texture_size: u32, + pub background_renderer: crate::background_renderer::BackgroundRenderer, + pub background_texture: Option, + pub background_bind_group: Option, +} + /// 窗口信息结构体 pub struct WindowInfo { - pub window: Rc, - pub render_backend: Option>, - pub softbuffer_context: Option>>, - pub softbuffer_surface: Option, Rc>>, + pub window: Arc, + pub egui_ctx: egui::Context, + pub egui_state: egui_winit::State, + pub wgpu_state: Option, pub size_px: PhysicalSize, pub scale: f64, pub virtual_x: i32, pub virtual_y: i32, + /// 创建时的正确物理尺寸(用于检测和恢复 winit 的错误报告) + pub initial_size_px: PhysicalSize, + /// 创建时的正确 scale(用于检测和恢复 winit 的错误报告) + pub initial_scale: f64, } impl WindowInfo { @@ -27,143 +45,431 @@ impl WindowInfo { virtual_x: i32, virtual_y: i32, ) -> Self { + let window = Arc::new(window); + + // 创建 egui context + let egui_ctx = egui::Context::default(); + + // 创建 egui_winit state + let egui_state = egui_winit::State::new( + egui_ctx.clone(), + egui::ViewportId::ROOT, + &window, + Some(scale as f32), + None, // 使用默认像素每点 + None, + ); + Self { - window: Rc::new(window), - render_backend: None, - softbuffer_context: None, - softbuffer_surface: None, + window, + egui_ctx, + egui_state, + wgpu_state: None, size_px, scale, virtual_x, virtual_y, + initial_size_px: size_px, // 保存创建时的正确值 + initial_scale: scale, // 保存创建时的正确值 } } pub fn update_size(&mut self, new_size: PhysicalSize) { + if self.size_px != new_size { + tracing::warn!( + "⚠️ WindowInfo size 变化: {}x{} -> {}x{} (可能导致背景不匹配)", + self.size_px.width, + self.size_px.height, + new_size.width, + new_size.height + ); + } self.size_px = new_size; + + // 更新 wgpu surface 配置 + if let Some(wgpu_state) = &mut self.wgpu_state { + wgpu_state.surface_config.width = new_size.width.max(1); + wgpu_state.surface_config.height = new_size.height.max(1); + wgpu_state + .surface + .configure(&wgpu_state.device, &wgpu_state.surface_config); + } } pub fn update_scale(&mut self, new_scale: f64) { self.scale = new_scale; } - pub fn init_softbuffer(&mut self) -> Result<(), String> { - if self.softbuffer_context.is_some() { - return Ok(()); - } + /// 检查是否已上传背景纹理 + pub fn has_background_texture(&self) -> bool { + self.wgpu_state + .as_ref() + .and_then(|s| s.background_bind_group.as_ref()) + .is_some() + } - let context = SoftbufferContext::new(self.window.clone()) - .map_err(|e| format!("Failed to create softbuffer context: {e}"))?; + /// 上传背景纹理 + /// + /// 从完整虚拟桌面背景中裁剪出当前窗口对应的区域并上传 + /// + /// # Arguments + /// * `full_bg_data` - 完整虚拟桌面背景图数据(RGBA8) + /// * `full_bg_width` - 完整背景图宽度 + /// * `full_bg_height` - 完整背景图高度 + /// * `virtual_min_x` - 虚拟桌面的最小X坐标(用于坐标转换) + /// * `virtual_min_y` - 虚拟桌面的最小Y坐标(用于坐标转换) + pub fn upload_background_texture( + &mut self, + full_bg_data: &[u8], + full_bg_width: u32, + full_bg_height: u32, + virtual_min_x: i32, + virtual_min_y: i32, + ) { + let wgpu_state = match &mut self.wgpu_state { + Some(state) => state, + None => { + tracing::error!("无法上传背景:WGPU 未初始化"); + return; + } + }; - let surface = SoftbufferSurface::new(&context, self.window.clone()) - .map_err(|e| format!("Failed to create softbuffer surface: {e}"))?; + // 重要:self.virtual_x/y 来自 winit 的 monitor.position() + // 根据实际测试,monitor.position() 返回的是物理坐标(即使类型名为 PhysicalPosition) + // 完整背景画布也使用物理坐标系统,因此直接使用,不需要转换 + // + // 证据: + // - 窗口1: monitor.scale=1, position=(-1080, -351), window.scale=2 + // - 如果 position 是逻辑坐标,用 monitor.scale=1 转换:(-1080, -351)*1=(-1080, -351) + // - 如果 position 已是物理坐标:(-1080, -351) + // - 背景画布使用物理坐标构建,所以应该直接使用 position + let phys_x = self.virtual_x; + let phys_y = self.virtual_y; - self.softbuffer_context = Some(context); - self.softbuffer_surface = Some(surface); + tracing::debug!( + "📍 窗口坐标: virtual=({}, {}), 直接作为物理坐标使用, scale={}", + phys_x, + phys_y, + self.scale + ); - Ok(()) - } + // 计算窗口在完整背景图中的位置(canvas 坐标) + let canvas_x = (phys_x - virtual_min_x).max(0) as u32; + let canvas_y = (phys_y - virtual_min_y).max(0) as u32; + let window_w = self.size_px.width; + let window_h = self.size_px.height; - pub fn present_pixels(&mut self, pixels: &[u8], width: u32, height: u32) -> Result<(), String> { - use std::num::NonZeroU32; + tracing::info!( + "🎨 为窗口裁剪背景: 完整背景={}x{} (物理画布)", + full_bg_width, + full_bg_height + ); + tracing::info!( + " └─ 窗口物理坐标 (直接使用 monitor.position): ({}, {}), scale={}", + phys_x, + phys_y, + self.scale + ); + tracing::info!(" └─ 窗口物理尺寸={}x{}", window_w, window_h); + tracing::info!( + " └─ 物理最小值=({}, {}) -> canvas坐标=({}, {})", + virtual_min_x, + virtual_min_y, + canvas_x, + canvas_y + ); - self.init_softbuffer()?; + // 裁剪出窗口对应的背景区域 + let cropped_data = Self::crop_background( + full_bg_data, + full_bg_width, + full_bg_height, + canvas_x, + canvas_y, + window_w, + window_h, + ); - let surface = self - .softbuffer_surface - .as_mut() - .ok_or_else(|| "Softbuffer surface not initialized".to_string())?; + tracing::info!( + " └─ 裁剪后纹理: {}x{} ({:.2}MB)", + window_w, + window_h, + cropped_data.len() as f64 / 1024.0 / 1024.0 + ); - if width == 0 || height == 0 { - return Ok(()); - } + // 创建纹理并直接全屏显示 + let (texture, bind_group) = wgpu_state.background_renderer.create_texture( + &wgpu_state.device, + &wgpu_state.queue, + &cropped_data, + window_w, + window_h, + ); - surface - .resize( - NonZeroU32::new(width).ok_or_else(|| "Invalid surface width".to_string())?, - NonZeroU32::new(height).ok_or_else(|| "Invalid surface height".to_string())?, - ) - .map_err(|e| format!("Failed to resize softbuffer surface: {e}"))?; - - let mut buffer = surface - .buffer_mut() - .map_err(|e| format!("Failed to lock softbuffer buffer: {e}"))?; - - // 转换 RGBA -> ARGB - for (slot, chunk) in buffer.iter_mut().zip(pixels.chunks_exact(4)) { - let r = chunk[0] as u32; - let g = chunk[1] as u32; - let b = chunk[2] as u32; - let a = chunk[3] as u32; - *slot = (a << 24) | (r << 16) | (g << 8) | b; - } + wgpu_state.background_texture = Some(texture); + wgpu_state.background_bind_group = Some(bind_group); + + tracing::info!("✅ 背景纹理上传成功"); + } - buffer - .present() - .map_err(|e| format!("Failed to present softbuffer frame: {e}"))?; + /// 从完整背景图中裁剪出指定区域 + fn crop_background( + src_data: &[u8], + src_width: u32, + src_height: u32, + crop_x: u32, + crop_y: u32, + crop_width: u32, + crop_height: u32, + ) -> Vec { + let mut cropped = vec![0u8; (crop_width * crop_height * 4) as usize]; - Ok(()) + for row in 0..crop_height { + let src_y = crop_y + row; + if src_y >= src_height { + break; + } + + let src_row_start = ((src_y * src_width + crop_x) * 4) as usize; + let src_row_end = src_row_start + (crop_width * 4) as usize; + let dst_row_start = (row * crop_width * 4) as usize; + let dst_row_end = dst_row_start + (crop_width * 4) as usize; + + if src_row_end <= src_data.len() && dst_row_end <= cropped.len() { + cropped[dst_row_start..dst_row_end] + .copy_from_slice(&src_data[src_row_start..src_row_end]); + } + } + + cropped } - /// 初始化渲染后端 - pub fn init_backend(&mut self) -> Result<(), String> { - if self.render_backend.is_some() { + /// 初始化 WGPU 渲染器 + pub fn init_wgpu(&mut self) -> Result<(), String> { + if self.wgpu_state.is_some() { return Ok(()); } - // 使用工厂函数创建最佳可用的 backend - let backend = create_backend( - Some(&self.window), - self.size_px.width as i32, - self.size_px.height as i32, + // 创建 wgpu 实例 + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + + // 创建 surface - 使用 Arc 获得 'static 生命周期 + let surface = instance + .create_surface(self.window.clone()) + .map_err(|e| format!("Failed to create surface: {}", e))?; + + // 请求 adapter + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .map_err(|e| format!("Failed to find suitable adapter: {}", e))?; + + // 请求 device 和 queue + let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: Some("egui_device"), + required_features: wgpu::Features::default(), + required_limits: wgpu::Limits::default(), + memory_hints: wgpu::MemoryHints::default(), + experimental_features: Default::default(), + trace: Default::default(), + })) + .map_err(|e| format!("Failed to create device: {}", e))?; + + let device = Arc::new(device); + let queue = Arc::new(queue); + + // 配置 surface + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .find(|f| f.is_srgb()) + .copied() + .unwrap_or(surface_caps.formats[0]); + + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: self.size_px.width.max(1), + height: self.size_px.height.max(1), + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + surface.configure(&device, &surface_config); + + // 创建 egui_wgpu 渲染器 + let renderer = egui_wgpu::Renderer::new( + &device, + surface_format, + egui_wgpu::RendererOptions::default(), ); - // 如果是 CPU backend,初始化 softbuffer - if backend.backend_type() == BackendType::CpuRaster { - self.init_softbuffer()?; - } + // 获取 GPU 的最大纹理尺寸限制 + let max_texture_size = device.limits().max_texture_dimension_2d; + + // 创建背景渲染器 + let background_renderer = + crate::background_renderer::BackgroundRenderer::new(&device, surface_format); + + self.wgpu_state = Some(WgpuState { + surface, + device, + queue, + surface_config, + renderer, + max_texture_size, + background_renderer, + background_texture: None, + background_bind_group: None, + }); + + tracing::info!( + "🚀 WGPU 渲染器初始化成功 ({}x{}, format: {:?}, max_texture: {})", + self.size_px.width, + self.size_px.height, + surface_format, + max_texture_size + ); - self.render_backend = Some(backend); Ok(()) } /// 渲染一帧 + /// + /// # Arguments + /// * `draw_fn` - 绘制函数,接收 egui Context 用于 UI 绘制 pub fn render(&mut self, draw_fn: F) -> Result<(), String> where - F: FnOnce(&skia_safe::Canvas), + F: FnMut(&egui::Context), { - let backend = self - .render_backend - .as_mut() - .ok_or("Render backend not initialized")?; + let wgpu_state = self.wgpu_state.as_mut().ok_or("WGPU not initialized")?; - let backend_type = backend.backend_type(); + // 1. 收集 egui input + let raw_input = self.egui_state.take_egui_input(&self.window); - // 1. 准备 surface - backend - .prepare_surface(self.size_px.width as i32, self.size_px.height as i32) - .map_err(|e| e.to_string())?; + // 2. 运行 egui 逻辑 + let full_output = self.egui_ctx.run(raw_input, draw_fn); - // 2. 获取 canvas 并绘制 - if let Some(canvas) = backend.canvas() { - draw_fn(canvas); + // 3. 处理 platform output + self.egui_state + .handle_platform_output(&self.window, full_output.platform_output); + + // 4. 获取下一帧的 surface texture + let surface_texture = wgpu_state + .surface + .get_current_texture() + .map_err(|e| format!("Failed to get surface texture: {}", e))?; + + let surface_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + // 5. 创建 command encoder + let mut encoder = + wgpu_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui_encoder"), + }); + + // 6. 准备 egui 渲染数据 + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [self.size_px.width, self.size_px.height], + pixels_per_point: self.scale as f32, + }; + + let clipped_primitives = self + .egui_ctx + .tessellate(full_output.shapes, full_output.pixels_per_point); + + // 7. 更新 egui textures + for (id, image_delta) in &full_output.textures_delta.set { + wgpu_state.renderer.update_texture( + &wgpu_state.device, + &wgpu_state.queue, + *id, + image_delta, + ); } - // 3. Flush 并获取像素数据 - let pixels = backend.flush_and_read_pixels().map_err(|e| e.to_string())?; + // 8. 更新 buffers + wgpu_state.renderer.update_buffers( + &wgpu_state.device, + &wgpu_state.queue, + &mut encoder, + &clipped_primitives, + &screen_descriptor, + ); - // 4. 根据后端类型决定如何呈现 - match backend_type { - BackendType::CpuRaster => { - // CPU backend: 通过 softbuffer 呈现 - if !pixels.is_empty() { - self.present_pixels(&pixels, self.size_px.width, self.size_px.height)?; - } - } - BackendType::MetalGpu | BackendType::Direct3dGpu => { - // GPU backend: 已经直接渲染到窗口,无需额外操作 - // flush_and_read_pixels 已经完成了渲染和呈现 - } + // 9. 渲染背景(如果有) + if let Some(ref bg_bind_group) = &wgpu_state.background_bind_group { + tracing::trace!("渲染 wgpu 背景层"); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("background_render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &surface_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + wgpu_state + .background_renderer + .render(&mut render_pass, bg_bind_group); + } else { + tracing::trace!("背景 bind_group 不存在,跳过背景渲染"); + } // render_pass 在这里被 drop + + // 10. 渲染 egui(UI 层,覆盖在背景之上) + { + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("egui_render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &surface_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, // 保留背景,不清除 + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // 使用 forget_lifetime() 来满足 egui_wgpu::Renderer::render() 的生命周期要求 + wgpu_state.renderer.render( + &mut render_pass.forget_lifetime(), + &clipped_primitives, + &screen_descriptor, + ); + } // render_pass 在这里被 drop + + // 11. 提交 command buffer + wgpu_state.queue.submit(std::iter::once(encoder.finish())); + + // 12. Present + surface_texture.present(); + + // 13. 释放不再使用的 textures + for id in &full_output.textures_delta.free { + wgpu_state.renderer.free_texture(id); } Ok(()) diff --git a/crates/ui_overlay/src/window_manager.rs b/crates/ui_overlay/src/window_manager.rs index 744fda6..795f2ae 100644 --- a/crates/ui_overlay/src/window_manager.rs +++ b/crates/ui_overlay/src/window_manager.rs @@ -1,28 +1,208 @@ /// 窗口管理器模块 /// -/// 提供多窗口的统一管理功能 +/// 负责管理多显示器环境下的窗口创建、布局和生命周期。 +/// +/// # 多显示器支持 +/// +/// 该模块自动检测所有可用显示器,并为每个显示器创建一个全屏覆盖窗口。 +/// 窗口坐标使用虚拟桌面坐标系,支持跨显示器的区域选择。 +/// +/// # HiDPI 支持 +/// +/// 正确处理不同 scale_factor 的显示器: +/// - scale = 1.0: 使用 LogicalSize(避免 macOS winit 的尺寸缩小问题) +/// - scale ≠ 1.0: 使用 PhysicalSize(Retina 等高 DPI 显示器) pub use crate::window_info::WindowInfo; /// 窗口管理器 +/// +/// 维护所有显示器窗口的集合,提供统一的窗口操作接口。 pub struct WindowManager { + /// 所有显示器的窗口信息列表,按显示器索引排序 pub windows: Vec, } +/// 窗口尺寸验证容差(像素)- 仅在 debug 模式下使用 +#[cfg(debug_assertions)] +const SIZE_VALIDATION_TOLERANCE: i32 = 1; + impl WindowManager { + /// 创建新的窗口管理器实例 pub fn new() -> Self { Self { windows: Vec::new(), } } + /// 根据窗口 ID 查找窗口索引 + /// + /// # Arguments + /// * `window_id` - winit 窗口标识符 + /// + /// # Returns + /// 窗口在列表中的索引,如果未找到则返回 None pub fn find_window_index(&self, window_id: winit::window::WindowId) -> Option { self.windows.iter().position(|w| w.window.id() == window_id) } + /// 检查是否已有窗口 pub fn has_windows(&self) -> bool { !self.windows.is_empty() } + /// 根据显示器特性选择合适的窗口尺寸类型 + /// + /// # Arguments + /// * `physical_size` - 显示器的物理尺寸 + /// * `scale_factor` - 显示器的缩放因子 + /// + /// # Returns + /// 适合该显示器的窗口尺寸规格 + fn determine_window_size( + physical_size: winit::dpi::PhysicalSize, + scale_factor: f64, + ) -> winit::dpi::Size { + // 关键修复:必须使用 LogicalSize,让 winit 自己处理 scale + // + // 问题分析: + // 1. monitor.size() 返回的是实际物理像素尺寸(如 1080x1920) + // 2. 如果我们用 PhysicalSize(1080, 1920),winit 会创建 1080x1920 像素的窗口 + // 3. 但如果窗口的 scale 是某个值,最终显示的"逻辑点"会被按 scale 缩放 + // 4. 我们需要的是逻辑尺寸 = 物理尺寸 / scale + // + // 解决方案:使用 LogicalSize,逻辑尺寸 = 物理尺寸 / monitor.scale + // 这样 winit 会根据窗口的实际 scale 计算出正确的物理尺寸 + let logical_width = physical_size.width as f64 / scale_factor; + let logical_height = physical_size.height as f64 / scale_factor; + + winit::dpi::Size::Logical(winit::dpi::LogicalSize::new(logical_width, logical_height)) + } + + /// 验证窗口尺寸是否符合预期 + /// + /// 检查窗口实际尺寸是否与显示器报告的尺寸匹配。 + /// 根据平台和运行模式的不同,winit 可能返回以下几种尺寸: + /// - 物理尺寸(physical) + /// - 逻辑尺寸(logical = physical / scale) + /// - 缩放物理尺寸(scaled physical = physical × scale) + /// + /// # Arguments + /// * `window_index` - 窗口索引(用于日志) + /// * `actual_size` - 窗口实际尺寸 + /// * `expected_size` - 显示器报告的尺寸 + /// * `scale_factor` - 显示器缩放因子 + #[cfg(debug_assertions)] + fn validate_window_size( + window_index: usize, + actual_size: winit::dpi::PhysicalSize, + expected_size: winit::dpi::PhysicalSize, + scale_factor: f64, + ) { + // 计算期望的逻辑尺寸 + let expected_logical_w = (expected_size.width as f64 / scale_factor) as u32; + let expected_logical_h = (expected_size.height as f64 / scale_factor) as u32; + + // 检查是否匹配物理尺寸 + let width_diff = (actual_size.width as i32 - expected_size.width as i32).abs(); + let height_diff = (actual_size.height as i32 - expected_size.height as i32).abs(); + let matches_physical = + width_diff <= SIZE_VALIDATION_TOLERANCE && height_diff <= SIZE_VALIDATION_TOLERANCE; + + // 检查是否匹配逻辑尺寸 + let width_diff_logical = (actual_size.width as i32 - expected_logical_w as i32).abs(); + let height_diff_logical = (actual_size.height as i32 - expected_logical_h as i32).abs(); + let matches_logical = width_diff_logical <= SIZE_VALIDATION_TOLERANCE + && height_diff_logical <= SIZE_VALIDATION_TOLERANCE; + + // 检查是否匹配 scaled physical(物理尺寸 × scale_factor) + let expected_scaled_w = (expected_size.width as f64 * scale_factor).round() as u32; + let expected_scaled_h = (expected_size.height as f64 * scale_factor).round() as u32; + let width_diff_scaled = (actual_size.width as i32 - expected_scaled_w as i32).abs(); + let height_diff_scaled = (actual_size.height as i32 - expected_scaled_h as i32).abs(); + let matches_scaled = width_diff_scaled <= SIZE_VALIDATION_TOLERANCE + && height_diff_scaled <= SIZE_VALIDATION_TOLERANCE; + + // 检查是否为预期尺寸的整数倍(处理隐式缩放) + // 某些情况下 winit 报告的 scale_factor 不准确,窗口会按实际显示器缩放创建 + let mut matches_multiple = false; + let mut detected_scale = 1.0; + for test_scale in [2.0, 1.5, 2.5, 3.0] { + let test_w = (expected_size.width as f64 * test_scale).round() as u32; + let test_h = (expected_size.height as f64 * test_scale).round() as u32; + let w_diff = (actual_size.width as i32 - test_w as i32).abs(); + let h_diff = (actual_size.height as i32 - test_h as i32).abs(); + // 允许更大的容差(可能有菜单栏等) + if w_diff <= SIZE_VALIDATION_TOLERANCE * 2 && h_diff <= SIZE_VALIDATION_TOLERANCE * 2 { + matches_multiple = true; + detected_scale = test_scale; + break; + } + } + + if matches_physical { + tracing::debug!( + "[Window {}] ✅ 窗口尺寸匹配物理尺寸 ({}x{})", + window_index, + actual_size.width, + actual_size.height + ); + } else if matches_logical { + tracing::debug!( + "[Window {}] ✅ 窗口尺寸匹配逻辑尺寸 ({}x{}, physical={}x{})", + window_index, + actual_size.width, + actual_size.height, + expected_size.width, + expected_size.height + ); + } else if matches_scaled { + tracing::debug!( + "[Window {}] ✅ 窗口尺寸匹配 scaled physical ({}x{}, scale={:.2})", + window_index, + actual_size.width, + actual_size.height, + scale_factor + ); + } else if matches_multiple { + tracing::debug!( + "[Window {}] ✅ 窗口尺寸匹配隐式缩放 ({}x{}, 检测到实际scale={:.1}x,报告scale={:.2})", + window_index, + actual_size.width, + actual_size.height, + detected_scale, + scale_factor + ); + } else { + // 只有在所有尺寸都不匹配时才警告 + tracing::warn!( + "[Window {}] ⚠️ 窗口尺寸异常! Monitor={}x{} (scale={:.2}), 实际={}x{}, 预期physical={}x{} 或 logical={}x{} 或 scaled={}x{}", + window_index, + expected_size.width, + expected_size.height, + scale_factor, + actual_size.width, + actual_size.height, + expected_size.width, + expected_size.height, + expected_logical_w, + expected_logical_h, + expected_scaled_w, + expected_scaled_h + ); + } + } + + /// 为所有可用显示器初始化窗口 + /// + /// 自动检测所有显示器并为每个显示器创建一个全屏覆盖窗口。 + /// 窗口会根据显示器的 DPI 设置正确的尺寸类型。 + /// + /// # Arguments + /// * `event_loop` - winit 事件循环的活动实例 + /// * `base_attrs` - 基础窗口属性(将被克隆并为每个显示器定制) + /// + /// # Note + /// 如果已有窗口,该方法会直接返回,避免重复初始化。 pub fn initialize_windows( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, @@ -32,7 +212,6 @@ impl WindowManager { return; } - // 使用 winit 作为唯一的显示器信息来源 let monitors: Vec<_> = event_loop.available_monitors().collect(); #[cfg(debug_assertions)] @@ -48,7 +227,7 @@ impl WindowManager { tracing::debug!("[Winit Monitor {}] name: {}", i, name); tracing::debug!( - "[Winit Monitor {}] 物理位置: ({}, {}), 物理尺寸: {}x{}", + "[Winit Monitor {}] position() 返回: ({}, {}), size() 返回: {}x{} (类型声称是 Physical)", i, pos.x, pos.y, @@ -56,6 +235,18 @@ impl WindowManager { size.height ); tracing::debug!("[Winit Monitor {}] scale_factor: {}", i, scale); + tracing::debug!( + "[Winit Monitor {}] 如果 position 是逻辑坐标,物理坐标应该是: ({}, {})", + i, + (pos.x as f64 * scale).round() as i32, + (pos.y as f64 * scale).round() as i32 + ); + tracing::debug!( + "[Winit Monitor {}] 如果 size 是逻辑尺寸,物理尺寸应该是: {}x{}", + i, + (size.width as f64 * scale).round() as u32, + (size.height as f64 * scale).round() as u32 + ); } } @@ -64,10 +255,40 @@ impl WindowManager { let physical_position = monitor.position(); let scale = monitor.scale_factor(); - // 在创建时就指定窗口的尺寸和位置 + #[cfg(debug_assertions)] + tracing::info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + tracing::info!( + "[创建窗口 {}] Monitor 信息: physical_size={}x{}, physical_position=({}, {}), scale={}", + window_index, + physical_size.width, + physical_size.height, + physical_position.x, + physical_position.y, + scale + ); + + // 根据显示器特性选择合适的窗口尺寸类型 + let size_spec = Self::determine_window_size(physical_size, scale); + + #[cfg(debug_assertions)] + { + let logical_w = physical_size.width as f64 / scale; + let logical_h = physical_size.height as f64 / scale; + tracing::info!( + "[创建窗口 {}] 窗口尺寸类型: Logical (物理尺寸={}x{} / monitor.scale={} = 逻辑尺寸={:.0}x{:.0})", + window_index, + physical_size.width, + physical_size.height, + scale, + logical_w, + logical_h + ); + } + + // 创建窗口属性:指定尺寸和位置 let attrs = base_attrs .clone() - .with_inner_size(physical_size) + .with_inner_size(size_spec) .with_position(winit::dpi::Position::Physical(physical_position)); match event_loop.create_window(attrs) { @@ -111,45 +332,93 @@ impl WindowManager { ); } - // 检查尺寸差异,考虑 HiDPI 缩放 - // 在 Retina 显示器上,实际窗口尺寸可能是逻辑尺寸 * backing_scale_factor - if actual_inner_size != physical_size { - let size_ratio = - actual_inner_size.width as f64 / physical_size.width as f64; - - // 如果比例接近整数(如 2.0),这是 HiDPI 的正常行为 - if (size_ratio - size_ratio.round()).abs() < 0.01 && size_ratio >= 1.0 { - tracing::debug!( - "[Window {}] HiDPI 缩放: Monitor报告 {}x{}, 窗口实际 {}x{} (backing_scale ≈ {:.1}x)", - window_index, - physical_size.width, - physical_size.height, - actual_inner_size.width, - actual_inner_size.height, - size_ratio - ); - } else { - // 只有在比例异常时才输出警告 - tracing::warn!( - "[Window {}] ⚠️ 尺寸异常! Monitor报告 {}x{}, 窗口实际 {}x{} (比例: {:.2})", - window_index, - physical_size.width, - physical_size.height, - actual_inner_size.width, - actual_inner_size.height, - size_ratio - ); - } - } + // 验证窗口尺寸是否符合预期 + Self::validate_window_size( + window_index, + actual_inner_size, + physical_size, + scale, + ); } + // 重要:从 window 本身获取实际的 scale_factor 和位置 + // 因为 winit 在 macOS 上 monitor 返回的值可能不准确 + let actual_scale = window.scale_factor(); + + // 检查窗口的实际位置 + let actual_position = window.outer_position().ok(); + + tracing::info!( + "[创建窗口 {}] Scale 对比: monitor.scale={}, window.scale={}", + window_index, + scale, + actual_scale + ); + + // 决定使用哪个位置 + // 注意:在 macOS 上,monitor.position() 和 window.outer_position() 可能使用不同的坐标系统 + // monitor.position() 在 high DPI 下可能返回逻辑坐标 + // window.outer_position() 也可能返回逻辑坐标 + // 为了保持一致性,始终使用 monitor.position() 并根据 scale 转换 + let final_position = if let Some(pos) = actual_position { + tracing::info!( + "[创建窗口 {}] 位置对比: monitor.position=({}, {}), window.outer_position=({}, {}), scale={}", + window_index, + physical_position.x, + physical_position.y, + pos.x, + pos.y, + actual_scale + ); + + // 始终使用 monitor.position(),以保证一致性 + // 因为背景画布是基于 monitor 信息构建的 + tracing::info!( + "[创建窗口 {}] 使用 monitor.position 保持坐标系统一致", + window_index + ); + (physical_position.x, physical_position.y) + } else { + (physical_position.x, physical_position.y) + }; + let info = WindowInfo::new( window, actual_inner_size, - scale, - physical_position.x, - physical_position.y, + actual_scale, // 使用 window 的实际 scale + final_position.0, + final_position.1, ); + + tracing::info!("[创建窗口 {}] ✅ WindowInfo 创建完成:", window_index); + tracing::info!( + " └─ size_px (物理尺寸): {}x{}", + info.size_px.width, + info.size_px.height + ); + tracing::info!( + " └─ virtual_x, virtual_y (物理坐标): ({}, {})", + info.virtual_x, + info.virtual_y + ); + tracing::info!(" └─ scale: {}", info.scale); + tracing::info!( + " └─ 逻辑尺寸计算: {}x{} / {} = {}x{}", + info.size_px.width, + info.size_px.height, + info.scale, + (info.size_px.width as f64 / info.scale) as u32, + (info.size_px.height as f64 / info.scale) as u32 + ); + tracing::info!( + " └─ 逻辑坐标计算: ({}, {}) / {} = ({}, {})", + info.virtual_x, + info.virtual_y, + info.scale, + (info.virtual_x as f64 / info.scale) as i32, + (info.virtual_y as f64 / info.scale) as i32 + ); + self.windows.push(info); } Err(e) => { diff --git a/docs/tech_design/ui_overlay.md b/docs/tech_design/ui_overlay.md index 3e90a42..ae6a74f 100644 --- a/docs/tech_design/ui_overlay.md +++ b/docs/tech_design/ui_overlay.md @@ -1,8 +1,8 @@ # ui_overlay 模块技术设计 ## 版本信息 -- 当前版本:v0.1.4 -- 最后更新:2025-10-25 +- 当前版本:v0.2.0 +- 最后更新:2025-10-28 ## 职责与边界 - 提供跨平台的屏幕"框选区域"交互层(Overlay Window),支持鼠标拖拽选择矩形区域。 @@ -39,7 +39,7 @@ crate 暴露: - **虚拟桌面坐标**:以主显示器左上角为原点的全局坐标系 - **显示器相对坐标**:每个显示器内部的局部坐标系 - **跨显示器区域**:使用虚拟桌面坐标描述跨越多个显示器的区域 -- **DPI 适配**:自动处理不同显示器间的 DPI 差异和缩放 +- **DPI 适配**:自动处理不同显示器间的DPI 差异和缩放 - **HiDPI 支持**:智能识别 Retina 显示器的 backing scale(如 2.0x),窗口缓冲区自动匹配物理像素 - **坐标转换一致性**:统一处理鼠标事件坐标转换和渲染坐标转换,确保选择框正确显示在对应位置 - **修复记录**: @@ -49,15 +49,52 @@ crate 暴露: ## 平台实现 -### 通用架构 -- 统一采用 `winit + Skia` 的跨平台实现,核心文件:`crates/ui_overlay/src/selector.rs`。 -- 渲染:使用 Skia 2D 图形库进行高质量渲染,支持 GPU 加速和 CPU fallback,绘制真实桌面截图背景、选区外暗化、选区白色描边,选区内部保持透明效果。 -- 平台胶水隔离:`ui_overlay::platform` 模块(`crates/ui_overlay/src/platform/mod.rs`),封装平台特定的窗口呈现设置,核心选择器仅调用 `start_presentation()/end_presentation()`,减少条件编译分支和跨端耦合。 -- 启动性能优化:窗口初始不可见(`with_visible(false)`),创建后立即预热 Skia Surface(`ensure_surface` + `render_once`),再显示并 `request_redraw`;空闲阶段不进行持续重绘(`about_to_wait` 不再 `request_redraw`),仅在输入/尺寸变化时重绘。 +### 通用架构 ✅ v0.2.0 重大重构 -### macOS 平台实现 ✅ 已优化(v0.1.4) +**核心技术栈**: +- **窗口管理**:`winit 0.30` - 跨平台窗口和事件循环 +- **UI 渲染**:`egui 0.33` - 即时模式 GUI(选择框、遮罩、工具栏) +- **背景渲染**:`wgpu` - 直接 GPU 渲染,绕过 egui 纹理限制(2048x2048) +- **平台适配**:`ui_overlay::platform` - macOS/Windows 特定窗口行为 + +**架构演进**(v0.1.x → v0.2.0): +- ~~Skia 2D 图形库(CPU/Metal GPU)~~ → **egui + wgpu 混合渲染** +- ~~RenderBackend 抽象层(metal_backend/cpu_backend)~~ → **统一 egui-wgpu 后端** +- ~~背景暗化图像缓存(47MB 内存)~~ → **egui 半透明遮罩(0 额外内存)** +- ~~手动渲染循环~~ → **winit 标准事件驱动模式(about_to_wait → RedrawRequested)** + +**渲染流程**(双层架构): +```rust +// 1. wgpu 背景层(底层) +BackgroundRenderer::render(&mut render_pass, background_bind_group); +// ↓ 渲染裁剪后的背景图(全屏显示,全分辨率) + +// 2. egui UI 层(上层) +egui::CentralPanel::default().show(ctx, |ui| { + let painter = ui.painter(); + + // 绘制 4 个半透明黑色矩形(暗化遮罩) + painter.rect_filled(top_rect, 0.0, Color32::from_rgba_unmultiplied(0, 0, 0, 128)); + painter.rect_filled(bottom_rect, 0.0, darken_color); + painter.rect_filled(left_rect, 0.0, darken_color); + painter.rect_filled(right_rect, 0.0, darken_color); + + // 绘制选择框白色边框 + painter.rect_stroke(selection_rect, 0.0, Stroke::new(2.0, Color32::WHITE)); + + // 绘制尺寸文本 + painter.text(text_pos, Align2::CENTER_CENTER, size_text, font, Color32::WHITE); +}); +``` -#### 窗口管理(v0.1.3 重构) +**为什么选择 egui + wgpu?** +1. **突破 egui 纹理限制**:多显示器场景下背景图可能 7680x4320,超过 egui 的 2048x2048 限制 +2. **代码简洁**:egui 即时模式 API 比 Skia Canvas API 更直观,选择框渲染从 50 行降到 20 行 +3. **内存优化**:不再需要预先暗化的背景图(-47MB),使用半透明遮罩实时合成 +4. **跨平台一致性**:wgpu 抽象了 Metal/D3D/Vulkan,无需平台特定代码 +5. **未来扩展**:egui 自带丰富的 UI 组件(按钮、滑块、颜色选择器),为标注工具栏做好准备 + +### 窗口管理(v0.1.3 重构) **文件**:`crates/ui_overlay/src/window_manager.rs` **单一信源原则**: @@ -88,7 +125,9 @@ for monitor in event_loop.available_monitors() { - 如果比例接近整数(如 2.0),识别为正常的 HiDPI backing scale - 只在比例异常时输出警告,避免误报 -#### 窗口层级和行为(v0.1.4 新增) +### macOS 平台实现 ✅ 已优化(v0.1.4) + +#### 窗口层级和行为 **文件**:`crates/ui_overlay/src/platform/macos.rs` **窗口层级设置**: @@ -108,12 +147,6 @@ let behavior: u64 = (1 << 0) | (1 << 7) | (1 << 4); // = 145 msg_send![ns_window, setCollectionBehavior: behavior]; ``` -**鼠标事件处理**: -```rust -// 确保窗口接受鼠标事件,不穿透到下层窗口 -msg_send![ns_window, setIgnoresMouseEvents: Bool::from(false)]; -``` - **完整特性**: - 通过 Cocoa API 设置 NSWindow 为无边框、不可拖动、最高层级 - 隐藏 Dock 和菜单栏(`NSApplicationPresentationOptions`) @@ -123,11 +156,11 @@ msg_send![ns_window, setIgnoresMouseEvents: Bool::from(false)]; - 支持横屏、竖屏、混合屏幕配置 ### Windows 平台实现 -- 使用同一套 winit 事件与 Skia 渲染路径 -- 可选增强为窗口置顶与穿透 -- Direct3D GPU 后端待实现(Phase 3) +- 使用同一套 winit 事件与 wgpu 渲染路径 +- 通过 `platform::windows` 设置窗口置顶与透明 +- wgpu 自动选择 DirectX 12/11 后端 -### 多显示器渲染架构 ✅ 已完成(v0.1.3) +### 多显示器渲染架构 ✅ 已完成(v0.1.3 + v0.2.0 优化) #### 窗口创建和管理 **架构改进**: @@ -151,142 +184,331 @@ msg_send![ns_window, setIgnoresMouseEvents: Bool::from(false)]; - **跨窗口渲染**:支持绘制跨越多个显示器的选择区域 - **交互连贯性**:鼠标在不同显示器间移动时,选择框连续显示 -#### 诊断日志系统(v0.1.3-v0.1.4) -**详细的调试信息**,便于问题排查: +## 渲染架构 ✅ v0.2.0 全新设计 + +### 混合渲染管线 + +**文件结构**: +``` +crates/ui_overlay/src/ +├── background_renderer.rs # wgpu 背景渲染器(直接 GPU 渲染) +├── selection_render.rs # egui 选择框渲染器(UI 层) +├── window_info.rs # 窗口信息 + egui-wgpu 集成 +├── selection_app.rs # 应用主逻辑 + winit 事件循环 +└── window_manager.rs # 多窗口管理 +``` + +### 1. 背景渲染层(wgpu) + +**文件**:`crates/ui_overlay/src/background_renderer.rs` +**设计目标**: +- 绕过 egui 的 2048x2048 纹理限制 +- 支持 7680x4320 等大尺寸虚拟桌面背景 +- GPU 硬件加速,全分辨率显示 + +**实现策略**: ```rust -// xcap 显示器检测 -[xcap Monitor 0] id=1, name="Display", primary=true -[xcap Monitor 0] 逻辑坐标: (0, 0), 逻辑尺寸: 3840x2160 -[xcap Monitor 0] scale_factor: 2.0 -[xcap Monitor 0] 实际捕获图像尺寸: 7680x4320 (物理像素) - -// winit 窗口创建 -[Winit Monitor 0] name: "Built-in Display" -[Winit Monitor 0] 物理位置: (0, 0), 物理尺寸: 3840x2160 -[Winit Monitor 0] scale_factor: 2.0 -[Window 0] 创建成功 -[Window 0] Monitor报告: size=3840x2160, pos=(0, 0), scale=2.0 -[Window 0] 实际inner: 7680x4320 -[Window 0] HiDPI 缩放: Monitor报告 3840x2160, 窗口实际 7680x4320 (backing_scale ≈ 2.0x) +pub struct BackgroundRenderer { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, + vertex_buffer: wgpu::Buffer, // 全屏四边形 + index_buffer: wgpu::Buffer, // 两个三角形 +} -// macOS 窗口层级 -[macOS Window] 窗口层级 level=1000, 集合行为 collectionBehavior=0b10010001 (145) -[macOS Window] 层级说明: NSMainMenuWindowLevel=24, NSScreenSaverWindowLevel=1000 +impl BackgroundRenderer { + /// 为每个窗口创建裁剪后的背景纹理 + /// + /// 从完整虚拟桌面背景中裁剪出当前窗口对应的区域, + /// 上传为独立纹理,直接全屏显示(无需 UV 映射) + pub fn create_texture( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + data: &[u8], // RGBA8 裁剪后的纹理数据 + width: u32, // 窗口宽度 + height: u32, // 窗口高度 + ) -> (wgpu::Texture, wgpu::BindGroup); + + /// 渲染全屏背景纹理 + pub fn render<'rpass>( + &'rpass self, + render_pass: &mut wgpu::RenderPass<'rpass>, + texture_bind_group: &'rpass wgpu::BindGroup, + ); +} ``` -### Skia 渲染架构 ✅ 已完成 - -#### RenderBackend 抽象层 -**文件**:`crates/ui_overlay/src/backend/` - -**架构设计**: -- `BackendType` 枚举:`MetalGpu`, `Direct3dGpu`, `CpuRaster` -- `RenderBackend` trait:统一的 GPU/CPU 渲染接口 - - `prepare_surface()`: 准备渲染表面 - - `canvas()`: 获取 Skia Canvas - - `flush_and_read_pixels()`: 提交渲染结果 - - `resize()`: 处理窗口尺寸变化 -- `factory.rs`:根据平台自动选择最佳可用后端 - -#### 后端实现 - -**1. macOS Metal GPU**(`metal_backend.rs`): -- 使用 `metal-rs` crate 实现原生 Metal 渲染 -- 通过 `CAMetalLayer` 直接渲染到窗口,无需 softbuffer -- 每帧从 layer 获取 `MetalDrawable`,创建 Skia GPU Surface -- `flush_and_read_pixels()` 中调用 `drawable.present()` 提交到屏幕 -- 支持 Skia `DirectContext` 硬件加速渲染 -- 异步提交 command buffer,复用 `CommandQueue`,利用 VSync 同步 - -**2. CPU Raster**(`cpu_backend.rs`): -- 使用 `Surface::new_raster_n32_premul` 创建 CPU 渲染表面 -- 读取像素后通过 `softbuffer` 提交到窗口 -- 作为 GPU 不可用时的降级方案 - -**3. Windows Direct3D**(待实现): -- 计划使用 `wgpu` 或 `windows-rs` 实现 D3D11/D3D12 渲染 -- 架构与 Metal 类似,通过 DirectX 直接渲染到窗口 - -#### 后端选择策略 -1. macOS 优先使用 Metal GPU(`DirectContext::make_metal` + `CAMetalLayer`) -2. Windows 将优先使用 Direct3D GPU(Phase 3) -3. 任一 GPU 初始化失败时自动降级到 CPU Raster -4. CPU Raster 使用 `softbuffer` 确保所有环境都能正常显示 - -#### Surface 生命周期 -- 每个窗口持有独立的 `RenderBackend` 实例 -- `prepare_surface()` 在每帧开始时准备渲染表面(GPU 获取 drawable,CPU 创建 raster surface) -- `canvas()` 返回 Skia Canvas 用于绘制 -- `flush_and_read_pixels()` 提交渲染结果(GPU 调用 present,CPU 返回像素数据) -- `resize()` 处理窗口尺寸变化 - -#### 渲染流程 -1. `WindowInfo::render()` 接收绘制闭包 -2. 调用 `backend.prepare_surface()` 准备当前帧 -3. 通过 `backend.canvas()` 获取 Canvas 并执行绘制闭包: - - 绘制暗化背景图像(Skia Image,缓存) - - 绘制选区内原始背景(裁剪 + 图像绘制) - - 绘制选择框边框(白色描边) -4. 调用 `backend.flush_and_read_pixels()` 提交结果 -5. GPU backend 直接 present,CPU backend 通过 softbuffer 呈现 - -#### 坐标转换 -- 统一虚拟桌面坐标到窗口本地坐标的转换公式:`local = virtual - window_virtual` -- 确保背景偏移和选择框坐标使用相同的转换逻辑 -- 修复了选择框在非主显示器上位置错误的问题 - -### 渲染性能与拖动优化 ✅ 已完成并持续优化 - -#### 核心性能优化 - -**鼠标移动防抖优化**(`event_handler.rs`): -- 使用固定阈值代替动态计算:拖动时 5px,非拖动时 10px -- 移除每次移动时的选择区域面积计算,减少 CPU 开销 -- 只在拖动状态下触发重绘,避免无效重绘(~50% 重绘次数减少) +**Shader 代码**(简化版,无 Uniform): +```wgsl +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.clip_position = vec4(input.position, 0.0, 1.0); + output.uv = input.uv; // 直接全屏映射 + return output; +} -**选择矩形计算缓存**(`SelectionState`): -- 添加 `cached_rect` 和 `cache_valid` 字段缓存计算结果 -- 在鼠标移动、修饰键变化时使缓存失效 -- 避免每帧多次重复计算选择矩形(~30% CPU 计算减少) +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return textureSample(t_diffuse, s_diffuse, input.uv); +} +``` + +**背景裁剪流程**: +```rust +// 1. 计算窗口在虚拟桌面中的位置(canvas 坐标) +let canvas_x = (window.virtual_x - virtual_min_x).max(0) as u32; +let canvas_y = (window.virtual_y - virtual_min_y).max(0) as u32; + +// 2. 从完整背景中裁剪出窗口区域 +let cropped_data = crop_background( + full_bg_data, // 完整虚拟桌面背景(7680x4320) + full_bg_width, + full_bg_height, + canvas_x, canvas_y, // 裁剪起点 + window_w, window_h, // 裁剪尺寸 +); + +// 3. 上传为独立纹理(窗口尺寸,如 3840x2160) +let (texture, bind_group) = background_renderer.create_texture( + &device, &queue, &cropped_data, window_w, window_h +); +``` + +**为什么裁剪而不是 UV 映射?** +- **代码简单**:每个窗口独立纹理,shader 无需 uniform 和 UV 计算 +- **性能好**:上传数据量更小(3840x2160 vs 7680x4320),GPU 内存占用更少 +- **易调试**:每个窗口纹理独立,坐标转换逻辑清晰 + +### 2. UI 渲染层(egui) -**图像缓存优化**(`ImageCache`): -- 改用 `initialized` 标志位替代哈希验证 -- 背景图片在选择过程中不变,只需初始化一次 -- 移除每帧的哈希计算开销,提升渲染性能 -- 使用 `Arc` 零拷贝共享,减少内存占用 +**文件**:`crates/ui_overlay/src/selection_render.rs` -**重绘频率控制简化**: -- 移除复杂的预算机制,只保留时间间隔限流(16ms) -- 简化 `should_throttle_redraw()` 逻辑,减少判断开销 -- 配合 `FrameTimer` 的 60 FPS 限制,实现稳定帧率 +**实现**: +```rust +pub struct EguiSelectionRenderer; + +impl EguiSelectionRenderer { + /// 使用 egui painter 渲染选择框 + pub fn render_selection( + painter: &egui::Painter, + selection_rect: Option<(f32, f32, f32, f32)>, // 逻辑坐标 + window_size: (f32, f32), + ) { + let darken_color = egui::Color32::from_rgba_unmultiplied(0, 0, 0, 128); + + if let Some((x, y, w, h)) = selection_rect { + // 绘制 4 个矩形遮罩,覆盖选择框外的区域 + painter.rect_filled(top_rect, 0.0, darken_color); + painter.rect_filled(bottom_rect, 0.0, darken_color); + painter.rect_filled(left_rect, 0.0, darken_color); + painter.rect_filled(right_rect, 0.0, darken_color); + + // 绘制白色边框(2px,外描边) + painter.rect_stroke( + sel_rect, 0.0, + egui::Stroke::new(2.0, egui::Color32::WHITE), + egui::epaint::StrokeKind::Outside, + ); + + // 绘制尺寸信息 + painter.text(text_pos, Align2::CENTER_CENTER, size_text, font, Color32::WHITE); + } else { + // 无选择框时,全屏暗化 + painter.rect_filled(fullscreen_rect, 0.0, darken_color); + } + } +} +``` + +**优势**: +- **透明效果**:4 个半透明矩形合成暗化效果,选择框内原始背景可见 +- **无额外内存**:不需要预先生成暗化背景图(-47MB 内存) +- **简洁易读**:egui API 比 Skia Canvas 更直观,代码从 50 行降到 30 行 + +### 3. 窗口渲染集成 + +**文件**:`crates/ui_overlay/src/window_info.rs` + +**核心结构**: +```rust +pub struct WgpuState { + pub surface: wgpu::Surface<'static>, + pub device: Arc, + pub queue: Arc, + pub surface_config: wgpu::SurfaceConfiguration, + pub renderer: egui_wgpu::Renderer, // egui 渲染器 + pub background_renderer: BackgroundRenderer, // wgpu 背景渲染器 + pub background_texture: Option, + pub background_bind_group: Option, +} + +pub struct WindowInfo { + pub window: Arc, + pub egui_ctx: egui::Context, // egui 上下文 + pub egui_state: egui_winit::State, // egui-winit 状态 + pub wgpu_state: Option, + // ... +} +``` + +**渲染流程**: +```rust +impl WindowInfo { + pub fn render(&mut self, draw_fn: F) -> Result<(), String> + where + F: FnMut(&egui::Context), + { + // 1. 准备 wgpu surface + let surface_texture = wgpu_state.surface.get_current_texture().unwrap(); + let surface_view = surface_texture.texture.create_view(&Default::default()); + let mut encoder = wgpu_state.device.create_command_encoder(&Default::default()); + + // 2. egui 输入处理 + let raw_input = self.egui_state.take_egui_input(&self.window); + + // 3. 执行 egui UI 逻辑 + let full_output = self.egui_ctx.run(raw_input, draw_fn); + + // 4. egui 更新纹理和缓冲区 + for (id, image_delta) in &full_output.textures_delta.set { + wgpu_state.renderer.update_texture(&device, &queue, id, image_delta); + } + wgpu_state.renderer.update_buffers(&device, &queue, &mut encoder, &clipped_primitives, &screen_descriptor); + + // 5. 渲染 wgpu 背景层(底层) + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &surface_view, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + // ... + })], + // ... + }); + + wgpu_state.background_renderer.render(&mut render_pass, bg_bind_group); + } // render_pass dropped + + // 6. 渲染 egui UI 层(上层) + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &surface_view, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, // 保留背景,不清除 + store: wgpu::StoreOp::Store, + }, + // ... + })], + // ... + }); + + wgpu_state.renderer.render(&mut render_pass.forget_lifetime(), &clipped_primitives, &screen_descriptor); + } // render_pass dropped + + // 7. 提交并 present + wgpu_state.queue.submit(std::iter::once(encoder.finish())); + surface_texture.present(); + + // 8. 释放 egui 纹理 + for id in &full_output.textures_delta.free { + wgpu_state.renderer.free_texture(id); + } + + Ok(()) + } +} +``` + +### 4. 事件循环(winit 标准模式) -**帧率控制**(`FrameTimer`): -- 限制渲染频率为 60 FPS(16.67ms/帧) +**文件**:`crates/ui_overlay/src/selection_app.rs` + +**v0.2.0 改进**: +```rust +impl ApplicationHandler for SelectionApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + // 窗口初始化 + 预热渲染 + self.window_manager.initialize_windows(event_loop, &self.attrs); + + for i in 0..self.window_manager.windows.len() { + self.window_manager.windows[i].init_wgpu().unwrap(); + self.ensure_background_uploaded(i); + self.render_window_by_index(i); // 预热 + } + + // 显示窗口 + for window in &self.window_manager.windows { + window.window.set_visible(true); + } + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + // 处理各种窗口事件(鼠标、键盘、缩放等) + self.on_window_event(event_loop, window_id, event); + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + // ✅ winit 0.30 推荐模式:在事件循环空闲时请求重绘 + if self.state.dragging { + self.window_manager.request_redraw_all(); + } + } +} +``` + +**关键改进**(v0.1.x → v0.2.0): +- ~~鼠标事件中直接渲染所有窗口~~ → **request_redraw() + RedrawRequested 事件** +- ~~手动管理渲染时机~~ → **about_to_wait() 统一控制重绘** +- 符合 winit 最佳实践,更好的跨平台兼容性 +- 事件循环控制渲染时机,性能更稳定 + +## 性能优化 ✅ v0.2.0 全面优化 + +### 内存优化(-47MB) +- ~~预先生成暗化背景图(47MB)~~ → **egui 半透明遮罩(0 额外内存)** +- ~~ImageCache 模块(图像对象缓存)~~ → **删除(背景只上传一次)** +- **总内存占用**:181MB → ~134MB(-26%) + +### 渲染性能 +**帧率控制**(v0.1.2 + v0.2.0): +- `FrameTimer` 限制渲染频率为 60 FPS(16.67ms/帧) - 在 `request_redraw_all()` 中统一检查帧率限制 -- 初始化时预设时间偏移,确保第一帧立即可渲染 +- 拖拽时持续 60 FPS,非拖拽时按需渲染 -**Metal Backend 优化**: +**鼠标移动防抖**(v0.1.2): +- 使用固定阈值代替动态计算:拖动时 5px,非拖动时 10px +- 移除每次移动时的选择区域面积计算,减少 CPU 开销 +- ~50% 重绘次数减少 + +**选择矩形计算缓存**(v0.1.2): +- 添加 `cached_rect` 和 `cache_valid` 字段缓存计算结果 +- 在鼠标移动、修饰键变化时使缓存失效 +- ~30% CPU 计算减少 + +**wgpu 渲染优化**(v0.2.0 新增): +- GPU 硬件加速(Metal/DirectX/Vulkan) - 异步提交 command buffer,不等待 GPU 完成 -- 复用 `CommandQueue` 避免每帧创建 - 利用 VSync 同步,避免画面撕裂 +- 背景裁剪上传(窗口尺寸),减少 GPU 内存和带宽占用 -#### 渲染性能策略 - -**窗口预热**: +### 窗口预热(v0.1.2 + v0.2.0) - 窗口初始不可见(`with_visible(false)`) -- 先创建 RenderBackend 并预热渲染 +- 先创建 wgpu surface 并预热渲染 - 完成首次渲染后再显示窗口,避免屏幕闪动 -**尺寸变化处理**: -- 窗口 resize 时调用 `backend.resize()` 更新尺寸 -- 下次 `prepare_surface()` 时会重建对应尺寸的 Surface - -#### 性能指标 +### 性能指标 - 拖动延迟:< 33ms(2 帧以内) -- 重绘频率:稳定 60 FPS -- CPU 占用:相比优化前减少 ~40% -- 内存占用:通过 Arc 共享,减少重复拷贝 +- 渲染频率:稳定 60 FPS +- CPU 占用:相比 v0.1.0 减少 ~40% +- 内存占用:相比 v0.1.4 减少 ~47MB(-26%) +- GPU 占用:Metal/DirectX 硬件加速,流畅无卡顿 ## 错误与取消语义 - 用户按 Esc/关闭窗口 -> `OverlayError::Cancelled` @@ -303,7 +525,7 @@ msg_send![ns_window, setIgnoresMouseEvents: Bool::from(false)]; - 无敏感权限;不读取剪贴板/文件系统,仅创建窗口 ## 已知问题和限制 -无(v0.1.4 已修复所有已知的多显示器和菜单栏干扰问题) +无(v0.2.0 已修复所有已知问题) ## 调试和诊断 @@ -338,6 +560,15 @@ RUST_LOG=ui_overlay=debug,platform_mac=debug cargo run --release --bin api_cli - ## 版本历史 +### v0.2.0 (2025-10-28) 🚀 重大架构重构 +- ✅ **渲染架构重构**:Skia → egui + wgpu 混合渲染 +- ✅ **内存优化**:删除暗化背景缓存(-47MB,-26%) +- ✅ **代码简化**:删除 RenderBackend 抽象层(-450 行) +- ✅ **事件循环优化**:符合 winit 0.30 最佳实践(about_to_wait → RedrawRequested) +- ✅ **背景裁剪上传**:每个窗口独立纹理,减少 GPU 内存占用 +- ✅ **egui 半透明遮罩**:实时合成暗化效果,0 额外内存 +- ✅ **架构文档更新**:详细说明新渲染管线和设计决策 + ### v0.1.4 (2025-10-25) - ✅ 屏幕顶部菜单栏干扰修复:设置窗口层级为 1000,集合行为为 145 - ✅ HiDPI 日志改进:智能识别 Retina 显示器的正常尺寸缩放,避免误报警告 @@ -346,7 +577,7 @@ RUST_LOG=ui_overlay=debug,platform_mac=debug cargo run --release --bin api_cli - - ✅ 多屏幕窗口尺寸修复:使用 winit 单一信源,正确支持横竖屏混合场景 - ✅ 详细诊断日志:添加完整的 debug 日志系统,便于问题排查 -### v0.1.2 +### v0.1.2 (2025-10-24) - ✅ Metal GPU 硬件加速:macOS 原生 Metal 渲染 - ✅ 性能优化:60 FPS 帧率控制 + 图像缓存 - ✅ 坐标转换修复:统一虚拟坐标到窗口本地坐标转换 @@ -357,15 +588,17 @@ RUST_LOG=ui_overlay=debug,platform_mac=debug cargo run --release --bin api_cli - ## 未来计划 -### v0.2 - 标注编辑 UI +### v0.3 - 标注编辑 UI(基于 egui) - 状态机扩展:增加 EditingState -- 集成 egui:工具栏 UI 框架 -- 基础工具:矩形、箭头、完成/取消按钮 -- 撤销/重做集成 +- 工具栏 UI:矩形、箭头、画笔、文字工具 +- egui 颜色选择器、粗细调节 +- 撤销/重做集成(core::UndoStack) +- 标注渲染:使用 egui Painter API 详见 `docs/todo/ui_overlay.md` ## 参考文档 +- 架构重构总结:`remove-skia-dependencies.plan.md`(Skia → egui 迁移) - 修复总结:`MULTI_MONITOR_FIX_SUMMARY.md`(多屏幕窗口修复) - 修复总结:`MENUBAR_FIX_SUMMARY.md`(菜单栏干扰修复) - 修复总结:`HIDPI_LOG_FIX.md`(HiDPI 日志改进) diff --git a/docs/todo/ui_overlay.md b/docs/todo/ui_overlay.md index dee74d4..30d9a8e 100644 --- a/docs/todo/ui_overlay.md +++ b/docs/todo/ui_overlay.md @@ -1,13 +1,16 @@ # ui_overlay TODO -## 当前状态(v0.1.4 已完成) +## 当前状态(v0.2.0 已完成 ✅) - ✅ 基础交互式选择器:Region / RegionSelector / 错误类型 - ✅ platform_mac 集成:MacCapturer 使用 RegionSelector - ✅ CLI 集成:capture-interactive 命令 -- ✅ 性能优化:预热渲染、背景暗化缓存、重绘节流、60 FPS 帧率控制 -- ✅ **RenderBackend 抽象层**:统一 GPU/CPU 渲染接口 -- ✅ **Metal GPU 硬件加速**:macOS 原生 Metal 渲染(metal-rs + CAMetalLayer) -- ✅ **CPU Raster 降级**:软件渲染后端,确保兼容性 +- ✅ **渲染架构重构**:Skia → egui + wgpu 混合渲染(v0.2.0) +- ✅ **wgpu 背景渲染器**:绕过 egui 2048x2048 纹理限制,支持 7680x4320 大尺寸背景 +- ✅ **egui UI 层**:半透明遮罩、选择框、尺寸文本(v0.2.0) +- ✅ **内存优化**:删除暗化背景缓存(-47MB),删除 ImageCache(v0.2.0) +- ✅ **事件循环优化**:符合 winit 0.30 最佳实践(about_to_wait → RedrawRequested)(v0.2.0) +- ✅ **代码简化**:删除 RenderBackend 抽象层(-450 行)(v0.2.0) +- ✅ 性能优化:预热渲染、重绘节流、60 FPS 帧率控制 - ✅ 多显示器支持(检测、跨屏选择) - ✅ 虚拟桌面坐标系统 - ✅ 坐标转换修复(统一虚拟坐标到窗口本地坐标转换) @@ -15,25 +18,23 @@ - ✅ 修饰键支持(Shift 正方形、Alt 中心拉伸) - ✅ 键盘控制(ESC 取消、Enter 确认、方向键微调) - ✅ 智能重绘节流(防抖、帧率预算管理) -- ✅ 图像缓存(Skia Image 对象复用) - ✅ 交互优化:鼠标拖动结束后需按 Enter 确认,支持调整后再确认 - ✅ **多屏幕窗口尺寸修复**:使用 winit 单一信源,正确支持横竖屏混合场景(v0.1.3) - ✅ **详细诊断日志**:添加完整的 debug 日志系统,便于问题排查(v0.1.3) - ✅ **屏幕顶部菜单栏干扰修复**:设置窗口层级为 1000(高于菜单栏),确保顶部拖拽正常(v0.1.4) - ✅ **HiDPI 日志改进**:智能识别 Retina 显示器的正常尺寸缩放,避免误报警告(v0.1.4) -## v0.2 - 标注编辑 UI(对标微信截图) +## v0.3 - 标注编辑 UI(基于 egui) ### Phase 1: 基础架构(优先级:P0) - [ ] **状态机扩展**:增加 EditingState(当前只有 SelectionState) - [ ] 定义 AppMode 枚举(SelectingRegion / EditingAnnotations) - [ ] 在 selector.rs 中实现模式切换逻辑 - [ ] 选区确认后自动进入编辑模式(不退出窗口) -- [ ] **集成 egui**:工具栏 UI 框架 - - [ ] 添加 egui, egui-winit, egui_skia 依赖 - - [ ] 在 SelectionApp 中初始化 egui 上下文 - - [ ] 实现基础工具栏渲染(空壳) - - [ ] 验证 egui → Skia 渲染管线 +- [ ] **工具栏 UI 框架**(egui 已集成 ✅) + - [ ] 在 SelectionApp 中添加 toolbar 状态 + - [ ] 实现基础工具栏布局(egui Window/Panel) + - [ ] 验证工具栏渲染流程 - [ ] **事件处理分离**:区分选择事件和编辑事件 - [ ] 重构 event_handler.rs 支持模式感知 - [ ] 编辑模式下的鼠标事件路由 @@ -41,7 +42,7 @@ ### Phase 2: 基础工具(优先级:P0) - [ ] **工具栏 UI** - [ ] 设计工具栏布局(顶部浮动/底部固定) - - [ ] 矩形工具按钮 + - [ ] 矩形工具按钮(egui Button) - [ ] 箭头工具按钮 - [ ] 完成/取消按钮 - [ ] 工具选择状态高亮 @@ -49,10 +50,10 @@ - [ ] 鼠标按下创建临时标注 - [ ] 鼠标移动更新临时标注(实时预览) - [ ] 鼠标释放确认标注(加入列表) - - [ ] 使用 Skia 渲染矩形预览 + - [ ] 使用 egui Painter 渲染矩形预览 - [ ] **箭头绘制交互** - [ ] 起点-终点拖拽逻辑 - - [ ] 箭头方向计算和渲染 + - [ ] 箭头方向计算和渲染(egui Path) - [ ] 箭头头部大小自适应 ### Phase 3: 撤销/重做和样式(优先级:P1) @@ -61,11 +62,11 @@ - [ ] 撤销/重做按钮(Ctrl+Z/Ctrl+Y) - [ ] 操作历史显示(可选) - [ ] **颜色选择器** - - [ ] 集成 egui 内置颜色选择器 + - [ ] 集成 egui 内置颜色选择器(egui::color_picker) - [ ] 预设颜色快速选择 - [ ] 当前颜色显示 - [ ] **粗细调节** - - [ ] egui 滑块(1-10px) + - [ ] egui 滑块(Slider,1-10px) - [ ] 实时预览粗细变化 ### Phase 4: 高级工具(优先级:P2) @@ -73,25 +74,26 @@ - [ ] 路径点收集 - [ ] Chaikin 平滑预览 - [ ] 笔刷粗细支持 + - [ ] egui Path 渲染 - [ ] **马赛克工具** - [ ] 区域选择 - - [ ] 马赛克 level 调节 - - [ ] 实时预览 + - [ ] 马赛克 level 调节(Slider) + - [ ] 实时预览(wgpu shader 或 image filter) - [ ] **文字工具**(难度较高) - - [ ] 文本输入框 - - [ ] 字体选择 - - [ ] 字号调节 - - [ ] 真正字形渲染(需 fontdue) - -### Phase 5: Skia 标注渲染(优先级:P1) -- [ ] **创建 SkiaAnnotationRenderer** - - [ ] 在 ui_overlay/skia/ 新建 annotation_renderer.rs - - [ ] 实现 Rect 的 Skia 渲染(canvas.draw_rect) - - [ ] 实现 Arrow 的 Skia 渲染(canvas.draw_line + draw_path) - - [ ] 实现 Freehand 的 Skia 渲染(canvas.draw_path) - - [ ] 实现 Mosaic 的 Skia 渲染(Shader 或 Image filter) + - [ ] 文本输入框(egui TextEdit) + - [ ] 字体选择(egui ComboBox) + - [ ] 字号调节(Slider) + - [ ] 真正字形渲染(egui 内置文本渲染) + +### Phase 5: egui 标注渲染(优先级:P1) +- [ ] **创建 EguiAnnotationRenderer** + - [ ] 在 ui_overlay/src/ 新建 annotation_renderer.rs + - [ ] 实现 Rect 的 egui 渲染(painter.rect_stroke) + - [ ] 实现 Arrow 的 egui 渲染(painter.line + painter.path) + - [ ] 实现 Freehand 的 egui 渲染(painter.path) + - [ ] 实现 Mosaic 的 wgpu shader 渲染 - [ ] **集成到编辑流程** - - [ ] 编辑模式实时使用 SkiaAnnotationRenderer + - [ ] 编辑模式实时使用 EguiAnnotationRenderer - [ ] 性能测试和优化 - [ ] 与 renderer CPU 路径对比验证 @@ -99,20 +101,20 @@ - [ ] **完成按钮逻辑** - [ ] 收集所有标注 - [ ] 调用 services::ExportService - - [ ] 渲染最终图像(renderer 或 Skia) + - [ ] 渲染最终图像(renderer 或 egui offscreen) - [ ] 保存文件 + 剪贴板 - [ ] 退出编辑模式 - [ ] **取消按钮逻辑** - [ ] 丢弃所有标注 - [ ] 直接退出 -## v0.3 - 用户体验优化 +## v0.4 - 用户体验优化 ### 界面美化 -- [ ] 工具栏主题定制(暗色/亮色) -- [ ] 图标设计(矢量图标) +- [ ] 工具栏主题定制(egui Style/Visuals) +- [ ] 图标设计(矢量图标,egui Image/Texture) - [ ] 动画效果(工具切换、标注创建) -- [ ] 快捷键提示 UI +- [ ] 快捷键提示 UI(egui Window/Tooltip) - [ ] 响应式布局(适配不同分辨率) ### 交互增强 @@ -127,11 +129,11 @@ ### 性能优化 - [ ] 标注增量渲染(只重绘变化部分) - [ ] 大尺寸截图虚拟化(按需加载) -- [ ] GPU 内存管理优化 +- [ ] wgpu 内存管理优化 - [ ] 复杂标注场景优化(>100 个标注) -## v0.4 - 高级功能 -- [ ] 图层面板(显示所有标注) +## v0.5 - 高级功能 +- [ ] 图层面板(egui Window,显示所有标注) - [ ] 标注分组(逻辑分组) - [ ] 标注锁定(防误操作) - [ ] 标注复制/粘贴 @@ -139,12 +141,37 @@ - [ ] 历史记录浏览(时间轴) ## v1.0 - 完整产品 -- [ ] Windows 平台完整支持 +- [ ] Windows 平台完整支持(wgpu 自动选择 DirectX) - [ ] 快捷键全局绑定(系统级) - [ ] 插件系统(自定义标注类型) - [ ] 协作模式(多人标注) - [ ] 云端同步 +## ✅ 已完成:渲染架构重构(v0.2.0) + +### 重大变更 +- [x] 从 Skia 迁移到 egui + wgpu 混合渲染 +- [x] 删除 RenderBackend 抽象层(metal_backend, cpu_backend, factory) +- [x] 实现 BackgroundRenderer(wgpu 直接 GPU 渲染) +- [x] 实现 EguiSelectionRenderer(egui Painter API) +- [x] 删除暗化背景缓存(bg_tinted,-47MB) +- [x] 删除 ImageCache 模块(不再需要) +- [x] 改进事件循环,符合 winit 0.30 最佳实践 +- [x] 所有测试通过,功能正常 + +### 性能提升 +- ✅ 内存占用:181MB → ~134MB(-26%) +- ✅ 代码行数:删除约 450 行冗余代码 +- ✅ 渲染流程:双层架构(wgpu 背景 + egui UI) +- ✅ 可维护性:代码更简洁,架构更清晰 + +### 架构优势 +- ✅ 绕过 egui 2048x2048 纹理限制,支持 7680x4320 大尺寸背景 +- ✅ egui 即时模式 API 更直观,选择框渲染代码从 50 行降到 30 行 +- ✅ 半透明遮罩实时合成,无需预先生成暗化背景(-47MB) +- ✅ wgpu 跨平台一致性(Metal/DirectX/Vulkan),无需平台特定代码 +- ✅ 为未来标注工具栏预留了 egui UI 组件支持 + ## ✅ 已完成:Metal GPU 渲染与性能优化(v0.1.2) ### Phase 0: CPU 优化(已完成) @@ -152,14 +179,15 @@ - [x] 在 `request_redraw_all()` 中统一检查帧率限制 - [x] 测试 CPU 性能提升效果 -### Phase 1: RenderBackend 抽象(已完成) +### Phase 1: RenderBackend 抽象(已完成,v0.2.0 已移除) - [x] 定义 `RenderBackend` trait 和 `BackendType` 枚举 - [x] 实现 `CpuRasterBackend`(软件渲染后端) - [x] 创建 `MetalBackend` 基础结构 - [x] 实现 `factory.rs` 自动选择最佳后端 - [x] 所有测试通过 +- **注**:v0.2.0 重构后,RenderBackend 抽象层已被 egui-wgpu 统一后端替代 -### Phase 2: Metal GPU Backend 实现(已完成) +### Phase 2: Metal GPU Backend 实现(已完成,v0.2.0 已替换为 wgpu) - [x] 使用 `metal-rs` 实现 Metal 渲染 - [x] 创建 `CAMetalLayer` 并绑定到窗口 - [x] 实现 `prepare_surface()`:从 layer 获取 drawable 并创建 Skia Surface @@ -167,8 +195,9 @@ - [x] 修复坐标转换问题:统一虚拟坐标到窗口本地坐标的转换公式 - [x] 修复多窗口渲染问题:确保所有窗口在同一帧配额内渲染 - [x] 测试验证:选择框正确显示,性能流畅 +- **注**:v0.2.0 重构后,直接使用 wgpu Metal backend,更简洁高效 -### 关键问题解决 +### 关键问题解决(v0.1.x) 1. **选择框不显示**: - **根本原因**:坐标转换公式错误,使用了 `x - (window_x - vx)` 而非 `x - window_x` - **解决方案**:统一坐标转换公式,确保新旧渲染系统使用相同逻辑 @@ -181,30 +210,11 @@ - **问题**:drawable 未保存,无法调用 `present()` - **解决方案**:在 `MetalBackend` 中添加 `current_drawable` 字段保存 drawable,在 flush 时调用 `present()` -### 性能提升 -- ✅ Metal GPU 硬件加速渲染 -- ✅ 60 FPS 帧率限制,CPU 使用率降低约 50% -- ✅ 图像缓存避免重复创建 Skia Image -- ✅ 拖动流畅,无明显卡顿 - -### 架构改进 -- ✅ RenderBackend trait 统一抽象 GPU/CPU 渲染 -- ✅ 平台特定代码隔离到 backend 模块 -- ✅ 支持自动降级到 CPU Raster -- ✅ 为 Windows Direct3D backend 预留架构空间 - ---- - -## GPU 后端维护(持续) -- [x] Metal 后端实现和测试 ✅ -- [x] 坐标转换修复 ✅ -- [x] 多窗口渲染修复 ✅ -- [ ] Windows Direct3D 后端实现(Phase 3) -- [ ] CPU fallback 性能优化 -- [ ] GPU 内存泄漏检测 -- [ ] 渲染性能监控 - -## 已解决问题记录(v0.1.x) +4. **v0.2.0 架构重构**: + - **问题**:Skia + RenderBackend 抽象层过于复杂,内存占用高 + - **解决方案**:迁移到 egui + wgpu,删除 450 行冗余代码,减少 47MB 内存 + +## 已解决问题记录(v0.1.x - v0.2.0) - ✅ macOS context leak 警告:通过复用 Pixels 实例解决 - ✅ 首次交互卡顿:通过预热渲染解决 - ✅ 拖动卡顿:通过背景缓存和重绘节流解决 @@ -227,17 +237,24 @@ - ✅ **详细诊断系统(v0.1.3)**:添加完整的 debug 日志,涵盖显示器检测、窗口创建、坐标映射等所有关键环节 - ✅ **屏幕顶部菜单栏干扰(v0.1.4)**:设置窗口层级为 1000(NSScreenSaverWindowLevel),集合行为为 145,确保窗口始终在菜单栏之上 - ✅ **HiDPI 日志误报(v0.1.4)**:改进窗口尺寸检查逻辑,识别 Retina 显示器的正常 backing scale(如 2.0x),避免误报"尺寸不匹配"警告 +- ✅ **渲染架构过于复杂(v0.2.0)**:从 Skia + RenderBackend → egui + wgpu,删除 450 行代码,简化架构 +- ✅ **内存占用过高(v0.2.0)**:删除暗化背景缓存(-47MB)和 ImageCache 模块 +- ✅ **egui 纹理限制(v0.2.0)**:使用 wgpu 直接渲染背景,支持 7680x4320 大尺寸 +- ✅ **事件循环不规范(v0.2.0)**:改为 about_to_wait → RedrawRequested 模式,符合 winit 0.30 最佳实践 ## 技术债务 -- [ ] 减少 selector.rs 文件行数(当前 497 行,建议拆分) +- [x] ~~减少 selector.rs 文件行数(当前 497 行,建议拆分)~~ → 已拆分为 selection_app, event_handler, selection_state 等模块 +- [x] ~~RenderBackend 抽象层维护~~ → v0.2.0 已移除,改用 egui-wgpu 统一后端 +- [x] ~~Skia 依赖管理~~ → v0.2.0 已移除,改用 egui + wgpu - [ ] event_handler.rs 测试覆盖率提升 - [ ] window_manager.rs 文档注释完善 - [ ] 清理 examples/ 中的实验性代码 - [ ] 统一错误处理策略(anyhow vs thiserror) ## 文档 -- [ ] 编写标注编辑器架构设计文档 -- [ ] egui 集成指南 -- [ ] Skia 标注渲染器开发文档 +- [x] 架构重构总结文档(v0.2.0) +- [x] 技术设计文档更新(v0.2.0) +- [ ] 编写标注编辑器架构设计文档(v0.3) +- [ ] egui 标注渲染器开发文档 - [ ] 性能优化最佳实践 - [ ] 用户使用手册(截图 + 标注流程)