diff --git a/crates/ely_app/src/services/servo_sidecar.rs b/crates/ely_app/src/services/servo_sidecar.rs index 29d7680..6defba0 100644 --- a/crates/ely_app/src/services/servo_sidecar.rs +++ b/crates/ely_app/src/services/servo_sidecar.rs @@ -86,6 +86,9 @@ impl ServoSidecarClient { .arg("--click-y") .arg(click_point.y.to_string()); } + if let Some(typed_text) = request.typed_text.as_deref() { + command.arg("--type-text").arg(typed_text); + } let mut child = command .stdout(Stdio::piped()) @@ -120,12 +123,13 @@ pub struct SidecarSnapshotRequest { scroll_x: i32, scroll_y: i32, click_point: Option, + typed_text: Option, } impl SidecarSnapshotRequest { #[must_use] pub fn new(url: UrlText, width: u32, height: u32) -> Self { - Self { url, width, height, scroll_x: 0, scroll_y: 0, click_point: None } + Self { url, width, height, scroll_x: 0, scroll_y: 0, click_point: None, typed_text: None } } #[must_use] @@ -140,6 +144,17 @@ impl SidecarSnapshotRequest { self.click_point = Some(SidecarClickPoint { x, y }); self } + + #[must_use] + pub fn with_typed_text(mut self, typed_text: String) -> Self { + self.typed_text = Some(typed_text); + self + } + + #[cfg(test)] + pub(crate) fn typed_text_for_test(&self) -> Option<&str> { + self.typed_text.as_deref() + } } #[derive(Clone, Copy, Debug)] diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 906c0ba..ac8ab7c 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -15,9 +15,11 @@ mod splits; mod tab_groups; mod tab_lifecycle; mod web_surface; +mod web_surface_controller; mod web_surface_frame; mod web_surface_geometry; mod web_surface_image; +mod web_surface_keyboard; mod web_surface_state; mod web_surface_view; diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index f285d0e..0fa7dbb 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -42,6 +42,7 @@ impl ElyShell { div() .size_full() .track_focus(&self.focus_handle) + .capture_key_down(cx.listener(Self::on_external_web_key_down)) .on_action(cx.listener(Self::on_close_current_tab)) .on_action(cx.listener(Self::on_focus_address_bar)) .on_action(cx.listener(Self::on_focus_command_mode)) diff --git a/crates/ely_app/src/shell/web_surface.rs b/crates/ely_app/src/shell/web_surface.rs index accf7d9..cbc4eb2 100644 --- a/crates/ely_app/src/shell/web_surface.rs +++ b/crates/ely_app/src/shell/web_surface.rs @@ -1,22 +1,18 @@ use std::collections::BTreeMap; use ely_domain::{BrowserTab, TabId}; -use gpui::{AnyElement, Bounds, Context, Pixels, Point}; +use gpui::{Bounds, Pixels, Point}; -use crate::services::servo_sidecar::{ServoSidecarError, SidecarSnapshot, SidecarSnapshotRequest}; +use crate::services::servo_sidecar::SidecarSnapshotRequest; use super::{ - ElyShell, web_surface_frame::WebSurfaceFrame, web_surface_geometry::{ WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize, }, web_surface_state::{ - WebSurfaceClickState, WebSurfaceClient, WebSurfaceRequest, WebSurfaceScrollState, - WebSurfaceState, - }, - web_surface_view::{ - render_failed_web_surface, render_loading_web_surface, render_ready_web_surface, + WebSurfaceClickState, WebSurfaceClient, WebSurfaceKeyboardFocusState, WebSurfaceRequest, + WebSurfaceScrollState, WebSurfaceState, WebSurfaceTextInputState, }, }; @@ -24,7 +20,9 @@ pub(super) struct WebSurfaceStore { client: WebSurfaceClient, pending_viewport_sizes: BTreeMap, click_points: BTreeMap, + keyboard_focus: Option, scroll_offsets: BTreeMap, + typed_texts: BTreeMap, viewport_bounds: BTreeMap>, viewport_sizes: BTreeMap, states: BTreeMap, @@ -36,18 +34,20 @@ impl WebSurfaceStore { client: WebSurfaceClient::new(), pending_viewport_sizes: BTreeMap::new(), click_points: BTreeMap::new(), + keyboard_focus: None, scroll_offsets: BTreeMap::new(), + typed_texts: BTreeMap::new(), viewport_bounds: BTreeMap::new(), viewport_sizes: BTreeMap::new(), states: BTreeMap::new(), } } - fn state(&self, tab_id: &TabId) -> Option<&WebSurfaceState> { + pub(super) fn state(&self, tab_id: &TabId) -> Option<&WebSurfaceState> { self.states.get(tab_id) } - fn prepare_request(&mut self, tab: &BrowserTab) -> Option { + pub(super) fn prepare_request(&mut self, tab: &BrowserTab) -> Option { if !is_external_web_url(tab.url().as_str()) { return None; } @@ -56,10 +56,19 @@ impl WebSurfaceStore { let requested_url = tab.url().as_str().to_string(); let scroll_offset = self.scroll_offset_for(tab.id(), requested_url.as_str()); let click_point = self.click_point_for(tab.id(), requested_url.as_str(), scroll_offset); + let typed_text = + self.typed_text_for(tab.id(), requested_url.as_str(), scroll_offset, click_point); if self.is_loading_requested_url(tab.id(), requested_url.as_str()) { return None; } - if self.has_current_state(tab.id(), &requested_url, size, scroll_offset, click_point) { + if self.has_current_state( + tab.id(), + &requested_url, + size, + scroll_offset, + click_point, + typed_text.as_deref(), + ) { return None; } @@ -73,6 +82,7 @@ impl WebSurfaceStore { size, scroll_offset, click_point, + typed_text: typed_text.clone(), message: message.clone(), }, ); @@ -87,6 +97,7 @@ impl WebSurfaceStore { size, scroll_offset, click_point, + typed_text: typed_text.clone(), previous_frame: self.previous_ready_frame(tab.id(), requested_url.as_str()), }, ); @@ -97,6 +108,9 @@ impl WebSurfaceStore { if let Some(click_point) = click_point { snapshot_request = snapshot_request.with_click_point(click_point.x(), click_point.y()); } + if let Some(typed_text) = typed_text.clone() { + snapshot_request = snapshot_request.with_typed_text(typed_text); + } Some(WebSurfaceRequest { tab_id: tab.id().clone(), @@ -104,6 +118,7 @@ impl WebSurfaceStore { size, scroll_offset, click_point, + typed_text, client, snapshot_request, }) @@ -116,6 +131,7 @@ impl WebSurfaceStore { size: WebSurfaceSize, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option<&str>, ) -> bool { match self.states.get(tab_id) { Some(WebSurfaceState::Loading { @@ -123,42 +139,48 @@ impl WebSurfaceStore { size: current_size, scroll_offset: current_scroll_offset, click_point: current_click_point, + typed_text: current_typed_text, .. }) => { current_url == requested_url && *current_size == size && *current_scroll_offset == scroll_offset && *current_click_point == click_point + && current_typed_text.as_deref() == typed_text } Some(WebSurfaceState::Ready(frame)) => { frame.requested_url == requested_url && frame.size() == size && frame.scroll_offset() == scroll_offset && frame.click_point() == click_point + && frame.typed_text() == typed_text } Some(WebSurfaceState::Failed { requested_url: current_url, size: current_size, scroll_offset: current_scroll_offset, click_point: current_click_point, + typed_text: current_typed_text, .. }) => { current_url == requested_url && *current_size == size && *current_scroll_offset == scroll_offset && *current_click_point == click_point + && current_typed_text.as_deref() == typed_text } None => false, } } - fn is_loading( + pub(super) fn is_loading( &self, tab_id: &TabId, requested_url: &str, size: WebSurfaceSize, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option<&str>, ) -> bool { matches!( self.states.get(tab_id), @@ -167,12 +189,14 @@ impl WebSurfaceStore { size: current_size, scroll_offset: current_scroll_offset, click_point: current_click_point, + typed_text: current_typed_text, .. }) if current_url == requested_url && *current_size == size && *current_scroll_offset == scroll_offset && *current_click_point == click_point + && current_typed_text.as_deref() == typed_text ) } @@ -196,11 +220,11 @@ impl WebSurfaceStore { } } - fn finish(&mut self, tab_id: TabId, state: WebSurfaceState) { + pub(super) fn finish(&mut self, tab_id: TabId, state: WebSurfaceState) { self.states.insert(tab_id, state); } - fn record_scroll_delta( + pub(super) fn record_scroll_delta( &mut self, tab_id: &TabId, requested_url: &str, @@ -225,10 +249,12 @@ impl WebSurfaceStore { state.offset = next_offset; self.click_points.remove(tab_id); + self.typed_texts.remove(tab_id); + self.keyboard_focus = None; true } - fn record_viewport_size(&mut self, tab_id: &TabId, bounds: Bounds) -> bool { + pub(super) fn record_viewport_size(&mut self, tab_id: &TabId, bounds: Bounds) -> bool { let Some(size) = WebSurfaceSize::from_bounds(bounds) else { return false; }; @@ -255,7 +281,7 @@ impl WebSurfaceStore { true } - fn record_click_point( + pub(super) fn record_click_point( &mut self, tab_id: &TabId, requested_url: &str, @@ -273,14 +299,60 @@ impl WebSurfaceStore { scroll_offset: self.scroll_offset_for(tab_id, requested_url), point, }; + self.keyboard_focus = Some(WebSurfaceKeyboardFocusState { + tab_id: tab_id.clone(), + requested_url: requested_url.to_string(), + scroll_offset: state.scroll_offset, + click_point: state.point, + }); if self.click_points.get(tab_id) == Some(&state) { return false; } + self.typed_texts.remove(tab_id); self.click_points.insert(tab_id.clone(), state); true } + pub(super) fn record_typed_text( + &mut self, + tab_id: &TabId, + requested_url: &str, + text: &str, + ) -> bool { + if text.is_empty() { + return false; + } + let Some(focus) = self.keyboard_focus.as_ref() else { + return false; + }; + if focus.tab_id != *tab_id || focus.requested_url != requested_url { + return false; + } + + let entry = + self.typed_texts.entry(tab_id.clone()).or_insert_with(|| WebSurfaceTextInputState { + requested_url: requested_url.to_string(), + scroll_offset: focus.scroll_offset, + click_point: focus.click_point, + text: String::new(), + }); + if entry.requested_url != requested_url + || entry.scroll_offset != focus.scroll_offset + || entry.click_point != focus.click_point + { + *entry = WebSurfaceTextInputState { + requested_url: requested_url.to_string(), + scroll_offset: focus.scroll_offset, + click_point: focus.click_point, + text: String::new(), + }; + } + + entry.text.push_str(text); + true + } + fn scroll_offset_for(&self, tab_id: &TabId, requested_url: &str) -> WebSurfaceScrollOffset { self.scroll_offsets .get(tab_id) @@ -302,150 +374,69 @@ impl WebSurfaceStore { }) .map(|state| state.point) } -} -impl ElyShell { - pub(super) fn render_external_web_canvas( - &mut self, - tab: &BrowserTab, - cx: &mut Context, - ) -> AnyElement { - self.ensure_external_web_frame(tab, cx); - - let state_entity = cx.entity().clone(); - match self.web_surfaces.state(tab.id()) { - Some(WebSurfaceState::Ready(frame)) => { - render_ready_web_surface(frame, tab, state_entity) - } - Some(WebSurfaceState::Failed { message, .. }) => { - render_failed_web_surface(tab, message.as_str(), state_entity) - } - Some(WebSurfaceState::Loading { previous_frame: Some(frame), .. }) => { - render_ready_web_surface(frame, tab, state_entity) - } - Some(WebSurfaceState::Loading { previous_frame: None, .. }) | None => { - render_loading_web_surface(tab, state_entity) - } - } - } - - fn ensure_external_web_frame(&mut self, tab: &BrowserTab, cx: &mut Context) { - let Some(request) = self.web_surfaces.prepare_request(tab) else { - return; - }; - - let WebSurfaceRequest { - tab_id, - requested_url, - size, - scroll_offset, - click_point, - client, - snapshot_request, - } = request; - cx.spawn(async move |shell, cx| { - let result = cx - .background_executor() - .spawn(async move { client.snapshot(snapshot_request) }) - .await; - - _ = shell.update(cx, |shell, cx| { - shell.handle_external_web_frame_result( - tab_id, - requested_url, - size, - scroll_offset, - click_point, - result, - ); - cx.notify(); - }); - }) - .detach(); - } - - fn handle_external_web_frame_result( - &mut self, - tab_id: TabId, - requested_url: String, - size: WebSurfaceSize, + fn typed_text_for( + &self, + tab_id: &TabId, + requested_url: &str, scroll_offset: WebSurfaceScrollOffset, click_point: Option, - result: Result, - ) { - if !self.web_surfaces.is_loading( - &tab_id, - requested_url.as_str(), - size, - scroll_offset, - click_point, - ) { - return; - } - - let state = match result { - Ok(snapshot) => match WebSurfaceFrame::from_snapshot( - requested_url.clone(), - scroll_offset, - click_point, - snapshot, - ) { - Ok(frame) => WebSurfaceState::Ready(frame), - Err(error) => WebSurfaceState::Failed { - requested_url, - size, - scroll_offset, - click_point, - message: error.to_string(), - }, - }, - Err(error) => WebSurfaceState::Failed { - requested_url, - size, - scroll_offset, - click_point, - message: error.to_string(), - }, - }; - self.web_surfaces.finish(tab_id, state); - } - - pub(super) fn record_external_web_viewport( - &mut self, - tab_id: TabId, - bounds: Bounds, - cx: &mut Context, - ) { - if self.web_surfaces.record_viewport_size(&tab_id, bounds) { - cx.notify(); - } - } - - pub(super) fn scroll_external_web_viewport( - &mut self, - tab_id: TabId, - requested_url: String, - delta: gpui::Point, - cx: &mut Context, - ) { - if self.web_surfaces.record_scroll_delta(&tab_id, requested_url.as_str(), delta) { - cx.notify(); - } - } - - pub(super) fn click_external_web_viewport( - &mut self, - tab_id: TabId, - requested_url: String, - position: Point, - cx: &mut Context, - ) { - if self.web_surfaces.record_click_point(&tab_id, requested_url.as_str(), position) { - cx.notify(); - } + ) -> Option { + let click_point = click_point?; + self.typed_texts + .get(tab_id) + .filter(|state| { + state.requested_url == requested_url + && state.scroll_offset == scroll_offset + && state.click_point == click_point + && !state.text.is_empty() + }) + .map(|state| state.text.clone()) } } pub(super) fn is_external_web_url(url: &str) -> bool { url.starts_with("https://") || url.starts_with("http://") } + +#[cfg(test)] +mod tests { + use std::error::Error; + + use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText}; + use gpui::{Bounds, point, px, size}; + + use super::WebSurfaceStore; + + #[test] + fn typed_text_enters_snapshot_request_after_clicked_viewport() -> Result<(), Box> { + let mut store = WebSurfaceStore::new(); + let tab = web_tab("https://example.com/form")?; + + let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0))); + assert!(store.record_viewport_size(tab.id(), bounds)); + assert!(store.record_click_point( + tab.id(), + tab.url().as_str(), + point(px(160.0), px(120.0)) + )); + assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "e")); + assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "l")); + + let request = store.prepare_request(&tab).ok_or("missing web surface request")?; + + assert_eq!(request.typed_text.as_deref(), Some("el")); + assert_eq!(request.snapshot_request.typed_text_for_test(), Some("el")); + Ok(()) + } + + fn web_tab(url: &str) -> Result> { + Ok(BrowserTab::new( + TabId::new(), + SpaceId::new(), + ProfileId::new(), + "Web", + UrlText::parse(url)?, + )) + } +} diff --git a/crates/ely_app/src/shell/web_surface_controller.rs b/crates/ely_app/src/shell/web_surface_controller.rs new file mode 100644 index 0000000..6792e6c --- /dev/null +++ b/crates/ely_app/src/shell/web_surface_controller.rs @@ -0,0 +1,192 @@ +use ely_domain::{BrowserTab, TabId}; +use gpui::{AnyElement, Bounds, Context, Pixels, Point}; + +use crate::services::servo_sidecar::{ServoSidecarError, SidecarSnapshot}; + +use super::{ + ElyShell, + web_surface_frame::WebSurfaceFrame, + web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset, WebSurfaceSize}, + web_surface_state::{WebSurfaceRequest, WebSurfaceState}, + web_surface_view::{ + render_failed_web_surface, render_loading_web_surface, render_ready_web_surface, + }, +}; + +struct PendingWebSurfaceFrame { + tab_id: TabId, + requested_url: String, + size: WebSurfaceSize, + scroll_offset: WebSurfaceScrollOffset, + click_point: Option, + typed_text: Option, +} + +impl ElyShell { + pub(super) fn render_external_web_canvas( + &mut self, + tab: &BrowserTab, + cx: &mut Context, + ) -> AnyElement { + self.ensure_external_web_frame(tab, cx); + + let state_entity = cx.entity().clone(); + match self.web_surfaces.state(tab.id()) { + Some(WebSurfaceState::Ready(frame)) => { + render_ready_web_surface(frame, tab, state_entity) + } + Some(WebSurfaceState::Failed { message, .. }) => { + render_failed_web_surface(tab, message.as_str(), state_entity) + } + Some(WebSurfaceState::Loading { previous_frame: Some(frame), .. }) => { + render_ready_web_surface(frame, tab, state_entity) + } + Some(WebSurfaceState::Loading { previous_frame: None, .. }) | None => { + render_loading_web_surface(tab, state_entity) + } + } + } + + fn ensure_external_web_frame(&mut self, tab: &BrowserTab, cx: &mut Context) { + let Some(request) = self.web_surfaces.prepare_request(tab) else { + return; + }; + + let WebSurfaceRequest { + tab_id, + requested_url, + size, + scroll_offset, + click_point, + typed_text, + client, + snapshot_request, + } = request; + let pending_frame = PendingWebSurfaceFrame { + tab_id, + requested_url, + size, + scroll_offset, + click_point, + typed_text, + }; + cx.spawn(async move |shell, cx| { + let result = cx + .background_executor() + .spawn(async move { client.snapshot(snapshot_request) }) + .await; + + _ = shell.update(cx, |shell, cx| { + shell.handle_external_web_frame_result(pending_frame, result); + cx.notify(); + }); + }) + .detach(); + } + + fn handle_external_web_frame_result( + &mut self, + pending_frame: PendingWebSurfaceFrame, + result: Result, + ) { + let PendingWebSurfaceFrame { + tab_id, + requested_url, + size, + scroll_offset, + click_point, + typed_text, + } = pending_frame; + if !self.web_surfaces.is_loading( + &tab_id, + requested_url.as_str(), + size, + scroll_offset, + click_point, + typed_text.as_deref(), + ) { + return; + } + + let state = match result { + Ok(snapshot) => match WebSurfaceFrame::from_snapshot( + requested_url.clone(), + scroll_offset, + click_point, + typed_text.clone(), + snapshot, + ) { + Ok(frame) => WebSurfaceState::Ready(frame), + Err(error) => WebSurfaceState::Failed { + requested_url, + size, + scroll_offset, + click_point, + typed_text, + message: error.to_string(), + }, + }, + Err(error) => WebSurfaceState::Failed { + requested_url, + size, + scroll_offset, + click_point, + typed_text, + message: error.to_string(), + }, + }; + self.web_surfaces.finish(tab_id, state); + } + + pub(super) fn record_external_web_viewport( + &mut self, + tab_id: TabId, + bounds: Bounds, + cx: &mut Context, + ) { + if self.web_surfaces.record_viewport_size(&tab_id, bounds) { + cx.notify(); + } + } + + pub(super) fn scroll_external_web_viewport( + &mut self, + tab_id: TabId, + requested_url: String, + delta: gpui::Point, + cx: &mut Context, + ) { + if self.web_surfaces.record_scroll_delta(&tab_id, requested_url.as_str(), delta) { + cx.notify(); + } + } + + pub(super) fn click_external_web_viewport( + &mut self, + tab_id: TabId, + requested_url: String, + position: Point, + window: &mut gpui::Window, + cx: &mut Context, + ) { + self.focus_handle.focus(window); + if self.web_surfaces.record_click_point(&tab_id, requested_url.as_str(), position) { + cx.notify(); + } + } + + pub(super) fn type_text_in_external_web_viewport( + &mut self, + tab_id: TabId, + requested_url: String, + text: &str, + cx: &mut Context, + ) -> bool { + if self.web_surfaces.record_typed_text(&tab_id, requested_url.as_str(), text) { + cx.notify(); + return true; + } + + false + } +} diff --git a/crates/ely_app/src/shell/web_surface_frame.rs b/crates/ely_app/src/shell/web_surface_frame.rs index 5b04fa3..7e61ef1 100644 --- a/crates/ely_app/src/shell/web_surface_frame.rs +++ b/crates/ely_app/src/shell/web_surface_frame.rs @@ -20,6 +20,7 @@ pub(super) struct WebSurfaceFrame { height: u32, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option, pub(super) image: Arc, } @@ -28,6 +29,7 @@ impl WebSurfaceFrame { requested_url: String, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option, snapshot: SidecarSnapshot, ) -> Result { let width = snapshot.width(); @@ -50,6 +52,7 @@ impl WebSurfaceFrame { height, scroll_offset, click_point, + typed_text, image: Arc::new(RenderImage::new([image::Frame::new(image_buffer)])), }) } @@ -63,9 +66,12 @@ impl WebSurfaceFrame { } pub(super) fn detail_label(&self) -> String { - let detail = self.scroll_offset.detail_label(self.size()); - match self.click_point { - Some(click_point) => format!("{detail} {}", click_point.detail_label()), + let mut detail = self.scroll_offset.detail_label(self.size()); + if let Some(click_point) = self.click_point { + detail = format!("{detail} {}", click_point.detail_label()); + } + match self.typed_text.as_ref() { + Some(typed_text) => format!("{detail} text={}b", typed_text.len()), None => detail, } } @@ -81,6 +87,10 @@ impl WebSurfaceFrame { pub(super) fn click_point(&self) -> Option { self.click_point } + + pub(super) fn typed_text(&self) -> Option<&str> { + self.typed_text.as_deref() + } } #[derive(Debug, Error)] diff --git a/crates/ely_app/src/shell/web_surface_keyboard.rs b/crates/ely_app/src/shell/web_surface_keyboard.rs new file mode 100644 index 0000000..0c9ea1f --- /dev/null +++ b/crates/ely_app/src/shell/web_surface_keyboard.rs @@ -0,0 +1,96 @@ +use ely_domain::TabId; +use gpui::{Context, KeyDownEvent, Window}; + +use super::{ElyShell, ShellState, web_surface::is_external_web_url}; + +impl ElyShell { + pub(super) fn on_external_web_key_down( + &mut self, + event: &KeyDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { + let Some(text) = typed_text_from_key_down(event) else { + return; + }; + let Some((tab_id, requested_url)) = self.active_external_tab_target() else { + return; + }; + + if self.type_text_in_external_web_viewport(tab_id, requested_url, text, cx) { + cx.stop_propagation(); + } + } + + fn active_external_tab_target(&self) -> Option<(TabId, String)> { + let ShellState::Ready(core) = &self.state else { + return None; + }; + let tab = core.active_tab().ok()?; + let requested_url = tab.url().as_str(); + if !is_external_web_url(requested_url) { + return None; + } + + Some((tab.id().clone(), requested_url.to_string())) + } +} + +fn typed_text_from_key_down(event: &KeyDownEvent) -> Option<&str> { + let modifiers = &event.keystroke.modifiers; + if modifiers.control || modifiers.platform || modifiers.function { + return None; + } + + let text = event.keystroke.key_char.as_deref()?; + let mut chars = text.chars(); + let character = chars.next()?; + if chars.next().is_some() || character.is_control() { + return None; + } + + Some(text) +} + +#[cfg(test)] +mod tests { + use gpui::{KeyDownEvent, Keystroke, Modifiers}; + + use super::typed_text_from_key_down; + + #[test] + fn typed_text_uses_printable_key_char() { + let event = key_down("e", Some("e"), Modifiers::none()); + + assert_eq!(typed_text_from_key_down(&event), Some("e")); + } + + #[test] + fn typed_text_keeps_shifted_characters() { + let mut modifiers = Modifiers::none(); + modifiers.shift = true; + let event = key_down("1", Some("!"), modifiers); + + assert_eq!(typed_text_from_key_down(&event), Some("!")); + } + + #[test] + fn typed_text_ignores_browser_shortcuts() { + let mut modifiers = Modifiers::none(); + modifiers.platform = true; + let event = key_down("l", None, modifiers); + + assert_eq!(typed_text_from_key_down(&event), None); + } + + fn key_down(key: &str, key_char: Option<&str>, modifiers: Modifiers) -> KeyDownEvent { + KeyDownEvent { + keystroke: Keystroke { + modifiers, + key: key.to_string(), + key_char: key_char.map(str::to_string), + }, + is_held: false, + } + } +} diff --git a/crates/ely_app/src/shell/web_surface_state.rs b/crates/ely_app/src/shell/web_surface_state.rs index fc69acd..40b8274 100644 --- a/crates/ely_app/src/shell/web_surface_state.rs +++ b/crates/ely_app/src/shell/web_surface_state.rs @@ -25,6 +25,21 @@ pub(super) struct WebSurfaceClickState { pub(super) point: WebSurfaceClickPoint, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct WebSurfaceKeyboardFocusState { + pub(super) tab_id: TabId, + pub(super) requested_url: String, + pub(super) scroll_offset: WebSurfaceScrollOffset, + pub(super) click_point: WebSurfaceClickPoint, +} + +pub(super) struct WebSurfaceTextInputState { + pub(super) requested_url: String, + pub(super) scroll_offset: WebSurfaceScrollOffset, + pub(super) click_point: WebSurfaceClickPoint, + pub(super) text: String, +} + pub(super) enum WebSurfaceClient { Ready(ServoSidecarClient), Unavailable(String), @@ -45,6 +60,7 @@ pub(super) struct WebSurfaceRequest { pub(super) size: WebSurfaceSize, pub(super) scroll_offset: WebSurfaceScrollOffset, pub(super) click_point: Option, + pub(super) typed_text: Option, pub(super) client: ServoSidecarClient, pub(super) snapshot_request: SidecarSnapshotRequest, } @@ -55,6 +71,7 @@ pub(super) enum WebSurfaceState { size: WebSurfaceSize, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option, previous_frame: Option, }, Ready(WebSurfaceFrame), @@ -63,6 +80,7 @@ pub(super) enum WebSurfaceState { size: WebSurfaceSize, scroll_offset: WebSurfaceScrollOffset, click_point: Option, + typed_text: Option, message: String, }, } diff --git a/crates/ely_app/src/shell/web_surface_view.rs b/crates/ely_app/src/shell/web_surface_view.rs index 227aab3..c074caf 100644 --- a/crates/ely_app/src/shell/web_surface_view.rs +++ b/crates/ely_app/src/shell/web_surface_view.rs @@ -156,7 +156,7 @@ fn render_input_overlay( .absolute() .size_full() .occlude() - .capture_any_mouse_up(move |event, _window, cx| { + .capture_any_mouse_up(move |event, window, cx| { if event.button != MouseButton::Left { return; } @@ -165,6 +165,7 @@ fn render_input_overlay( click_tab_id.clone(), click_url.clone(), event.position, + window, cx, ); });