use ely_domain::{BrowserTab, TabId}; use gpui::{ AnyElement, App, Entity, ImageSource, InteractiveElement, IntoElement, MouseButton, ObjectFit, ParentElement, Styled, StyledImage, Window, canvas, div, img, px, rgb, }; use super::{ElyShell, web_surface_frame::WebSurfaceFrame}; use ely_design_system::colors; pub(super) fn render_ready_web_surface( frame: &WebSurfaceFrame, tab: &BrowserTab, state_entity: Entity, ) -> AnyElement { // T14: the `gpui::surface(...)` hardware path is disabled. // // GPUI 0.2.2's Blade Metal renderer hard-asserts that any // CVPixelBuffer handed to `surface(...)` is NV12 YUV // (kCVPixelFormatType_420YpCbCr8BiPlanarFullRange). See // ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ // gpui-0.2.2/src/platform/blade/blade_renderer.rs:832 // for the assert. Our sidecar produces BGRA IOSurfaces, so // calling `surface()` with that buffer panics the renderer on // the first frame. // // We keep `services::iosurface_metal` and // `WebSurfaceFrame::pixel_buffer` intact so the wire-side // IOSurfaceHandle import path stays exercised; once GPUI gains a // BGRA-capable Surface element this branch can come back. Until // then every frame must go through `img()` below. if let Some(image) = frame.image.as_ref() { return render_web_surface( tab, state_entity, img(ImageSource::Render(image.clone())).size_full().object_fit(ObjectFit::Fill), ); } // Both image variants empty: the sidecar should always publish // RGBA while the hardware path is disabled, but a blank canvas is // the honest user-facing fallback if it ever does not. render_web_surface(tab, state_entity, div().size_full()) } pub(super) fn render_loading_web_surface( tab: &BrowserTab, state_entity: Entity, ) -> AnyElement { render_web_surface(tab, state_entity, div().size_full()) } pub(super) fn render_failed_web_surface( tab: &BrowserTab, message: &str, state_entity: Entity, ) -> AnyElement { render_web_surface(tab, state_entity, error_page(message)) } fn error_page(message: &str) -> impl IntoElement { div() .size_full() .p(px(48.0)) .flex() .flex_col() .items_center() .justify_center() .gap(px(12.0)) .child( div() .text_size(px(22.0)) .font_weight(gpui::FontWeight(400.0)) .text_color(rgb(colors::INK)) .child("Page unavailable"), ) .child( div() .text_size(px(14.0)) .text_color(rgb(colors::INK_3)) .child(message.to_string()), ) } fn render_web_surface( tab: &BrowserTab, state_entity: Entity, content: impl IntoElement, ) -> AnyElement { let input_tab_id = tab.id().clone(); let input_url = tab.url().as_str().to_string(); let input_entity = state_entity.clone(); let tracker_entity = state_entity; div() .relative() .size_full() .min_w_0() .overflow_hidden() .child( div() .absolute() .inset_0() .child(content), ) .child(render_viewport_tracker(tab.id().clone(), tracker_entity)) .child(render_input_overlay(input_tab_id, input_url, input_entity)) .into_any_element() } fn render_input_overlay( tab_id: TabId, url: String, state_entity: Entity, ) -> impl IntoElement { let down_entity = state_entity.clone(); let click_tab_id = tab_id.clone(); let click_url = url.clone(); let click_entity = state_entity.clone(); let hover_tab_id = tab_id.clone(); let hover_entity = state_entity.clone(); let scroll_tab_id = tab_id; let scroll_url = url; let scroll_entity = state_entity; div() .absolute() .size_full() .occlude() // Mouse-down hands focus to the shell's root focus handle so // subsequent keystrokes route to the web surface instead of // the omnibar Input. Doing this on mouse-down (not mouse-up) // lets the user start typing as soon as they press, matching // native browser focus semantics. .on_mouse_down(MouseButton::Left, move |_event, window, cx| { down_entity.update(cx, |shell, _cx| { shell.focus_web_surface(window); }); }) .capture_any_mouse_up(move |event, window, cx| { if event.button != MouseButton::Left { return; } click_entity.update(cx, |shell, cx| { shell.click_external_web_viewport( click_tab_id.clone(), click_url.clone(), event.position, window, cx, ); }); cx.stop_propagation(); }) .on_mouse_move(move |event, window, cx| { let scale_factor = window.scale_factor(); hover_entity.update(cx, |shell, cx| { shell.hover_external_web_viewport( hover_tab_id.clone(), event.position, scale_factor, cx, ); }); }) .on_scroll_wheel(move |event, window, cx| { let delta = event.delta.pixel_delta(window.line_height()); let scale_factor = window.scale_factor(); scroll_entity.update(cx, |shell, cx| { shell.scroll_external_web_viewport( scroll_tab_id.clone(), scroll_url.clone(), delta, scale_factor, cx, ); }); cx.stop_propagation(); }) } fn render_viewport_tracker(tab_id: TabId, state_entity: Entity) -> impl IntoElement { canvas( move |bounds, window: &mut Window, cx: &mut App| { let scale_factor = window.scale_factor(); state_entity.update(cx, |shell, cx| { shell.record_external_web_viewport(tab_id, bounds, scale_factor, cx); }); }, |_, _, _, _| {}, ) .absolute() .size_full() }