diff --git a/Cargo.lock b/Cargo.lock index b59176fd3..a36f3422e 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=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "accessory" @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "arrayvec" version = "0.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "arrayvec" @@ -239,7 +239,7 @@ checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" [[package]] name = "ash" version = "0.38.0+1.3.281" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "libloading 0.8.9", ] @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "bit-set" version = "0.8.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bit-vec", ] @@ -700,12 +700,12 @@ dependencies = [ [[package]] name = "bit-vec" version = "0.8.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "bitflags" @@ -854,7 +854,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "bytemuck" @@ -871,7 +871,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "bytes" @@ -939,7 +939,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg-if" version = "1.0.4" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "cfg_aliases" @@ -950,7 +950,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "cfg_aliases" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "chacha20" @@ -1323,7 +1323,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crunchy" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "crypto-bigint" @@ -1739,7 +1739,7 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "downcast-rs" version = "1.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "dunce" @@ -1899,7 +1899,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "equivalent" version = "1.0.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "errno" @@ -2082,7 +2082,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foldhash" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "foreign-types" @@ -2238,9 +2238,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", ] [[package]] @@ -2446,9 +2446,9 @@ dependencies = [ [[package]] name = "hashbrown" version = "0.16.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ - "foldhash 0.2.0 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "foldhash 0.2.0 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", ] [[package]] @@ -2499,7 +2499,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hexf-parse" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "hilog-sys" @@ -2938,10 +2938,10 @@ dependencies = [ [[package]] name = "indexmap" version = "2.13.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ - "equivalent 1.0.2 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", - "hashbrown 0.16.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "equivalent 1.0.2 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", + "hashbrown 0.16.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", ] [[package]] @@ -3142,15 +3142,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "libloading" version = "0.8.9" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ - "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "windows-link 0.2.1", ] @@ -3222,7 +3222,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "log" version = "0.4.29" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "loom" @@ -3303,7 +3303,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-objc-sys", ] @@ -3311,12 +3311,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-widgets", ] @@ -3324,7 +3324,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-proc-macro", ] @@ -3332,7 +3332,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -3341,7 +3341,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3358,7 +3358,7 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "unicode-linebreak", "unicode-segmentation 1.12.0", ] @@ -3366,7 +3366,7 @@ dependencies = [ [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-serde", ] @@ -3374,27 +3374,27 @@ dependencies = [ [[package]] name = "makepad-fast-inflate" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-gif" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "weezl", ] @@ -3402,10 +3402,10 @@ dependencies = [ [[package]] name = "makepad-half" version = "2.7.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ - "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", - "crunchy 0.2.4 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", + "crunchy 0.2.4 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "num-traits 0.2.20", "zerocopy 0.8.39", ] @@ -3413,7 +3413,7 @@ dependencies = [ [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-live-id", ] @@ -3427,7 +3427,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "ttf-parser", ] @@ -3435,7 +3435,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-live-id-macros", "serde", @@ -3444,7 +3444,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-proc-macro", ] @@ -3452,7 +3452,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-filesystem-watcher", ] @@ -3460,7 +3460,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-serde", ] @@ -3468,12 +3468,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3482,7 +3482,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-proc-macro", ] @@ -3490,7 +3490,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3504,12 +3504,12 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "ash", "bitflags 2.10.0", @@ -3535,7 +3535,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "wayland-client", "wayland-egl", "wayland-protocols", @@ -3547,12 +3547,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-error-log", "makepad-html", @@ -3561,13 +3561,13 @@ dependencies = [ "makepad-regex", "makepad-script-derive", "makepad-stitch", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-proc-macro", ] @@ -3575,7 +3575,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-network", "makepad-script", @@ -3584,17 +3584,17 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-stitch" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.10.0", "makepad-error-log", @@ -3606,7 +3606,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-html", "makepad-live-id", @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "makepad-tsdf" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-math", "makepad-micro-serde", @@ -3624,7 +3624,7 @@ dependencies = [ [[package]] name = "makepad-video" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-apple-sys", "makepad-objc-sys", @@ -3634,7 +3634,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3643,7 +3643,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-byteorder-lite", ] @@ -3651,7 +3651,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3666,7 +3666,7 @@ dependencies = [ [[package]] name = "makepad-zune-bmp" version = "0.5.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "log 0.4.28", "makepad-zune-core", @@ -3675,7 +3675,7 @@ dependencies = [ [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "log 0.4.28", ] @@ -3683,7 +3683,7 @@ dependencies = [ [[package]] name = "makepad-zune-inflate" version = "0.2.54" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "simd-adler32", ] @@ -3691,7 +3691,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.15" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-zune-core", ] @@ -3699,7 +3699,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-fast-inflate", "makepad-zune-core", @@ -3709,7 +3709,7 @@ dependencies = [ [[package]] name = "makepad-zune-qoi" version = "0.5.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-zune-core", ] @@ -4092,7 +4092,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "memoffset" @@ -4164,20 +4164,20 @@ dependencies = [ [[package]] name = "naga" version = "27.0.3" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "arrayvec 0.7.6", "bit-set", "bitflags 2.13.1", - "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", - "cfg_aliases 0.2.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", - "hashbrown 0.16.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "cfg-if 1.0.4 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", + "cfg_aliases 0.2.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", + "hashbrown 0.16.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "hexf-parse", "indexmap 2.13.0", "makepad-error-log", "makepad-half", "num-traits 0.2.20", - "once_cell 1.21.3 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "once_cell 1.21.3 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "rustc-hash 1.1.0", "spirv", "thiserror 2.0.18", @@ -4348,7 +4348,7 @@ dependencies = [ [[package]] name = "num-traits" version = "0.2.20" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "num_cpus" @@ -4512,7 +4512,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell" version = "1.21.3" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "once_cell_polyfill" @@ -4784,7 +4784,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "pkg-config" version = "0.3.32" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "polling" @@ -4932,10 +4932,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.10.0", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "unicase 2.9.0", ] @@ -5384,12 +5384,12 @@ dependencies = [ [[package]] name = "robius-common" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" [[package]] name = "robius-directories" version = "6.0.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "dirs-sys", "jni", @@ -5399,7 +5399,7 @@ dependencies = [ [[package]] name = "robius-file-picker" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "android-build", "block2", @@ -5421,7 +5421,7 @@ dependencies = [ [[package]] name = "robius-location" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "android-build", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5437,7 +5437,7 @@ dependencies = [ [[package]] name = "robius-open" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5454,7 +5454,7 @@ dependencies = [ [[package]] name = "robius-share" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "android-build", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5473,7 +5473,7 @@ dependencies = [ [[package]] name = "robius-speech" version = "0.3.1" -source = "git+https://github.com/project-robius/robius?branch=speech_to_text#166991b1b18188ee2d8441f7118721cee2a0c6eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "jni", "robius-android-env", @@ -5492,7 +5492,7 @@ dependencies = [ [[package]] name = "robius-web-auth-session" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cca3cc3dc99d8fcb6c69a127cd28d47ba8f3e3eb" +source = "git+https://github.com/project-robius/robius#71966dc2194307e27d3f9eedc9138614624b4265" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5727,7 +5727,7 @@ dependencies = [ [[package]] name = "rustc-hash" version = "1.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "rustc-hash" @@ -5852,12 +5852,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.10.0", "bytemuck 1.25.0", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5955,7 +5955,7 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scoped-tls" version = "1.0.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "scopeguard" @@ -5966,7 +5966,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "sealed" @@ -6296,7 +6296,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.9" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "siphasher" @@ -6322,7 +6322,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "socket2" @@ -6346,7 +6346,7 @@ dependencies = [ [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.13.1", ] @@ -6708,7 +6708,7 @@ dependencies = [ [[package]] name = "thiserror" version = "2.0.18" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "thiserror-impl 2.0.18", ] @@ -6736,7 +6736,7 @@ dependencies = [ [[package]] name = "thiserror-impl" version = "2.0.18" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "makepad-micro-proc-macro", ] @@ -7120,7 +7120,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "tungstenite" @@ -7183,7 +7183,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-bidi" @@ -7194,17 +7194,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-ident" @@ -7215,7 +7215,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-normalization" @@ -7235,17 +7235,17 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "unicode-segmentation" @@ -7546,11 +7546,11 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "downcast-rs", "libc", - "scoped-tls 1.0.1 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "scoped-tls 1.0.1 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-sys", ] @@ -7558,7 +7558,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.13.1", "libc", @@ -7568,7 +7568,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "wayland-backend", "wayland-sys", @@ -7577,7 +7577,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "bitflags 2.13.1", "wayland-backend", @@ -7587,10 +7587,10 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "log 0.4.29", - "pkg-config 0.3.32 (git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus)", + "pkg-config 0.3.32 (git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar)", ] [[package]] @@ -7638,7 +7638,7 @@ dependencies = [ [[package]] name = "weezl" version = "0.1.12" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "whoami" @@ -7672,7 +7672,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]] @@ -7701,7 +7701,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -7720,7 +7720,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-core 0.62.2", ] @@ -7753,7 +7753,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -7774,7 +7774,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-core 0.62.2", ] @@ -7838,7 +7838,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "windows-numerics" @@ -7882,7 +7882,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-link 0.2.1", ] @@ -7899,7 +7899,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" dependencies = [ "windows-link 0.2.1", ] @@ -8293,7 +8293,7 @@ dependencies = [ [[package]] name = "zerocopy" version = "0.8.39" -source = "git+https://github.com/kevinaboos/makepad?branch=button-grab-key-focus#47837267faf6970a6cc36acedf9f83846b277307" +source = "git+https://github.com/kevinaboos/makepad?branch=text-input-scroll-bar#a8794e8f8f6e9592db3ecf378e53b4243f623a27" [[package]] name = "zerocopy-derive" diff --git a/Cargo.toml b/Cargo.toml index de27bab8c..a7bb6ea37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" # 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 = "button-grab-key-focus", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "button-grab-key-focus" } +makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "text-input-scroll-bar", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "text-input-scroll-bar" } robius-directories = { git = "https://github.com/project-robius/robius" } @@ -26,7 +26,7 @@ robius-file-picker = { git = "https://github.com/project-robius/robius" } robius-location = { git = "https://github.com/project-robius/robius" } robius-open = { git = "https://github.com/project-robius/robius" } robius-share = { git = "https://github.com/project-robius/robius" } -robius-speech = { git = "https://github.com/project-robius/robius", branch = "speech_to_text" } +robius-speech = { git = "https://github.com/project-robius/robius" } robius-use-makepad = "0.1.1" ## auto-configures all `robius-*` crates to work with Makepad. diff --git a/src/app.rs b/src/app.rs index fb775b972..fc59f850b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,7 +17,7 @@ use crate::{ 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, clear_timeline_states, invalidate_single_timeline_state}, 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, settings::app_preferences::{AppPreferences, UiZoom}, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, context_menu::{ContextMenuClosed, menu_position_margin}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, TimelineKind, 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, settings::app_preferences::{AppPreferences, UiZoom}, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, context_menu::{ContextMenuClosed, menu_position_margin}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, speech_text_input::cancel_all_dictation}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, TimelineKind, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -273,7 +273,7 @@ impl MatchEvent for App { match action.downcast_ref() { Some(LogoutAction::LogoutSuccess) => { - robius_speech::cancel_all(); + cancel_all_dictation(); self.app_state.logged_in = false; self.ui.modal(cx, ids!(logout_confirm_modal)).close(cx); self.update_login_visibility(cx); @@ -308,6 +308,7 @@ impl MatchEvent for App { if let Some(LoginAction::LoginFailure(_)) = action.downcast_ref() { if self.app_state.logged_in { log!("Received LoginAction::LoginFailure while logged in; showing login screen."); + cancel_all_dictation(); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); @@ -859,7 +860,7 @@ impl App { crate::sliding_sync::set_sync_service_desired_running(true, "app resume"); } Event::Background => { - robius_speech::cancel_all(); + cancel_all_dictation(); if self.lifecycle.is_foreground { log!("App entered background; persisting state and stopping Matrix sync."); self.lifecycle.is_foreground = false; @@ -880,7 +881,7 @@ impl App { crate::sliding_sync::set_sync_service_desired_running(true, "app foreground"); } Event::Shutdown => { - robius_speech::cancel_all(); + cancel_all_dictation(); self.handle_shutdown(cx); } _ => {} diff --git a/src/home/editing_pane.rs b/src/home/editing_pane.rs index 192100ed3..fab29e9cb 100644 --- a/src/home/editing_pane.rs +++ b/src/home/editing_pane.rs @@ -237,6 +237,7 @@ impl Widget for EditingPane { if self.button(cx, ids!(cancel_button)).clicked(actions) || edit_text_input.escaped(actions) { + mentionable_input.cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; @@ -247,6 +248,8 @@ impl Widget for EditingPane { if self.button(cx, ids!(accept_button)).clicked(actions) || edit_text_input.returned(actions).is_some() { + // The edit is the text as shown now, so stop adding dictated words to it. + mentionable_input.cancel_dictation(cx); let edited_text = edit_text_input.text().trim().to_string(); let edited_content = match info.event_tl_item.content() { TimelineItemContent::MsgLike(msg_like_content) => { @@ -325,6 +328,7 @@ impl Widget for EditingPane { PopupKind::Error, None, ); + mentionable_input.cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; @@ -489,6 +493,8 @@ impl EditingPane { } match edit_result { Ok(()) => { + self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)) + .cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); }, Err(e) => { @@ -562,6 +568,7 @@ impl EditingPane { pub fn save_state(&self) -> Option { let info = self.info.as_ref()?; let mentionable_input = self.child_by_path(ids!(editing_content.edit_text_input)).as_mentionable_text_input(); + mentionable_input.release_microphone(); Some(EditingPaneState { event_tl_item: info.event_tl_item.clone(), mentionable_input_state: mentionable_input.save_state(), @@ -664,6 +671,8 @@ impl EditingPaneRef { /// This function *DOES NOT* emit an [`EditingPaneAction::Hidden`] action. pub fn force_reset_hide(&self, cx: &mut Cx) { let Some(mut inner) = self.borrow_mut() else { return }; + inner.mentionable_text_input(cx, ids!(editing_content.edit_text_input)) + .cancel_dictation(cx); if inner.visible { cx.revert_key_focus(); } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index e01e1923d..22790101b 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -14,8 +14,8 @@ use crate::{ app_preferences::{AppPreferencesGlobal, AppPreferencesAction, ViewModeOverride}, settings_screen::SettingsScreenWidgetRefExt, }, - shared::room_filter_input_bar::{MainFilterAction, RoomFilterInputBarWidgetExt}, shared::mention_popup::MentionablePopupRef, + shared::speech_text_input::cancel_all_dictation, utils::RoomNameId, }; @@ -263,7 +263,7 @@ script_mod! { align: Align{y: 0.5} CachedWidget { - room_filter_input_bar := RoomFilterInputBar {} + room_filter_input_bar := RoomFilterInputBar { is_main_filter: true } } // Hide this until it's implemented. @@ -482,6 +482,16 @@ pub fn effective_is_desktop(cx: &mut Cx) -> bool { .unwrap_or(true) // Before the first selection, default to desktop mode } +/// Returns the id of the page that shows the given navigation tab. +fn page_for_tab(tab: &SelectedTab) -> LiveId { + match tab { + SelectedTab::Space { .. } + | SelectedTab::Home => id!(home_page), + SelectedTab::Settings => id!(settings_page), + SelectedTab::AddRoom => id!(add_room_page), + } +} + #[derive(Script, Widget)] pub struct HomeScreen { @@ -522,14 +532,6 @@ impl ScriptHook for HomeScreen { impl Widget for HomeScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { - // On desktop, the RoomFilterInputBar is inside this HomeScreen. - // Check if it changed and re-emit as a MainFilterAction so that - // RoomsList and SpacesBar can respond without cross-talk from - // other RoomFilterInputBar instances (e.g., SpaceLobbyScreen's). - if let Some(keywords) = self.view.room_filter_input_bar(cx, ids!(room_filter_input_bar)).changed(actions) { - cx.action(MainFilterAction::Changed(keywords)); - } - let app_state = scope.data.get_mut::().unwrap(); for action in actions { match action.downcast_ref() { @@ -545,6 +547,7 @@ impl Widget for HomeScreen { // Only open the settings screen if it is not currently open. Some(NavigationBarAction::OpenSettings) => { if !matches!(app_state.selected_tab, SelectedTab::Settings) { + self.cancel_dictation_if_page_changes(cx, &app_state.selected_tab, &SelectedTab::Settings); self.previous_selection = std::mem::replace(&mut app_state.selected_tab, SelectedTab::Settings); cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { @@ -748,15 +751,16 @@ impl HomeScreen { ) -> Option { self.view .page_flip(cx, ids!(home_screen_page_flip)) - .set_active_page( - cx, - match app_state.selected_tab { - SelectedTab::Space { .. } - | SelectedTab::Home => id!(home_page), - SelectedTab::Settings => id!(settings_page), - SelectedTab::AddRoom => id!(add_room_page), - }, - ) + .set_active_page(cx, page_for_tab(&app_state.selected_tab)) + } + + /// Cancels dictation if showing `new_tab` instead of `old_tab` hides the page the user is looking at. + fn cancel_dictation_if_page_changes(&self, cx: &mut Cx, old_tab: &SelectedTab, new_tab: &SelectedTab) { + // On mobile, a pushed screen stays in front of whichever page is shown. + let is_page_in_front = self.view.stack_navigation(cx, ids!(view_stack)).current_view().is_none(); + if is_page_in_front && page_for_tab(old_tab) != page_for_tab(new_tab) { + cancel_all_dictation(); + } } /// Populates a `StackNavigationView` with the given room/screen's info. @@ -897,6 +901,8 @@ impl HomeScreen { } } app_state.selected_room = Some(sr); + // The pushed screen covers whatever the user was dictating into. + cancel_all_dictation(); stack_navigation.push(cx, view_id); self.view.redraw(cx); } @@ -904,6 +910,7 @@ impl HomeScreen { /// Switches to (selects) the given navigation tab, if it isn't already the selected one. fn switch_to_tab(&mut self, cx: &mut Cx, app_state: &mut AppState, new_tab: SelectedTab) { if app_state.selected_tab == new_tab { return } + self.cancel_dictation_if_page_changes(cx, &app_state.selected_tab, &new_tab); self.previous_selection = std::mem::replace(&mut app_state.selected_tab, new_tab); cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); self.update_active_page_from_selection(cx, app_state); @@ -952,6 +959,8 @@ impl HomeScreen { if stack_nav.is_transitioning() { return; } + // The popped screen is what the user was dictating into. + cancel_all_dictation(); let Some(current_screen) = app_state.selected_room.take() else { // If we didn't have a current screen, something's buggy, // so the safest option is to clear the mobile stack and start over. nbd. diff --git a/src/home/loading_pane.rs b/src/home/loading_pane.rs index 699471c29..21a4cabda 100644 --- a/src/home/loading_pane.rs +++ b/src/home/loading_pane.rs @@ -1,6 +1,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::{EventId, OwnedEventId}; +use crate::shared::speech_text_input::escape_stopped_dictation; use crate::sliding_sync::TimelineRequestSender; @@ -172,7 +173,7 @@ impl Widget for LoadingPane { ) || event.back_pressed() || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::KeyUp(key) => key.key_code == KeyCode::Escape && !escape_stopped_dictation(), Hit::FingerDown(_fde) => { cx.set_key_focus(area); false diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 25cc82f77..2ab3a6469 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::{HashMap, HashSet}, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, utils::RoomNameId}; +use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, shared::speech_text_input::cancel_all_dictation, utils::RoomNameId}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::{AcceptedInviteKind, RoomsListAction}, spaces_bar::SpacesBarAction}; script_mod! { @@ -166,6 +166,13 @@ impl MainDesktopUI { } } + /// Cancels dictation unless `room` is already shown, since showing it hides the current tab. + fn cancel_dictation_unless_shown(&self, room: &SelectedRoom) { + if self.most_recently_selected_room.as_ref() != Some(room) { + cancel_all_dictation(); + } + } + /// Focuses on a room if it is already open, otherwise creates a new tab for the room. fn focus_or_create_tab(&mut self, cx: &mut Cx, room: SelectedRoom) { // Do nothing if the room to select is already created and focused. @@ -546,7 +553,9 @@ impl WidgetMatchEvent for MainDesktopUI { self.switch_dock_to_space(cx, app_state, None); } cx.action(NavigationBarAction::GoToHome); - self.focus_or_create_tab(cx, SelectedRoom::InvitedRoom { room_name_id: space_name_id }); + let invite = SelectedRoom::InvitedRoom { room_name_id: space_name_id }; + self.cancel_dictation_unless_shown(&invite); + self.focus_or_create_tab(cx, invite); continue; } @@ -572,6 +581,12 @@ impl WidgetMatchEvent for MainDesktopUI { match widget_action.cast() { // Whenever a tab (except for the home_tab) is pressed, notify the app state. DockAction::TabWasPressed(tab_id) => { + // Switching tabs hides whatever the user was dictating into. + let current_tab_id = self.most_recently_selected_room.as_ref() + .map_or(id!(home_tab), SelectedRoom::tab_id); + if tab_id != current_tab_id { + cancel_all_dictation(); + } if tab_id == id!(home_tab) { self.select_room(cx, None); } @@ -622,6 +637,7 @@ impl WidgetMatchEvent for MainDesktopUI { // Handle RoomsList actions, which are updates from the rooms list. match widget_action.cast_ref() { RoomsListAction::Selected(selected_room) => { + self.cancel_dictation_unless_shown(selected_room); // Note that this cannot be performed within draw_walk() as the draw flow prevents from // performing actions that would trigger a redraw, and the Dock internally performs (and expects) // a redraw to be happening in order to draw the tab content. diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index c514ed9e8..5b434fd37 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -6,7 +6,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::{OwnedEventId, events::room::message::MessageType}; use matrix_sdk_ui::timeline::{EventSendState, EventTimelineItem, MsgLikeContent, MsgLikeKind, TimelineEventItemId}; -use crate::{home::send_status_indicator::is_send_error_retryable, shared::context_menu::{BUTTON_HEIGHT, ContextMenuClosed, expected_menu_size}, sliding_sync::UserPowerLevels}; +use crate::{home::send_status_indicator::is_send_error_retryable, shared::{context_menu::{BUTTON_HEIGHT, ContextMenuClosed, expected_menu_size}, speech_text_input::escape_stopped_dictation}, sliding_sync::UserPowerLevels}; use super::room_screen::MessageAction; @@ -311,7 +311,8 @@ impl Widget for NewMessageContextMenu { let close_menu = { event.back_pressed() || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + // An `Escape` that stopped dictation shouldn't also close this menu. + Hit::KeyUp(key) => key.key_code == KeyCode::Escape && !escape_stopped_dictation(), Hit::FingerDown(fde) => { let reaction_text_input = self.view.text_input(cx, ids!(reaction_input_view.reaction_text_input)); if reaction_text_input.area().rect(cx).contains(fde.abs) { @@ -355,7 +356,7 @@ impl WidgetMatchEvent for NewMessageContextMenu { ); close_menu = true; } - else if reaction_text_input.escaped(actions) { + else if reaction_text_input.escaped(actions) && !escape_stopped_dictation() { close_menu = true; } else if self.button(cx, ids!(react_button)).clicked(actions) { diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index a03425c50..158551fb0 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,7 +3,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{home::invite_modal::InviteModalAction, settings::app_preferences::preferred_receipt_type, shared::{context_menu::{ContextMenuClosed, expected_menu_size}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; +use crate::{home::invite_modal::InviteModalAction, settings::app_preferences::preferred_receipt_type, shared::{context_menu::{ContextMenuClosed, expected_menu_size}, popup_list::{PopupKind, enqueue_popup_notification}, speech_text_input::escape_stopped_dictation}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; /// Nothing here is conditionally shown, so keep these matching the DSL below. const NUM_BUTTONS: usize = 9; @@ -134,7 +134,7 @@ impl Widget for RoomContextMenu { let close_menu = { event.back_pressed() || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::KeyUp(key) => key.key_code == KeyCode::Escape && !escape_stopped_dictation(), Hit::FingerUp(fue) if fue.is_over => { !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) } diff --git a/src/home/rooms_sidebar.rs b/src/home/rooms_sidebar.rs index bd15f8e93..81d790123 100644 --- a/src/home/rooms_sidebar.rs +++ b/src/home/rooms_sidebar.rs @@ -11,7 +11,6 @@ use makepad_widgets::*; use crate::home::rooms_list::RoomsListWidgetExt; use crate::settings::app_preferences::{AppPreferencesGlobal, AppPreferencesAction, ViewModeOverride}; -use crate::shared::room_filter_input_bar::{MainFilterAction, RoomFilterInputBarWidgetExt}; script_mod! { use mod.prelude.widgets.* @@ -109,7 +108,7 @@ script_mod! { align: Align{y: 0.5} CachedWidget { - room_filter_input_bar := RoomFilterInputBar {} + room_filter_input_bar := RoomFilterInputBar { is_main_filter: true } } // Hide this until it's implemented. @@ -169,13 +168,7 @@ impl RoomsSideBar { impl Widget for RoomsSideBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - // If the main room filter input bar changed keywords, re-emit that action - // as a MainFilterAction so that other widgets can handle it. if let Event::Actions(actions) = event { - if let Some(keywords) = self.view.room_filter_input_bar(cx, ids!(room_filter_input_bar)).changed(actions) { - cx.action(MainFilterAction::Changed(keywords)); - } - for action in actions { if let Some(AppPreferencesAction::ViewModeChanged(new_mode)) = action.downcast_ref() { if *new_mode != self.applied_view_mode { diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index d6cdb044e..74a48b81c 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -552,7 +552,7 @@ script_mod! { // Filter input bar for searching rooms/spaces in this space filter_bar := mod.widgets.RoomFilterInputBar { input +: { - empty_text: "Filter this space..." + text_input +: { empty_text: "Filter this space..." } } } } @@ -1891,8 +1891,7 @@ impl SpaceLobbyScreen { // Clear the filter bar when switching to a new space. self.filter_keywords.clear(); - self.view.text_input(cx, ids!(filter_bar.input)).set_text(cx, ""); - self.view.button(cx, ids!(filter_bar.clear_button)).set_visible(cx, false); + self.view.room_filter_input_bar(cx, ids!(filter_bar)).clear(cx); // Restore UI state if we've viewed this space before, otherwise start fresh self.expanded_spaces = SPACE_LOBBY_STATES.with_borrow(|states| { @@ -1920,8 +1919,7 @@ impl SpaceLobbyScreen { self.loading_subspaces.clear(); self.is_loading = false; self.filter_keywords.clear(); - self.view.text_input(cx, ids!(filter_bar.input)).set_text(cx, ""); - self.view.button(cx, ids!(filter_bar.clear_button)).set_visible(cx, false); + self.view.room_filter_input_bar(cx, ids!(filter_bar)).clear(cx); self.set_space_topic(cx, None); self.redraw(cx); } diff --git a/src/profile/user_profile.rs b/src/profile/user_profile.rs index 02f24bb6f..e86a1bf8b 100644 --- a/src/profile/user_profile.rs +++ b/src/profile/user_profile.rs @@ -4,7 +4,7 @@ use std::{borrow::Cow, ops::{Deref, DerefMut}}; use makepad_widgets::*; use matrix_sdk::{room::{RoomMember, RoomMemberRole}, ruma::{events::room::member::MembershipState, OwnedRoomId, OwnedUserId}}; use crate::{ - avatar_cache, block_user_modal::{BlockUserModalAction, BlockUserRequest}, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, current_user_id, is_user_blocked, submit_async_request}, utils + avatar_cache, block_user_modal::{BlockUserModalAction, BlockUserRequest}, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}, speech_text_input::escape_stopped_dictation}, sliding_sync::{MatrixRequest, current_user_id, is_user_blocked, submit_async_request}, utils }; use super::user_profile_cache; @@ -410,7 +410,7 @@ impl Widget for UserProfileSlidingPane { ) || event.back_pressed() || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::KeyUp(key) => key.key_code == KeyCode::Escape && !escape_stopped_dictation(), Hit::FingerDown(_fde) => { cx.set_key_focus(area); false diff --git a/src/room/mod.rs b/src/room/mod.rs index 50ea65cff..70f94d723 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -10,7 +10,6 @@ use crate::utils::RoomNameId; pub mod reply_preview; pub mod room_input_bar; -mod speech_input; pub mod room_display_filter; pub mod typing_notice; diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index ad7fa95fa..5e24ef4cf 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -19,9 +19,7 @@ use std::sync::Arc; -use makepad_widgets::*; -use robius_speech::{NativeSpeechEvent, NativeSpeechSession, Replacement, SpeechErrorKind}; -use super::speech_input::{SpeechInput, SpeechPhase}; +use makepad_widgets::{text::selection::Cursor, *}; use matrix_sdk::room::RoomMember; use matrix_sdk::room::reply::{EnforceThread, Reply}; use ruma::events::room::message::AddMentions; @@ -112,80 +110,14 @@ script_mod! { mentionable_text_input := MentionableTextInput { width: Fill, - flow: Overlay, margin: Inset { top: 3, // add some space between the top border of the text input and the top border of the room input bar bottom: 5.75, // to line up the middle of the text input with the middle of the buttons left: 3, right: 3 // to give a bit of breathing room between the text input and the buttons on the sides }, - text_input := RobrixTextInput { - empty_text: "Write a message (in Markdown) ..." - is_multiline: true, - // Reserve the microphone's gutter so it never overlaps the draft. - padding: Inset{top: 10, bottom: 10, left: 10, right: 48} - } - - speech_overlay := View { - width: Fill, height: Fill - align: Align{x: 1.0, y: 1.0} - // No vertical padding: the button is nearly as tall as a - // single-line input, so any would push it past the bottom edge. - padding: Inset{top: 0, bottom: 0, left: 4, right: 4} - - // One widget for both states: the icon at rest, a meter while recording. - speech_button := RobrixIconButton { - width: 32, height: 32 - padding: 7 - spacing: 0 - grab_key_focus: false - enable_long_press: false - icon_walk: Walk{width: 18, height: 18} - draw_icon +: { - svg: crate_resource("self://resources/icons/microphone.svg") - color: #333 - } - draw_bg +: { - color: #0000 - color_hover: #xE0E8F0 - color_down: #xD0D8E8 - // Set from Rust: 1.0 while recording, plus the three - // most recent microphone levels, each normalized 0..=1. - recording: instance(0.0) - level_0: instance(0.0) - level_1: instance(0.0) - level_2: instance(0.0) - bar_color: instance(vec4(1.0, 1.0, 1.0, 1.0)) - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size) - // Same state blend the stock button face uses. - let face = mix( - mix(self.color, self.color_hover, self.hover), - self.color_down, - self.down - ) - sdf.box(0.5, 0.5, self.rect_size.x - 1.0, self.rect_size.y - 1.0, self.border_radius) - sdf.fill(face) - if self.recording > 0.5 { - // Three bars, centred in the button by construction. - let bar = 3.0 - let gap = 3.0 - let x = (self.rect_size.x - (bar * 3.0 + gap * 2.0)) * 0.5 - let middle = self.rect_size.y * 0.5 - let h0 = 4.0 + self.level_0 * 13.0 - let h1 = 4.0 + self.level_1 * 13.0 - let h2 = 4.0 + self.level_2 * 13.0 - sdf.box(x, middle - h0 * 0.5, bar, h0, 1.5) - sdf.fill(self.bar_color) - sdf.box(x + bar + gap, middle - h1 * 0.5, bar, h1, 1.5) - sdf.fill(self.bar_color) - sdf.box(x + (bar + gap) * 2.0, middle - h2 * 0.5, bar, h2, 1.5) - sdf.fill(self.bar_color) - } - return sdf.result - } - } - } + speech_text_input +: { + text_input +: { empty_text: "Write a message (in Markdown) ..." } } } @@ -266,26 +198,6 @@ pub struct RoomInputBar { #[rust] timeline_kind: Option, /// The widget UID of the RoomScreen containing this RoomInputBar. #[rust] room_screen_widget_uid: Option, - /// The currently-running speech-to-text dictation session. - #[rust] speech: Option, - /// What the microphone button is currently displaying. - #[rust] speech_controls: SpeechControls, -} - -/// The microphone buttons current display state. -#[derive(Default, PartialEq)] -pub(super) struct SpeechControls { - /// Whether this platform has STT recognition at all. - /// * `Some(true)` if speech input is supported and available. - /// * `Some(false)` if speech input is not supported or available. - /// * `None` if it's unknown. - pub supported: Option, - /// The phase being shown, or `None` when no session is running - pub phase: Option, - /// The three most recent microphone level samples, oldest first. - /// These determine the height/amplitude of the three bars that are drawn - /// in place of the microphone icon as a little sound waveform. - pub levels: Option<[f32; 3]>, } impl ScriptHook for RoomInputBar { @@ -295,27 +207,12 @@ impl ScriptHook for RoomInputBar { self.mentionable_text_input(cx, ids!(mentionable_text_input)) .text_input_ref() .set_submit_on_enter(send_on_enter); - self.initialize_speech_controls(cx); }); } } impl Widget for RoomInputBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - // Pressing `Escape` will stop speech recording/recognition regardless of key focus. - let should_end_speech = matches!(event, Event::KeyDown(KeyEvent { key_code: KeyCode::Escape, .. })) - && matches!(self.speech_phase(), Some(SpeechPhase::Listening) | Some(SpeechPhase::Starting)); - if should_end_speech { - match self.speech.as_mut() { - Some(speech) if speech.phase == SpeechPhase::Listening => speech.stop(), - _ => self.cancel_dictation(cx), - } - self.update_speech_controls(cx); - return; - } - - self.handle_speech_event(cx, event); - match event.hits(cx, self.view.widget(cx, ids!(replying_preview.reply_preview_content)).area()) { // If the hit occurred on the replying message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { @@ -344,30 +241,6 @@ impl Widget for RoomInputBar { } self.view.handle_event(cx, event, scope); - - // Now that the text in the input bar is up to date (after handling this event), - // we can add any dictated words to it. - // But we still wait if an IME is composing, or the user is dragging a selection - // since we don't want those two things to conflict. - if self.speech.is_some() { - let input = self.mentionable_text_input(cx, ids!(mentionable_text_input)).text_input_ref(); - if input.is_composing() || cx.fingers.is_area_captured(input.area()) { - if let Some(speech) = self.speech.as_mut() { - speech.dictation.interrupt(); - } - } else { - let selection = input.selection(); - let replacement = self.speech.as_mut().and_then(|speech| { - speech.dictation.settle(&input.text(), selection.start().index..selection.end().index) - }); - if let Some(replacement) = replacement { - self.apply_replacement(cx, replacement); - } - if self.speech.as_ref().is_some_and(|speech| speech.ended) { - self.cancel_dictation(cx); - } - } - } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { @@ -415,167 +288,12 @@ impl Widget for RoomInputBar { } impl RoomInputBar { - fn initialize_speech_controls(&mut self, cx: &mut Cx) { - static IS_SPEECH_SUPPORTED: std::sync::OnceLock = std::sync::OnceLock::new(); - let is_speech_supported = *IS_SPEECH_SUPPORTED.get_or_init(|| { - let supported = NativeSpeechSession::is_supported(); - log!("Native speech-to-text input is {} (engine: {}).", - if supported { "available" } else { "unavailable" }, - robius_speech::engine_name(), - ); - supported - }); - self.apply_speech_support(cx, is_speech_supported); - } - - /// Puts a dictated replacement text snippet into the text input. - /// - /// This goes into the message box as an ordinary series of undo-able text edits. - fn apply_replacement(&mut self, cx: &mut Cx, replacement: Replacement) { - let mentionable = self.mentionable_text_input(cx, ids!(mentionable_text_input)); - let undo = if replacement.continues { text_input::UndoGroup::Extend } else { text_input::UndoGroup::New }; - match mentionable.text_input_ref().replace_range(cx, replacement.range, &replacement.text, undo) { - Ok(()) => { - if let Some(speech) = self.speech.as_mut() { - speech.dictation.applied(); - } - } - // If the text replacement was refused due to a live IME composition or the input filter, - // nothing gets lost, so no worries. - // The next transcript will still include the same words that weren't applied. - Err(error) => log!("Speech input could not update the draft: {error:?}"), - } - self.redraw(cx); - } - - /// Show or hide the microphone button for speech-to-text recognition. - fn apply_speech_support(&mut self, cx: &mut Cx, is_speech_supported: bool) { - self.speech_controls.supported = Some(is_speech_supported); - self.view(cx, ids!(speech_overlay)).set_visible(cx, is_speech_supported); - if !is_speech_supported { - let mut input = self.mentionable_text_input(cx, ids!(mentionable_text_input)).text_input_ref(); - script_apply_eval!(cx, input, {padding: 10}); - } - } - - /// Stop and discard any pending speech-to-text callbacks (with recognition text results). + /// Stops any speech-to-text dictation in either the message input or the editing pane. /// - /// Any already-displayed text will stay in the text input. + /// Any words already in a text input will stay there, they won't be cleared. fn cancel_dictation(&mut self, cx: &mut Cx) { - self.speech = None; - self.update_speech_controls(cx); - } - - /// Returns the phase of the current speech recognition session, if one is running. - fn speech_phase(&self) -> Option { - self.speech.as_ref().filter(|speech| !speech.ended).map(|speech| speech.phase) - } - - fn update_speech_controls(&mut self, cx: &mut Cx) { - let new_phase = self.speech_phase(); - let levels = self.speech.as_ref().map(|speech| speech.levels); - // Starting or ending a session completely re-styles the whole button; - // a phase change only affects the tooltip, so only that needs redrawing. - let is_active = new_phase.is_some(); - if is_active != self.speech_controls.phase.is_some() { - let bg = if is_active { vec4(0.08, 0.08, 0.08, 1.0) } else { vec4(0.0, 0.0, 0.0, 0.0) }; - let hover = if is_active { vec4(0.25, 0.25, 0.25, 1.0) } else { vec4(0.88, 0.91, 0.94, 1.0) }; - // The soundwave animation replaces the icon rather than sitting beside it, - // so we just hide the microphone icon while recording. - let icon = if is_active { vec4(0.0, 0.0, 0.0, 0.0) } else { vec4(0.2, 0.2, 0.2, 1.0) }; - let recording = if is_active { 1.0 } else { 0.0 }; - let mut button = self.button(cx, ids!(speech_button)); - script_apply_eval!(cx, button, { - draw_icon +: {color: #(icon)} - draw_bg +: { - color: #(bg), color_hover: #(hover), color_down: #(hover), - recording: #(recording) - } - }); - self.redraw(cx); - } - if new_phase != self.speech_controls.phase { - self.speech_controls.phase = new_phase; - self.redraw(cx); - } - if levels != self.speech_controls.levels { - self.speech_controls.levels = levels; - if let Some(levels) = levels { - let mut button = self.button(cx, ids!(speech_button)); - script_apply_eval!(cx, button, { - draw_bg +: {level_0: #(levels[0]), level_1: #(levels[1]), level_2: #(levels[2])} - }); - } - } - } - - fn handle_speech_event(&mut self, cx: &mut Cx, event: &Event) { - // Leaving the room or the app ends dictation. Typing in the message box does not. - let switched_tab = if let Event::Actions(actions) = event { - actions.iter().any(|action| matches!(action.as_widget_action().cast(), DockAction::TabWasPressed(_))) - } else { false }; - if self.speech.is_some() { - if switched_tab || matches!(event, Event::Background | Event::Shutdown) { - self.cancel_dictation(cx); - } else if let Some(speech) = self.speech.as_mut() { - // This event may be about to change the message text, and we read new - // speech results below, before it gets there. So hold the words back. - speech.dictation.interrupt(); - } - } - let events = self.speech.as_mut().map(|speech| speech.poll()).unwrap_or_default(); - for event in events { - match event { - NativeSpeechEvent::Transcript { text, is_final } => { - // Returns nothing while held: the keystroke hasn't reached the text - // input yet, so writing now would drag the caret out from under it. - let Some(speech) = self.speech.as_mut() else { break }; - if let Some(replacement) = speech.dictation.transcript(&text, is_final) { - self.apply_replacement(cx, replacement); - } - } - // The session is dropped once this event has been dispatched, so - // a final utterance that arrived alongside its end still lands. - NativeSpeechEvent::Stopped => { - if let Some(speech) = self.speech.as_mut() { speech.ended = true; } - break; - } - NativeSpeechEvent::Error(error) => { - if let Some(speech) = self.speech.as_mut() { speech.ended = true; } - // Only hide the microphone once the recognizer is really gone: - // Unavailable can also mean something retryable, like lost network. - if error.kind() == SpeechErrorKind::Unavailable - && !NativeSpeechSession::is_supported() - { - self.apply_speech_support(cx, false); - } - enqueue_popup_notification(format!("Speech input: {error}"), PopupKind::Error, Some(8.0)); - break; - } - _ => {} - } - } - self.update_speech_controls(cx); - - let area = self.button(cx, ids!(speech_button)).area(); - match event.hits(cx, area) { - Hit::FingerHoverIn(_) => { - cx.widget_action(self.widget_uid(), TooltipAction::HoverIn { - // Name the shortcut only while it does something, and name what - // it does: while finishing there is nothing left for it to stop. - text: match self.speech_phase() { - Some(SpeechPhase::Listening) => "Stop speech input (Esc)", - Some(SpeechPhase::Starting) => "Cancel speech input (Esc)", - Some(SpeechPhase::Finishing) => "Finishing transcription…", - None => "Dictate a message", - }.into(), - widget_rect: area.rect(cx), - options: CalloutTooltipOptions { position: TooltipPosition::Top, ..Default::default() }, - }); - } - Hit::FingerHoverOut(_) => cx.widget_action(self.widget_uid(), TooltipAction::HoverOut), - _ => {} - } + self.mentionable_text_input(cx, ids!(mentionable_text_input)).cancel_dictation(cx); + self.mentionable_text_input(cx, ids!(editing_pane.editing_content.edit_text_input)).cancel_dictation(cx); } fn handle_actions( @@ -586,26 +304,6 @@ impl RoomInputBar { let mentionable_text_input = self.mentionable_text_input(cx, ids!(mentionable_text_input)); let text_input = mentionable_text_input.text_input_ref(); - // Handle a click on the microphone button, which starts or stops speech-to-text dictation. - if self.button(cx, ids!(speech_button)).clicked(actions) - && self.view(cx, ids!(input_bar)).visible() - && !self.editing_pane(cx, ids!(editing_pane)).is_currently_shown(cx) - && self.timeline_kind.is_some() - { - match self.speech_phase() { - Some(SpeechPhase::Listening) => self.speech.as_mut().unwrap().stop(), - Some(_) => self.cancel_dictation(cx), - None => { - text_input.set_key_focus(cx); - match SpeechInput::start(&text_input) { - Ok(speech) => self.speech = Some(speech), - Err(error) => enqueue_popup_notification(format!("Speech input: {error}"), PopupKind::Error, Some(8.0)), - } - } - } - self.update_speech_controls(cx); - } - for action in actions { // Handle changes to the `send_on_enter` preference. if let Some(AppPreferencesAction::SendOnEnterChanged(v)) = action.downcast_ref() { @@ -947,12 +645,21 @@ impl RoomInputBar { }; let mentionable_text_input = self.mentionable_text_input(cx, ids!(mentionable_text_input)); let existing = mentionable_text_input.text(); - let new_text = if existing.trim().is_empty() { + let was_draft_blank = existing.trim().is_empty(); + let new_text = if was_draft_blank { restored } else { format!("{existing}\n{restored}") }; mentionable_text_input.set_text(cx, &new_text); + // A blank draft leaves the caret at the start, so move it after the restored message. + if was_draft_blank { + mentionable_text_input.text_input_ref().set_cursor( + cx, + Cursor { index: new_text.len(), prefer_next_row: false }, + false, + ); + } self.enable_send_message_button(cx, true); } } @@ -1319,9 +1026,10 @@ impl RoomInputBarRef { mentionable_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input)).as_mentionable_text_input().save_state(), upload: inner.child_by_path(ids!(upload_progress_view)).as_upload_progress_view().save_state(), }; - if let Some(speech) = &inner.speech { - speech.cancel(); - } + + inner.child_by_path(ids!(input_bar.mentionable_text_input)).as_mentionable_text_input().release_microphone(); + // Note: the editing pane's `save_state()` call above already released its microphone. + // Clear the location preview. We don't save this state because the // current location might change by the next time the user opens this same room. inner.child_by_path(ids!(location_preview)).as_location_preview().clear(); diff --git a/src/settings/privacy_settings.rs b/src/settings/privacy_settings.rs index 9b5dc90cb..af4a9cba1 100644 --- a/src/settings/privacy_settings.rs +++ b/src/settings/privacy_settings.rs @@ -71,7 +71,8 @@ script_mod! { width: Fill, height: Fit Label { width: Fill, height: Fit - margin: Inset{top: 10, bottom: 8, left: 13, right: 10}, + padding: 0 + margin: Inset{top: 10, bottom: 0, left: 13, right: 10}, flow: Flow.Right{wrap: true}, draw_text +: { color: (COLOR_TEXT_WARNING_NOT_FOUND), diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index cf071fe3a..75423b238 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,7 +1,7 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab, get_own_profile}, profile::user_profile::UserProfile, settings::{PopulateMode, account_settings::AccountSettingsWidgetExt, app_settings::AppSettingsWidgetExt, privacy_settings::PrivacySettingsWidgetExt}}; +use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab, get_own_profile}, profile::user_profile::UserProfile, shared::speech_text_input::escape_stopped_dictation, settings::{PopulateMode, account_settings::AccountSettingsWidgetExt, app_settings::AppSettingsWidgetExt, privacy_settings::PrivacySettingsWidgetExt}}; script_mod! { use mod.prelude.widgets.* @@ -127,7 +127,7 @@ impl Widget for SettingsScreen { && event.back_pressed() ) || match event.hits(cx, area) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::KeyUp(key) => key.key_code == KeyCode::Escape && !escape_stopped_dictation(), Hit::FingerDown(_fde) => { cx.set_key_focus(area); false diff --git a/src/shared/image_viewer.rs b/src/shared/image_viewer.rs index 2509e1ac1..84e2e9c24 100644 --- a/src/shared/image_viewer.rs +++ b/src/shared/image_viewer.rs @@ -17,7 +17,7 @@ use crate::home::room_image_viewer::ImageViewerFetchAction; use crate::utils::format_decimal_file_size; use thiserror::Error; use crate::{ - shared::{attachment_download::{DownloadableAttachment, save_loaded_attachment, share_loaded_attachment, start_attachment_download, start_attachment_share}, avatar::AvatarWidgetExt, timestamp::TimestampWidgetRefExt}, + shared::{attachment_download::{DownloadableAttachment, save_loaded_attachment, share_loaded_attachment, start_attachment_download, start_attachment_share}, avatar::AvatarWidgetExt, speech_text_input::escape_stops_dictation, timestamp::TimestampWidgetRefExt}, sliding_sync::TimelineKind, }; @@ -641,9 +641,12 @@ impl Widget for ImageViewer { self.advance_rotation(cx, ne.time); } - if event.back_pressed() - || matches!(event, Event::KeyDown(KeyEvent { key_code: KeyCode::Escape, .. })) - { + // An `Escape` that stopped speech dictation shouldn't also close the viewer. + let escape_closes = match event { + Event::KeyDown(key) => key.key_code == KeyCode::Escape && !escape_stops_dictation(key), + _ => false, + }; + if event.back_pressed() || escape_closes { self.reset(cx); cx.action(ImageViewerAction::Hide); } diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 76eaba3f1..e5eeeb9e4 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -1,4 +1,5 @@ -//! Wrapper around a `TextInput` that shows an auto-complete popup upon trigger characters. +//! Wrapper around a `TextInput` that shows an auto-complete popup upon trigger characters, +//! with a microphone button for speech-to-text dictation (see [`SpeechTextInput`](crate::shared::speech_text_input::SpeechTextInput)). //! //! Currently we use it for: //! 1. Showing members in a room (upon pressing '@') @@ -17,7 +18,7 @@ use matrix_sdk::{ }; use crate::{ home::rooms_list::RoomsListRef, - shared::{mention_popup::{MentionItem, MentionablePopupRef}, slash_commands::{self, SlashCommandOutcome}}, + shared::{mention_popup::{MentionItem, MentionablePopupRef}, slash_commands::{self, SlashCommandOutcome}, speech_text_input::{SpeechTextInputRef, SpeechTextInputWidgetRefExt, escape_stops_dictation}}, sliding_sync::{submit_async_request, MatrixRequest}, utils::{self, MatchQuality}, }; @@ -32,8 +33,9 @@ script_mod! { flow: Down allow_slash_commands: true - text_input := RobrixTextInput { - is_multiline: true, + speech_text_input := SpeechTextInput { + mic_tooltip: "Dictate a message" + text_input +: { is_multiline: true } } } } @@ -104,7 +106,8 @@ impl Widget for MentionableTextInput { return; } } - KeyCode::Escape => { + // Let an `Escape` that stops dictation do only that. + KeyCode::Escape if !escape_stops_dictation(ke) => { self.close_popup(cx); return; } @@ -168,7 +171,7 @@ impl Widget for MentionableTextInput { } fn set_text(&mut self, cx: &mut Cx, text: &str) { - self.text_input_ref().set_text(cx, text); + self.speech_text_input_ref().set_text(cx, text); if text.trim().is_empty() { self.possible_mentions = Mentions::new(); } @@ -185,6 +188,10 @@ impl MentionableTextInput { self.child_by_path(ids!(text_input)).as_text_input() } + fn speech_text_input_ref(&self) -> SpeechTextInputRef { + self.child_by_path(ids!(speech_text_input)).as_speech_text_input() + } + fn popup_ref(&self, cx: &mut Cx) -> MentionablePopupRef { if cx.has_global::() { cx.get_global::().clone() @@ -340,6 +347,23 @@ impl MentionableTextInputRef { .unwrap_or_default() } + /// Returns the inner text input's wrapper, which handles speech-to-text dictation. + pub fn speech_text_input_ref(&self) -> SpeechTextInputRef { + self.borrow() + .map(|inner| inner.speech_text_input_ref()) + .unwrap_or_default() + } + + /// Stops any speech-to-text dictation into this input; words already in the text stay there. + pub fn cancel_dictation(&self, cx: &mut Cx) { + self.speech_text_input_ref().cancel_dictation(cx); + } + + /// See [`SpeechTextInputRef::release_microphone()`]. + pub fn release_microphone(&self) { + self.speech_text_input_ref().release_microphone(); + } + /// Updates whether the user can `@room`. Refreshes an open `@` popup so the /// "Notify the entire room" entry appears or disappears accordingly. pub fn set_can_notify_room(&self, cx: &mut Cx, can_notify: bool) { @@ -401,6 +425,7 @@ impl MentionableTextInputRef { pub fn restore_state(&self, cx: &mut Cx, state: MentionableTextInputState) { let Some(mut inner) = self.borrow_mut() else { return }; + inner.speech_text_input_ref().cancel_dictation(cx); inner.text_input_ref().restore_state(cx, state.text_input_state); inner.possible_mentions = state.possible_mentions; } diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 897857363..54e753e37 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -20,6 +20,8 @@ pub mod progress_bar; pub mod room_filter_input_bar; pub mod room_input_popup_menu; pub mod slash_commands; +mod speech_input; +pub mod speech_text_input; pub mod styles; pub mod text_or_image; pub mod timestamp; @@ -35,6 +37,7 @@ pub fn script_mod(vm: &mut ScriptVm) { styles::script_mod(vm); helpers::script_mod(vm); icon_button::script_mod(vm); + speech_text_input::script_mod(vm); context_menu::script_mod(vm); navigation_bar_button::script_mod(vm); expand_arrow::script_mod(vm); diff --git a/src/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index 6464745fe..3f5092933 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -1,7 +1,8 @@ //! A text input used to filter a list of rooms/spaces -//! with a search icon and a button to clear the input. +//! with a search icon, a microphone button for dictation, and a button to clear the input. use makepad_widgets::*; +use crate::shared::speech_text_input::SpeechTextInputWidgetExt; script_mod! { use mod.prelude.widgets.* @@ -35,18 +36,33 @@ script_mod! { icon_walk: Walk{width: 14, height: 14} } - input := RobrixTextInput { - width: Fill, - height: Fit, - flow: Flow.Right { wrap: false }, - padding: 5 - - empty_text: "Filter rooms & spaces..." - autocapitalize: None, - - draw_bg.border_size: 0.0 - draw_text +: { - text_style: theme.font_regular { font_size: 10 }, + input := SpeechTextInput { + text_padding: 5 + mic_gutter: 32 + mic_tooltip: "Filter by voice" + drop_trailing_punctuation: true + + text_input +: { + flow: Flow.Right { wrap: false }, + empty_text: "Filter rooms & spaces..." + autocapitalize: None, + + draw_bg.border_size: 0.0 + draw_text +: { + text_style: theme.font_regular { font_size: 10 }, + } + } + + // A smaller microphone button, centred in the single-line bar. + speech_overlay +: { + align: Align{x: 1.0, y: 0.5} + padding: Inset{top: 0, bottom: 0, left: 2, right: 2} + speech_button +: { + margin: 0 + width: 26, height: 26 + padding: 5 + icon_walk: Walk{width: 16, height: 16} + } } } @@ -68,6 +84,10 @@ script_mod! { #[derive(Script, Widget)] pub struct RoomFilterInputBar { #[deref] view: View, + + /// Whether this is the app's main filter bar that applies to rooms and spaces. + /// All other filter bars should set this to false. + #[live] is_main_filter: bool, } impl ScriptHook for RoomFilterInputBar { @@ -85,7 +105,7 @@ impl ScriptHook for RoomFilterInputBar { return; } let cx = _vm.cx_mut(); - let has_text = !self.text_input(cx, ids!(input)).text().is_empty(); + let has_text = !self.text_input(cx, ids!(input.text_input)).text().is_empty(); self.button(cx, ids!(clear_button)).set_visible(cx, has_text); } } @@ -148,11 +168,18 @@ impl RoomFilterInputBarRef { pub fn changed(&self, actions: &Actions) -> Option { self.borrow().and_then(|inner| inner.changed(actions)) } + + /// Clears the filter text (ending any dictation into it) without emitting a `Changed` action. + pub fn clear(&self, cx: &mut Cx) { + let Some(inner) = self.borrow() else { return }; + inner.speech_text_input(cx, ids!(input)).set_text(cx, ""); + inner.button(cx, ids!(clear_button)).set_visible(cx, false); + } } impl WidgetMatchEvent for RoomFilterInputBar { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let input = self.text_input(cx, ids!(input)); + let input = self.text_input(cx, ids!(input.text_input)); let clear_button = self.button(cx, ids!(clear_button)); // Handle user changing the input text @@ -166,18 +193,24 @@ impl WidgetMatchEvent for RoomFilterInputBar { }; clear_button.set_visible(cx, !keywords.is_empty()); clear_button.reset_hover(cx); + if self.is_main_filter { + cx.action(MainFilterAction::Changed(keywords.clone())); + } cx.widget_action( - self.widget_uid(), + self.widget_uid(), FilterAction::Changed(keywords) ); } if clear_button.clicked(actions) { - input.set_text(cx, ""); + self.speech_text_input(cx, ids!(input)).set_text(cx, ""); clear_button.set_visible(cx, false); input.set_key_focus(cx); + if self.is_main_filter { + cx.action(MainFilterAction::Changed(String::new())); + } cx.widget_action( - self.widget_uid(), + self.widget_uid(), FilterAction::Changed(String::new()) ); } diff --git a/src/shared/room_input_popup_menu.rs b/src/shared/room_input_popup_menu.rs index f0c5d9bb0..bb599b80f 100644 --- a/src/shared/room_input_popup_menu.rs +++ b/src/shared/room_input_popup_menu.rs @@ -4,6 +4,7 @@ use makepad_widgets::*; use makepad_widgets::makepad_platform::event::finger::TouchState; +use crate::shared::speech_text_input::escape_stopped_dictation; script_mod! { use mod.prelude.widgets.* @@ -112,7 +113,9 @@ impl Widget for RoomInputPopupMenu { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if !self.visible { return; } - if matches!(event, Event::KeyUp(KeyEvent {key_code: KeyCode::Escape, .. })) + // An `Escape` that stopped dictation shouldn't also close this menu. + if (matches!(event, Event::KeyUp(KeyEvent {key_code: KeyCode::Escape, .. })) + && !escape_stopped_dictation()) || event.back_pressed() { self.close(cx); diff --git a/src/room/speech_input.rs b/src/shared/speech_input.rs similarity index 95% rename from src/room/speech_input.rs rename to src/shared/speech_input.rs index 8c7ed2a9b..7b63150f1 100644 --- a/src/room/speech_input.rs +++ b/src/shared/speech_input.rs @@ -1,4 +1,4 @@ -//! A native speech-to-text dictation session, scoped to a single room. +//! A native speech-to-text dictation session, scoped to a single text input. use std::cell::{Cell, RefCell}; use std::sync::{Arc, atomic::{AtomicU32, Ordering}, mpsc::{self, Receiver}}; @@ -36,10 +36,10 @@ pub(super) struct SpeechInput { latest_level: Arc, pub phase: SpeechPhase, phase_since: Instant, - /// Tracks where dictated words go in the message text, and which ones are already there. + /// Tracks where dictated words go in the text input, and which ones are already there. pub dictation: Dictation, /// True once the recognizer has stopped. We keep the session around until its - /// last words have been added to the message text, then drop it. + /// last words have been added to the text input, then drop it. pub ended: bool, pub levels: [f32; 3], } @@ -139,7 +139,7 @@ impl SpeechInput { if events.is_empty() && timeout.is_some_and(|timeout| self.phase_since.elapsed() > timeout) { return vec![NativeSpeechEvent::Error(SpeechError::new( SpeechErrorKind::Other, - "Speech input timed out. Your transcribed draft has been kept; please try again.", + "Speech input timed out. Any words already transcribed were kept; please try again.", ))]; } events diff --git a/src/shared/speech_text_input.rs b/src/shared/speech_text_input.rs new file mode 100644 index 000000000..23b2640bb --- /dev/null +++ b/src/shared/speech_text_input.rs @@ -0,0 +1,511 @@ +//! A text input with a microphone button inside it, for native speech-to-text dictation. +//! +//! Dictated words go into the text as ordinary undo-able edits, and the user can keep +//! typing, moving the caret, or composing with an IME while dictation is running. + +use std::sync::{OnceLock, atomic::{AtomicBool, AtomicU64, Ordering}}; +use makepad_widgets::{thread::SignalToUI, *}; +use robius_speech::{NativeSpeechEvent, NativeSpeechSession, Replacement, SpeechErrorKind}; +use crate::shared::popup_list::{PopupKind, enqueue_popup_notification}; +use super::speech_input::{SpeechInput, SpeechPhase}; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + // Style the inner input via `text_input +: {..}`, but set its padding with `text_padding`; + // while the microphone is shown, `mic_gutter` replaces its right padding to make room for it. + mod.widgets.SpeechTextInput = #(SpeechTextInput::register_widget(vm)) { + width: Fill, height: Fit + flow: Overlay + text_padding: 10 + mic_gutter: 40 + mic_clearance: 36 + scroll_bar_inset: Inset{top: 3, right: 4, bottom: 3} + mic_tooltip: "Dictate" + + text_input := RobrixTextInput {} + + speech_overlay := View { + width: Fill, height: Fill + align: Align{x: 1.0, y: 1.0} + // No vertical padding: the button is nearly as tall as a + // single-line input, so any would push it past the bottom edge. + padding: Inset{top: 0, bottom: 0, left: 4, right: 3} + + // One widget for both states: the icon at rest, a meter while recording. + speech_button := RobrixIconButton { + width: 32, height: 32 + padding: 7 + spacing: 0 + grab_key_focus: false + enable_long_press: false + icon_walk: Walk{width: 18, height: 18} + draw_icon +: { + svg: crate_resource("self://resources/icons/microphone.svg") + color: #555 + } + draw_bg +: { + color: #0000 + color_hover: #xE0E8F0 + color_down: #xD0D8E8 + // Set from Rust: 1.0 while recording, plus the three + // most recent microphone levels, each normalized 0..=1. + recording: instance(0.0) + level_0: instance(0.0) + level_1: instance(0.0) + level_2: instance(0.0) + bar_color: instance(vec4(1.0, 1.0, 1.0, 1.0)) + blend: fn(under: vec4, over: vec4) -> vec4 { + let alpha = over.w + under.w * (1.0 - over.w) + let color = over.xyz * over.w + under.xyz * under.w * (1.0 - over.w) + return vec4(color / max(alpha, 0.0001), alpha) + } + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + let hovered = vec4(self.color_hover.xyz, self.color_hover.w * self.hover) + let pressed = vec4(self.color_down.xyz, self.color_down.w * self.down) + let face = self.blend(self.blend(self.color, hovered), pressed) + sdf.box(0.5, 0.5, self.rect_size.x - 1.0, self.rect_size.y - 1.0, self.border_radius) + sdf.fill(face) + if self.recording > 0.5 { + // Three bars, centred in the button and scaled to its size. + let unit = self.rect_size.y / 32.0 + let bar = 3.0 * unit + let gap = 3.0 * unit + let x = (self.rect_size.x - (bar * 3.0 + gap * 2.0)) * 0.5 + let middle = self.rect_size.y * 0.5 + let h0 = (4.0 + self.level_0 * 13.0) * unit + let h1 = (4.0 + self.level_1 * 13.0) * unit + let h2 = (4.0 + self.level_2 * 13.0) * unit + sdf.box(x, middle - h0 * 0.5, bar, h0, 1.5 * unit) + sdf.fill(self.bar_color) + sdf.box(x + bar + gap, middle - h1 * 0.5, bar, h1, 1.5 * unit) + sdf.fill(self.bar_color) + sdf.box(x + (bar + gap) * 2.0, middle - h2 * 0.5, bar, h2, 1.5 * unit) + sdf.fill(self.bar_color) + } + return sdf.result + } + } + } + } + } +} + +// Only one session can run at a time, so these track that session across all `SpeechTextInput`s. +/// The widget UID of the `SpeechTextInput` whose session is starting or listening, or 0 if none. +static LISTENER: AtomicU64 = AtomicU64::new(0); +/// Whether a stopped session is still transcribing its last words. +static IS_FINISHING: AtomicBool = AtomicBool::new(false); +/// The time (as `f64` bits) of the `Escape` key press that last stopped dictation. +static STOPPING_ESCAPE: AtomicU64 = AtomicU64::new(u64::MAX); + +/// Cancels all dictation, e.g., when navigation hides the text input being dictated into. +pub fn cancel_all_dictation() { + robius_speech::cancel_all(); + // Wake every `SpeechTextInput` up so that it notices its session has ended. + SignalToUI::set_ui_signal(); +} + +/// Whether the given `Escape` key press stops dictation, in which case nothing else should act on it. +/// +/// This is for key *down* handlers; anything acting on the key's release, or on a text input's +/// `Escaped` action, should use [`escape_stopped_dictation()`] instead. +pub fn escape_stops_dictation(key: &KeyEvent) -> bool { + LISTENER.load(Ordering::Relaxed) != 0 || STOPPING_ESCAPE.load(Ordering::Relaxed) == key.time.to_bits() +} + +/// Whether the `Escape` key press being delivered now is the one that stopped dictation, +/// so that its release, and the `Escaped` action it produced, should be ignored too. +pub fn escape_stopped_dictation() -> bool { + STOPPING_ESCAPE.load(Ordering::Relaxed) != u64::MAX +} + +/// Whether this platform has native speech recognition, checked once per app run. +fn is_speech_supported() -> bool { + static IS_SPEECH_SUPPORTED: OnceLock = OnceLock::new(); + *IS_SPEECH_SUPPORTED.get_or_init(|| { + let supported = NativeSpeechSession::is_supported(); + log!("Native speech-to-text input is {} (engine: {}).", + if supported { "available" } else { "unavailable" }, + robius_speech::engine_name(), + ); + supported + }) +} + +/// A text input with a microphone button for speech-to-text dictation. +/// +/// Its inner `text_input` is an ordinary `TextInput`; check its actions via [`SpeechTextInputRef::text_input_ref()`]. +#[derive(Script, Widget)] +pub struct SpeechTextInput { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// The inner text input's padding, not counting the microphone's gutter. + #[live] text_padding: Inset, + /// The inner text input's right padding while the microphone button is shown. + #[live] mic_gutter: f64, + /// How far above the bottom edge the text input's scroll bar ends while the microphone is shown. + #[live] mic_clearance: f64, + /// Space between the text input's scroll bar and its borders; the microphone deepens the bottom. + #[live] scroll_bar_inset: Inset, + /// The microphone button's tooltip while no session is running. + #[live] mic_tooltip: String, + /// Whether to drop the sentence punctuation that recognizers end each transcript with, + /// e.g., for search filters that match names. + #[live] drop_trailing_punctuation: bool, + + /// The currently-running speech-to-text dictation session. + #[rust] speech: Option, + /// The phase shown by the microphone button, or `None` when no session is running. + #[rust] shown_phase: Option, + /// The three most recent microphone levels shown in the button, oldest first. + #[rust] shown_levels: Option<[f32; 3]>, + /// Whether the recognizer turned out to be unavailable after all. + #[rust] is_mic_hidden: bool, + /// Whether this widget's session set [`IS_FINISHING`]. + #[rust] holds_finishing: bool, +} + +impl ScriptHook for SpeechTextInput { + fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue) { + if apply.is_eval() || apply.is_animate() { return; } + let show_mic = is_speech_supported() && !self.is_mic_hidden; + vm.with_cx_mut(|cx| { + self.show_mic(cx, show_mic); + // Re-applying the DSL resets the button to its idle look, so restyle it for any running session. + self.shown_phase = None; + self.shown_levels = None; + self.update_speech_controls(cx); + }); + } +} + +impl Drop for SpeechTextInput { + fn drop(&mut self) { + self.clear_finishing(); + let _ = LISTENER.compare_exchange(self.widget_uid().0, 0, Ordering::Relaxed, Ordering::Relaxed); + } +} + +impl Widget for SpeechTextInput { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + // Pressing `Escape` will stop speech recording/recognition regardless of key focus, + // and the key press is kept from every text input, not just this one. + if let Event::KeyDown(key) = event && key.key_code == KeyCode::Escape { + if escape_stops_dictation(key) { + STOPPING_ESCAPE.store(key.time.to_bits(), Ordering::Relaxed); + match self.phase() { + Some(SpeechPhase::Listening) => self.stop(), + Some(SpeechPhase::Starting) => self.cancel(cx), + _ => {} + } + self.update_speech_controls(cx); + return; + } + // This press isn't ours, so whatever acts on its release shouldn't ignore it. + STOPPING_ESCAPE.store(u64::MAX, Ordering::Relaxed); + } + + self.handle_speech_event(cx, event); + self.view.handle_event(cx, event, scope); + + if let Event::Actions(actions) = event + && self.speech_button().clicked(actions) + { + self.toggle(cx); + } + + // Now that the text in the input is up to date (after handling this event), + // we can add any dictated words to it. + // But we still wait if an IME is composing, or the user is dragging a selection + // since we don't want those two things to conflict. + let Some(speech) = self.speech.as_mut() else { return }; + let input = self.view.child_by_path(ids!(text_input)).as_text_input(); + if input.is_composing() || cx.fingers.is_area_captured(input.area()) { + speech.dictation.interrupt(); + return; + } + let selection = input.selection(); + if let Some(replacement) = speech.dictation.settle(&input.text(), selection.start().index..selection.end().index) { + self.apply_replacement(cx, replacement); + } + if self.speech.as_ref().is_some_and(|speech| speech.ended) { + self.cancel(cx); + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } + + fn text(&self) -> String { + self.text_input_ref().text() + } + + /// Replaces the text, which ends any dictation into it. + fn set_text(&mut self, cx: &mut Cx, text: &str) { + self.cancel(cx); + self.text_input_ref().set_text(cx, text); + } + + fn set_key_focus(&self, cx: &mut Cx) { + self.text_input_ref().set_key_focus(cx); + } + + fn key_focus(&self, cx: &Cx) -> bool { + self.text_input_ref().key_focus(cx) + } +} + +impl SpeechTextInput { + fn text_input_ref(&self) -> TextInputRef { + self.view.child_by_path(ids!(text_input)).as_text_input() + } + + fn speech_button(&self) -> ButtonRef { + self.view.child_by_path(ids!(speech_button)).as_button() + } + + /// Returns the phase of the current speech recognition session, if one is running. + fn phase(&self) -> Option { + self.speech.as_ref().filter(|speech| !speech.ended).map(|speech| speech.phase) + } + + /// Starts, stops, or cancels dictation, as a click on the microphone button does. + fn toggle(&mut self, cx: &mut Cx) { + match self.phase() { + Some(SpeechPhase::Listening) => self.stop(), + Some(SpeechPhase::Starting) => self.cancel(cx), + // Its last words are still on their way, so don't throw them away. + Some(SpeechPhase::Finishing) => {} + None => { + let input = self.text_input_ref(); + input.set_key_focus(cx); + let started = match SpeechInput::start(&input) { + // Only one session can run at a time, so the newest microphone press wins, + // unless the other session has stopped recording and is just finishing up. + Err(error) if error.kind() == SpeechErrorKind::Busy && !IS_FINISHING.load(Ordering::Relaxed) => { + cancel_all_dictation(); + SpeechInput::start(&input) + } + started => started, + }; + match started { + Ok(speech) => self.speech = Some(speech), + Err(error) => enqueue_popup_notification(format!("Speech input: {error}"), PopupKind::Error, Some(8.0)), + } + } + } + self.update_speech_controls(cx); + } + + /// Stops recording, but keeps the session until its last words have been transcribed. + fn stop(&mut self) { + if let Some(speech) = self.speech.as_mut() { + speech.stop(); + IS_FINISHING.store(true, Ordering::Relaxed); + self.holds_finishing = true; + } + } + + /// Stop and discard any pending speech-to-text callbacks (with recognition text results). + /// + /// Any already-displayed text will stay in the text input. + fn cancel(&mut self, cx: &mut Cx) { + self.speech = None; + self.clear_finishing(); + self.update_speech_controls(cx); + } + + /// Marks the session as ended, committing any words held back while the user was editing. + fn end_session(&mut self, cx: &mut Cx) { + let replacement = self.speech.as_mut().and_then(|speech| { + speech.ended = true; + speech.dictation.transcript("", true) + }); + if let Some(replacement) = replacement { + self.apply_replacement(cx, replacement); + } + self.clear_finishing(); + } + + fn clear_finishing(&mut self) { + if std::mem::take(&mut self.holds_finishing) { + IS_FINISHING.store(false, Ordering::Relaxed); + } + } + + /// Puts a dictated replacement text snippet into the text input. + /// + /// This goes into the text input as an ordinary series of undo-able text edits. + fn apply_replacement(&mut self, cx: &mut Cx, replacement: Replacement) { + let undo = if replacement.continues { text_input::UndoGroup::Extend } else { text_input::UndoGroup::New }; + match self.text_input_ref().replace_range(cx, replacement.range, &replacement.text, undo) { + Ok(()) => { + if let Some(speech) = self.speech.as_mut() { + speech.dictation.applied(); + } + } + // If the text replacement was refused due to a live IME composition or the input filter, + // nothing gets lost, so no worries. + // The next transcript will still include the same words that weren't applied. + Err(error) => log!("Speech input could not update the text: {error:?}"), + } + self.redraw(cx); + } + + /// Shows or hides the microphone button, and the gutter it needs. + fn show_mic(&mut self, cx: &mut Cx, show: bool) { + self.view.view(cx, ids!(speech_overlay)).set_visible(cx, show); + let right = if show { self.mic_gutter } else { self.text_padding.right }; + let padding = Inset { right, ..self.text_padding }; + let mut scroll_bar_inset = self.scroll_bar_inset; + if show { + scroll_bar_inset.bottom = self.mic_clearance; + } + let mut input = self.text_input_ref(); + script_apply_eval!(cx, input, {padding: #(padding), scroll_bar_inset: #(scroll_bar_inset)}); + } + + fn update_speech_controls(&mut self, cx: &mut Cx) { + let new_phase = self.phase(); + let uid = self.widget_uid().0; + if matches!(new_phase, Some(SpeechPhase::Starting | SpeechPhase::Listening)) { + LISTENER.store(uid, Ordering::Relaxed); + } else { + let _ = LISTENER.compare_exchange(uid, 0, Ordering::Relaxed, Ordering::Relaxed); + } + let levels = self.speech.as_ref().map(|speech| speech.levels); + // Starting or ending a session completely re-styles the whole button; + // a phase change only affects the tooltip, so only that needs redrawing. + let is_active = new_phase.is_some(); + if is_active != self.shown_phase.is_some() { + let bg = if is_active { vec4(0.08, 0.08, 0.08, 1.0) } else { vec4(0.0, 0.0, 0.0, 0.0) }; + let hover = if is_active { vec4(0.25, 0.25, 0.25, 1.0) } else { vec4(0.88, 0.91, 0.94, 1.0) }; + let down = if is_active { hover } else { vec4(0.82, 0.85, 0.91, 1.0) }; + // The soundwave animation replaces the icon rather than sitting beside it, + // so we just hide the microphone icon while recording. + let icon = if is_active { vec4(0.0, 0.0, 0.0, 0.0) } else { vec4(0.2, 0.2, 0.2, 1.0) }; + let recording = if is_active { 1.0 } else { 0.0 }; + let mut button = self.speech_button(); + script_apply_eval!(cx, button, { + draw_icon +: {color: #(icon)} + draw_bg +: { + color: #(bg), color_hover: #(hover), color_down: #(down), + recording: #(recording) + } + }); + self.redraw(cx); + } + if new_phase != self.shown_phase { + self.shown_phase = new_phase; + self.redraw(cx); + } + if levels != self.shown_levels { + self.shown_levels = levels; + if let Some(levels) = levels { + let mut button = self.speech_button(); + script_apply_eval!(cx, button, { + draw_bg +: {level_0: #(levels[0]), level_1: #(levels[1]), level_2: #(levels[2])} + }); + } + } + } + + fn handle_speech_event(&mut self, cx: &mut Cx, event: &Event) { + // Leaving the app ends dictation. Typing in the text input does not. + if self.speech.is_some() && matches!(event, Event::Background | Event::Shutdown) { + self.cancel(cx); + } else if let Some(speech) = self.speech.as_mut() { + // This event may be about to change the text, and we read new + // speech results below, before it gets there. So hold the words back. + speech.dictation.interrupt(); + } + let events = self.speech.as_mut().map(|speech| speech.poll()).unwrap_or_default(); + for event in events { + match event { + NativeSpeechEvent::Transcript { text, is_final } => { + let text = if self.drop_trailing_punctuation { + text.trim_end().trim_end_matches(['.', '!', '?', '。', '!', '?']) + } else { + &text + }; + // Returns nothing while held: the keystroke hasn't reached the text + // input yet, so writing now would drag the caret out from under it. + let Some(speech) = self.speech.as_mut() else { break }; + if let Some(replacement) = speech.dictation.transcript(text, is_final) { + self.apply_replacement(cx, replacement); + } + } + // The session is dropped once this event has been dispatched, so + // a final utterance that arrived alongside its end still lands. + NativeSpeechEvent::Stopped => { + self.end_session(cx); + break; + } + NativeSpeechEvent::Error(error) => { + self.end_session(cx); + // Only hide the microphone once the recognizer is really gone: + // Unavailable can also mean something retryable, like lost network. + if error.kind() == SpeechErrorKind::Unavailable + && !NativeSpeechSession::is_supported() + { + self.is_mic_hidden = true; + self.show_mic(cx, false); + } + enqueue_popup_notification(format!("Speech input: {error}"), PopupKind::Error, Some(8.0)); + break; + } + _ => {} + } + } + self.update_speech_controls(cx); + + let button = self.speech_button(); + let area = button.area(); + match event.hits(cx, area) { + Hit::FingerHoverIn(_) => { + cx.widget_action(button.widget_uid(), TooltipAction::HoverIn { + // Name the shortcut only while it does something, and name what + // it does: while finishing there is nothing left for it to stop. + text: match self.phase() { + Some(SpeechPhase::Listening) => "Stop speech input (Esc)".into(), + Some(SpeechPhase::Starting) => "Cancel speech input (Esc)".into(), + Some(SpeechPhase::Finishing) => "Finishing transcription…".into(), + None => self.mic_tooltip.clone(), + }, + widget_rect: area.rect(cx), + options: CalloutTooltipOptions { position: TooltipPosition::Top, ..Default::default() }, + }); + } + Hit::FingerHoverOut(_) => cx.widget_action(button.widget_uid(), TooltipAction::HoverOut), + _ => {} + } + } +} + +impl SpeechTextInputRef { + /// Returns the inner text input, e.g., to check for its actions like `changed()` or `returned()`. + pub fn text_input_ref(&self) -> TextInputRef { + self.child_by_path(ids!(text_input)).as_text_input() + } + + /// Stops dictation, discarding words not yet transcribed; words already in the text input stay. + pub fn cancel_dictation(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.cancel(cx); + } + } + + /// Like [`Self::cancel_dictation()`] for callers without a `Cx`: + /// releases the microphone now, and cleans up upon the next event. + pub fn release_microphone(&self) { + if let Some(speech) = self.borrow().as_ref().and_then(|inner| inner.speech.as_ref()) { + speech.cancel(); + } + } + +}