From 6ca28dd25adf1cf2a1242b8241c786545cc1701c Mon Sep 17 00:00:00 2001 From: Jason Tsai <git@pews.dev> Date: Wed, 4 Dec 2024 20:55:38 +0800 Subject: [PATCH] feat(prompt): add prompt support (#250) * feat(context-menu): create webview on wayland * dynamic generate menu items on html * add context_menu.html * add check mouse hit on context-menu webview * fix: context menu prompt back to verso * feat(context-menu): on linux, handle selection * fix: disble right click on context menu * organize code * adding cfg target linux * fix(linux): shift context menu to avoid overflow, best effort * prompt temp * add alert prompt * refactor: move prompt and context menu to components * remove redundant import * feat(prompt): add ok/cancel, yes/no prompt * feat(prompt): add input prompt dialog * fix: add serde to all platform's dep * refactor: organize verso html files * update gitignore * refactor: remove dialog show method depends on Window * refactor: code clean * fix: don't show context menu when prompt exist * update css * fix(prompt): handle resize when prompt exists * fix(prompt): close prompt when navigate to new url * chore: restore default home page * chore: fix linux mod path, remove unused pipeline fn * feat: handle EmbedderMsg::PromptPermission * refactor: rename components to webview and move WebView into it --- .gitignore | 6 +- Cargo.toml | 2 - resources/{ => components}/context_menu.html | 0 resources/{ => components}/panel.html | 0 resources/components/prompt/alert.html | 54 +++++ resources/components/prompt/ok_cancel.html | 57 +++++ resources/components/prompt/prompt.html | 72 ++++++ resources/components/prompt/prompt_test.html | 30 +++ resources/components/prompt/yes_no.html | 57 +++++ src/compositor.rs | 17 +- src/config.rs | 4 +- src/lib.rs | 2 - src/{ => webview}/context_menu.rs | 8 +- src/webview/mod.rs | 7 + src/webview/prompt.rs | 229 +++++++++++++++++++ src/{ => webview}/webview.rs | 148 +++++++++++- src/window.rs | 80 ++++++- 17 files changed, 741 insertions(+), 32 deletions(-) rename resources/{ => components}/context_menu.html (100%) rename resources/{ => components}/panel.html (100%) create mode 100644 resources/components/prompt/alert.html create mode 100644 resources/components/prompt/ok_cancel.html create mode 100644 resources/components/prompt/prompt.html create mode 100644 resources/components/prompt/prompt_test.html create mode 100644 resources/components/prompt/yes_no.html rename src/{ => webview}/context_menu.rs (97%) create mode 100644 src/webview/mod.rs create mode 100644 src/webview/prompt.rs rename src/{ => webview}/webview.rs (68%) diff --git a/.gitignore b/.gitignore index 519b7e3b..c470e98e 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 5f852afe..d6f6213f 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 00000000..395597e6 --- /dev/null +++ b/resources/components/prompt/alert.html @@ -0,0 +1,54 @@ +<html> + <head> + <style> + body { + font-family: Arial, Helvetica, sans-serif; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + background-color: #7d818644; + } + .dialog { + display: flex; + background: #ffffff; + width: 400px; + min-height: 110px; + max-height: 300px; + flex-direction: column; + align-items: center; + border-radius: 10px; + box-shadow: 0 0 50px #ccc; + box-sizing: border-box; + padding: 8px; + gap: 8px; + } + .msg { + display: inline-block; + width: 100%; + min-height: 90px; + text-align: center; + } + </style> + </head> + <body> + <div class="dialog"> + <div id="msg" class="msg"></div> + <button onclick="sendToVersoAndClose()">Ok</button> + </div> + </body> + <script> + let url = URL.parse(window.location.href); + let msg = url.searchParams.get('msg'); + + // Set dialog message + const msgEl = document.getElementById('msg'); + msgEl.textContent = msg ?? ''; + + function sendToVersoAndClose() { + window.alert(''); // Use as an IPC between Verso and WebView + window.close(); + } + </script> +</html> diff --git a/resources/components/prompt/ok_cancel.html b/resources/components/prompt/ok_cancel.html new file mode 100644 index 00000000..385fce53 --- /dev/null +++ b/resources/components/prompt/ok_cancel.html @@ -0,0 +1,57 @@ +<html> + <head> + <style> + body { + font-family: Arial, Helvetica, sans-serif; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + background-color: #7d818644; + } + .dialog { + display: flex; + background: #ffffff; + width: 400px; + min-height: 110px; + max-height: 300px; + flex-direction: column; + align-items: center; + border-radius: 10px; + box-shadow: 0 0 50px #ccc; + box-sizing: border-box; + padding: 8px; + gap: 8px; + } + .msg { + display: inline-block; + width: 100%; + min-height: 90px; + text-align: center; + } + </style> + </head> + <body> + <div class="dialog"> + <div id="msg" class="msg"></div> + <div class="btn-group"> + <button onclick="sendToVersoAndClose('cancel')">Cancel</button> + <button onclick="sendToVersoAndClose('ok')">Ok</button> + </div> + </div> + </body> + <script> + let url = URL.parse(window.location.href); + let msg = url.searchParams.get('msg'); + + // Set dialog message + const msgEl = document.getElementById('msg'); + msgEl.textContent = msg ?? ''; + + function sendToVersoAndClose(action) { + window.alert(action); // Use as an IPC between Verso and WebView + window.close(); + } + </script> +</html> diff --git a/resources/components/prompt/prompt.html b/resources/components/prompt/prompt.html new file mode 100644 index 00000000..14a63f82 --- /dev/null +++ b/resources/components/prompt/prompt.html @@ -0,0 +1,72 @@ +<html> + <head> + <style> + body { + font-family: Arial, Helvetica, sans-serif; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + background-color: #7d818644; + } + .dialog { + display: flex; + background: #ffffff; + width: 400px; + min-height: 110px; + max-height: 300px; + flex-direction: column; + align-items: center; + border-radius: 10px; + box-shadow: 0 0 50px #ccc; + box-sizing: border-box; + padding: 8px; + gap: 8px; + } + .msg { + display: inline-block; + width: 100%; + min-height: 90px; + text-align: center; + } + </style> + </head> + <body> + <div class="dialog"> + <div id="msg" class="msg"></div> + <input type="text" id="input" /> + <div class="btn-group"> + <button onclick="sendToVersoAndClose('cancel')">Cancel</button> + <button onclick="sendToVersoAndClose('ok')">Ok</button> + </div> + </div> + </body> + <script> + const inputEl = document.getElementById('input'); + const msgEl = document.getElementById('msg'); + + const params = URL.parse(window.location.href).searchParams; + + // Set input default value + const defaultValue = params.get('defaultValue'); + if (typeof defaultValue === 'string' || defaultValue instanceof String) { + inputEl.defaultValue = defaultValue; + } + + // Set dialog message + const msg = params.get('msg'); + msgEl.textContent = msg ?? ''; + + function sendToVersoAndClose(action) { + // Use as an IPC between Verso and WebView + window.alert( + JSON.stringify({ + action, + value: inputEl.value, + }) + ); + window.close(); + } + </script> +</html> diff --git a/resources/components/prompt/prompt_test.html b/resources/components/prompt/prompt_test.html new file mode 100644 index 00000000..228325a4 --- /dev/null +++ b/resources/components/prompt/prompt_test.html @@ -0,0 +1,30 @@ +<html> + <body> + <div style="display: block"> + <button onclick="window.alert('<a> HREF </a>');">alert</button> + <button onclick="sendConfirm();">confirm</button> + <button onclick="sendPrompt('');">prompt</button> + <button onclick="sendPrompt(null, '>> default value >>');"> + prompt with default value + </button> + </div> + + <div style="display: inline-block" id="result"></div> + + <script> + const resultEl = document.getElementById('result'); + + function sendConfirm(text) { + let result = window.confirm(text); + resultEl.textContent = JSON.stringify(result); + console.log(result); + } + + function sendPrompt(text, defaultValue) { + let result = window.prompt(text, defaultValue); + resultEl.textContent = JSON.stringify(result); + console.log(result); + } + </script> + </body> +</html> diff --git a/resources/components/prompt/yes_no.html b/resources/components/prompt/yes_no.html new file mode 100644 index 00000000..6072fac6 --- /dev/null +++ b/resources/components/prompt/yes_no.html @@ -0,0 +1,57 @@ +<html> + <head> + <style> + body { + font-family: Arial, Helvetica, sans-serif; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + background-color: #7d818644; + } + .dialog { + display: flex; + background: #ffffff; + width: 400px; + min-height: 110px; + max-height: 300px; + flex-direction: column; + align-items: center; + border-radius: 10px; + box-shadow: 0 0 50px #ccc; + box-sizing: border-box; + padding: 8px; + gap: 8px; + } + .msg { + display: inline-block; + width: 100%; + min-height: 90px; + text-align: center; + } + </style> + </head> + <body> + <div class="dialog"> + <div id="msg" class="msg"></div> + <div class="btn-group"> + <button onclick="sendToVersoAndClose('no')">No</button> + <button onclick="sendToVersoAndClose('yes')">Yes</button> + </div> + </div> + </body> + <script> + let url = URL.parse(window.location.href); + let msg = url.searchParams.get('msg'); + + // Set dialog message + const msgEl = document.getElementById('msg'); + msgEl.textContent = msg ?? ''; + + function sendToVersoAndClose(action) { + window.alert(action); // Use as an IPC between Verso and WebView + window.close(); + } + </script> +</html> diff --git a/src/compositor.rs b/src/compositor.rs index efcf7de6..e2498f85 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 cfdcb05d..c587fb7a 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<Box<dyn std::future::Future<Output = Response> + 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 bdbc716b..73c34cb0 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 f8ae73b2..a1073377 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 00000000..6e7bf8c5 --- /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 00000000..80b150a0 --- /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 + /// + /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/alert> + Alert(String), + /// Confitm dialog, Ok/Cancel + /// + /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm> + OkCancel(String), + /// Confirm dialog, Yes/No + /// + /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm> + YesNo(String), + /// Input dialog + /// + /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt> + Input(String, Option<String>), +} + +/// 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<PromptResult>), + /// Input sender + InputSender(IpcSender<Option<String>>), + /// Yes/No Permission sender + PermissionSender(IpcSender<PermissionRequest>), +} + +/// 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 +/// +/// <https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt#return_value> +#[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<PromptSender>, +} + +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<PromptSender> { + 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<ConstellationMsg>, + 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<ConstellationMsg>, + rect: DeviceIntRect, + message: String, + prompt_sender: IpcSender<PromptResult>, + ) { + 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<ConstellationMsg>, + 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<ConstellationMsg>, + rect: DeviceIntRect, + message: String, + default_value: Option<String>, + prompt_sender: IpcSender<Option<String>>, + ) { + self.prompt_sender = Some(PromptSender::InputSender(prompt_sender)); + self.show(sender, rect, PromptType::Input(message, default_value)); + } + + fn show( + &mut self, + sender: &Sender<ConstellationMsg>, + 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 a870dff8..37735ad4 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<ConstellationMsg>, + _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::<PromptInputResult>(&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 d9bde9a5..0ae1fdeb 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<PromptDialog>, } 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<ConstellationMsg>) -> 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<ConstellationMsg>, - 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 {