servoshell: Migrate to egui-file-dialog from tinyfiledialogs (#34823)

This is the first step toward completely replacing tinyfiledialogs with
an egui-based solution.

Signed-off-by: L Ashwin B <lashwinib@gmail.com>
This commit is contained in:
chickenleaf 2025-02-04 23:54:24 +05:30 committed by GitHub
parent e41b34a1bf
commit 62f1dbebff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 261 additions and 70 deletions

View file

@ -122,6 +122,7 @@ serde_json = { workspace = true }
shellwords = "1.0.0"
surfman = { workspace = true, features = ["sm-x11", "sm-raw-window-handle-06"] }
tinyfiledialogs = "3.0"
egui-file-dialog = "0.8.0"
winit = "0.30.8"
[target.'cfg(any(all(target_os = "linux", not(target_env = "ohos")), target_os = "windows"))'.dependencies]

View file

@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::path::{Path, PathBuf};
use std::sync::Arc;
use egui_file_dialog::{DialogState, FileDialog as EguiFileDialog};
use log::warn;
use servo::ipc_channel::ipc::IpcSender;
use servo::FilterPattern;
#[derive(Debug)]
pub struct FileDialog {
dialog: EguiFileDialog,
multiple: bool,
response_sender: IpcSender<Option<Vec<PathBuf>>>,
}
#[derive(Debug)]
pub enum Dialog {
File(FileDialog),
}
impl Dialog {
pub fn new_file_dialog(
multiple: bool,
response_sender: IpcSender<Option<Vec<PathBuf>>>,
patterns: Vec<FilterPattern>,
) -> Self {
let mut dialog = EguiFileDialog::new();
if !patterns.is_empty() {
dialog = dialog
.add_file_filter(
"All Supported Types",
Arc::new(move |path: &Path| {
path.extension()
.and_then(|e| e.to_str())
.map_or(false, |ext| {
let ext = ext.to_lowercase();
patterns.iter().any(|pattern| ext == pattern.0)
})
}),
)
.default_file_filter("All Supported Types");
}
let dialog = FileDialog {
dialog,
multiple,
response_sender,
};
Dialog::File(dialog)
}
pub fn update(&mut self, ctx: &egui::Context) -> bool {
match self {
Dialog::File(dialog) => {
if dialog.dialog.state() == DialogState::Closed {
if dialog.multiple {
dialog.dialog.pick_multiple();
} else {
dialog.dialog.pick_file();
}
}
let state = dialog.dialog.update(ctx).state();
match state {
DialogState::Open => true,
DialogState::Selected(path) => {
if let Err(e) = dialog.response_sender.send(Some(vec![path])) {
warn!("Failed to send file selection response: {}", e);
}
false
},
DialogState::SelectedMultiple(paths) => {
if let Err(e) = dialog.response_sender.send(Some(paths)) {
warn!("Failed to send file selection response: {}", e);
}
false
},
DialogState::Cancelled => {
if let Err(e) = dialog.response_sender.send(None) {
warn!("Failed to send cancellation response: {}", e);
}
false
},
DialogState::Closed => false,
}
},
}
}
}

View file

@ -391,6 +391,10 @@ impl Minibrowser {
return;
};
egui::CentralPanel::default().show(ctx, |_| {
webview.update(ctx);
});
CentralPanel::default()
.frame(Frame::none())
.show(ctx, |ui| {

View file

@ -6,6 +6,7 @@
pub(crate) mod app;
pub(crate) mod cli;
mod dialog;
mod egui_glue;
mod embedder;
pub(crate) mod events_loop;

View file

@ -5,6 +5,7 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::rc::Rc;
use std::{env, thread};
@ -29,6 +30,7 @@ use servo::{
};
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo};
use super::dialog::Dialog;
use super::keyutils::CMD_OR_CONTROL;
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
use crate::desktop::tracing::trace_embedder_msg;
@ -62,6 +64,7 @@ pub struct WebView {
pub focused: bool,
pub load_status: LoadStatus,
pub servo_webview: ::servo::WebView,
dialogs: Vec<Dialog>,
}
impl WebView {
@ -73,8 +76,23 @@ impl WebView {
focused: false,
load_status: LoadStatus::Complete,
servo_webview,
dialogs: vec![],
}
}
pub fn add_file_dialog(
&mut self,
multiple: bool,
response_sender: IpcSender<Option<Vec<PathBuf>>>,
patterns: Vec<FilterPattern>,
) {
self.dialogs
.push(Dialog::new_file_dialog(multiple, response_sender, patterns));
}
pub fn update(&mut self, ctx: &egui::Context) {
self.dialogs.retain_mut(|dialog| dialog.update(ctx));
}
}
pub struct ServoEventResponse {
@ -173,6 +191,15 @@ impl WebViewManager {
self.status_text.clone()
}
pub fn has_pending_file_dialog(&self) -> bool {
self.focused_webview().map_or(false, |webview| {
webview
.dialogs
.iter()
.any(|dialog| matches!(dialog, Dialog::File(_)))
})
}
// Returns the webviews in the creation order.
pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> {
let mut res = vec![];
@ -417,7 +444,8 @@ impl WebViewManager {
opts: &Opts,
messages: Vec<EmbedderMsg>,
) -> ServoEventResponse {
let mut need_present = self.load_status() != LoadStatus::Complete;
let mut need_present =
self.load_status() != LoadStatus::Complete || self.has_pending_file_dialog();
let mut need_update = false;
for message in messages {
trace_embedder_msg!(message, "{message:?}");
@ -662,16 +690,10 @@ impl WebViewManager {
};
},
EmbedderMsg::SelectFiles(webview_id, patterns, multiple_files, sender) => {
let result = match (opts.headless, get_selected_files(patterns, multiple_files))
{
(true, _) | (false, None) => sender.send(None),
(false, Some(files)) => sender.send(Some(files)),
};
if let Err(e) = result {
self.send_error(
webview_id,
format!("Failed to send SelectFiles response: {e}"),
);
if let Some(webview) = self.get_mut(webview_id) {
webview.add_file_dialog(multiple_files, sender, patterns);
need_update = true;
need_present = true;
};
},
EmbedderMsg::PromptPermission(_, prompt, sender) => {
@ -863,39 +885,6 @@ fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
None
}
fn get_selected_files(patterns: Vec<FilterPattern>, multiple_files: bool) -> Option<Vec<String>> {
let picker_name = if multiple_files {
"Pick files"
} else {
"Pick a file"
};
thread::Builder::new()
.name("FilePicker".to_owned())
.spawn(move || {
let mut filters = vec![];
for p in patterns {
let s = "*.".to_string() + &p.0;
filters.push(tiny_dialog_escape(&s))
}
let filter_ref = &(filters.iter().map(|s| s.as_str()).collect::<Vec<&str>>()[..]);
let filter_opt = if !filters.is_empty() {
Some((filter_ref, ""))
} else {
None
};
if multiple_files {
tinyfiledialogs::open_file_dialog_multi(picker_name, "", filter_opt)
} else {
let file = tinyfiledialogs::open_file_dialog(picker_name, "", filter_opt);
file.map(|x| vec![x])
}
})
.unwrap()
.join()
.expect("Thread spawning failed")
}
// This is a mitigation for #25498, not a verified solution.
// There may be codepaths in tinyfiledialog.c that this is
// inadquate against, as it passes the string via shell to