libservo: Clean up interfaces for alert()/confirm()/prompt() (#35579)

Signed-off-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
Delan Azabani 2025-02-27 10:49:08 +08:00 committed by GitHub
parent 03e953e22c
commit 276f6a3ba7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 278 additions and 222 deletions

View file

@ -18,8 +18,8 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
use servo::webrender_api::ScrollLocation;
use servo::{
AllowOrDenyRequest, AuthenticationRequest, FilterPattern, GamepadHapticEffectType, LoadStatus,
PermissionRequest, PromptDefinition, PromptOrigin, PromptResult, Servo, ServoDelegate,
ServoError, TouchEventType, WebView, WebViewDelegate,
PermissionRequest, Servo, ServoDelegate, ServoError, SimpleDialog, TouchEventType, WebView,
WebViewDelegate,
};
use url::Url;
@ -415,36 +415,25 @@ impl WebViewDelegate for RunningAppState {
self.inner().window.request_resize(&webview, new_size);
}
fn show_prompt(
&self,
webview: servo::WebView,
definition: PromptDefinition,
_origin: PromptOrigin,
) {
fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) {
if self.servoshell_preferences.headless {
let _ = match definition {
PromptDefinition::Alert(_message, sender) => sender.send(()),
PromptDefinition::OkCancel(_message, sender) => sender.send(PromptResult::Primary),
PromptDefinition::Input(_message, default, sender) => {
sender.send(Some(default.to_owned()))
},
// TODO: Avoid copying this from the default trait impl?
// Return the DOM-specified default value for when we **cannot show simple dialogs**.
let _ = match dialog {
SimpleDialog::Alert {
response_sender, ..
} => response_sender.send(Default::default()),
SimpleDialog::Confirm {
response_sender, ..
} => response_sender.send(Default::default()),
SimpleDialog::Prompt {
response_sender, ..
} => response_sender.send(Default::default()),
};
return;
}
match definition {
PromptDefinition::Alert(message, sender) => {
let alert_dialog = Dialog::new_alert_dialog(message, sender);
self.add_dialog(webview, alert_dialog);
},
PromptDefinition::OkCancel(message, sender) => {
let okcancel_dialog = Dialog::new_okcancel_dialog(message, sender);
self.add_dialog(webview, okcancel_dialog);
},
PromptDefinition::Input(message, default, sender) => {
let input_dialog = Dialog::new_input_dialog(message, default, sender);
self.add_dialog(webview, input_dialog);
},
}
let dialog = Dialog::new_simple_dialog(dialog);
self.add_dialog(webview, dialog);
}
fn request_authentication(

View file

@ -9,7 +9,10 @@ use egui::Modal;
use egui_file_dialog::{DialogState, FileDialog as EguiFileDialog};
use log::warn;
use servo::ipc_channel::ipc::IpcSender;
use servo::{AuthenticationRequest, FilterPattern, PermissionRequest, PromptResult};
use servo::{
AlertResponse, AuthenticationRequest, ConfirmResponse, FilterPattern, PermissionRequest,
PromptResponse, SimpleDialog,
};
pub enum Dialog {
File {
@ -17,19 +20,8 @@ pub enum Dialog {
multiple: bool,
response_sender: IpcSender<Option<Vec<PathBuf>>>,
},
Alert {
message: String,
sender: IpcSender<()>,
},
OkCancel {
message: String,
sender: IpcSender<PromptResult>,
},
Input {
message: String,
input_text: String,
sender: IpcSender<Option<String>>,
},
#[allow(clippy::enum_variant_names, reason = "spec terminology")]
SimpleDialog(SimpleDialog),
Authentication {
username: String,
password: String,
@ -76,24 +68,8 @@ impl Dialog {
}
}
pub fn new_alert_dialog(message: String, sender: IpcSender<()>) -> Self {
Dialog::Alert { message, sender }
}
pub fn new_okcancel_dialog(message: String, sender: IpcSender<PromptResult>) -> Self {
Dialog::OkCancel { message, sender }
}
pub fn new_input_dialog(
message: String,
default: String,
sender: IpcSender<Option<String>>,
) -> Self {
Dialog::Input {
message,
input_text: default,
sender,
}
pub fn new_simple_dialog(dialog: SimpleDialog) -> Self {
Self::SimpleDialog(dialog)
}
pub fn new_authentication_dialog(authentication_request: AuthenticationRequest) -> Self {
@ -165,9 +141,12 @@ impl Dialog {
DialogState::Closed => false,
}
},
Dialog::Alert { message, sender } => {
Dialog::SimpleDialog(SimpleDialog::Alert {
message,
response_sender,
}) => {
let mut is_open = true;
let modal = Modal::new("alert".into());
let modal = Modal::new("Alert".into());
modal.show(ctx, |ui| {
make_dialog_label(message, ui, None);
egui::Sides::new().show(
@ -176,7 +155,7 @@ impl Dialog {
|ui| {
if ui.button("Close").clicked() {
is_open = false;
if let Err(e) = sender.send(()) {
if let Err(e) = response_sender.send(AlertResponse::Ok) {
warn!("Failed to send alert dialog response: {}", e);
}
}
@ -185,9 +164,12 @@ impl Dialog {
});
is_open
},
Dialog::OkCancel { message, sender } => {
Dialog::SimpleDialog(SimpleDialog::Confirm {
message,
response_sender,
}) => {
let mut is_open = true;
let modal = Modal::new("OkCancel".into());
let modal = Modal::new("Confirm".into());
modal.show(ctx, |ui| {
make_dialog_label(message, ui, None);
egui::Sides::new().show(
@ -196,13 +178,13 @@ impl Dialog {
|ui| {
if ui.button("Ok").clicked() {
is_open = false;
if let Err(e) = sender.send(PromptResult::Primary) {
if let Err(e) = response_sender.send(ConfirmResponse::Ok) {
warn!("Failed to send alert dialog response: {}", e);
}
}
if ui.button("Cancel").clicked() {
is_open = false;
if let Err(e) = sender.send(PromptResult::Secondary) {
if let Err(e) = response_sender.send(ConfirmResponse::Cancel) {
warn!("Failed to send alert dialog response: {}", e);
}
}
@ -211,27 +193,30 @@ impl Dialog {
});
is_open
},
Dialog::Input {
Dialog::SimpleDialog(SimpleDialog::Prompt {
message,
input_text,
sender,
} => {
// The `default` field gets reused as the input buffer.
default: input,
response_sender,
}) => {
let mut is_open = true;
Modal::new("input".into()).show(ctx, |ui| {
make_dialog_label(message, ui, Some(input_text));
Modal::new("Prompt".into()).show(ctx, |ui| {
make_dialog_label(message, ui, Some(input));
egui::Sides::new().show(
ui,
|_ui| {},
|ui| {
if ui.button("Ok").clicked() {
is_open = false;
if let Err(e) = sender.send(Some(input_text.clone())) {
if let Err(e) =
response_sender.send(PromptResponse::Ok(input.clone()))
{
warn!("Failed to send input dialog response: {}", e);
}
}
if ui.button("Cancel").clicked() {
is_open = false;
if let Err(e) = sender.send(None) {
if let Err(e) = response_sender.send(PromptResponse::Cancel) {
warn!("Failed to send input dialog response: {}", e);
}
}

View file

@ -19,10 +19,11 @@ use log::{debug, error, info, warn};
use raw_window_handle::{
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
};
use servo::{LoadStatus, MediaSessionActionType};
use servo::{
AlertResponse, LoadStatus, MediaSessionActionType, PermissionRequest, SimpleDialog, WebView,
};
use simpleservo::{
DeviceIntRect, EventLoopWaker, InitOptions, InputMethodType, MediaSessionPlaybackState,
PromptResult, APP,
DeviceIntRect, EventLoopWaker, InitOptions, InputMethodType, MediaSessionPlaybackState, APP,
};
use super::app_state::{Coordinates, RunningAppState};
@ -461,11 +462,8 @@ impl HostCallbacks {
let jvm = env.get_java_vm().unwrap();
HostCallbacks { callbacks, jvm }
}
}
impl HostTrait for HostCallbacks {
fn prompt_alert(&self, message: String, _trusted: bool) {
debug!("prompt_alert");
fn show_alert(&self, message: String) {
let mut env = self.jvm.get_env().unwrap();
let Ok(string) = new_string_as_jvalue(&mut env, &message) else {
return;
@ -478,20 +476,41 @@ impl HostTrait for HostCallbacks {
)
.unwrap();
}
}
fn prompt_ok_cancel(&self, message: String, _trusted: bool) -> PromptResult {
warn!("Prompt not implemented. Cancelled. {}", message);
PromptResult::Secondary
impl HostTrait for HostCallbacks {
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
warn!("Permissions prompt not implemented. Denied.");
request.deny();
}
fn prompt_yes_no(&self, message: String, _trusted: bool) -> PromptResult {
warn!("Prompt not implemented. Cancelled. {}", message);
PromptResult::Secondary
}
fn prompt_input(&self, message: String, default: String, _trusted: bool) -> Option<String> {
warn!("Input prompt not implemented. {}", message);
Some(default)
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) {
let _ = match dialog {
SimpleDialog::Alert {
message,
response_sender,
} => {
debug!("SimpleDialog::Alert");
// TODO: Indicate that this message is untrusted, and what origin it came from.
self.show_alert(message);
response_sender.send(AlertResponse::Ok)
},
SimpleDialog::Confirm {
message,
response_sender,
} => {
warn!("Confirm dialog not implemented. Cancelled. {}", message);
response_sender.send(Default::default())
},
SimpleDialog::Prompt {
message,
response_sender,
..
} => {
warn!("Prompt dialog not implemented. Cancelled. {}", message);
response_sender.send(Default::default())
},
};
}
fn notify_load_status_changed(&self, load_status: LoadStatus) {

View file

@ -14,7 +14,7 @@ pub use servo::webrender_api::units::DeviceIntRect;
/// and that perform_updates need to be called
pub use servo::EventLoopWaker;
use servo::{self, resources, Servo};
pub use servo::{InputMethodType, MediaSessionPlaybackState, PromptResult, WindowRenderingContext};
pub use servo::{InputMethodType, MediaSessionPlaybackState, WindowRenderingContext};
use crate::egl::android::resources::ResourceReaderInstance;
use crate::egl::app_state::{

View file

@ -22,9 +22,9 @@ use servo::{
AllowOrDenyRequest, ContextMenuResult, EmbedderProxy, EventLoopWaker, ImeEvent, InputEvent,
InputMethodType, Key, KeyState, KeyboardEvent, LoadStatus, MediaSessionActionType,
MediaSessionEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
NavigationRequest, PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
RenderingContext, Servo, ServoDelegate, ServoError, TouchEvent, TouchEventType, TouchId,
WebView, WebViewDelegate, WindowRenderingContext,
NavigationRequest, PermissionRequest, RenderingContext, Servo, ServoDelegate, ServoError,
SimpleDialog, TouchEvent, TouchEventType, TouchId, WebView, WebViewDelegate,
WindowRenderingContext,
};
use url::Url;
@ -198,15 +198,10 @@ impl WebViewDelegate for RunningAppState {
Some(new_webview)
}
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
let message = format!(
"Do you want to grant permission for {:?}?",
request.feature()
);
let result = match self.callbacks.host_callbacks.prompt_yes_no(message, true) {
PromptResult::Primary => request.allow(),
PromptResult::Secondary | PromptResult::Dismissed => request.deny(),
};
fn request_permission(&self, webview: WebView, request: PermissionRequest) {
self.callbacks
.host_callbacks
.request_permission(webview, request);
}
fn request_resize_to(&self, _webview: WebView, size: DeviceIntSize) {
@ -231,21 +226,10 @@ impl WebViewDelegate for RunningAppState {
}
}
fn show_prompt(&self, _webview: WebView, prompt: PromptDefinition, origin: PromptOrigin) {
let cb = &self.callbacks.host_callbacks;
let trusted = origin == PromptOrigin::Trusted;
let _ = match prompt {
PromptDefinition::Alert(message, response_sender) => {
cb.prompt_alert(message, trusted);
response_sender.send(())
},
PromptDefinition::OkCancel(message, response_sender) => {
response_sender.send(cb.prompt_ok_cancel(message, trusted))
},
PromptDefinition::Input(message, default, response_sender) => {
response_sender.send(cb.prompt_input(message, default, trusted))
},
};
fn show_simple_dialog(&self, webview: WebView, dialog: SimpleDialog) {
self.callbacks
.host_callbacks
.show_simple_dialog(webview, dialog);
}
fn show_ime(

View file

@ -3,18 +3,22 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use servo::webrender_api::units::DeviceIntRect;
use servo::{InputMethodType, LoadStatus, MediaSessionPlaybackState, PromptResult};
use servo::{
InputMethodType, LoadStatus, MediaSessionPlaybackState, PermissionRequest, SimpleDialog,
WebView,
};
/// Callbacks. Implemented by embedder. Called by Servo.
/// Callbacks implemented by embedder. Called by our RunningAppState, generally on behalf of Servo.
pub trait HostTrait {
/// Show alert.
fn prompt_alert(&self, msg: String, trusted: bool);
/// Ask Yes/No question.
fn prompt_yes_no(&self, msg: String, trusted: bool) -> PromptResult;
/// Ask Ok/Cancel question.
fn prompt_ok_cancel(&self, msg: String, trusted: bool) -> PromptResult;
/// Ask for string
fn prompt_input(&self, msg: String, default: String, trusted: bool) -> Option<String>;
/// Content in a [`WebView`] is requesting permission to access a feature requiring
/// permission from the user. The embedder should allow or deny the request, either by
/// reading a cached value or querying the user for permission via the user interface.
fn request_permission(&self, _webview: WebView, _: PermissionRequest);
/// Show the user a [simple dialog](https://html.spec.whatwg.org/multipage/#simple-dialogs) (`alert()`, `confirm()`,
/// or `prompt()`). Since their messages are controlled by web content, they should be presented to the user in a
/// way that makes them impossible to mistake for browser UI.
/// TODO: This API needs to be reworked to match the new model of how responses are sent.
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog);
/// Show context menu
fn show_context_menu(&self, title: Option<String>, items: Vec<String>);
/// Notify that the load status of the page has changed.

View file

@ -21,7 +21,10 @@ use napi_ohos::{Env, JsObject, JsString, NapiRaw};
use ohos_ime::{AttachOptions, Ime, ImeProxy, RawTextEditorProxy};
use ohos_ime_sys::types::InputMethod_EnterKeyType;
use servo::style::Zero;
use servo::{InputMethodType, LoadStatus, MediaSessionPlaybackState, PromptResult};
use servo::{
AlertResponse, InputMethodType, LoadStatus, MediaSessionPlaybackState, PermissionRequest,
SimpleDialog, WebView,
};
use simpleservo::EventLoopWaker;
use xcomponent_sys::{
OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent,
@ -671,6 +674,19 @@ impl HostCallbacks {
ime_proxy: RefCell::new(None),
}
}
pub fn show_alert(&self, message: String) {
match PROMPT_TOAST.get() {
Some(prompt_fn) => {
let status = prompt_fn.call(message, ThreadsafeFunctionCallMode::NonBlocking);
if status != napi_ohos::Status::Ok {
// Queue could be full.
error!("show_alert failed with {status}");
}
},
None => error!("PROMPT_TOAST not set. Dropping message {message}"),
}
}
}
struct ServoIme {
@ -698,33 +714,38 @@ impl Ime for ServoIme {
#[allow(unused)]
impl HostTrait for HostCallbacks {
fn prompt_alert(&self, msg: String, _trusted: bool) {
debug!("prompt_alert: {msg}");
match PROMPT_TOAST.get() {
Some(prompt_fn) => {
let status = prompt_fn.call(msg, ThreadsafeFunctionCallMode::NonBlocking);
if status != napi_ohos::Status::Ok {
// Queue could be full.
error!("prompt_alert failed with {status}");
}
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
warn!("Permissions prompt not implemented. Denied.");
request.deny();
}
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) {
let _ = match dialog {
SimpleDialog::Alert {
message,
response_sender,
} => {
debug!("SimpleDialog::Alert");
// TODO: Indicate that this message is untrusted, and what origin it came from.
self.show_alert(message);
response_sender.send(AlertResponse::Ok)
},
None => error!("PROMPT_TOAST not set. Dropping msg {msg}"),
}
}
fn prompt_yes_no(&self, msg: String, trusted: bool) -> PromptResult {
warn!("Prompt not implemented. Cancelled. {}", msg);
PromptResult::Secondary
}
fn prompt_ok_cancel(&self, msg: String, trusted: bool) -> PromptResult {
warn!("Prompt not implemented. Cancelled. {}", msg);
PromptResult::Secondary
}
fn prompt_input(&self, msg: String, default: String, trusted: bool) -> Option<String> {
warn!("Input prompt not implemented. Cancelled. {}", msg);
Some(default)
SimpleDialog::Confirm {
message,
response_sender,
} => {
warn!("Confirm dialog not implemented. Cancelled. {}", message);
response_sender.send(Default::default())
},
SimpleDialog::Prompt {
message,
response_sender,
..
} => {
warn!("Prompt dialog not implemented. Cancelled. {}", message);
response_sender.send(Default::default())
},
};
}
fn show_context_menu(&self, title: Option<String>, items: Vec<String>) {
@ -740,7 +761,7 @@ impl HostTrait for HostCallbacks {
if load_status == LoadStatus::Complete {
#[cfg(feature = "tracing-hitrace")]
let _scope = hitrace::ScopedTrace::start_trace(&c"PageLoadEndedPrompt");
self.prompt_alert("Page finished loading!".to_string(), true);
self.show_alert("Page finished loading!".to_string());
}
}
@ -838,7 +859,7 @@ impl HostTrait for HostCallbacks {
if let Some(bt) = backtrace {
error!("Backtrace: {bt:?}")
}
self.prompt_alert("Servo crashed!".to_string(), true);
self.prompt_alert(reason, true);
self.show_alert("Servo crashed!".to_string());
self.show_alert(reason);
}
}