diff --git a/Cargo.lock b/Cargo.lock index 5e87a838..a353b4e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "accessory" @@ -239,6 +239,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend 0.3.14", + "wayland-client 0.31.13", + "wayland-protocols 0.32.11", + "zbus", +] + [[package]] name = "askar-crypto" version = "0.3.7" @@ -334,6 +356,18 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -359,6 +393,49 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-lock" version = "3.4.1" @@ -370,12 +447,52 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-once-cell" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-rx" version = "0.1.3" @@ -386,6 +503,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -408,6 +543,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -610,7 +751,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "bitmaps" @@ -691,6 +832,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bls12_381" version = "0.8.0" @@ -728,7 +882,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "byteorder" @@ -739,7 +893,13 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" @@ -1459,7 +1619,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -1469,6 +1629,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", + "libc", "objc2", ] @@ -1483,6 +1645,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1592,6 +1763,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1605,7 +1803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -1710,6 +1908,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ff" version = "0.13.1" @@ -1936,9 +2143,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] @@ -2532,6 +2739,21 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + [[package]] name = "imbl" version = "6.1.0" @@ -2800,7 +3022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -2953,7 +3175,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +3183,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-widgets", ] @@ -2974,7 +3196,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +3204,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +3213,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3005,15 +3227,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-bidi 0.3.18 (git+https://github.com/makepad/makepad?branch=dev)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3243,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3272,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ttf-parser", ] @@ -3058,7 +3280,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3289,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3297,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3305,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3313,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3327,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3335,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,15 +3349,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3155,10 +3377,10 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "wayland-client", "wayland-egl", - "wayland-protocols", + "wayland-protocols 0.32.10", "windows 0.62.2", "windows-core 0.62.2", "windows-targets 0.52.6", @@ -3167,12 +3389,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-error-log", "makepad-html", @@ -3180,13 +3402,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3416,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-network", "makepad-script", @@ -3203,14 +3425,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3220,7 +3442,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3451,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3460,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3468,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3255,26 +3477,26 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "simd-adler32", + "simd-adler32 0.3.8 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", ] [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3504,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3900,16 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -3692,6 +3923,16 @@ version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase 2.8.1", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3705,6 +3946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3718,6 +3960,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multihash" version = "0.19.3" @@ -3949,6 +4201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", "objc2", "objc2-foundation", ] @@ -4077,6 +4330,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "p256" version = "0.13.2" @@ -4246,6 +4509,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -4273,6 +4547,39 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.1", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "poly1305" version = "0.8.0" @@ -4410,10 +4717,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", "unicase 2.9.0", ] @@ -4435,6 +4742,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quinn" version = "0.11.9" @@ -4580,6 +4902,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "readlock" version = "0.1.9" @@ -4722,6 +5050,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -4838,6 +5190,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "htmlize", + "image", "imbl", "imghdr", "indexmap 2.13.0", @@ -4847,11 +5200,14 @@ dependencies = [ "matrix-sdk", "matrix-sdk-base", "matrix-sdk-ui", + "mime", + "mime_guess", "percent-encoding", "quinn", "rand 0.8.5", "rangemap", "reqwest", + "rfd", "robius-directories", "robius-location", "robius-open", @@ -5101,7 +5457,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -5172,12 +5528,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "bytemuck", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5272,7 +5628,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "sealed" @@ -5448,6 +5804,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_spanned" version = "1.0.3" @@ -5571,7 +5938,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "siphasher" @@ -5597,7 +5964,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "socket2" @@ -5949,7 +6316,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -6363,7 +6730,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "tungstenite" @@ -6405,6 +6772,17 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.1", +] + [[package]] name = "ulid" version = "1.2.1" @@ -6424,7 +6802,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-bidi" @@ -6435,17 +6813,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-ident" @@ -6456,7 +6834,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-normalization" @@ -6476,12 +6854,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-segmentation" @@ -6492,7 +6870,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-width" @@ -6772,49 +7150,109 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "downcast-rs", "libc", "scoped-tls", "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-sys", + "wayland-sys 0.31.8", +] + +[[package]] +name = "wayland-backend" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-sys 0.31.10", ] [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", - "wayland-backend", + "wayland-backend 0.3.12", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustix", + "wayland-backend 0.3.14", + "wayland-scanner", ] [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "wayland-backend", - "wayland-sys", + "wayland-backend 0.3.12", + "wayland-sys 0.31.8", ] [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-backend", - "wayland-client", + "wayland-backend 0.3.12", + "wayland-client 0.31.12", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-backend 0.3.14", + "wayland-client 0.31.13", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", ] [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "wayland-sys" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" dependencies = [ + "dlib", "log", "pkg-config", ] @@ -6883,7 +7321,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.1", ] [[package]] @@ -6912,7 +7350,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +7369,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +7402,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +7423,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7487,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "windows-numerics" @@ -7093,7 +7531,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7548,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", ] @@ -7484,6 +7922,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.1", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -7583,3 +8082,59 @@ name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 8bd24357..0bf9cfe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,8 @@ version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -# makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } -# makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } - -makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "stack_nav_improvements", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "stack_nav_improvements" } +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. @@ -44,6 +41,9 @@ hashbrown = { version = "0.16", features = ["raw-entry"] } htmlize = "1.0.5" indexmap = "2.6.0" imghdr = "0.7.0" +image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } +mime = "0.3" +mime_guess = "2.0" linkify = "0.10.0" matrix-sdk-base = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main" } matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main", default-features = false, features = [ @@ -103,6 +103,10 @@ reqwest = { version = "0.12", default-features = false, features = [ "macos-system-configuration", ] } +# Desktop-only file dialog (doesn't work on iOS/Android) +[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies] +rfd = "0.15" + [features] default = [] diff --git a/resources/icon_home.svg b/resources/icon_home.svg deleted file mode 100644 index f5edd734..00000000 --- a/resources/icon_home.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/add_attachment.svg b/resources/icons/add_attachment.svg new file mode 100644 index 00000000..523461c6 --- /dev/null +++ b/resources/icons/add_attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/add_user.svg b/resources/icons/add_user.svg index fad47b63..640aa9d9 100644 --- a/resources/icons/add_user.svg +++ b/resources/icons/add_user.svg @@ -1,4 +1,6 @@ - - + + + + \ No newline at end of file diff --git a/resources/icons/file.svg b/resources/icons/file.svg new file mode 100644 index 00000000..2b0852bf --- /dev/null +++ b/resources/icons/file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/home.svg b/resources/icons/home.svg index 519a1bf2..5b5b85c8 100644 --- a/resources/icons/home.svg +++ b/resources/icons/home.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + \ No newline at end of file diff --git a/resources/icons/import.svg b/resources/icons/import.svg index b23a1d1e..b07d957e 100644 --- a/resources/icons/import.svg +++ b/resources/icons/import.svg @@ -1,2 +1,4 @@ - - \ No newline at end of file + + + + diff --git a/resources/icons/import2.svg b/resources/icons/import2.svg deleted file mode 100644 index 8eef3aa3..00000000 --- a/resources/icons/import2.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index f04e177d..39b97e42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,14 +4,15 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; +use makepad_widgets::SignalToUI; use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ - event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, invite_screen::InviteScreenWidgetRefExt, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, RoomScreenWidgetRefExt, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, space_lobby::SpaceLobbyScreenWidgetRefExt + event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, TimelineUpdate, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update} }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, get_timeline_update_sender, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -148,6 +149,15 @@ script_mod! { } } + // A modal to preview and confirm file uploads. + file_upload_modal := Modal { + content +: { + width: Fit, height: Fit, + align: Align{x: 0.5, y: 0.5}, + file_upload_modal_inner := FileUploadModal {} + } + } + PopupList {} // Tooltips must be shown in front of all other UI elements, @@ -171,9 +181,6 @@ pub struct App { /// This can be either a room we're waiting to join, or one we're waiting to be invited to. /// Also includes an optional room ID to be closed once the awaited room has been loaded. #[rust] waiting_to_navigate_to_room: Option<(BasicRoomDetails, Option)>, - /// A stack of previously-selected rooms for mobile navigation. - /// When a view is popped off the stack, the previous `selected_room` is restored from here. - #[rust] mobile_room_nav_stack: Vec, } impl ScriptHook for App { @@ -311,6 +318,36 @@ impl MatchEvent for App { continue; } + // Handle file upload modal actions + match action.downcast_ref() { + Some(FilePreviewerAction::Show(file_data)) => { + self.ui.file_upload_modal(cx, ids!(file_upload_modal_inner)) + .set_file_data(cx, file_data.clone()); + self.ui.modal(cx, ids!(file_upload_modal)).open(cx); + continue; + } + Some(FilePreviewerAction::Hide) => { + self.ui.modal(cx, ids!(file_upload_modal)).close(cx); + continue; + } + Some(FilePreviewerAction::UploadConfirmed(file_data)) => { + // Send the file upload request to the currently selected room + if let Some(selected_room) = &self.app_state.selected_room { + if let Some(timeline_kind) = selected_room.timeline_kind() { + if let Some(sender) = get_timeline_update_sender(&timeline_kind) { + let _ = sender.send(TimelineUpdate::FileUploadConfirmed(file_data.clone())); + SignalToUI::set_ui_signal(); + } + } + } + continue; + } + Some(FilePreviewerAction::Cancelled) => { + continue; + } + _ => {} + } + // Handle an action requesting to open the new message context menu. if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); @@ -357,33 +394,6 @@ impl MatchEvent for App { continue; } - // A new room has been selected; push the appropriate view onto the mobile - // StackNavigation and update the app state. - // In Desktop mode, MainDesktopUI also handles this action to manage dock tabs; - // the mobile push is harmless there (the view isn't drawn). - match action.as_widget_action().cast() { - RoomsListAction::Selected(selected_room) => { - self.push_selected_room_view(cx, selected_room); - continue; - } - // An invite was accepted; upgrade the selected room from invite to joined. - // In Desktop mode, MainDesktopUI also handles this (harmless duplicate). - RoomsListAction::InviteAccepted { room_name_id } => { - cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name_id.room_id().clone())); - continue; - } - _ => {} - } - - // When a stack navigation pop is initiated (back button pressed), - // pop the mobile nav stack so it stays in sync with StackNavigation. - if let StackNavigationAction::Pop = action.as_widget_action().cast() { - if self.app_state.selected_room.is_some() { - self.app_state.selected_room = self.mobile_room_nav_stack.pop(); - } - // Don't `continue` — let StackNavigation also process this Pop. - } - // Handle actions that instruct us to update the top-level app state. match action.downcast_ref() { Some(AppStateAction::RoomFocused(selected_room)) => { @@ -660,6 +670,7 @@ impl AppMain for App { crate::home::location_preview::script_mod(vm); crate::home::tombstone_footer::script_mod(vm); crate::home::editing_pane::script_mod(vm); + crate::home::upload_progress::script_mod(vm); crate::room::script_mod(vm); crate::join_leave_room_modal::script_mod(vm); crate::verification_modal::script_mod(vm); @@ -826,104 +837,6 @@ impl App { } } - /// Room StackNavigationView instances, one per stack depth. - /// Each depth gets its own dedicated view widget to avoid - /// complex state save/restore when views would otherwise be reused. - const ROOM_VIEW_IDS: [LiveId; 16] = [ - live_id!(room_view_0), live_id!(room_view_1), - live_id!(room_view_2), live_id!(room_view_3), - live_id!(room_view_4), live_id!(room_view_5), - live_id!(room_view_6), live_id!(room_view_7), - live_id!(room_view_8), live_id!(room_view_9), - live_id!(room_view_10), live_id!(room_view_11), - live_id!(room_view_12), live_id!(room_view_13), - live_id!(room_view_14), live_id!(room_view_15), - ]; - - /// The RoomScreen widget IDs inside each room view, - /// corresponding 1:1 with [`Self::ROOM_VIEW_IDS`]. - const ROOM_SCREEN_IDS: [LiveId; 16] = [ - live_id!(room_screen_0), live_id!(room_screen_1), - live_id!(room_screen_2), live_id!(room_screen_3), - live_id!(room_screen_4), live_id!(room_screen_5), - live_id!(room_screen_6), live_id!(room_screen_7), - live_id!(room_screen_8), live_id!(room_screen_9), - live_id!(room_screen_10), live_id!(room_screen_11), - live_id!(room_screen_12), live_id!(room_screen_13), - live_id!(room_screen_14), live_id!(room_screen_15), - ]; - - /// Returns the room view and room screen LiveIds for the given stack depth. - /// Clamps to the last available view if depth exceeds the pool size. - fn room_ids_for_depth(depth: usize) -> (LiveId, LiveId) { - let index = depth.min(Self::ROOM_VIEW_IDS.len() - 1); - (Self::ROOM_VIEW_IDS[index], Self::ROOM_SCREEN_IDS[index]) - } - - /// Pushes the appropriate StackNavigationView for the given `SelectedRoom`, - /// configuring the view's content widget and header title. - /// - /// Each stack depth gets its own dedicated room view widget, - /// supporting deep navigation (room → thread → room → thread → ...). - /// - /// In Desktop mode, the StackNavigation isn't drawn, so the push and - /// screen configuration are effectively no-ops — MainDesktopUI handles - /// room display via dock tabs instead. - fn push_selected_room_view(&mut self, cx: &mut Cx, selected_room: SelectedRoom) { - // Use the actual StackNavigation depth to pick the next room view slot. - let new_depth = self.ui.stack_navigation(cx, ids!(view_stack)).depth(); - - // Determine which view to push and configure its content. - // The `set_displayed_room` / `set_displayed_invite` / `set_displayed_space` calls - // configure the screen widget inside the mobile StackNavigationView. - // In Desktop mode, these widgets exist but aren't drawn; the configuration - // consumes timeline endpoints, but Desktop's MainDesktopUI processes the same - // `RoomsListAction::Selected` in its own handler to set up dock tabs. - let view_id = match &selected_room { - SelectedRoom::JoinedRoom { room_name_id } - | SelectedRoom::Thread { room_name_id, .. } => { - let (view_id, room_screen_id) = Self::room_ids_for_depth(new_depth); - - let thread_root = if let SelectedRoom::Thread { thread_root_event_id, .. } = &selected_room { - Some(thread_root_event_id.clone()) - } else { - None - }; - self.ui - .room_screen(cx, &[room_screen_id]) - .set_displayed_room(cx, room_name_id, thread_root); - - view_id - } - SelectedRoom::InvitedRoom { room_name_id } => { - self.ui - .invite_screen(cx, ids!(invite_screen)) - .set_displayed_invite(cx, room_name_id); - id!(invite_view) - } - SelectedRoom::Space { space_name_id } => { - self.ui - .space_lobby_screen(cx, ids!(space_lobby_screen)) - .set_displayed_space(cx, space_name_id); - id!(space_lobby_view) - } - }; - - // Set the header title for the view being pushed. - let title_path = &[view_id, live_id!(header), live_id!(content), live_id!(title_container), live_id!(title)]; - self.ui.label(cx, title_path).set_text(cx, &selected_room.display_name()); - - // Save the current selected_room onto the navigation stack before replacing it. - if let Some(prev) = self.app_state.selected_room.take() { - self.mobile_room_nav_stack.push(prev); - } - // Update app state (used by both Desktop and Mobile paths). - self.app_state.selected_room = Some(selected_room); - - // Push the view onto the mobile navigation stack. - self.ui.stack_navigation(cx, ids!(view_stack)).push(cx, view_id); - self.ui.redraw(cx); - } } @@ -1053,6 +966,26 @@ impl SelectedRoom { SelectedRoom::Thread { room_name_id, .. } => format!("[Thread] {room_name_id}"), } } + + /// Returns the `TimelineKind` for this selected room. + /// + /// Returns `None` for `InvitedRoom` and `Space` variants, as they don't have timelines. + pub fn timeline_kind(&self) -> Option { + match self { + SelectedRoom::JoinedRoom { room_name_id } => { + Some(crate::sliding_sync::TimelineKind::MainRoom { + room_id: room_name_id.room_id().clone(), + }) + } + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + Some(crate::sliding_sync::TimelineKind::Thread { + room_id: room_name_id.room_id().clone(), + thread_root_event_id: thread_root_event_id.clone(), + }) + } + SelectedRoom::InvitedRoom { .. } | SelectedRoom::Space { .. } => None, + } + } } impl PartialEq for SelectedRoom { diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 910f817e..5fcd8c29 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,6 +1,16 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; +use crate::{ + app::{AppState, AppStateAction, SelectedRoom}, + home::{ + invite_screen::InviteScreenWidgetExt, + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room_screen::RoomScreenWidgetExt, + rooms_list::RoomsListAction, + space_lobby::SpaceLobbyScreenWidgetExt, + }, + settings::settings_screen::SettingsScreenWidgetRefExt, +}; script_mod! { use mod.prelude.widgets.* @@ -406,6 +416,10 @@ pub struct HomeScreen { /// other widgets can easily access it. #[rust] previous_selection: SelectedTab, #[rust] is_spaces_bar_shown: bool, + + /// A stack of previously-selected rooms for mobile stack navigation. + /// When a view is popped off the stack, the previous `selected_room` is restored. + #[rust] mobile_room_nav_stack: Vec, } impl Widget for HomeScreen { @@ -475,6 +489,29 @@ impl Widget for HomeScreen { Some(NavigationBarAction::TabSelected(_)) | None => { } } + + // Handle mobile stack navigation actions (push/pop room views). + // In Desktop mode, MainDesktopUI also handles RoomsListAction::Selected + // to manage dock tabs; the mobile push is harmless there (views aren't drawn). + match action.as_widget_action().cast() { + RoomsListAction::Selected(selected_room) => { + self.push_selected_room_view(cx, app_state, selected_room); + } + RoomsListAction::InviteAccepted { room_name_id } => { + cx.action(AppStateAction::UpgradedInviteToJoinedRoom( + room_name_id.room_id().clone(), + )); + } + _ => {} + } + + // When a stack navigation pop is initiated (back button pressed), + // pop the mobile nav stack so it stays in sync with StackNavigation. + if let StackNavigationAction::Pop = action.as_widget_action().cast() { + if app_state.selected_room.is_some() { + app_state.selected_room = self.mobile_room_nav_stack.pop(); + } + } } } @@ -511,5 +548,95 @@ impl HomeScreen { }, ) } + + /// Room StackNavigationView instances, one per stack depth. + /// Each depth gets its own dedicated view widget to avoid + /// complex state save/restore when views would otherwise be reused. + const ROOM_VIEW_IDS: [LiveId; 16] = [ + live_id!(room_view_0), live_id!(room_view_1), + live_id!(room_view_2), live_id!(room_view_3), + live_id!(room_view_4), live_id!(room_view_5), + live_id!(room_view_6), live_id!(room_view_7), + live_id!(room_view_8), live_id!(room_view_9), + live_id!(room_view_10), live_id!(room_view_11), + live_id!(room_view_12), live_id!(room_view_13), + live_id!(room_view_14), live_id!(room_view_15), + ]; + + /// The RoomScreen widget IDs inside each room view, + /// corresponding 1:1 with [`Self::ROOM_VIEW_IDS`]. + const ROOM_SCREEN_IDS: [LiveId; 16] = [ + live_id!(room_screen_0), live_id!(room_screen_1), + live_id!(room_screen_2), live_id!(room_screen_3), + live_id!(room_screen_4), live_id!(room_screen_5), + live_id!(room_screen_6), live_id!(room_screen_7), + live_id!(room_screen_8), live_id!(room_screen_9), + live_id!(room_screen_10), live_id!(room_screen_11), + live_id!(room_screen_12), live_id!(room_screen_13), + live_id!(room_screen_14), live_id!(room_screen_15), + ]; + + /// Returns the room view and room screen LiveIds for the given stack depth. + /// Clamps to the last available view if depth exceeds the pool size. + fn room_ids_for_depth(depth: usize) -> (LiveId, LiveId) { + let index = depth.min(Self::ROOM_VIEW_IDS.len() - 1); + (Self::ROOM_VIEW_IDS[index], Self::ROOM_SCREEN_IDS[index]) + } + + /// Pushes the appropriate StackNavigationView for the given `SelectedRoom`, + /// configuring the view's content widget and header title. + /// + /// Each stack depth gets its own dedicated room view widget, + /// supporting deep navigation (room → thread → room → thread → ...). + fn push_selected_room_view( + &mut self, + cx: &mut Cx, + app_state: &mut AppState, + selected_room: SelectedRoom, + ) { + let new_depth = self.view.stack_navigation(cx, ids!(view_stack)).depth(); + + let view_id = match &selected_room { + SelectedRoom::JoinedRoom { room_name_id } + | SelectedRoom::Thread { room_name_id, .. } => { + let (view_id, room_screen_id) = Self::room_ids_for_depth(new_depth); + let thread_root = if let SelectedRoom::Thread { thread_root_event_id, .. } = &selected_room { + Some(thread_root_event_id.clone()) + } else { + None + }; + self.view + .room_screen(cx, &[room_screen_id]) + .set_displayed_room(cx, room_name_id, thread_root); + view_id + } + SelectedRoom::InvitedRoom { room_name_id } => { + self.view + .invite_screen(cx, ids!(invite_screen)) + .set_displayed_invite(cx, room_name_id); + id!(invite_view) + } + SelectedRoom::Space { space_name_id } => { + self.view + .space_lobby_screen(cx, ids!(space_lobby_screen)) + .set_displayed_space(cx, space_name_id); + id!(space_lobby_view) + } + }; + + // Set the header title for the view being pushed. + let title_path = &[view_id, live_id!(header), live_id!(content), live_id!(title_container), live_id!(title)]; + self.view.label(cx, title_path).set_text(cx, &selected_room.display_name()); + + // Save the current selected_room onto the navigation stack before replacing it. + if let Some(prev) = app_state.selected_room.take() { + self.mobile_room_nav_stack.push(prev); + } + app_state.selected_room = Some(selected_room); + + // Push the view onto the mobile navigation stack. + self.view.stack_navigation(cx, ids!(view_stack)).push(cx, view_id); + self.view.redraw(cx); + } } diff --git a/src/home/mod.rs b/src/home/mod.rs index 23a1de96..b96ee099 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -29,6 +29,7 @@ pub mod new_message_context_menu; pub mod room_context_menu; pub mod link_preview; pub mod room_image_viewer; +pub mod upload_progress; pub fn script_mod(vm: &mut ScriptVm) { search_messages::script_mod(vm); @@ -58,6 +59,7 @@ pub fn script_mod(vm: &mut ScriptVm) { main_desktop_ui::script_mod(vm); spaces_bar::script_mod(vm); navigation_tab_bar::script_mod(vm); + upload_progress::script_mod(vm); // Keep HomeScreen last, it references many widgets registered above. home_screen::script_mod(vm); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced..c9c77771 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1586,6 +1586,34 @@ impl RoomScreen { tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} + TimelineUpdate::FileUploadConfirmed(file_data) => { + let room_input_bar = self.view.room_input_bar(cx, ids!(room_input_bar)); + if let Some(replied_to) = room_input_bar.handle_file_upload_confirmed(cx, &file_data.name) { + submit_async_request(MatrixRequest::SendAttachment { + timeline_kind: tl.kind.clone(), + file_data, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp: room_input_bar.is_tsp_signing_enabled(cx), + }); + } + } + TimelineUpdate::FileUploadUpdate { current, total } => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .set_upload_progress(cx, current, total); + } + TimelineUpdate::FileUploadAbortHandle(handle) => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .set_upload_abort_handle(handle); + } + TimelineUpdate::FileUploadError { error, file_data } => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .show_upload_error(cx, &error, file_data); + } + TimelineUpdate::FileUploadComplete => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .hide_upload_progress(cx); + } } } @@ -2738,6 +2766,22 @@ pub enum TimelineUpdate { Tombstoned(SuccessorRoomDetails), /// A notice that link preview data for a URL has been fetched and is now available. LinkPreviewFetched, + /// User confirmed a file upload via the file upload modal. + FileUploadConfirmed(crate::shared::file_upload_modal::FileData), + /// Progress update for an ongoing file upload. + FileUploadUpdate { + current: u64, + total: u64, + }, + /// The abort handle for an in-progress file upload. + FileUploadAbortHandle(tokio::task::AbortHandle), + /// An error occurred during file upload. + FileUploadError { + error: String, + file_data: crate::shared::file_upload_modal::FileData, + }, + /// File upload completed successfully. + FileUploadComplete, } thread_local! { diff --git a/src/home/upload_progress.rs b/src/home/upload_progress.rs new file mode 100644 index 00000000..d59fba8a --- /dev/null +++ b/src/home/upload_progress.rs @@ -0,0 +1,277 @@ +//! A widget that displays upload progress with a progress bar, status label, +//! and cancel/retry buttons. + +use makepad_widgets::*; +use tokio::task::AbortHandle; + +use crate::shared::file_upload_modal::FileData; +use crate::shared::progress_bar::ProgressBarWidgetRefExt; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.UploadProgressView = set_type_default() do #(UploadProgressView::register_widget(vm)) { + visible: false, + width: Fill, + height: Fit, + flow: Down, + padding: 10, + spacing: 8, + + show_bg: true, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + border_radius: 4.0 + } + + // Header with file name and cancel button + header := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + uploading_label := Label { + width: Fit, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (COLOR_TEXT) + } + text: "Uploading: " + } + + file_name_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (COLOR_TEXT) + } + text: "" + } + + cancel_button := RobrixNeutralIconButton { + width: 24, height: 24, + padding: 4, + draw_icon +: { svg: (ICON_CLOSE) } + icon_walk: Walk{width: 14, height: 14} + text: "" + } + } + + // Progress bar + progress_bar := ProgressBar { + width: Fill, + height: 6, + } + + // Status/error area + status_view := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + status_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + + retry_button := RobrixPositiveIconButton { + enabled: false, + padding: Inset{top: 4, bottom: 4, left: 8, right: 8} + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9 }, + } + text: "Retry" + } + } + } +} + +/// The current state of the upload view. +#[derive(Clone, Debug, Default)] +pub enum UploadViewState { + /// Normal state - upload in progress or ready. + #[default] + Normal, + /// Error state - upload failed. + Error { + message: String, + file_data: FileData, + }, +} + +/// Actions emitted by the UploadProgressView. +#[derive(Clone, Debug, Default)] +pub enum UploadProgressViewAction { + /// No action. + #[default] + None, + /// User cancelled the upload. + Cancelled, + /// User requested retry of a failed upload. + Retry(FileData), +} + +/// A widget showing upload progress with cancel/retry functionality. +#[derive(Script, ScriptHook, Widget)] +pub struct UploadProgressView { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// Handle to abort the current upload task. + #[rust] abort_handle: Option, + /// Current progress value (0.0 to 1.0). + #[rust] progress: f32, + /// Current state of the upload view. + #[rust] state: UploadViewState, +} + +impl Widget for UploadProgressView { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Event::Actions(actions) = event { + // Handle cancel button + if self.button(cx, ids!(cancel_button)).clicked(actions) { + if let Some(handle) = self.abort_handle.take() { + handle.abort(); + } + cx.widget_action(self.widget_uid(), UploadProgressViewAction::Cancelled); + self.hide(cx); + } + + // Handle retry button + if self.button(cx, ids!(retry_button)).clicked(actions) { + if let UploadViewState::Error { file_data, .. } = &self.state { + let file_data = file_data.clone(); + cx.widget_action(self.widget_uid(), UploadProgressViewAction::Retry(file_data)); + self.hide(cx); + } + } + } + + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl UploadProgressView { + /// Shows the upload progress view with the given file name. + pub fn show(&mut self, cx: &mut Cx, file_name: &str) { + self.set_visible(cx, true); + self.state = UploadViewState::Normal; + self.progress = 0.0; + + self.label(cx, ids!(file_name_label)).set_text(cx, file_name); + self.label(cx, ids!(status_label)).set_text(cx, "Starting upload..."); + self.button(cx, ids!(retry_button)).set_enabled(cx, false); + self.button(cx, ids!(cancel_button)).set_enabled(cx, true); + + // Reset progress bar + self.child_by_path(ids!(progress_bar)).as_progress_bar().set_progress(cx, 0.0); + + self.redraw(cx); + } + + /// Hides the upload progress view. + pub fn hide(&mut self, cx: &mut Cx) { + self.set_visible(cx, false); + self.abort_handle = None; + self.state = UploadViewState::Normal; + self.redraw(cx); + } + + /// Updates the progress value. + pub fn set_progress(&mut self, cx: &mut Cx, current: u64, total: u64) { + self.progress = if total > 0 { + (current as f32 / total as f32).clamp(0.0, 1.0) + } else { + 0.0 + }; + + self.child_by_path(ids!(progress_bar)).as_progress_bar() + .set_progress(cx, self.progress); + + // Update status label + let percent = (self.progress * 100.0) as u32; + let status = format!( + "Uploading... {}% ({} / {})", + percent, + crate::utils::format_file_size(current), + crate::utils::format_file_size(total) + ); + self.label(cx, ids!(status_label)).set_text(cx, &status); + + self.redraw(cx); + } + + /// Sets the abort handle for the current upload task. + pub fn set_abort_handle(&mut self, handle: AbortHandle) { + self.abort_handle = Some(handle); + } + + /// Shows an error state with the given message. + pub fn show_error(&mut self, cx: &mut Cx, error: &str, file_data: FileData) { + self.state = UploadViewState::Error { + message: error.to_string(), + file_data, + }; + + // Update UI for error state + self.label(cx, ids!(status_label)) + .set_text(cx, &format!("Error: {}", error)); + self.button(cx, ids!(retry_button)).set_enabled(cx, true); + self.button(cx, ids!(cancel_button)).set_enabled(cx, true); + + // Set progress bar to error color - no longer apply color change via script_apply_eval + // The progress bar will use the default color for now + + self.redraw(cx); + } +} + +impl UploadProgressViewRef { + /// Shows the upload progress view with the given file name. + pub fn show(&self, cx: &mut Cx, file_name: &str) { + if let Some(mut inner) = self.borrow_mut() { + inner.show(cx, file_name); + } + } + + /// Hides the upload progress view. + pub fn hide(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.hide(cx); + } + } + + /// Updates the progress value. + pub fn set_progress(&self, cx: &mut Cx, current: u64, total: u64) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_progress(cx, current, total); + } + } + + /// Sets the abort handle for the current upload task. + pub fn set_abort_handle(&self, handle: AbortHandle) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_abort_handle(handle); + } + } + + /// Shows an error state with the given message. + pub fn show_error(&self, cx: &mut Cx, error: &str, file_data: FileData) { + if let Some(mut inner) = self.borrow_mut() { + inner.show_error(cx, error, file_data); + } + } +} diff --git a/src/image_utils.rs b/src/image_utils.rs new file mode 100644 index 00000000..3b866dcb --- /dev/null +++ b/src/image_utils.rs @@ -0,0 +1,104 @@ +//! Image processing utilities for thumbnail generation and image manipulation. + +use std::io::Cursor; + +/// The maximum dimension (width or height) for generated thumbnails. +pub const THUMBNAIL_MAX_DIMENSION: u32 = 800; + +/// Generates a thumbnail from the given image data. +/// +/// The thumbnail is scaled to fit within `THUMBNAIL_MAX_DIMENSION` while preserving aspect ratio. +/// Returns the thumbnail as JPEG-encoded bytes, along with the thumbnail's dimensions. +/// +/// # Arguments +/// * `image_data` - The raw bytes of the source image (PNG, JPEG, etc.) +/// +/// # Returns +/// * `Ok((jpeg_bytes, width, height))` - The thumbnail data and dimensions +/// * `Err(String)` - Error message if thumbnail generation fails +pub fn generate_thumbnail(image_data: &[u8]) -> Result<(Vec, u32, u32), String> { + use image::{ImageFormat, ImageReader}; + + // Load the image from bytes + let img = ImageReader::new(Cursor::new(image_data)) + .with_guessed_format() + .map_err(|e| format!("Failed to guess image format: {e}"))? + .decode() + .map_err(|e| format!("Failed to decode image: {e}"))?; + + let (orig_width, orig_height) = (img.width(), img.height()); + + // Calculate thumbnail dimensions while preserving aspect ratio + let (thumb_width, thumb_height) = if orig_width > THUMBNAIL_MAX_DIMENSION + || orig_height > THUMBNAIL_MAX_DIMENSION + { + let ratio = f64::from(orig_width) / f64::from(orig_height); + if orig_width > orig_height { + let new_width = THUMBNAIL_MAX_DIMENSION; + let new_height = (f64::from(new_width) / ratio) as u32; + (new_width, new_height) + } else { + let new_height = THUMBNAIL_MAX_DIMENSION; + let new_width = (f64::from(new_height) * ratio) as u32; + (new_width, new_height) + } + } else { + (orig_width, orig_height) + }; + + // Resize the image using a high-quality filter + let thumbnail = img.resize( + thumb_width, + thumb_height, + image::imageops::FilterType::Lanczos3, + ); + + // Encode as JPEG + let mut jpeg_bytes = Vec::new(); + thumbnail + .write_to(&mut Cursor::new(&mut jpeg_bytes), ImageFormat::Jpeg) + .map_err(|e| format!("Failed to encode thumbnail as JPEG: {e}"))?; + + Ok((jpeg_bytes, thumb_width, thumb_height)) +} + +/// Returns the MIME type string for the given image data by inspecting its header bytes. +/// +/// Returns `None` if the image format cannot be determined. +pub fn detect_mime_type(data: &[u8]) -> Option<&'static str> { + match imghdr::from_bytes(data) { + Some(imghdr::Type::Png) => Some("image/png"), + Some(imghdr::Type::Jpeg) => Some("image/jpeg"), + Some(imghdr::Type::Gif) => Some("image/gif"), + Some(imghdr::Type::Webp) => Some("image/webp"), + Some(imghdr::Type::Bmp) => Some("image/bmp"), + Some(imghdr::Type::Tiff) => Some("image/tiff"), + _ => None, + } +} + +/// Returns true if the given MIME type represents an image format that can be displayed. +pub fn is_displayable_image(mime_type: &str) -> bool { + matches!( + mime_type, + "image/png" + | "image/jpeg" + | "image/jpg" + | "image/gif" + | "image/webp" + | "image/bmp" + ) +} + +/// Gets the dimensions of an image from its raw bytes. +/// +/// Returns `None` if the image cannot be decoded. +pub fn get_image_dimensions(data: &[u8]) -> Option<(u32, u32)> { + use image::ImageReader; + + ImageReader::new(Cursor::new(data)) + .with_guessed_format() + .ok()? + .into_dimensions() + .ok() +} diff --git a/src/lib.rs b/src/lib.rs index 346c0314..750bee2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ pub mod verification; pub mod utils; pub mod temp_storage; pub mod location; +pub mod image_utils; pub const APP_QUALIFIER: &str = "org"; pub const APP_ORGANIZATION: &str = "robius"; diff --git a/src/room/reply_preview.rs b/src/room/reply_preview.rs index 03ec0794..5a53687b 100644 --- a/src/room/reply_preview.rs +++ b/src/room/reply_preview.rs @@ -107,7 +107,7 @@ script_mod! { padding: 13, spacing: 0, margin: Inset{left: 5, right: 0}, - draw_bg.border_radius: 5.0 + draw_bg.border_radius: 4.0 draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{width: 16, height: 16, margin: 0} } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 93b8d4a9..f1c2844c 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -20,7 +20,7 @@ use makepad_widgets::*; use matrix_sdk::room::reply::{EnforceThread, Reply}; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId}; -use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, RoomScreenProps, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; +use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, RoomScreenProps, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::UploadProgressViewWidgetRefExt}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, file_upload_modal::{FileData, FileLoadedData, FilePreviewerAction, FilePreviewerMetaData, ThumbnailData}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; script_mod! { use mod.prelude.widgets.* @@ -60,6 +60,9 @@ script_mod! { // Below that, display a preview of the current location that a user is about to send. location_preview := LocationPreview { } + // Upload progress view (shown when a file upload is in progress) + upload_progress_view := UploadProgressView { } + // Below that, display one of multiple possible views: // * the message input bar (buttons and message TextInput). // * a notice that the user can't send messages to this room. @@ -80,6 +83,23 @@ script_mod! { align: Align{y: 1.0}, padding: 6, + // Attachment button for uploading files/images + send_attachment_button := RobrixIconButton { + margin: 4 + spacing: 0, + draw_icon +: { + svg: (ICON_ADD_ATTACHMENT) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + } + icon_walk: Walk{width: 21, height: 21} + text: "", + } + location_button := RobrixIconButton { margin: 4 spacing: 0, @@ -169,6 +189,9 @@ pub struct RoomInputBar { #[rust] was_replying_preview_visible: bool, /// Info about the message event that the user is currently replying to, if any. #[rust] replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, + /// The pending file load operation, if any. Contains the receiver channel + /// for receiving the loaded file data from a background thread. + #[rust] pending_file_load: Option, } impl Widget for RoomInputBar { @@ -203,6 +226,36 @@ impl Widget for RoomInputBar { self.handle_actions(cx, actions, room_screen_props); } + // Handle signal events for pending file loads from background threads + if let Event::Signal = event { + if let Some(receiver) = &self.pending_file_load { + let mut remove_receiver = false; + match receiver.try_recv() { + Ok(Some(loaded_data)) => { + // Convert FileLoadedData to FileData for the modal + let file_data = convert_loaded_data_to_file_data(loaded_data); + Cx::post_action(FilePreviewerAction::Show(file_data)); + remove_receiver = true; + } + Ok(None) => { + // File loading failed, hide modal if shown + remove_receiver = true; + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + // Still waiting for data + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + // Channel disconnected + remove_receiver = true; + } + } + if remove_receiver { + self.pending_file_load = None; + self.redraw(cx); + } + } + } + self.view.handle_event(cx, event, scope); } @@ -230,6 +283,12 @@ impl RoomInputBar { self.redraw(cx); } + // Handle the add attachment button being clicked. + if self.button(cx, ids!(send_attachment_button)).clicked(actions) { + log!("Add attachment button clicked; opening file picker..."); + self.open_file_picker(cx); + } + // Handle the add location button being clicked. if self.button(cx, ids!(location_button)).clicked(actions) { log!("Add location button clicked; requesting current location..."); @@ -532,6 +591,101 @@ impl RoomInputBar { fn is_tsp_signing_enabled(&self, cx: &mut Cx) -> bool { self.view.check_box(cx, ids!(tsp_sign_checkbox)).active(cx) } + + /// Opens the native file picker dialog to select a file for upload. + #[cfg(not(any(target_os = "ios", target_os = "android")))] + fn open_file_picker(&mut self, cx: &mut Cx) { + // Run file dialog on main thread (required for non-windowed environments) + let dialog = rfd::FileDialog::new() + .set_title("Select file to upload") + .add_filter("All files", &["*"]) + .add_filter("Images", &["png", "jpg", "jpeg", "gif", "webp", "bmp"]) + .add_filter("Documents", &["pdf", "doc", "docx", "txt", "rtf"]); + + if let Some(selected_file_path) = dialog.pick_file() { + // Get file metadata + let file_size = match std::fs::metadata(&selected_file_path) { + Ok(metadata) => metadata.len(), + Err(e) => { + makepad_widgets::error!("Failed to read file metadata: {e}"); + enqueue_popup_notification( + format!("Unable to access file: {e}"), + PopupKind::Error, + None, + ); + return; + } + }; + + // Check for empty files + if file_size == 0 { + enqueue_popup_notification("Cannot upload empty file", PopupKind::Error, None); + return; + } + + // Detect the MIME type from the file extension + let mime = mime_guess::from_path(&selected_file_path) + .first_or_octet_stream(); + + // Create channel for receiving loaded file data + let (sender, receiver) = std::sync::mpsc::channel(); + self.pending_file_load = Some(receiver); + + // Spawn background thread to generate thumbnail (for images) + let path_clone = selected_file_path.clone(); + let mime_clone = mime.clone(); + cx.spawn_thread(move || { + // Generate thumbnail for images + let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(mime_clone.as_ref()) { + match std::fs::read(&path_clone) { + Ok(data) => { + match crate::image_utils::generate_thumbnail(&data) { + Ok((thumb_data, width, height)) => ( + Some(ThumbnailData { data: thumb_data, width, height }), + Some((width, height)) + ), + Err(e) => { + makepad_widgets::error!("Failed to generate thumbnail: {e}"); + (None, None) + } + } + } + Err(e) => { + makepad_widgets::error!("Failed to read file for thumbnail: {e}"); + (None, None) + } + } + } else { + (None, None) + }; + + let loaded_data = FileLoadedData { + metadata: FilePreviewerMetaData { + mime: mime_clone, + file_size, + file_path: path_clone, + }, + thumbnail, + dimensions, + }; + + if sender.send(Some(loaded_data)).is_err() { + makepad_widgets::error!("Failed to send file data to UI: receiver dropped"); + } + SignalToUI::set_ui_signal(); + }); + } + } + + /// Shows a "not supported" message on mobile platforms. + #[cfg(any(target_os = "ios", target_os = "android"))] + fn open_file_picker(&mut self, _cx: &mut Cx) { + enqueue_popup_notification( + "File uploads are not yet supported on this platform.", + PopupKind::Error, + None, + ); + } } impl RoomInputBarRef { @@ -664,6 +818,84 @@ impl RoomInputBarRef { // This depends on the `EditingPane` state, so it must be done after Step 3. inner.update_tombstone_footer(cx, timeline_kind.room_id(), tombstone_info); } + + /// Shows the upload progress view for a file upload. + pub fn show_upload_progress(&self, cx: &mut Cx, file_name: &str) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show(cx, file_name); + } + + /// Hides the upload progress view. + pub fn hide_upload_progress(&self, cx: &mut Cx) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .hide(cx); + } + + /// Updates the upload progress. + pub fn set_upload_progress(&self, cx: &mut Cx, current: u64, total: u64) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .set_progress(cx, current, total); + } + + /// Sets the abort handle for the current upload. + pub fn set_upload_abort_handle(&self, handle: tokio::task::AbortHandle) { + let Some(inner) = self.borrow_mut() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .set_abort_handle(handle); + } + + /// Shows an upload error with retry option. + pub fn show_upload_error(&self, cx: &mut Cx, error: &str, file_data: FileData) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show_error(cx, error, file_data); + } + + /// Handles a confirmed file upload from the file upload modal. + /// + /// This method: + /// - Shows the upload progress view + /// - Gets and clears any "replying to" state + /// - Returns the reply metadata needed to submit the upload request + pub fn handle_file_upload_confirmed(&self, cx: &mut Cx, file_name: &str) -> Option> { + let mut inner = self.borrow_mut()?; + + // Get the reply metadata if replying to a message + let replied_to = inner + .replying_to + .take() + .and_then(|(event_tl_item, _embedded_event)| { + event_tl_item.event_id().map(|event_id| Reply { + event_id: event_id.to_owned(), + enforce_thread: EnforceThread::MaybeThreaded, + }) + }); + + // Show the upload progress view + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show(cx, file_name); + + // Clear the replying-to state + inner.clear_replying_to(cx); + + Some(replied_to) + } + + /// Returns whether TSP signing is enabled. + #[cfg(feature = "tsp")] + pub fn is_tsp_signing_enabled(&self, cx: &mut Cx) -> bool { + let Some(inner) = self.borrow() else { return false }; + inner.is_tsp_signing_enabled(cx) + } } /// The saved UI state of a `RoomInputBar` widget. @@ -691,3 +923,22 @@ enum ShowEditingPaneBehavior { editing_pane_state: EditingPaneState, }, } + +/// Converts `FileLoadedData` from background thread to `FileData` for the modal. +fn convert_loaded_data_to_file_data(loaded: FileLoadedData) -> FileData { + // Read the file data from the path + let data = std::fs::read(&loaded.metadata.file_path).unwrap_or_default(); + let name = loaded.metadata.file_path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + FileData { + path: loaded.metadata.file_path, + name, + mime_type: loaded.metadata.mime.to_string(), + data, + size: loaded.metadata.file_size, + thumbnail: loaded.thumbnail, + } +} diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 877f66bf..8f37cca1 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -57,6 +57,7 @@ script_mod! { upload_avatar_button := RobrixIconButton { width: 140, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: 0, draw_icon.svg: (ICON_UPLOAD) @@ -79,6 +80,7 @@ script_mod! { delete_avatar_button := RobrixNegativeIconButton { width: 140, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: 0, draw_icon.svg: (ICON_TRASH) @@ -117,7 +119,8 @@ script_mod! { // their styles to RobrixNeutralIconButton / RobrixPositiveIconButton. cancel_display_name_button := RobrixNeutralIconButton { enabled: false, - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, draw_icon.svg: (ICON_FORBIDDEN) @@ -127,10 +130,10 @@ script_mod! { accept_display_name_button := RobrixPositiveIconButton { enabled: false, - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_CHECKMARK) icon_walk: Walk{width: 16, height: 16, margin: 0} text: "Save Name" @@ -186,7 +189,8 @@ script_mod! { spacing: 10 manage_account_button := RobrixIconButton { - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + padding: Inset{left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_EXTERNAL_LINK) icon_walk: Walk{width: 16, height: 16} @@ -194,6 +198,7 @@ script_mod! { } logout_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_LOGOUT) diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 24baf849..a255b703 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -7,7 +7,6 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* - // The main, top-level settings screen widget. mod.widgets.SettingsScreen = #(SettingsScreen::register_widget(vm)) { width: Fill, height: Fill, diff --git a/src/shared/file_upload_modal.rs b/src/shared/file_upload_modal.rs new file mode 100644 index 00000000..851b6c7b --- /dev/null +++ b/src/shared/file_upload_modal.rs @@ -0,0 +1,338 @@ +//! A modal dialog for previewing and confirming file uploads. +//! +//! This modal shows a preview of the file (image thumbnail or file icon) +//! along with file metadata and upload/cancel buttons. + +use makepad_widgets::*; +use std::path::PathBuf; + +use crate::utils::format_file_size; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.FileUploadModal = set_type_default() do #(FileUploadModal::register_widget(vm)) { + ..mod.widgets.RoundedView + + width: 400, + height: Fit, + flow: Down, + padding: 20, + spacing: 15, + + show_bg: true, + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 8.0 + shadow_color: #00000044 + shadow_radius: 10.0 + shadow_offset: vec2(0.0, 2.0) + } + + // Header + header := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + title := Label { + width: Fill, + draw_text +: { + text_style: TITLE_TEXT { font_size: 14 }, + color: (COLOR_TEXT) + } + text: "Upload File" + } + + close_button := RobrixNeutralIconButton { + width: Fit, + height: Fit, + align: Align{x: 1.0, y: 0.0}, + spacing: 0, + margin: Inset{top: 4.5} // vertically align with the title + padding: 15, + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14} + } + } + + // Preview area + preview_container := View { + width: Fill, + height: 200, + flow: Overlay, + align: Align{x: 0.5, y: 0.5}, + + show_bg: true, + draw_bg.color: (COLOR_SECONDARY) + + // Image preview container (visible when file is an image) + image_preview_container := View { + visible: false, + width: Fill, + height: Fill, + align: Align{x: 0.5, y: 0.5}, + // cannot center align for tall images + image_preview := Image { + width: Fill, + height: Fill, + fit: ImageFit.Smallest, + } + } + + // File icon (visible when file is not an image) + file_icon_container := View { + visible: false, + width: Fill, + height: Fill, + align: Align{x: 0.5, y: 0.5}, + flow: Down, + spacing: 10, + + Icon { + width: Fit, height: Fit, + draw_icon +: { + svg: (ICON_FILE) + color: (COLOR_TEXT) + } + icon_walk: Walk{width: 64, height: 64} + } + + file_type_label := Label { + width: Fit, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + } + } + + // File info + file_info := View { + width: Fill, + height: Fit, + flow: Down, + spacing: 5, + + file_name_label := Label { + width: Fill, + flow: Flow.Right{wrap: true}, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11 }, + color: (COLOR_TEXT) + } + text: "" + } + + file_size_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + } + + // Buttons + buttons := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 1.0, y: 0.5}, + spacing: 10, + + cancel_button := RobrixNeutralIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + text: "Cancel" + } + + upload_button := RobrixPositiveIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + draw_icon +: { svg: (ICON_UPLOAD) } + icon_walk: Walk{width: 16, height: Fit, margin: Inset{right: 4}} + text: "Upload" + } + } + } +} + +/// Data describing a file to be uploaded. +#[derive(Clone, Debug)] +pub struct FileData { + /// The file path on the local filesystem. + pub path: PathBuf, + /// The file name (without directory path). + pub name: String, + /// The MIME type of the file. + pub mime_type: String, + /// The raw file data. + pub data: Vec, + /// The file size in bytes. + pub size: u64, + /// Optional thumbnail data for images (JPEG bytes). + pub thumbnail: Option, +} + +/// Thumbnail data for image files. +#[derive(Clone, Debug)] +pub struct ThumbnailData { + /// The thumbnail image data (JPEG). + pub data: Vec, + /// Width of the thumbnail. + pub width: u32, + /// Height of the thumbnail. + pub height: u32, +} + +/// Metadata for the file previewer (used in background loading). +#[derive(Debug, Clone)] +pub struct FilePreviewerMetaData { + /// MIME type of the file. + pub mime: mime_guess::Mime, + /// File size in bytes. + pub file_size: u64, + /// Path to the original file. + pub file_path: PathBuf, +} + +/// Data loaded from a file by a background thread. +/// This is sent through a channel and combined with additional data to create `FileData`. +#[derive(Debug, Clone)] +pub struct FileLoadedData { + /// Metadata about the file (path, size, MIME type). + pub metadata: FilePreviewerMetaData, + /// Optional thumbnail for image files. + pub thumbnail: Option, + /// Optional dimensions for image/video files, width and height in pixels. + pub dimensions: Option<(u32, u32)>, +} + +/// Type alias for the receiver that gets loaded file data from a background thread. +pub type FileLoadReceiver = std::sync::mpsc::Receiver>; + +/// Actions emitted by the FileUploadModal. +#[derive(Clone, Debug, Default)] +pub enum FilePreviewerAction { + /// No action. + #[default] + None, + /// Show the file upload modal with the given file data. + Show(FileData), + /// Hide the file upload modal. + Hide, + /// User confirmed the upload. + UploadConfirmed(FileData), + /// User cancelled the upload. + Cancelled, +} + +/// A modal for previewing and confirming file uploads. +#[derive(Script, ScriptHook, Widget)] +pub struct FileUploadModal { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// The current file data being previewed. + #[rust] file_data: Option, +} + +impl Widget for FileUploadModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Event::Actions(actions) = event { + // Handle close button + if self.button(cx, ids!(close_button)).clicked(actions) + || self.button(cx, ids!(cancel_button)).clicked(actions) + { + Cx::post_action(FilePreviewerAction::Cancelled); + Cx::post_action(FilePreviewerAction::Hide); + } + + // Handle upload button + if self.button(cx, ids!(upload_button)).clicked(actions) { + if let Some(file_data) = self.file_data.take() { + Cx::post_action(FilePreviewerAction::UploadConfirmed(file_data)); + Cx::post_action(FilePreviewerAction::Hide); + } + } + } + + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl FileUploadModal { + /// Sets the file data and updates the preview UI. + pub fn set_file_data(&mut self, cx: &mut Cx, file_data: FileData) { + // Update file name label + self.label(cx, ids!(file_name_label)) + .set_text(cx, &file_data.name); + + // Update file size label + self.label(cx, ids!(file_size_label)) + .set_text(cx, &format_file_size(file_data.size)); + + // Determine if this is an image + let is_image = crate::image_utils::is_displayable_image(&file_data.mime_type); + + // Show/hide appropriate preview widgets + let image_preview = self.view.image(cx, ids!(image_preview_container.image_preview)); + let image_preview_container = self.view.view(cx, ids!(image_preview_container)); + let file_icon_container = self.view.view(cx, ids!(file_icon_container)); + + if is_image { + makepad_widgets::log!("FileUploadModal: Loading image preview, data size: {} bytes, mime: {}", file_data.data.len(), file_data.mime_type); + // Hide file icon first + file_icon_container.set_visible(cx, false); + + // Load image data into the preview + if let Err(e) = crate::utils::load_png_or_jpg(&image_preview, cx, &file_data.data) { + makepad_widgets::error!("Failed to load image preview: {:?}", e); + // Fall back to file icon + image_preview_container.set_visible(cx, false); + file_icon_container.set_visible(cx, true); + self.update_file_type_label(cx, &file_data.mime_type); + } else { + makepad_widgets::log!("FileUploadModal: Image loaded successfully"); + // Set container visible after loading + image_preview_container.set_visible(cx, true); + } + } else { + image_preview_container.set_visible(cx, false); + file_icon_container.set_visible(cx, true); + self.update_file_type_label(cx, &file_data.mime_type); + } + + self.file_data = Some(file_data); + self.redraw(cx); + } + + /// Updates the file type label based on MIME type. + fn update_file_type_label(&mut self, cx: &mut Cx, mime_type: &str) { + let type_text = mime_type + .split('/') + .next_back() + .unwrap_or("Unknown") + .to_uppercase(); + self.label(cx, ids!(file_type_label)) + .set_text(cx, &format!("{} File", type_text)); + } +} + +impl FileUploadModalRef { + /// Sets the file data and updates the preview UI. + pub fn set_file_data(&self, cx: &mut Cx, file_data: FileData) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_file_data(cx, file_data); + } + } +} diff --git a/src/shared/mod.rs b/src/shared/mod.rs index a92a81fd..e9a04b02 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -4,12 +4,14 @@ pub mod avatar; pub mod collapsible_header; pub mod expand_arrow; pub mod confirmation_modal; +pub mod file_upload_modal; pub mod helpers; pub mod html_or_plaintext; pub mod icon_button; pub mod jump_to_bottom_button; pub mod mentionable_text_input; pub mod popup_list; +pub mod progress_bar; pub mod room_filter_input_bar; pub mod styles; pub mod text_or_image; @@ -44,4 +46,6 @@ pub fn script_mod(vm: &mut ScriptVm) { restore_status_view::script_mod(vm); confirmation_modal::script_mod(vm); image_viewer::script_mod(vm); + progress_bar::script_mod(vm); + file_upload_modal::script_mod(vm); } diff --git a/src/shared/progress_bar.rs b/src/shared/progress_bar.rs new file mode 100644 index 00000000..36f2aaab --- /dev/null +++ b/src/shared/progress_bar.rs @@ -0,0 +1,106 @@ +//! A progress bar widget with capsule-shaped design for showing upload/download progress. + +use makepad_widgets::*; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.ProgressBar = set_type_default() do #(ProgressBar::register_widget(vm)) { + width: Fill, + height: 8, + show_bg: true, + + draw_bg +: { + progress: instance(0.0) + + // Background color (track) + color: (COLOR_SECONDARY) + // Filled portion color + progress_color: instance((COLOR_ACTIVE_PRIMARY)) + + border_radius: 4.0 + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size); + + // Draw background track (full width, rounded) + sdf.box( + 0.0, + 0.0, + self.rect_size.x, + self.rect_size.y, + self.border_radius + ); + sdf.fill(self.color); + + // Draw progress fill (partial width based on progress, rounded) + let progress_width = self.rect_size.x * self.progress; + if progress_width > 0.0 { + sdf.box( + 0.0, + 0.0, + progress_width, + self.rect_size.y, + self.border_radius + ); + sdf.fill(self.progress_color); + } + + return sdf.result; + } + } + } +} + +/// A capsule-shaped progress bar widget. +#[derive(Script, ScriptHook, Widget)] +pub struct ProgressBar { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// Current progress value between 0.0 and 1.0 + #[rust] progress: f32, +} + +impl Widget for ProgressBar { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + // Update the progress uniform before drawing + let progress = self.progress.clamp(0.0, 1.0); + script_apply_eval!(cx, self.view, { + draw_bg.progress: #(progress as f64), + }); + self.view.draw_walk(cx, scope, walk) + } +} + +impl ProgressBar { + /// Sets the progress value (0.0 to 1.0). + pub fn set_progress(&mut self, cx: &mut Cx, value: f32) { + self.progress = value.clamp(0.0, 1.0); + self.redraw(cx); + } + + /// Gets the current progress value. + pub fn progress(&self) -> f32 { + self.progress + } +} + +impl ProgressBarRef { + /// Sets the progress value (0.0 to 1.0). + pub fn set_progress(&self, cx: &mut Cx, value: f32) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_progress(cx, value); + } + } + + /// Gets the current progress value. + pub fn progress(&self) -> f32 { + self.borrow().map(|inner| inner.progress()).unwrap_or(0.0) + } +} diff --git a/src/shared/styles.rs b/src/shared/styles.rs index a80fa55e..1f042591 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -7,7 +7,7 @@ script_mod! { mod.widgets.ICON_ADD = crate_resource("self://resources/icons/add.svg") mod.widgets.ICON_ADD_REACTION = crate_resource("self://resources/icons/add_reaction.svg") - mod.widgets.ICON_ADD_USER = crate_resource("self://resources/icons/add_user.svg") // TODO: FIX + mod.widgets.ICON_ADD_USER = crate_resource("self://resources/icons/add_user.svg") mod.widgets.ICON_ADD_WALLET = crate_resource("self://resources/icons/add_wallet.svg") mod.widgets.ICON_FORBIDDEN = crate_resource("self://resources/icons/forbidden.svg") mod.widgets.ICON_CHECKMARK = crate_resource("self://resources/icons/checkmark.svg") @@ -19,7 +19,7 @@ script_mod! { mod.widgets.ICON_COPY = crate_resource("self://resources/icons/copy.svg") mod.widgets.ICON_EDIT = crate_resource("self://resources/icons/edit.svg") mod.widgets.ICON_EXTERNAL_LINK = crate_resource("self://resources/icons/external_link.svg") - mod.widgets.ICON_IMPORT = crate_resource("self://resources/icons/import.svg") // TODO: FIX + mod.widgets.ICON_IMPORT = crate_resource("self://resources/icons/import.svg") mod.widgets.ICON_HIERARCHY = crate_resource("self://resources/icons/hierarchy.svg") mod.widgets.ICON_HOME = crate_resource("self://resources/icons/home.svg") mod.widgets.ICON_HTML_FILE = crate_resource("self://resources/icons/html_file.svg") @@ -44,6 +44,8 @@ script_mod! { mod.widgets.ICON_WARNING = crate_resource("self://resources/icons/warning.svg") mod.widgets.ICON_ZOOM_IN = crate_resource("self://resources/icons/zoom_in.svg") mod.widgets.ICON_ZOOM_OUT = crate_resource("self://resources/icons/zoom_out.svg") + mod.widgets.ICON_ADD_ATTACHMENT = crate_resource("self://resources/icons/add_attachment.svg") + mod.widgets.ICON_FILE = crate_resource("self://resources/icons/file.svg") mod.widgets.TITLE_TEXT = theme.font_regular { font_size: (13), @@ -187,6 +189,10 @@ script_mod! { mod.widgets.COLOR_IMAGE_VIEWER_META_BACKGROUND = #E8E8E8 + // Ensure all settings buttons have a consistent height + mod.widgets.SETTINGS_BUTTON_HEIGHT = 40 + + // A text input widget styled for Robrix. mod.widgets.RobrixTextInput = TextInput { width: Fill, height: Fit diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 30fccc5a..590a4814 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail, Result}; use bitflags::bitflags; +use mime::Mime; use clap::Parser; use eyeball::Subscriber; use eyeball_im::VectorDiff; @@ -573,6 +574,14 @@ pub enum MatrixRequest { #[cfg(feature = "tsp")] sign_with_tsp: bool, }, + /// Request to send a file attachment to the given room. + SendAttachment { + timeline_kind: TimelineKind, + file_data: crate::shared::file_upload_modal::FileData, + replied_to: Option, + #[cfg(feature = "tsp")] + sign_with_tsp: bool, + }, /// Sends a notice to the given room that the current user is or is not typing. /// /// This request does not return a response or notify the UI thread, and @@ -1452,18 +1461,15 @@ async fn matrix_worker_task( let _typing_notices_task = Handle::current().spawn(async move { while let Ok(user_ids) = typing_notice_receiver.recv().await { // log!("Received typing notifications for room {room_id}: {user_ids:?}"); - let mut users = Vec::with_capacity(user_ids.len()); - for user_id in user_ids { - users.push( - main_timeline.room() - .get_member_no_sync(&user_id) - .await - .ok() - .flatten() + let users = join_all(user_ids.into_iter().map(|user_id| { + let tl = main_timeline.clone(); + async move { + tl.room().get_member_no_sync(&user_id).await + .ok().flatten() .and_then(|m| m.display_name().map(|d| d.to_owned())) .unwrap_or_else(|| user_id.to_string()) - ); - } + } + })).await; if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); } @@ -1692,6 +1698,94 @@ async fn matrix_worker_task( }); } + MatrixRequest::SendAttachment { + timeline_kind, + file_data, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp: _sign_with_tsp, + } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for send attachment request"); + continue; + }; + + // Spawn a new async task to send the attachment. + let _send_attachment_task = Handle::current().spawn(async move { + use matrix_sdk::attachment::AttachmentConfig; + use eyeball::SharedObservable; + + log!("Sending attachment to {timeline_kind}: {} ({} bytes)...", + file_data.name, file_data.size); + + // For now, we'll just send the attachment without reply support + // TODO: Add proper reply support for attachments + let _ = replied_to; // Suppress unused warning for now + + // Parse MIME type + let content_type: Mime = file_data.mime_type.parse() + .unwrap_or_else(|_| "application/octet-stream".parse().unwrap()); + + // Create a progress observable to track upload progress + let send_progress: SharedObservable = Default::default(); + let progress_subscriber = send_progress.subscribe(); + + // Spawn a task to handle progress updates + let sender_clone = sender.clone(); + Handle::current().spawn(async move { + let mut subscriber = progress_subscriber; + loop { + let progress = subscriber.get(); + let current: u64 = progress.current as u64; + let total: u64 = progress.total as u64; + if sender_clone.send(TimelineUpdate::FileUploadUpdate { + current, + total, + }).is_err() { + break; + } + SignalToUI::set_ui_signal(); + // Wait for next update + if subscriber.next().await.is_none() { + break; + } + } + }); + + // Use the Room's send_attachment method directly + let room = timeline.room(); + let config = AttachmentConfig::new(); + + let send_future = room.send_attachment( + &file_data.name, + &content_type, + file_data.data.clone(), + config, + ).with_send_progress_observable(send_progress); + + match send_future.await { + Ok(_response) => { + log!("Successfully sent attachment to {timeline_kind}."); + let _ = sender.send(TimelineUpdate::FileUploadComplete); + } + Err(e) => { + error!("Failed to send attachment to {timeline_kind}: {e:?}"); + let _ = sender.send(TimelineUpdate::FileUploadError { + error: format!("{e}"), + file_data: file_data.clone(), + }); + enqueue_popup_notification( + format!("Failed to upload file: {e}"), + PopupKind::Error, + None, + ); + } + } + + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { let Some(timeline) = get_timeline(&timeline_kind) else { log!("BUG: {timeline_kind} not found when sending read receipt, {event_id}"); @@ -2178,6 +2272,19 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option }) } +/// Returns a clone of the timeline update sender for the given timeline. +/// +/// This can be called multiple times, as it only clones the sender. +pub fn get_timeline_update_sender(kind: &TimelineKind) -> Option> { + let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let jrd = all_joined_rooms.get(kind.room_id())?; + let details = match kind { + TimelineKind::MainRoom { .. } => &jrd.main_timeline, + TimelineKind::Thread { thread_root_event_id, .. } => jrd.thread_timelines.get(thread_root_event_id)?, + }; + Some(details.timeline_update_sender.clone()) +} + const DEFAULT_HOMESERVER: &str = "matrix.org"; fn username_to_full_user_id( diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index f51e8bcc..361d6202 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -86,14 +86,17 @@ script_mod! { width: Fit, height: Fit, did_web := RadioButtonFlat { text: "Web" + draw_text +: { color: (COLOR_TEXT) } animator: { active: { default: on } } } did_webvh := RadioButtonFlat { text: "WebVH" + draw_text +: { color: (COLOR_TEXT) } animator: { disabled: { default: on } } } did_peer := RadioButtonFlat { text: "Peer", + draw_text +: { color: (COLOR_TEXT) } animator: { disabled: { default: on } } } } @@ -105,7 +108,7 @@ script_mod! { server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "p.teaspoon.world", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} @@ -147,7 +150,7 @@ script_mod! { did_server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "did.teaspoon.world", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} diff --git a/src/tsp/create_wallet_modal.rs b/src/tsp/create_wallet_modal.rs index 79c47759..1e1709b3 100644 --- a/src/tsp/create_wallet_modal.rs +++ b/src/tsp/create_wallet_modal.rs @@ -101,7 +101,7 @@ script_mod! { wallet_file_name_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "my_wallet_file", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} diff --git a/src/tsp/sign_anycast_checkbox.rs b/src/tsp/sign_anycast_checkbox.rs index 8634c05e..971a4854 100644 --- a/src/tsp/sign_anycast_checkbox.rs +++ b/src/tsp/sign_anycast_checkbox.rs @@ -13,5 +13,10 @@ script_mod! { mod.widgets.TspSignAnycastCheckbox = CheckBoxFlat { text: "TSP", active: false, + draw_text +: { + color: COLOR_TEXT, + text_style: theme.font_regular {font_size: 11}, + mark_color_active: COLOR_TEXT, + } } } diff --git a/src/tsp/tsp_settings_screen.rs b/src/tsp/tsp_settings_screen.rs index 83d0e6f8..879ae3de 100644 --- a/src/tsp/tsp_settings_screen.rs +++ b/src/tsp/tsp_settings_screen.rs @@ -3,15 +3,12 @@ use makepad_widgets::*; use crate::{shared::{popup_list::{enqueue_popup_notification, PopupKind}, styles::*}, tsp::{create_did_modal::CreateDidModalAction, create_wallet_modal::CreateWalletModalAction, submit_tsp_request, tsp_state_ref, TspIdentityAction, TspRequest, TspWalletAction, TspWalletEntry, TspWalletMetadata}}; -const REPUBLISH_IDENTITY_BUTTON_TEXT: &str = "Republish Current Identity to DID Server"; - script_mod! { link tsp_enabled use mod.prelude.widgets.* use mod.widgets.* - mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT = "Republish Current Identity to DID Server" // The view containing all TSP-related settings. @@ -43,7 +40,7 @@ script_mod! { current_identity_label := Label { width: Fill, height: Fit flow: Flow.Right{wrap: true}, - margin: Inset{top: 10} + margin: Inset{top: 8} draw_text +: { text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } @@ -51,13 +48,13 @@ script_mod! { } republish_identity_button := RobrixIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{top: 8, bottom: 10, left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_UPLOAD) icon_walk: Walk{width: 16, height: 16} - text: (REPUBLISH_IDENTITY_BUTTON_TEXT) + text: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT } @@ -111,36 +108,36 @@ script_mod! { spacing: 10 create_did_button := RobrixPositiveIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_USER) - icon_walk: Walk{width: 21, height: Fit, margin: 0} + icon_walk: Walk{width: 19, height: Fit, margin: 0} text: "Create New Identity (DID)" } create_wallet_button := RobrixPositiveIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_WALLET) icon_walk: Walk{width: 21, height: Fit, margin: 0} text: "Create New Wallet" } import_wallet_button := RobrixIconButton { + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} text: "Import Existing Wallet" - // TODO: fix this icon, or pick a different SVG - // draw_icon +: { - // svg: (ICON_IMPORT) - // color: (COLOR_PRIMARY) - // } - // icon_walk: Walk{width: 16, height: 16} - icon_walk: Walk{width: 0, height: 0} + draw_icon +: { + svg: (ICON_IMPORT) + color: (COLOR_PRIMARY) + } + icon_walk: Walk{width: 16, height: 16} } } } @@ -381,7 +378,7 @@ impl MatchEvent for TspSettingsScreen { // restore the republish button to its original state. script_apply_eval!(cx, republish_identity_button, { enabled: true, - text: #(REPUBLISH_IDENTITY_BUTTON_TEXT), + text: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT, }); match result { Ok(did) => { diff --git a/src/tsp/wallet_entry/mod.rs b/src/tsp/wallet_entry/mod.rs index 2c2de8ab..68bb2c4c 100644 --- a/src/tsp/wallet_entry/mod.rs +++ b/src/tsp/wallet_entry/mod.rs @@ -18,11 +18,13 @@ script_mod! { mod.widgets.WalletEntry = #(WalletEntry::register_widget(vm)) { width: Fill, height: Fit flow: Down + align: Align { y: 0.5 } View { width: Fill, height: Fit flow: Flow.Right{wrap: true}, padding: 10 + align: Align { y: 0.5 } wallet_name := Label { width: Fit, height: Fit @@ -50,9 +52,11 @@ script_mod! { visible: false, width: Fit, height: Fit margin: Inset{left: 20} + align: Align { y: 0.5 } Label { - margin: Inset{top: 2.9} width: Fit, height: Fit + margin: Inset{top: 3} + align: Align { y: 0.5 } flow: Right, draw_text +: { color: (COLOR_FG_ACCEPT_GREEN), @@ -66,10 +70,12 @@ script_mod! { visible: false, width: Fit, height: Fit margin: Inset{left: 20} + align: Align { y: 0.5 } Label { margin: Inset{top: 2.9} width: Fit, height: Fit flow: Right, + align: Align { y: 0.5 } draw_text +: { color: (COLOR_FG_DANGER_RED), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, @@ -79,6 +85,7 @@ script_mod! { } set_default_wallet_button := RobrixIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_CHECKMARK) @@ -87,6 +94,7 @@ script_mod! { } remove_wallet_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_CLOSE) @@ -95,6 +103,7 @@ script_mod! { } delete_wallet_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_TRASH) diff --git a/src/utils.rs b/src/utils.rs index aa3ac814..63605317 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1007,6 +1007,25 @@ impl From<(Option, OwnedRoomId)> for RoomNameId { } } +/// Formats a file size in bytes to a human-readable string. +/// +/// Examples: "1.5 KB", "2.3 MB", "4.0 GB" +pub fn format_file_size(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.1} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.1} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.1} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + /// Returns a text avatar string containing the first character of the room name. /// /// Skips the first character if it is a `#` or `!`, the sigils used for Room aliases and Room IDs.