From cf6fab584ded7fbe0bf19153a00f258309aab04e Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 09:47:13 -0700 Subject: [PATCH 1/7] Update to upstream `robius-speech` dependency now that its PR has merged --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b59176fd3..a4bac5b84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" [[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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6680,7 +6680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.3", "once_cell 1.21.3 (registry+https://github.com/rust-lang/crates.io-index)", "rustix", "windows-sys 0.61.1", diff --git a/Cargo.toml b/Cargo.toml index de27bab8c..1a4d59417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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. From 4cbaee6070aaa78e2f1d48aafb9d2875ba49519c Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 10:26:41 -0700 Subject: [PATCH 2/7] Stop speech input when a logout occurs This can happen if your token gets revoked, etc. --- src/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.rs b/src/app.rs index fb775b972..02188a003 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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."); + robius_speech::cancel_all(); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); From a092b0522634a164401cb9a248cdd5a649e45878 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 11:09:52 -0700 Subject: [PATCH 3/7] Update robius-speech for our new dictation char-boundary fix See project-robius/robius#26 --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4bac5b84..faf1b7234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3142,7 +3142,7 @@ 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]] @@ -5384,12 +5384,12 @@ dependencies = [ [[package]] name = "robius-common" version = "0.3.1" -source = "git+https://github.com/project-robius/robius#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" [[package]] name = "robius-directories" version = "6.0.1" -source = "git+https://github.com/project-robius/robius#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" 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#cf5f5dc24eb3607b26f816da0c14b1d2e7ff039f" +source = "git+https://github.com/project-robius/robius#b3014108d8a0c1d6cbc38af452fe1cc332aea889" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6680,7 +6680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.3", "once_cell 1.21.3 (registry+https://github.com/rust-lang/crates.io-index)", "rustix", "windows-sys 0.61.1", @@ -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]] From 58d5cef98f3a036e78b9e88ac803231235f21707 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 13:38:07 -0700 Subject: [PATCH 4/7] Add SpeechTextInput, a text input wrapper with a mic button It handles all the dictation stuff now, which is necessary because we're supporting speech recognition in multiple text inputs across Robrix. The `MentionableTextInput` widget now uses it, which means it's visible and supported in the regular room input bar and the editing pane. Refactoring it into one widget makes it easier to fix key problems: * If you navigated away from a room, dictation kept recording and inserting text into a now-hidden text input, which is dumb. Now any kind of navigation away from the text input will cancel dictation. But not lost focus, so you can still dictate into one room while typing into a different room, side-by-side. * We modified `robius-speech` to enforce the "one session at a time" rule, which makes everything easier to deal with at the app-level. * Pressing escape to stop dictation only stops dictation, it's not treated as a regular escape press by other widgets like modals. --- src/home/editing_pane.rs | 11 + src/home/home_screen.rs | 36 +- src/home/main_desktop_ui.rs | 20 +- src/room/mod.rs | 1 - src/room/room_input_bar.rs | 332 +------------------ src/shared/mentionable_text_input.rs | 27 +- src/shared/mod.rs | 3 + src/{room => shared}/speech_input.rs | 8 +- src/shared/speech_text_input.rs | 477 +++++++++++++++++++++++++++ 9 files changed, 578 insertions(+), 337 deletions(-) rename src/{room => shared}/speech_input.rs (95%) create mode 100644 src/shared/speech_text_input.rs diff --git a/src/home/editing_pane.rs b/src/home/editing_pane.rs index 192100ed3..907d6c641 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.speech_text_input_ref().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.speech_text_input_ref().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.speech_text_input_ref().cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; @@ -489,6 +493,9 @@ impl EditingPane { } match edit_result { Ok(()) => { + self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)) + .speech_text_input_ref() + .cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); }, Err(e) => { @@ -562,6 +569,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.speech_text_input_ref().release_microphone(); Some(EditingPaneState { event_tl_item: info.event_tl_item.clone(), mentionable_input_state: mentionable_input.save_state(), @@ -664,6 +672,9 @@ 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)) + .speech_text_input_ref() + .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..17820f7fb 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -16,6 +16,7 @@ use crate::{ }, shared::room_filter_input_bar::{MainFilterAction, RoomFilterInputBarWidgetExt}, shared::mention_popup::MentionablePopupRef, + shared::speech_text_input::cancel_all_dictation, utils::RoomNameId, }; @@ -482,6 +483,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 { @@ -545,6 +556,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 +760,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 +910,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 +919,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 +968,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/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/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..3c4e195f2 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -19,15 +19,13 @@ 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; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedEventId, OwnedRoomId, OwnedTransactionId}; -use crate::{block_user_modal::{BlockUserModalAction, BlockUserRequest}, home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, populate_preview_of_timeline_item}, rooms_list::RoomsListRef, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::{UploadProgressViewWidgetRefExt, UploadState}}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, location::init_location_subscriber, profile::user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, room::BasicRoomDetails, settings::app_preferences::{AppPreferencesAction, AppPreferencesGlobal}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, file_upload_modal::{AttachmentUpload, FileUploadAttemptId, PendingUpload, handle_picked_file, handle_picker_launch_errors}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::{MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt, MentionableTextInputState}, popup_list::{PopupKind, enqueue_popup_notification}, room_input_popup_menu::RoomInputPopupMenuAction, slash_commands::{SlashCommandAction, SlashCommandOutcome}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; +use crate::{block_user_modal::{BlockUserModalAction, BlockUserRequest}, home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, populate_preview_of_timeline_item}, rooms_list::RoomsListRef, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::{UploadProgressViewWidgetRefExt, UploadState}}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, location::init_location_subscriber, profile::user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, room::BasicRoomDetails, settings::app_preferences::{AppPreferencesAction, AppPreferencesGlobal}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, file_upload_modal::{AttachmentUpload, FileUploadAttemptId, PendingUpload, handle_picked_file, handle_picker_launch_errors}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::{MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt, MentionableTextInputState}, popup_list::{PopupKind, enqueue_popup_notification}, room_input_popup_menu::RoomInputPopupMenuAction, slash_commands::{SlashCommandAction, SlashCommandOutcome}, speech_text_input::{SpeechTextInputWidgetExt, SpeechTextInputWidgetRefExt}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; use crate::room::reply_preview::CollapsiblePreviewWidgetRefExt; script_mod! { @@ -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,9 @@ 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). - /// - /// Any already-displayed text will stay in the text input. + /// Stops any speech-to-text dictation; words already in the message stay there. 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.speech_text_input(cx, ids!(mentionable_text_input.speech_text_input)).cancel_dictation(cx); } fn handle_actions( @@ -586,26 +301,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 +642,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 +1023,7 @@ 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!(mentionable_text_input.speech_text_input)).as_speech_text_input().release_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/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 76eaba3f1..e00f4f343 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,13 @@ 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() + } + /// 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 +415,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/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..9e2f11e42 --- /dev/null +++ b/src/shared/speech_text_input.rs @@ -0,0 +1,477 @@ +//! 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`, which this widget widens on the right to make room for the microphone. + mod.widgets.SpeechTextInput = #(SpeechTextInput::register_widget(vm)) { + width: Fill, height: Fit + flow: Overlay + text_padding: 10 + mic_gutter: 38 + 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: 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 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. +pub fn escape_stops_dictation(key: &KeyEvent) -> bool { + LISTENER.load(Ordering::Relaxed) != 0 || STOPPING_ESCAPE.load(Ordering::Relaxed) == key.time.to_bits() +} + +/// 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 width reserved on the right of the text for the microphone button. + #[live] mic_gutter: f64, + /// 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 + && 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; + } + + 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(_) => self.cancel(cx), + 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); + } + + 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 gutter = if show { self.mic_gutter } else { 0.0 }; + let padding = Inset { right: self.text_padding.right + gutter, ..self.text_padding }; + let mut input = self.text_input_ref(); + script_apply_eval!(cx, input, {padding: #(padding)}); + } + + 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 => { + if let Some(speech) = self.speech.as_mut() { speech.ended = true; } + self.clear_finishing(); + break; + } + NativeSpeechEvent::Error(error) => { + if let Some(speech) = self.speech.as_mut() { speech.ended = true; } + self.clear_finishing(); + // 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(); + } + } + +} From 75203f321edc5d9aa04e38fe7625badb83e19ca1 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 15:12:34 -0700 Subject: [PATCH 5/7] Add dictation support to the rooms list and Space lobby filter bars The `RoomFilterInputBar` widget now uses `SpeechTextInput`. We drop punctuation and other irrelevant characters that are often added by speech recognizers, e.g., "Robrix." will become "Robrix" so it matches any room containing "Robrix". Clearing the filter input bar's inputted text also stops dictation. --- src/home/space_lobby.rs | 8 ++--- src/shared/room_filter_input_bar.rs | 54 ++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 21 deletions(-) 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/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index 6464745fe..5c05d93e6 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,32 @@ 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: 28 + 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 +: { + width: 24, height: 24 + padding: 5 + icon_walk: Walk{width: 14, height: 14} + } } } @@ -85,7 +100,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 +163,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 @@ -173,7 +195,7 @@ impl WidgetMatchEvent for RoomFilterInputBar { } 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); cx.widget_action( From 9250a0caa253954700aa75cfaa4ce7b7f5763d46 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 17:21:56 -0700 Subject: [PATCH 6/7] Make sure dictation actions are handled by every SpeechTextInput Pressing escape stops dictation but is also handled by other widgets like modals, so we don't want it to be doubly handled. UI-wise, fix up the microphone button's spacing and padding and ensure that it's vertically centered and nicely sized in the text input --- Cargo.lock | 256 +++++++++++++-------------- Cargo.toml | 4 +- src/app.rs | 10 +- src/home/editing_pane.rs | 10 +- src/home/loading_pane.rs | 3 +- src/home/new_message_context_menu.rs | 7 +- src/home/room_context_menu.rs | 4 +- src/profile/user_profile.rs | 4 +- src/room/room_input_bar.rs | 14 +- src/settings/privacy_settings.rs | 3 +- src/settings/settings_screen.rs | 4 +- src/shared/image_viewer.rs | 11 +- src/shared/mentionable_text_input.rs | 10 ++ src/shared/room_filter_input_bar.rs | 7 +- src/shared/room_input_popup_menu.rs | 5 +- src/shared/speech_text_input.rs | 96 ++++++---- 16 files changed, 253 insertions(+), 195 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index faf1b7234..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]] @@ -3148,9 +3148,9 @@ dependencies = [ [[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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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#b3014108d8a0c1d6cbc38af452fe1cc332aea889" +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" @@ -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 1a4d59417..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" } diff --git a/src/app.rs b/src/app.rs index 02188a003..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,7 +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."); - robius_speech::cancel_all(); + cancel_all_dictation(); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); @@ -860,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; @@ -881,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 907d6c641..fab29e9cb 100644 --- a/src/home/editing_pane.rs +++ b/src/home/editing_pane.rs @@ -237,7 +237,7 @@ impl Widget for EditingPane { if self.button(cx, ids!(cancel_button)).clicked(actions) || edit_text_input.escaped(actions) { - mentionable_input.speech_text_input_ref().cancel_dictation(cx); + mentionable_input.cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; @@ -249,7 +249,7 @@ impl Widget for EditingPane { || edit_text_input.returned(actions).is_some() { // The edit is the text as shown now, so stop adding dictated words to it. - mentionable_input.speech_text_input_ref().cancel_dictation(cx); + 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) => { @@ -328,7 +328,7 @@ impl Widget for EditingPane { PopupKind::Error, None, ); - mentionable_input.speech_text_input_ref().cancel_dictation(cx); + mentionable_input.cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; @@ -494,7 +494,6 @@ impl EditingPane { match edit_result { Ok(()) => { self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)) - .speech_text_input_ref() .cancel_dictation(cx); self.animator_play(cx, ids!(panel.hide)); }, @@ -569,7 +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.speech_text_input_ref().release_microphone(); + mentionable_input.release_microphone(); Some(EditingPaneState { event_tl_item: info.event_tl_item.clone(), mentionable_input_state: mentionable_input.save_state(), @@ -673,7 +672,6 @@ impl EditingPaneRef { 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)) - .speech_text_input_ref() .cancel_dictation(cx); if inner.visible { cx.revert_key_focus(); 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/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/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/room_input_bar.rs b/src/room/room_input_bar.rs index 3c4e195f2..5e24ef4cf 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -25,7 +25,7 @@ use matrix_sdk::room::reply::{EnforceThread, Reply}; use ruma::events::room::message::AddMentions; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedEventId, OwnedRoomId, OwnedTransactionId}; -use crate::{block_user_modal::{BlockUserModalAction, BlockUserRequest}, home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, populate_preview_of_timeline_item}, rooms_list::RoomsListRef, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::{UploadProgressViewWidgetRefExt, UploadState}}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, location::init_location_subscriber, profile::user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, room::BasicRoomDetails, settings::app_preferences::{AppPreferencesAction, AppPreferencesGlobal}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, file_upload_modal::{AttachmentUpload, FileUploadAttemptId, PendingUpload, handle_picked_file, handle_picker_launch_errors}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::{MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt, MentionableTextInputState}, popup_list::{PopupKind, enqueue_popup_notification}, room_input_popup_menu::RoomInputPopupMenuAction, slash_commands::{SlashCommandAction, SlashCommandOutcome}, speech_text_input::{SpeechTextInputWidgetExt, SpeechTextInputWidgetRefExt}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; +use crate::{block_user_modal::{BlockUserModalAction, BlockUserRequest}, home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, populate_preview_of_timeline_item}, rooms_list::RoomsListRef, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::{UploadProgressViewWidgetRefExt, UploadState}}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, location::init_location_subscriber, profile::user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, room::BasicRoomDetails, settings::app_preferences::{AppPreferencesAction, AppPreferencesGlobal}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, file_upload_modal::{AttachmentUpload, FileUploadAttemptId, PendingUpload, handle_picked_file, handle_picker_launch_errors}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::{MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt, MentionableTextInputState}, popup_list::{PopupKind, enqueue_popup_notification}, room_input_popup_menu::RoomInputPopupMenuAction, slash_commands::{SlashCommandAction, SlashCommandOutcome}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; use crate::room::reply_preview::CollapsiblePreviewWidgetRefExt; script_mod! { @@ -288,9 +288,12 @@ impl Widget for RoomInputBar { } impl RoomInputBar { - /// Stops any speech-to-text dictation; words already in the message stay there. + /// Stops any speech-to-text dictation in either the message input or the editing pane. + /// + /// 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_text_input(cx, ids!(mentionable_text_input.speech_text_input)).cancel_dictation(cx); + 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( @@ -1023,7 +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(), }; - inner.child_by_path(ids!(mentionable_text_input.speech_text_input)).as_speech_text_input().release_microphone(); + + 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 e00f4f343..e5eeeb9e4 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -354,6 +354,16 @@ impl MentionableTextInputRef { .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) { diff --git a/src/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index 5c05d93e6..7f2d3eaf9 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -38,7 +38,7 @@ script_mod! { input := SpeechTextInput { text_padding: 5 - mic_gutter: 28 + mic_gutter: 32 mic_tooltip: "Filter by voice" drop_trailing_punctuation: true @@ -58,9 +58,10 @@ script_mod! { align: Align{x: 1.0, y: 0.5} padding: Inset{top: 0, bottom: 0, left: 2, right: 2} speech_button +: { - width: 24, height: 24 + margin: 0 + width: 26, height: 26 padding: 5 - icon_walk: Walk{width: 14, height: 14} + icon_walk: Walk{width: 16, height: 16} } } } 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/shared/speech_text_input.rs b/src/shared/speech_text_input.rs index 9e2f11e42..23b2640bb 100644 --- a/src/shared/speech_text_input.rs +++ b/src/shared/speech_text_input.rs @@ -13,13 +13,15 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* - // Style the inner input via `text_input +: {..}`, but set its padding with - // `text_padding`, which this widget widens on the right to make room for the microphone. + // 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: 38 + mic_gutter: 40 + mic_clearance: 36 + scroll_bar_inset: Inset{top: 3, right: 4, bottom: 3} mic_tooltip: "Dictate" text_input := RobrixTextInput {} @@ -29,7 +31,7 @@ script_mod! { 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} + 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 { @@ -41,7 +43,7 @@ script_mod! { icon_walk: Walk{width: 18, height: 18} draw_icon +: { svg: crate_resource("self://resources/icons/microphone.svg") - color: #333 + color: #555 } draw_bg +: { color: #0000 @@ -54,14 +56,16 @@ script_mod! { 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) - // Same state blend the stock button face uses. - let face = mix( - mix(self.color, self.color_hover, self.hover), - self.color_down, - self.down - ) + 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 { @@ -105,10 +109,19 @@ pub fn cancel_all_dictation() { } /// 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(); @@ -132,8 +145,12 @@ pub struct SpeechTextInput { /// The inner text input's padding, not counting the microphone's gutter. #[live] text_padding: Inset, - /// The width reserved on the right of the text for the microphone button. + /// 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, @@ -177,18 +194,19 @@ 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 - && 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), - _ => {} + 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; } - 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); @@ -260,7 +278,9 @@ impl SpeechTextInput { fn toggle(&mut self, cx: &mut Cx) { match self.phase() { Some(SpeechPhase::Listening) => self.stop(), - Some(_) => self.cancel(cx), + 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); @@ -300,6 +320,18 @@ impl SpeechTextInput { 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); @@ -328,10 +360,14 @@ impl SpeechTextInput { /// 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 gutter = if show { self.mic_gutter } else { 0.0 }; - let padding = Inset { right: self.text_padding.right + gutter, ..self.text_padding }; + 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)}); + script_apply_eval!(cx, input, {padding: #(padding), scroll_bar_inset: #(scroll_bar_inset)}); } fn update_speech_controls(&mut self, cx: &mut Cx) { @@ -407,13 +443,11 @@ impl SpeechTextInput { // 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; } - self.clear_finishing(); + self.end_session(cx); break; } NativeSpeechEvent::Error(error) => { - if let Some(speech) = self.speech.as_mut() { speech.ended = true; } - self.clear_finishing(); + 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 From 52306fd559a445282016306a0cbcc04f0b6498c9 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Fri, 11 Sep 2026 18:07:43 -0700 Subject: [PATCH 7/7] fix main room filter input bar that had broken due to makepad changes --- src/home/home_screen.rs | 11 +---------- src/home/rooms_sidebar.rs | 9 +-------- src/shared/room_filter_input_bar.rs | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 17820f7fb..22790101b 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -14,7 +14,6 @@ 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, @@ -264,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. @@ -533,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() { 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/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index 7f2d3eaf9..3f5092933 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -84,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 { @@ -189,8 +193,11 @@ 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) ); } @@ -199,8 +206,11 @@ impl WidgetMatchEvent for RoomFilterInputBar { 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()) ); }