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 {