use std::collections::BTreeMap; use ely_domain::{BrowserTab, TabId}; use gpui::{Bounds, Pixels, Point}; use crate::services::ProfileDataMode; use super::{ web_surface_frame::WebSurfaceFrame, web_surface_geometry::{ WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceScrollOffset, WebSurfaceSize, }, web_surface_permissions::WebSurfaceSitePermission, web_surface_runtime::{WebSurfaceRuntime, WebSurfaceRuntimeFrame}, web_surface_state::{ WebSurfaceClickState, WebSurfaceKeyboardFocusState, WebSurfacePendingInput, WebSurfaceScrollState, WebSurfaceState, WebSurfaceTextInputState, }, }; pub(super) struct WebSurfaceStore { runtime: WebSurfaceRuntime, pending_viewport_sizes: BTreeMap, click_points: BTreeMap, hover_points: BTreeMap, keyboard_focus: Option, pending_scroll_deltas: BTreeMap, scroll_offsets: BTreeMap, typed_texts: BTreeMap, viewport_bounds: BTreeMap>, viewport_sizes: BTreeMap, states: BTreeMap, } impl WebSurfaceStore { pub(super) fn new() -> Self { Self { runtime: WebSurfaceRuntime::new(), pending_viewport_sizes: BTreeMap::new(), click_points: BTreeMap::new(), hover_points: BTreeMap::new(), keyboard_focus: None, pending_scroll_deltas: BTreeMap::new(), scroll_offsets: BTreeMap::new(), typed_texts: BTreeMap::new(), viewport_bounds: BTreeMap::new(), viewport_sizes: BTreeMap::new(), states: BTreeMap::new(), } } pub(super) fn state(&self, tab_id: &TabId) -> Option<&WebSurfaceState> { self.states.get(tab_id) } pub(super) fn ensure_surface( &mut self, tab: &BrowserTab, profile_data_mode: ProfileDataMode, permissions: &[WebSurfaceSitePermission], ) { if !is_external_web_url(tab.url().as_str()) { return; } let requested_url = tab.url().as_str().to_string(); let Some(size) = self.viewport_sizes.get(tab.id()).copied() else { return; }; let input = self.take_pending_input(tab.id(), requested_url.as_str()); let previous_frame = self.previous_ready_frame(tab.id(), requested_url.as_str(), tab.zoom_percent()); match self.runtime.ensure_tab(tab, size, profile_data_mode, permissions, input) { Ok(result) if result.frame.is_some() => { let Some(frame) = result.frame else { return; }; self.states.insert(tab.id().clone(), WebSurfaceState::Ready(frame)); } Ok(result) if result.started_loading => { self.states.insert( tab.id().clone(), WebSurfaceState::Loading { requested_url: result.requested_url, previous_frame, }, ); } Ok(_) => {} Err(message) => { self.states.insert(tab.id().clone(), WebSurfaceState::Failed { message }); } } } pub(super) fn tick(&mut self) -> bool { let frames = self.runtime.tick(); let mut changed = false; for frame in frames { match frame { WebSurfaceRuntimeFrame::Ready { tab_id, frame } => { self.states.insert(tab_id, WebSurfaceState::Ready(frame)); changed = true; } WebSurfaceRuntimeFrame::Failed { tab_id, message } => { self.states.insert(tab_id, WebSurfaceState::Failed { message }); changed = true; } } } changed } pub(super) fn record_scroll_delta( &mut self, tab_id: &TabId, requested_url: &str, delta: Point, ) -> bool { let Some(delta) = WebSurfaceScrollDelta::from_point(delta) else { return false; }; let state = self .scroll_offsets .entry(tab_id.clone()) .or_insert_with(|| WebSurfaceScrollState::new(requested_url.to_string())); if state.requested_url != requested_url { *state = WebSurfaceScrollState::new(requested_url.to_string()); } state.offset = state.offset.scrolled_by(delta); self.pending_scroll_deltas .entry(tab_id.clone()) .and_modify(|current| *current = current.combined_with(delta)) .or_insert(delta); self.click_points.remove(tab_id); self.typed_texts.remove(tab_id); self.keyboard_focus = None; true } pub(super) fn record_viewport_size(&mut self, tab_id: &TabId, bounds: Bounds) -> bool { let Some(size) = WebSurfaceSize::from_bounds(bounds) else { return false; }; self.viewport_bounds.insert(tab_id.clone(), bounds); let Some(current_size) = self.viewport_sizes.get(tab_id).copied() else { self.viewport_sizes.insert(tab_id.clone(), size); self.pending_viewport_sizes.remove(tab_id); return true; }; if current_size == size { self.pending_viewport_sizes.remove(tab_id); return false; } if self.pending_viewport_sizes.get(tab_id) != Some(&size) { self.pending_viewport_sizes.insert(tab_id.clone(), size); return false; } self.pending_viewport_sizes.remove(tab_id); self.viewport_sizes.insert(tab_id.clone(), size); true } pub(super) fn record_hover_point( &mut self, tab_id: &TabId, position: Point, ) -> bool { let Some(bounds) = self.viewport_bounds.get(tab_id).copied() else { return false; }; let Some(point) = WebSurfaceClickPoint::from_window_position(bounds, position) else { return false; }; self.hover_points.insert(tab_id.clone(), point); true } pub(super) fn record_click_point( &mut self, tab_id: &TabId, requested_url: &str, position: Point, ) -> bool { let Some(bounds) = self.viewport_bounds.get(tab_id).copied() else { return false; }; let Some(point) = WebSurfaceClickPoint::from_window_position(bounds, position) else { return false; }; let state = WebSurfaceClickState { requested_url: requested_url.to_string(), 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, }); 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 take_pending_input( &mut self, tab_id: &TabId, requested_url: &str, ) -> WebSurfacePendingInput { let scroll_offset = self.scroll_offset_for(tab_id, requested_url); let scroll_delta = self.pending_scroll_deltas.remove(tab_id); let click_point = self .click_points .remove(tab_id) .filter(|state| { state.requested_url == requested_url && state.scroll_offset == scroll_offset }) .map(|state| state.point); let typed_text = self .typed_texts .remove(tab_id) .filter(|state| state.requested_url == requested_url) .map(|state| state.text); let hover_point = self.hover_points.remove(tab_id); WebSurfacePendingInput { scroll_offset, scroll_delta, click_point, hover_point, typed_text } } fn previous_ready_frame( &self, tab_id: &TabId, requested_url: &str, zoom_percent: u16, ) -> Option { match self.states.get(tab_id) { Some(WebSurfaceState::Ready(frame)) if frame.requested_url == requested_url && frame.zoom_percent() == zoom_percent => { Some(frame.clone()) } Some(WebSurfaceState::Loading { requested_url: current_url, previous_frame, .. }) if current_url == requested_url => previous_frame .as_ref() .filter(|frame| frame.zoom_percent() == zoom_percent) .cloned(), _ => None, } } fn scroll_offset_for(&self, tab_id: &TabId, requested_url: &str) -> WebSurfaceScrollOffset { self.scroll_offsets .get(tab_id) .filter(|state| state.requested_url == requested_url) .map(|state| state.offset) .unwrap_or_default() } } pub(super) fn is_external_web_url(url: &str) -> bool { url.starts_with("https://") || url.starts_with("http://") } #[cfg(test)] #[path = "web_surface_tests.rs"] mod tests; #[cfg(all(test, feature = "live-site-smoke"))] #[path = "web_surface_live_site_tests.rs"] mod web_surface_live_site_tests;