diff --git a/.gitignore b/.gitignore index 519b7e3..c470e98 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ Cargo.lock libmozjs* cargo-sources.json -resources/ -!resources/panel.html -!resources/context-menu.html \ No newline at end of file +resources/* +!resources/components/ +!resources/prefs.json \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5f852af..d6f6213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,8 +106,6 @@ cargo-packager-resource-resolver = { version = "0.1.1", features = [ url = { workspace = true } headers = "0.3" versoview_messages = { path = "./versoview_messages" } - -[target.'cfg(all(unix, not(apple), not(android)))'.dependencies] serde_json = "1.0" serde = { workspace = true } diff --git a/resources/context_menu.html b/resources/components/context_menu.html similarity index 100% rename from resources/context_menu.html rename to resources/components/context_menu.html diff --git a/resources/panel.html b/resources/components/panel.html similarity index 100% rename from resources/panel.html rename to resources/components/panel.html diff --git a/resources/components/prompt/alert.html b/resources/components/prompt/alert.html new file mode 100644 index 0000000..395597e --- /dev/null +++ b/resources/components/prompt/alert.html @@ -0,0 +1,54 @@ + + + + + +
+
+ +
+ + + diff --git a/resources/components/prompt/ok_cancel.html b/resources/components/prompt/ok_cancel.html new file mode 100644 index 0000000..385fce5 --- /dev/null +++ b/resources/components/prompt/ok_cancel.html @@ -0,0 +1,57 @@ + + + + + +
+
+
+ + +
+
+ + + diff --git a/resources/components/prompt/prompt.html b/resources/components/prompt/prompt.html new file mode 100644 index 0000000..14a63f8 --- /dev/null +++ b/resources/components/prompt/prompt.html @@ -0,0 +1,72 @@ + + + + + +
+
+ +
+ + +
+
+ + + diff --git a/resources/components/prompt/prompt_test.html b/resources/components/prompt/prompt_test.html new file mode 100644 index 0000000..228325a --- /dev/null +++ b/resources/components/prompt/prompt_test.html @@ -0,0 +1,30 @@ + + +
+ + + + +
+ +
+ + + + diff --git a/resources/components/prompt/yes_no.html b/resources/components/prompt/yes_no.html new file mode 100644 index 0000000..6072fac --- /dev/null +++ b/resources/components/prompt/yes_no.html @@ -0,0 +1,57 @@ + + + + + +
+
+
+ + +
+
+ + + diff --git a/src/compositor.rs b/src/compositor.rs index efcf7de..e2498f8 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -916,19 +916,6 @@ impl IOCompositor { .expect("Insert then get failed!") } - fn pipeline(&self, pipeline_id: PipelineId) -> Option<&CompositionPipeline> { - match self.pipeline_details.get(&pipeline_id) { - Some(details) => details.pipeline.as_ref(), - None => { - warn!( - "Compositor layer has an unknown pipeline ({:?}).", - pipeline_id - ); - None - } - } - } - /// Set the root pipeline for our WebRender scene to a display list that consists of an iframe /// for each visible top-level browsing context, applying a transformation on the root for /// pinch zoom, page zoom, and HiDPI scaling. @@ -1246,6 +1233,10 @@ impl IOCompositor { w.set_size(content_size); self.on_resize_webview_event(w.webview_id, w.rect); } + if let Some(prompt) = &mut window.prompt { + prompt.resize(content_size); + self.on_resize_webview_event(prompt.webview().webview_id, rect); + } self.send_root_pipeline_display_list(window); } diff --git a/src/config.rs b/src/config.rs index cfdcb05..c587fb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -314,7 +314,9 @@ impl ProtocolHandler for ResourceReader { _done_chan: &mut net::fetch::methods::DoneChannel, _context: &net::fetch::methods::FetchContext, ) -> std::pin::Pin + Send>> { - let path = self.0.join(request.current_url().domain().unwrap()); + let current_url = request.current_url(); + let path = current_url.path(); + let path = self.0.join(path.strip_prefix('/').unwrap_or(path)); let response = if let Ok(file) = fs::read(path) { let mut response = Response::new( diff --git a/src/lib.rs b/src/lib.rs index bdbc716..73c34cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,5 +29,3 @@ pub use errors::{Error, Result}; pub use verso::Verso; /// Re-exporting Winit for the sake of convenience. pub use winit; -/// Context -pub mod context_menu; diff --git a/src/context_menu.rs b/src/webview/context_menu.rs similarity index 97% rename from src/context_menu.rs rename to src/webview/context_menu.rs index f8ae73b..a107337 100644 --- a/src/context_menu.rs +++ b/src/webview/context_menu.rs @@ -131,7 +131,10 @@ impl ContextMenu { /// Get resource URL of the context menu fn resource_url(&self) -> ServoUrl { let items_json: String = self.to_items_json(); - let url_str = format!("verso://context_menu.html?items={}", items_json); + let url_str = format!( + "verso://resources/components/context_menu.html?items={}", + items_json + ); ServoUrl::parse(&url_str).unwrap() } @@ -221,8 +224,9 @@ impl MenuItem { /// Context Menu Click Result #[cfg(linux)] #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ContextMenuResult { - /// The id of the menu ite /// Get the label of the menu item + /// The id of the menu item pub id: String, /// Close the context menu pub close: bool, diff --git a/src/webview/mod.rs b/src/webview/mod.rs new file mode 100644 index 0000000..6e7bf8c --- /dev/null +++ b/src/webview/mod.rs @@ -0,0 +1,7 @@ +mod webview; +/// WebView +pub use webview::{Panel, WebView}; +/// Context Menu +pub mod context_menu; +/// Prompt Dialog +pub mod prompt; diff --git a/src/webview/prompt.rs b/src/webview/prompt.rs new file mode 100644 index 0000000..80b150a --- /dev/null +++ b/src/webview/prompt.rs @@ -0,0 +1,229 @@ +use base::id::WebViewId; +use compositing_traits::ConstellationMsg; +use crossbeam_channel::Sender; +use embedder_traits::{PermissionRequest, PromptResult}; +use ipc_channel::ipc::IpcSender; +use serde::{Deserialize, Serialize}; +use servo_url::ServoUrl; +use webrender_api::units::DeviceIntRect; + +use crate::{verso::send_to_constellation, webview::WebView}; + +/// Prompt Type +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum PromptType { + /// Alert dialog + /// + /// + Alert(String), + /// Confitm dialog, Ok/Cancel + /// + /// + OkCancel(String), + /// Confirm dialog, Yes/No + /// + /// + YesNo(String), + /// Input dialog + /// + /// + Input(String, Option), +} + +/// Prompt Sender, used to send prompt result back to the caller +#[derive(Clone)] +pub enum PromptSender { + /// Alert sender + AlertSender(IpcSender<()>), + /// Ok/Cancel, Yes/No sender + ConfirmSender(IpcSender), + /// Input sender + InputSender(IpcSender>), + /// Yes/No Permission sender + PermissionSender(IpcSender), +} + +/// Prompt input result send from prompt dialog to backend +/// - action: "ok" / "cancel" +/// - value: user input value in input prompt +/// +/// Behavior: +/// - **Ok**: return string, or an empty string if user leave input empty +/// - **Cancel**: return null +/// +/// +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PromptInputResult { + /// User action: "ok" / "cancel" + pub action: String, + /// User input value + pub value: String, +} + +/// Prompt Dialog +#[derive(Clone)] +pub struct PromptDialog { + webview: WebView, + prompt_sender: Option, +} + +impl PromptDialog { + /// New prompt dialog + pub fn new() -> Self { + PromptDialog { + webview: WebView::new(WebViewId::new(), DeviceIntRect::zero()), + prompt_sender: None, + } + } + /// Get prompt webview + pub fn webview(&self) -> &WebView { + &self.webview + } + + /// Get prompt sender. Send user interaction result back to caller. + pub fn sender(&self) -> Option { + self.prompt_sender.clone() + } + + /// Resize prompt webview size with new window context size + /// + /// ## Example: + /// ```rust + /// let rect = window.webview.as_ref().unwrap().rect; + /// let content_size = window.get_content_size(rect); + /// prompt.resize(content_size); + /// ``` + pub fn resize(&mut self, rect: DeviceIntRect) { + self.webview.set_size(rect); + } + + /// Show alert prompt. + /// + /// After you call `alert(..)`, you must call `sender()` to get prompt sender, + /// then send user interaction result back to caller. + /// + /// ## Example + /// + /// ```rust + /// if let Some(PromptSender::AlertSender(sender)) = prompt.sender() { + /// let _ = sender.send(()); + /// } + /// ``` + pub fn alert( + &mut self, + sender: &Sender, + rect: DeviceIntRect, + message: String, + prompt_sender: IpcSender<()>, + ) { + self.prompt_sender = Some(PromptSender::AlertSender(prompt_sender)); + self.show(sender, rect, PromptType::Alert(message)); + } + + /// Show Ok/Cancel confirm prompt + /// + /// After you call `ok_cancel(..)`, you must call `sender()` to get prompt sender, + /// then send user interaction result back to caller. + /// + /// ## Example + /// + /// ```rust + /// if let Some(PromptSender::ConfirmSender(sender)) = prompt.sender() { + /// let _ = sender.send(PromptResult::Primary); + /// } + /// ``` + pub fn ok_cancel( + &mut self, + sender: &Sender, + rect: DeviceIntRect, + message: String, + prompt_sender: IpcSender, + ) { + self.prompt_sender = Some(PromptSender::ConfirmSender(prompt_sender)); + self.show(sender, rect, PromptType::OkCancel(message)); + } + + /// Show Yes/No confirm prompt + /// + /// After you call `yes_no(..)`, you must call `sender()` to get prompt sender, + /// then send user interaction result back to caller. + /// + /// ## Example + /// + /// ```rust + /// let mut prompt = PromptDialog::new(); + /// prompt.yes_no(sender, rect, message, prompt_sender); + /// if let Some(PromptSender::PermissionSender(sender)) = prompt.sender() { + /// let _ = sender.send(PermissionRequest::Granted); + /// } + /// ``` + pub fn yes_no( + &mut self, + sender: &Sender, + rect: DeviceIntRect, + message: String, + prompt_sender: PromptSender, + ) { + self.prompt_sender = Some(prompt_sender); + self.show(sender, rect, PromptType::YesNo(message)); + } + + /// Show input prompt + /// + /// After you call `input(..)`, you must call `sender()` to get prompt sender, + /// then send user interaction result back to caller. + /// + /// ## Example + /// + /// ```rust + /// if let Some(PromptSender::InputSender(sender)) = prompt.sender() { + /// let _ = sender.send(Some("user input value".to_string())); + /// } + /// ``` + pub fn input( + &mut self, + sender: &Sender, + rect: DeviceIntRect, + message: String, + default_value: Option, + prompt_sender: IpcSender>, + ) { + self.prompt_sender = Some(PromptSender::InputSender(prompt_sender)); + self.show(sender, rect, PromptType::Input(message, default_value)); + } + + fn show( + &mut self, + sender: &Sender, + rect: DeviceIntRect, + prompt_type: PromptType, + ) { + self.webview.set_size(rect); + send_to_constellation( + sender, + ConstellationMsg::NewWebView(self.resource_url(prompt_type), self.webview.webview_id), + ); + } + + fn resource_url(&self, prompt_type: PromptType) -> ServoUrl { + let url = match prompt_type { + PromptType::Alert(msg) => { + format!("verso://resources/components/prompt/alert.html?msg={msg}") + } + PromptType::OkCancel(msg) => { + format!("verso://resources/components/prompt/ok_cancel.html?msg={msg}") + } + PromptType::YesNo(msg) => { + format!("verso://resources/components/prompt/yes_no.html?msg={msg}") + } + PromptType::Input(msg, default_value) => { + let mut url = format!("verso://resources/components/prompt/prompt.html?msg={msg}"); + if let Some(default_value) = default_value { + url.push_str(&format!("&defaultValue={}", default_value)); + } + url + } + }; + ServoUrl::parse(&url).unwrap() + } +} diff --git a/src/webview.rs b/src/webview/webview.rs similarity index 68% rename from src/webview.rs rename to src/webview/webview.rs index a870dff..37735ad 100644 --- a/src/webview.rs +++ b/src/webview/webview.rs @@ -2,7 +2,10 @@ use arboard::Clipboard; use base::id::{BrowsingContextId, WebViewId}; use compositing_traits::ConstellationMsg; use crossbeam_channel::Sender; -use embedder_traits::{CompositorEventVariant, EmbedderMsg, PromptDefinition}; +use embedder_traits::{ + CompositorEventVariant, EmbedderMsg, PermissionPrompt, PermissionRequest, PromptDefinition, + PromptResult, +}; use ipc_channel::ipc; use script_traits::{ webdriver_msg::{WebDriverJSResult, WebDriverScriptCommand}, @@ -12,10 +15,15 @@ use servo_url::ServoUrl; use url::Url; use webrender_api::units::DeviceIntRect; -use crate::{compositor::IOCompositor, verso::send_to_constellation, window::Window}; +use crate::{ + compositor::IOCompositor, + verso::send_to_constellation, + webview::prompt::{PromptDialog, PromptInputResult, PromptSender}, + window::Window, +}; #[cfg(linux)] -use crate::context_menu::ContextMenuResult; +use crate::webview::context_menu::ContextMenuResult; /// A web view is an area to display web browsing context. It's what user will treat as a "web page". #[derive(Debug, Clone)] @@ -121,6 +129,7 @@ impl Window { } } EmbedderMsg::HistoryChanged(list, index) => { + self.close_prompt_dialog(); self.update_history(&list, index); let url = list.get(index).unwrap(); if let Some(panel) = self.panel.as_ref() { @@ -146,6 +155,61 @@ impl Window { EmbedderMsg::ShowContextMenu(_sender, _title, _options) => { // TODO: Implement context menu } + EmbedderMsg::Prompt(prompt_type, _origin) => { + let mut prompt = PromptDialog::new(); + let rect = self.webview.as_ref().unwrap().rect; + + match prompt_type { + PromptDefinition::Alert(message, prompt_sender) => { + prompt.alert(sender, rect, message, prompt_sender); + } + PromptDefinition::OkCancel(message, prompt_sender) => { + prompt.ok_cancel(sender, rect, message, prompt_sender); + } + PromptDefinition::YesNo(message, prompt_sender) => { + prompt.yes_no( + sender, + rect, + message, + PromptSender::ConfirmSender(prompt_sender), + ); + } + PromptDefinition::Input(message, default_value, prompt_sender) => { + prompt.input(sender, rect, message, Some(default_value), prompt_sender); + } + } + + // save prompt in window to keep prompt_sender alive + // so that we can send the result back to the prompt after user clicked the button + self.prompt = Some(prompt); + } + EmbedderMsg::PromptPermission(prompt, prompt_sender) => { + let message = match prompt { + PermissionPrompt::Request(permission_name) => { + format!( + "This website would like to request permission for {:?}.", + permission_name + ) + } + PermissionPrompt::Insecure(permission_name) => { + format!( + "This website would like to request permission for {:?}. However current connection is not secure. Do you want to proceed?", + permission_name + ) + } + }; + + let mut prompt = PromptDialog::new(); + let rect = self.webview.as_ref().unwrap().rect; + prompt.yes_no( + sender, + rect, + message, + PromptSender::PermissionSender(prompt_sender), + ); + + self.prompt = Some(prompt); + } e => { log::trace!("Verso WebView isn't supporting this message yet: {e:?}") } @@ -327,4 +391,82 @@ impl Window { } false } + + /// Handle servo messages with prompt. Return true it requests a new window. + pub fn handle_servo_messages_with_prompt( + &mut self, + webview_id: WebViewId, + message: EmbedderMsg, + _sender: &Sender, + _clipboard: Option<&mut Clipboard>, + _compositor: &mut IOCompositor, + ) -> bool { + log::trace!("Verso Prompt {webview_id:?} is handling Embedder message: {message:?}",); + match message { + EmbedderMsg::Prompt(prompt, _origin) => match prompt { + PromptDefinition::Alert(msg, ignored_prompt_sender) => { + let prompt = self.prompt.as_ref().unwrap(); + let prompt_sender = prompt.sender().unwrap(); + + match prompt_sender { + PromptSender::AlertSender(sender) => { + let _ = sender.send(()); + } + PromptSender::ConfirmSender(sender) => { + let result: PromptResult = match msg.as_str() { + "ok" | "yes" => PromptResult::Primary, + "cancel" | "no" => PromptResult::Secondary, + _ => { + log::error!("prompt result message invalid: {msg}"); + PromptResult::Dismissed + } + }; + let _ = sender.send(result); + } + PromptSender::InputSender(sender) => { + if let Ok(PromptInputResult { action, value }) = + serde_json::from_str::(&msg) + { + match action.as_str() { + "ok" => { + let _ = sender.send(Some(value)); + } + "cancel" => { + let _ = sender.send(None); + } + _ => { + log::error!("prompt result message invalid: {msg}"); + let _ = sender.send(None); + } + } + } else { + log::error!("prompt result message invalid: {msg}"); + let _ = sender.send(None); + } + } + PromptSender::PermissionSender(sender) => { + let result: PermissionRequest = match msg.as_str() { + "ok" | "yes" => PermissionRequest::Granted, + "cancel" | "no" => PermissionRequest::Denied, + _ => { + log::error!("prompt result message invalid: {msg}"); + PermissionRequest::Denied + } + }; + let _ = sender.send(result); + } + } + + let _ = ignored_prompt_sender.send(()); + } + _ => { + log::trace!("Verso WebView isn't supporting this prompt yet") + } + }, + e => { + log::trace!("Verso Dialog isn't supporting this message yet: {e:?}") + } + } + false + } } diff --git a/src/window.rs b/src/window.rs index d9bde9a..0ae1fde 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use base::id::WebViewId; use compositing_traits::ConstellationMsg; use crossbeam_channel::Sender; -use embedder_traits::{Cursor, EmbedderMsg}; +use embedder_traits::{Cursor, EmbedderMsg, PermissionRequest, PromptResult}; use euclid::{Point2D, Size2D}; use glutin::{ config::{ConfigTemplateBuilder, GlConfig}, @@ -33,11 +33,14 @@ use winit::{ use crate::{ compositor::{IOCompositor, MouseWindowEvent}, - context_menu::{ContextMenu, Menu}, keyboard::keyboard_event_from_winit, rendering::{gl_config_picker, RenderingContext}, verso::send_to_constellation, - webview::{Panel, WebView}, + webview::{ + context_menu::{ContextMenu, Menu}, + prompt::{PromptDialog, PromptSender}, + Panel, WebView, + }, }; use arboard::Clipboard; @@ -69,6 +72,9 @@ pub struct Window { /// Global menu event receiver for muda crate #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEventReceiver, + + /// Current Prompt + pub(crate) prompt: Option, } impl Window { @@ -123,6 +129,7 @@ impl Window { context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), + prompt: None, }, rendering_context, ) @@ -166,6 +173,7 @@ impl Window { context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), + prompt: None, }; compositor.swap_current_window(&mut window); window @@ -200,7 +208,7 @@ impl Window { }, }); - let url = ServoUrl::parse("verso://panel.html").unwrap(); + let url = ServoUrl::parse("verso://resources/components/panel.html").unwrap(); send_to_constellation( constellation_sender, ConstellationMsg::NewWebView(url, panel_id), @@ -293,6 +301,9 @@ impl Window { match (state, button) { #[cfg(any(target_os = "macos", target_os = "windows"))] (ElementState::Pressed, winit::event::MouseButton::Right) => { + if self.prompt.is_some() { + return; + } self.show_context_menu(); // FIXME: there's chance to lose the event since the channel is async. if let Ok(event) = self.menu_event_receiver.try_recv() { @@ -301,6 +312,9 @@ impl Window { } #[cfg(linux)] (ElementState::Pressed, winit::event::MouseButton::Right) => { + if self.prompt.is_some() { + return; + } if self.context_menu.is_none() { self.context_menu = Some(self.show_context_menu(sender)); return; @@ -445,6 +459,15 @@ impl Window { return false; } } + if let Some(prompt) = &self.prompt { + if prompt.webview().webview_id == webview_id { + self.handle_servo_messages_with_prompt( + webview_id, message, sender, clipboard, compositor, + ); + return false; + } + } + // Handle message in Verso WebView self.handle_servo_messages_with_webview(webview_id, message, sender, clipboard, compositor); false @@ -482,6 +505,14 @@ impl Window { return true; } + if self + .prompt + .as_ref() + .map_or(false, |w| w.webview().webview_id == id) + { + return true; + } + self.panel .as_ref() .map_or(false, |w| w.webview.webview_id == id) @@ -506,6 +537,16 @@ impl Window { return (Some(context_menu.webview().clone()), false); } + if self + .prompt + .as_ref() + .filter(|menu| menu.webview().webview_id == id) + .is_some() + { + let prompt = self.prompt.take().expect("Prompt should exist"); + return (Some(prompt.webview().clone()), false); + } + if self .panel .as_ref() @@ -546,6 +587,10 @@ impl Window { order.push(context_menu.webview()); } + if let Some(prompt) = &self.prompt { + order.push(prompt.webview()); + } + order } @@ -623,7 +668,7 @@ impl Window { #[cfg(linux)] pub(crate) fn show_context_menu(&mut self, sender: &Sender) -> ContextMenu { - use crate::context_menu::MenuItem; + use crate::webview::context_menu::MenuItem; let history_len = self.history.len(); @@ -694,7 +739,7 @@ impl Window { pub(crate) fn handle_context_menu_event( &mut self, sender: &Sender, - event: crate::context_menu::ContextMenuResult, + event: crate::webview::context_menu::ContextMenuResult, ) { self.close_context_menu(sender); match event.id.as_str() { @@ -727,6 +772,29 @@ impl Window { } } +// Prompt methods +impl Window { + /// Close window's prompt dialog + pub(crate) fn close_prompt_dialog(&mut self) { + if let Some(sender) = self.prompt.take().and_then(|prompt| prompt.sender()) { + match sender { + PromptSender::AlertSender(sender) => { + let _ = sender.send(()); + } + PromptSender::ConfirmSender(sender) => { + let _ = sender.send(PromptResult::Dismissed); + } + PromptSender::InputSender(sender) => { + let _ = sender.send(None); + } + PromptSender::PermissionSender(sender) => { + let _ = sender.send(PermissionRequest::Denied); + } + } + } + } +} + // Non-decorated window resizing for Windows and Linux. #[cfg(any(linux, target_os = "windows"))] impl Window {