use std::{ops::Range, rc::Rc}; use gpui::{ App, Bounds, ClipboardItem, CursorStyle, Empty, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, KeyDownEvent, KeyUpEvent, MouseDownEvent, Pixels, ScrollWheelEvent, Subscription, UTF16Selection, WeakEntity, Window, anchored, deferred, div, point, prelude::*, px, }; use wef::{Browser, FuncRegistry, LogicalUnit, Point, Rect}; use crate::{ browser_handler::WebViewHandler, context_menu::{ContextMenuAction, ContextMenuInfo}, element::WebViewElement, events::*, frame_view::FrameView, utils::*, }; /// A web view based on the Chromium Embedded Framework (CEF). pub struct WebView { pub(crate) main: FrameView, pub(crate) popup: Option, pub(crate) popup_rect: Option>>, pub(crate) cursor: CursorStyle, pub(crate) context_menu: Option, pub(crate) bounds: Bounds, focus_handle: FocusHandle, browser: Rc, _subscriptions: Vec, } impl WebView { /// Creates a new `WebView` instance with the given URL. pub fn new(url: &str, window: &mut Window, cx: &mut App) -> Entity { Self::with_func_registry(url, FuncRegistry::default(), window, cx) } /// Creates a new `WebView` instance with the given URL and function /// registry. pub fn with_func_registry( url: &str, function_registry: FuncRegistry, window: &mut Window, cx: &mut App, ) -> Entity { let window_handle = window.window_handle(); let entity = cx.new(|cx| { let entity = cx.entity(); let browser = Rc::new( Browser::builder() .parent( raw_window_handle::HasWindowHandle::window_handle(window) .ok() .map(|handle| handle.as_raw()), ) .device_scale_factor(window.scale_factor()) .url(url) .handler(WebViewHandler::new( window_handle, entity.downgrade(), cx.to_async(), )) .func_registry(function_registry) .build(), ); let focus_handle = cx.focus_handle(); let _subscriptions = vec![ cx.on_focus(&focus_handle, window, Self::on_focus), cx.on_blur(&focus_handle, window, Self::on_blur), ]; Self { focus_handle, main: FrameView::default(), popup: None, popup_rect: None, browser, cursor: CursorStyle::Arrow, context_menu: None, bounds: Bounds::default(), _subscriptions, } }); cx.observe_release(&entity, |webview, cx| { for window in cx.windows() { _ = cx.update_window(window, |_, window, _cx| { webview.main.clear(window); if let Some(popup_frame) = &mut webview.popup { popup_frame.clear(window); } }); } }) .detach(); entity } /// Returns the browser instance. #[inline] pub fn browser(&self) -> &Rc { &self.browser } fn scroll_wheel_handler( &mut self, event: &ScrollWheelEvent, _window: &mut Window, _cx: &mut Context, ) { let (delta_x, delta_y) = match event.delta { gpui::ScrollDelta::Pixels(point) => (point.x.0, point.y.0), gpui::ScrollDelta::Lines(point) => (point.x * 20.0, point.y * 20.0), }; self.browser().send_mouse_wheel_event(Point::new( LogicalUnit(delta_x as i32), LogicalUnit(delta_y as i32), )); } fn keydown_handler( &mut self, event: &KeyDownEvent, _window: &mut Window, _cx: &mut Context, ) { let modifiers = to_wef_key_modifiers(&event.keystroke.modifiers); if let Some(key_code) = to_wef_key_code(&event.keystroke.key) { self.browser().send_key_event(true, key_code, modifiers); }; } fn keyup_handler(&mut self, event: &KeyUpEvent, _window: &mut Window, _cx: &mut Context) { let modifiers = to_wef_key_modifiers(&event.keystroke.modifiers); let Some(key_code) = to_wef_key_code(&event.keystroke.key) else { return; }; self.browser().send_key_event(false, key_code, modifiers); } fn on_focus(&mut self, _window: &mut Window, _cx: &mut Context) { self.browser().set_focus(true); } fn on_blur(&mut self, _window: &mut Window, _cx: &mut Context) { self.browser().set_focus(false); } fn on_context_menu_action( &mut self, action: &ContextMenuAction, _window: &mut Window, cx: &mut Context, ) { use ContextMenuAction::*; if let Some(info) = self.context_menu.take() { let action = *action; cx.spawn(async move |webview: WeakEntity, cx| { let Ok(browser) = webview.read_with(cx, |webview, _cx| webview.browser().clone()) else { return; }; match action { CopyLinkAddress => { if let Some(link_url) = &info.link_url { _ = cx.update(|cx| { cx.write_to_clipboard(ClipboardItem::new_string(link_url.clone())) }); } } Undo => info.frame.undo(), Redo => info.frame.redo(), Cut => info.frame.cut(), Copy => info.frame.copy(), Paste => info.frame.paste(), ParseAsPlainText => info.frame.paste_and_match_style(), SelectAll => info.frame.select_all(), GoBack => browser.back(), GoForward => browser.forward(), Reload => browser.reload(), } }) .detach(); } } fn context_menu_mousedown_out_handler( &mut self, _event: &MouseDownEvent, _window: &mut Window, cx: &mut Context, ) { self.context_menu = None; cx.notify(); } } impl Focusable for WebView { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for WebView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(image) = self.main.render(window) else { return Empty.into_any_element(); }; let mut root = div().size_full().child( WebViewElement::new( cx.entity(), self.focus_handle.clone(), self.browser.clone(), image.clone(), self.popup_rect .zip(self.popup.as_mut().and_then(|f| f.render(window))), ) .size_full() .track_focus(&self.focus_handle) .on_scroll_wheel(cx.listener(Self::scroll_wheel_handler)) .on_key_down(cx.listener(Self::keydown_handler)) .on_key_up(cx.listener(Self::keyup_handler)), ); if let Some(info) = &self.context_menu { root = root.child(deferred( anchored() .position(point(px(info.crood.x.0 as f32), px(info.crood.y.0 as f32))) .child( div() .child(info.menu.clone()) .shadow_2xl() .on_mouse_down_out( cx.listener(Self::context_menu_mousedown_out_handler), ), ), )); } root.on_action(cx.listener(Self::on_context_menu_action)) .into_any_element() } } impl EntityInputHandler for WebView { fn text_for_range( &mut self, _range: Range, _adjusted_range: &mut Option>, _window: &mut Window, _cx: &mut Context, ) -> Option { None } fn selected_text_range( &mut self, _ignore_disabled_input: bool, _window: &mut Window, _cx: &mut Context, ) -> Option { None } fn marked_text_range( &self, _window: &mut Window, _cx: &mut Context, ) -> Option> { None } fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} fn replace_text_in_range( &mut self, _range: Option>, text: &str, _window: &mut Window, _cx: &mut Context, ) { self.browser().ime_commit(text); } fn replace_and_mark_text_in_range( &mut self, _range: Option>, new_text: &str, new_selected_range: Option>, _window: &mut Window, _cx: &mut Context, ) { let new_selected_range = new_selected_range.unwrap_or_default(); self.browser().ime_set_composition( new_text, new_selected_range.start, new_selected_range.end, ); } fn bounds_for_range( &mut self, _range_utf16: Range, _element_bounds: Bounds, _window: &mut Window, _cx: &mut Context, ) -> Option> { None } fn character_index_for_point( &mut self, _point: gpui::Point, _window: &mut Window, _cx: &mut Context, ) -> Option { None } } macro_rules! impl_emiter { ($($ty:ty),*) => { $( impl EventEmitter<$ty> for WebView {} )* }; } impl_emiter!( LoadingProgressChangedEvent, CreatedEvent, AddressChangedEvent, TitleChangedEvent, TooltipEvent, StatusMessageEvent, ConsoleMessageEvent, BeforePopupEvent, LoadingStateChangedEvent, LoadStartEvent, LoadEndEvent, LoadErrorEvent );