mirror of
https://github.com/tauri-apps/verso.git
synced 2026-01-31 00:55:21 +01:00
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
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,6 +6,6 @@ Cargo.lock
|
||||
libmozjs*
|
||||
cargo-sources.json
|
||||
|
||||
resources/
|
||||
!resources/panel.html
|
||||
!resources/context-menu.html
|
||||
resources/*
|
||||
!resources/components/
|
||||
!resources/prefs.json
|
||||
@@ -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 }
|
||||
|
||||
|
||||
54
resources/components/prompt/alert.html
Normal file
54
resources/components/prompt/alert.html
Normal file
@@ -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>
|
||||
57
resources/components/prompt/ok_cancel.html
Normal file
57
resources/components/prompt/ok_cancel.html
Normal file
@@ -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>
|
||||
72
resources/components/prompt/prompt.html
Normal file
72
resources/components/prompt/prompt.html
Normal file
@@ -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>
|
||||
30
resources/components/prompt/prompt_test.html
Normal file
30
resources/components/prompt/prompt_test.html
Normal file
@@ -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>
|
||||
57
resources/components/prompt/yes_no.html
Normal file
57
resources/components/prompt/yes_no.html
Normal file
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
7
src/webview/mod.rs
Normal file
7
src/webview/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod webview;
|
||||
/// WebView
|
||||
pub use webview::{Panel, WebView};
|
||||
/// Context Menu
|
||||
pub mod context_menu;
|
||||
/// Prompt Dialog
|
||||
pub mod prompt;
|
||||
229
src/webview/prompt.rs
Normal file
229
src/webview/prompt.rs
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user