diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index cbf6e405cf..4167aa0990 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -288,11 +288,12 @@ mod tests { use super::*; use crate::{ AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction, canvas, div, + ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction, + TextInputStateChange, canvas, div, }; #[gpui::test] - fn text_input_configuration_forwarded_only_on_change(cx: &mut TestAppContext) { + fn text_input_configuration_and_focus_state_are_forwarded_on_change(cx: &mut TestAppContext) { let custom = TextInputConfiguration { autocorrect: true, input_action: TextInputAction::Send, @@ -319,6 +320,7 @@ mod tests { test_window.text_input_configurations(), vec![TextInputConfiguration::default()] ); + assert!(test_window.text_input_state_changes().is_empty()); // Focusing the view routes its configuration to the platform. cx.update_window(window, |_, window, cx| { @@ -331,10 +333,15 @@ mod tests { test_window.text_input_configurations(), vec![TextInputConfiguration::default(), custom.clone()] ); + assert_eq!( + test_window.text_input_state_changes(), + vec![TextInputStateChange::FocusGained] + ); // Redrawing without a change forwards nothing. draw(cx); assert_eq!(test_window.text_input_configurations().len(), 2); + assert_eq!(test_window.text_input_state_changes().len(), 1); // Changing the configuration forwards the new value. let updated = TextInputConfiguration { @@ -354,6 +361,7 @@ mod tests { Some(&updated) ); assert_eq!(test_window.text_input_configurations().len(), 3); + assert_eq!(test_window.text_input_state_changes().len(), 1); // Losing focus reverts the platform to the default configuration. cx.update_window(window, |_, window, cx| window.blur(cx)) @@ -364,6 +372,13 @@ mod tests { Some(&TextInputConfiguration::default()) ); assert_eq!(test_window.text_input_configurations().len(), 4); + assert_eq!( + test_window.text_input_state_changes(), + vec![ + TextInputStateChange::FocusGained, + TextInputStateChange::FocusLost + ] + ); } struct ConfigurationTestView { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 223a2370e4..38926b649c 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -2,8 +2,9 @@ use crate::{ AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, Pixels, PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration, TileId, - WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams, + PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, TextInputConfiguration, + TextInputStateChange, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControlArea, WindowParams, }; use collections::HashMap; use gpui_util::ResultExt as _; @@ -43,6 +44,7 @@ pub(crate) struct TestWindowState { frame_callback_pending: bool, input_handler: Option, text_input_configurations: Vec, + text_input_state_changes: Vec, is_fullscreen: bool, appearance: WindowAppearance, external_drag_files: Vec<(PathBuf, bool)>, @@ -106,6 +108,7 @@ impl TestWindow { frame_callback_pending: false, input_handler: None, text_input_configurations: Vec::new(), + text_input_state_changes: Vec::new(), is_fullscreen: false, appearance: WindowAppearance::Light, external_drag_files: Vec::new(), @@ -140,6 +143,10 @@ impl TestWindow { self.0.lock().text_input_configurations.clone() } + pub fn text_input_state_changes(&self) -> Vec { + self.0.lock().text_input_state_changes.clone() + } + pub fn simulate_resize(&mut self, size: Size) { let scale_factor = self.scale_factor(); let mut lock = self.0.lock(); @@ -269,6 +276,10 @@ impl PlatformWindow for TestWindow { self.0.lock().text_input_configurations.push(configuration); } + fn text_input_state_changed(&self, change: TextInputStateChange) { + self.0.lock().text_input_state_changes.push(change); + } + fn prompt( &self, _level: crate::PromptLevel, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 63b11d95b5..0950071c3f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -19,10 +19,10 @@ use crate::{ SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration, - TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, - Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point, - prelude::*, px, rems, size, transparent_black, + TextInputStateChange, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + point, prelude::*, px, rems, size, transparent_black, }; use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer}; @@ -1162,6 +1162,7 @@ pub struct Window { /// window, so that only actual changes are forwarded (reconfiguring a live /// input session can restart the IME connection). last_text_input_configuration: Option, + focused_text_input_active: bool, pub(crate) image_cache_stack: Vec, pub(crate) rendered_frame: Frame, pub(crate) next_frame: Frame, @@ -1859,6 +1860,7 @@ impl Window { element_opacity: 1.0, requested_autoscroll: None, last_text_input_configuration: None, + focused_text_input_active: false, rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame_callbacks, @@ -2935,16 +2937,29 @@ impl Window { // paint_range indices remain valid for reuse_paint on the next frame. // Search backwards to find the last Some entry, since reuse_paint may // have copied None slots from the previous frame. (Fixes #50456) - if let Some(input_handler) = self + let focused_text_input_active = if let Some(mut input_handler) = self .next_frame .input_handlers .iter_mut() .rev() .find_map(|h| h.take()) { + let accepts_text_input = input_handler.accepts_text_input(self, cx); self.platform_window.set_input_handler(input_handler); - } + accepts_text_input + } else { + false + }; self.apply_text_input_configuration(cx); + if focused_text_input_active != self.focused_text_input_active { + self.focused_text_input_active = focused_text_input_active; + self.platform_window + .text_input_state_changed(if focused_text_input_active { + TextInputStateChange::FocusGained + } else { + TextInputStateChange::FocusLost + }); + } self.layout_engine.as_mut().unwrap().clear(); self.text_system().finish_frame(); diff --git a/crates/gpui_web/src/events.rs b/crates/gpui_web/src/events.rs index 7afedeb4e8..a6a1b2f4be 100644 --- a/crates/gpui_web/src/events.rs +++ b/crates/gpui_web/src/events.rs @@ -480,7 +480,7 @@ impl WebWindowInner { /// /// We don't use `navigator.virtualKeyboard` here because it's /// Chromium-only. - fn sync_virtual_keyboard(self: &Rc, editable: bool) { + pub(crate) fn sync_virtual_keyboard(self: &Rc, editable: bool) { let was_editable = !self.ime_mirror.read_only(); self.ime_mirror.set_read_only(!editable); // Trigger a focus event only when the keyboard actually needs diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index 799c3fadcf..8d6b0bd688 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -11,8 +11,9 @@ use gpui::{ AnyWindowHandle, Bounds, Capslock, Decorations, DevicePixels, DispatchEventResult, GpuSpecs, Modifiers, MouseButton, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, - ResizeEdge, Scene, Size, TextInputConfiguration, WindowAppearance, WindowBackgroundAppearance, - WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, + ResizeEdge, Scene, Size, TextInputConfiguration, TextInputStateChange, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations, + WindowParams, px, }; use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu}; use wasm_bindgen::prelude::*; @@ -691,6 +692,14 @@ impl PlatformWindow for WebWindow { self.inner.ime_mirror.apply_configuration(&configuration); } + fn text_input_state_changed(&self, change: TextInputStateChange) { + match change { + TextInputStateChange::FocusGained => self.inner.sync_virtual_keyboard(true), + TextInputStateChange::FocusLost => self.inner.sync_virtual_keyboard(false), + TextInputStateChange::SelectionChanged | TextInputStateChange::ContentChanged => {} + } + } + fn prompt( &self, _level: PromptLevel,