1use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use egui::Modal;
9use egui_file_dialog::{DialogState, FileDialog as EguiFileDialog};
10use euclid::Length;
11use log::warn;
12use servo::base::generic_channel::GenericSender;
13use servo::servo_geometry::DeviceIndependentPixel;
14use servo::{
15 AlertResponse, AuthenticationRequest, ColorPicker, ConfirmResponse, FilterPattern,
16 PermissionRequest, PromptResponse, RgbColor, SelectElement, SelectElementOption,
17 SelectElementOptionOrOptgroup, SimpleDialog, WebDriverUserPrompt,
18};
19
20#[allow(clippy::large_enum_variant)]
21pub enum Dialog {
22 File {
23 dialog: EguiFileDialog,
24 multiple: bool,
25 response_sender: GenericSender<Option<Vec<PathBuf>>>,
26 },
27 #[allow(clippy::enum_variant_names, reason = "spec terminology")]
28 SimpleDialog(SimpleDialog),
29 Authentication {
30 username: String,
31 password: String,
32 request: Option<AuthenticationRequest>,
33 },
34 Permission {
35 message: String,
36 request: Option<PermissionRequest>,
37 },
38 SelectDevice {
39 devices: Vec<String>,
40 selected_device_index: usize,
41 response_sender: GenericSender<Option<String>>,
42 },
43 SelectElement {
44 maybe_prompt: Option<SelectElement>,
45 toolbar_offset: Length<f32, DeviceIndependentPixel>,
46 },
47 ColorPicker {
48 current_color: egui::Color32,
49 maybe_prompt: Option<ColorPicker>,
50 toolbar_offset: Length<f32, DeviceIndependentPixel>,
51 },
52}
53
54impl Dialog {
55 pub fn new_file_dialog(
56 multiple: bool,
57 response_sender: GenericSender<Option<Vec<PathBuf>>>,
58 patterns: Vec<FilterPattern>,
59 ) -> Self {
60 let mut dialog = EguiFileDialog::new();
61 if !patterns.is_empty() {
62 dialog = dialog
63 .add_file_filter(
64 "All Supported Types",
65 Arc::new(move |path: &Path| {
66 path.extension()
67 .and_then(|e| e.to_str())
68 .is_some_and(|ext| {
69 let ext = ext.to_lowercase();
70 patterns.iter().any(|pattern| ext == pattern.0)
71 })
72 }),
73 )
74 .default_file_filter("All Supported Types");
75 }
76
77 Dialog::File {
78 dialog,
79 multiple,
80 response_sender,
81 }
82 }
83
84 pub fn new_simple_dialog(dialog: SimpleDialog) -> Self {
85 Self::SimpleDialog(dialog)
86 }
87
88 pub fn new_authentication_dialog(authentication_request: AuthenticationRequest) -> Self {
89 Dialog::Authentication {
90 username: String::new(),
91 password: String::new(),
92 request: Some(authentication_request),
93 }
94 }
95
96 pub fn new_permission_request_dialog(permission_request: PermissionRequest) -> Self {
97 let message = format!(
98 "Do you want to grant permission for {:?}?",
99 permission_request.feature()
100 );
101 Dialog::Permission {
102 message,
103 request: Some(permission_request),
104 }
105 }
106
107 pub fn new_device_selection_dialog(
108 devices: Vec<String>,
109 response_sender: GenericSender<Option<String>>,
110 ) -> Self {
111 Dialog::SelectDevice {
112 devices,
113 selected_device_index: 0,
114 response_sender,
115 }
116 }
117
118 pub fn new_select_element_dialog(
119 prompt: SelectElement,
120 toolbar_offset: Length<f32, DeviceIndependentPixel>,
121 ) -> Self {
122 Dialog::SelectElement {
123 maybe_prompt: Some(prompt),
124 toolbar_offset,
125 }
126 }
127
128 pub fn new_color_picker_dialog(
129 prompt: ColorPicker,
130 toolbar_offset: Length<f32, DeviceIndependentPixel>,
131 ) -> Self {
132 let current_color = egui::Color32::from_rgb(
133 prompt.current_color().red,
134 prompt.current_color().green,
135 prompt.current_color().blue,
136 );
137 Dialog::ColorPicker {
138 current_color,
139 maybe_prompt: Some(prompt),
140 toolbar_offset,
141 }
142 }
143
144 pub fn accept(&self) {
145 #[allow(clippy::single_match)]
146 match self {
147 Dialog::SimpleDialog(dialog) => {
148 dialog.accept();
149 },
150 _ => {},
151 }
152 }
153
154 pub fn dismiss(&self) {
155 #[allow(clippy::single_match)]
156 match self {
157 Dialog::SimpleDialog(dialog) => {
158 dialog.dismiss();
159 },
160 _ => {},
161 }
162 }
163
164 pub fn message(&self) -> Option<String> {
165 #[allow(clippy::single_match)]
166 match self {
167 Dialog::SimpleDialog(dialog) => Some(dialog.message().to_string()),
168 _ => None,
169 }
170 }
171
172 pub fn set_message(&mut self, text: String) {
173 if let Dialog::SimpleDialog(dialog) = self {
174 dialog.set_message(text);
175 }
176 }
177
178 pub fn update(&mut self, ctx: &egui::Context) -> bool {
179 match self {
180 Dialog::File {
181 dialog,
182 multiple,
183 response_sender,
184 } => {
185 if dialog.state() == DialogState::Closed {
186 if *multiple {
187 dialog.pick_multiple();
188 } else {
189 dialog.pick_file();
190 }
191 }
192
193 let state = dialog.update(ctx).state();
194 match state {
195 DialogState::Open => true,
196 DialogState::Picked(path) => {
197 if let Err(e) = response_sender.send(Some(vec![path])) {
198 warn!("Failed to send file selection response: {}", e);
199 }
200 false
201 },
202 DialogState::PickedMultiple(paths) => {
203 if let Err(e) = response_sender.send(Some(paths)) {
204 warn!("Failed to send file selection response: {}", e);
205 }
206 false
207 },
208 DialogState::Cancelled => {
209 if let Err(e) = response_sender.send(None) {
210 warn!("Failed to send cancellation response: {}", e);
211 }
212 false
213 },
214 DialogState::Closed => false,
215 }
216 },
217 Dialog::SimpleDialog(SimpleDialog::Alert {
218 message,
219 response_sender,
220 }) => {
221 let mut is_open = true;
222 let modal = Modal::new("Alert".into());
223 modal.show(ctx, |ui| {
224 make_dialog_label(message, ui, None);
225 egui::Sides::new().show(
226 ui,
227 |_ui| {},
228 |ui| {
229 if ui.button("Close").clicked() ||
230 ui.input(|i| i.key_pressed(egui::Key::Escape))
231 {
232 is_open = false;
233 if let Err(e) = response_sender.send(AlertResponse::Ok) {
234 warn!("Failed to send alert dialog response: {}", e);
235 }
236 }
237 },
238 );
239 });
240 is_open
241 },
242 Dialog::SimpleDialog(SimpleDialog::Confirm {
243 message,
244 response_sender,
245 }) => {
246 let mut is_open = true;
247 let modal = Modal::new("Confirm".into());
248 modal.show(ctx, |ui| {
249 make_dialog_label(message, ui, None);
250 egui::Sides::new().show(
251 ui,
252 |_ui| {},
253 |ui| {
254 if ui.button("Ok").clicked() ||
255 ui.input(|i| i.key_pressed(egui::Key::Enter))
256 {
257 is_open = false;
258 if let Err(e) = response_sender.send(ConfirmResponse::Ok) {
259 warn!("Failed to send alert dialog response: {}", e);
260 }
261 }
262 if ui.button("Cancel").clicked() ||
263 ui.input(|i| i.key_pressed(egui::Key::Escape))
264 {
265 is_open = false;
266 if let Err(e) = response_sender.send(ConfirmResponse::Cancel) {
267 warn!("Failed to send alert dialog response: {}", e);
268 }
269 }
270 },
271 );
272 });
273 is_open
274 },
275 Dialog::SimpleDialog(SimpleDialog::Prompt {
276 message,
277 default: input,
279 response_sender,
280 }) => {
281 let mut is_open = true;
282 Modal::new("Prompt".into()).show(ctx, |ui| {
283 make_dialog_label(message, ui, Some(input));
284 egui::Sides::new().show(
285 ui,
286 |_ui| {},
287 |ui| {
288 if ui.button("Ok").clicked() ||
289 ui.input(|i| i.key_pressed(egui::Key::Enter))
290 {
291 is_open = false;
292 if let Err(e) =
293 response_sender.send(PromptResponse::Ok(input.clone()))
294 {
295 warn!("Failed to send input dialog response: {}", e);
296 }
297 }
298 if ui.button("Cancel").clicked() ||
299 ui.input(|i| i.key_pressed(egui::Key::Escape))
300 {
301 is_open = false;
302 if let Err(e) = response_sender.send(PromptResponse::Cancel) {
303 warn!("Failed to send input dialog response: {}", e);
304 }
305 }
306 },
307 );
308 });
309 is_open
310 },
311 Dialog::Authentication {
312 username,
313 password,
314 request,
315 } => {
316 let mut is_open = true;
317 Modal::new("authentication".into()).show(ctx, |ui| {
318 let mut frame = egui::Frame::default().inner_margin(10.0).begin(ui);
319 frame.content_ui.set_min_width(150.0);
320
321 if let Some(request) = request {
322 let url =
323 egui::RichText::new(request.url().origin().unicode_serialization());
324 frame.content_ui.heading(url);
325 }
326
327 frame.content_ui.add_space(10.0);
328
329 frame
330 .content_ui
331 .label("This site is asking you to sign in.");
332 frame.content_ui.add_space(10.0);
333
334 frame.content_ui.label("Username:");
335 frame.content_ui.text_edit_singleline(username);
336 frame.content_ui.add_space(10.0);
337
338 frame.content_ui.label("Password:");
339 frame
340 .content_ui
341 .add(egui::TextEdit::singleline(password).password(true));
342
343 frame.end(ui);
344
345 egui::Sides::new().show(
346 ui,
347 |_ui| {},
348 |ui| {
349 if ui.button("Sign in").clicked() ||
350 ui.input(|i| i.key_pressed(egui::Key::Enter))
351 {
352 let request =
353 request.take().expect("non-None until dialog is closed");
354 request.authenticate(username.clone(), password.clone());
355 is_open = false;
356 }
357 if ui.button("Cancel").clicked() ||
358 ui.input(|i| i.key_pressed(egui::Key::Escape))
359 {
360 is_open = false;
361 }
362 },
363 );
364 });
365 is_open
366 },
367 Dialog::Permission { message, request } => {
368 let mut is_open = true;
369 let modal = Modal::new("permission".into());
370 modal.show(ctx, |ui| {
371 make_dialog_label(message, ui, None);
372 egui::Sides::new().show(
373 ui,
374 |_ui| {},
375 |ui| {
376 if ui.button("Allow").clicked() ||
377 ui.input(|i| i.key_pressed(egui::Key::Enter))
378 {
379 let request =
380 request.take().expect("non-None until dialog is closed");
381 request.allow();
382 is_open = false;
383 }
384 if ui.button("Deny").clicked() ||
385 ui.input(|i| i.key_pressed(egui::Key::Escape))
386 {
387 let request =
388 request.take().expect("non-None until dialog is closed");
389 request.deny();
390 is_open = false;
391 }
392 },
393 );
394 });
395 is_open
396 },
397 Dialog::SelectDevice {
398 devices,
399 selected_device_index,
400 response_sender,
401 } => {
402 let mut is_open = true;
403 let modal = Modal::new("device_picker".into());
404 modal.show(ctx, |ui| {
405 let mut frame = egui::Frame::default().inner_margin(10.0).begin(ui);
406 frame.content_ui.set_min_width(150.0);
407
408 frame.content_ui.heading("Choose a Device");
409 frame.content_ui.add_space(10.0);
410
411 egui::ComboBox::from_label("")
412 .selected_text(&devices[*selected_device_index + 1])
413 .show_ui(&mut frame.content_ui, |ui| {
414 for i in (0..devices.len() - 1).step_by(2) {
415 let device_name = &devices[i + 1];
416 ui.selectable_value(selected_device_index, i, device_name);
417 }
418 });
419
420 frame.end(ui);
421
422 egui::Sides::new().show(
423 ui,
424 |_ui| {},
425 |ui| {
426 if ui.button("Ok").clicked() ||
427 ui.input(|i| i.key_pressed(egui::Key::Enter))
428 {
429 if let Err(e) = response_sender
430 .send(Some(devices[*selected_device_index].clone()))
431 {
432 warn!("Failed to send device selection: {}", e);
433 }
434 is_open = false;
435 }
436 if ui.button("Cancel").clicked() ||
437 ui.input(|i| i.key_pressed(egui::Key::Escape))
438 {
439 if let Err(e) = response_sender.send(None) {
440 warn!("Failed to send cancellation: {}", e);
441 }
442 is_open = false;
443 }
444 },
445 );
446 });
447 is_open
448 },
449 Dialog::SelectElement {
450 maybe_prompt,
451 toolbar_offset,
452 } => {
453 let Some(prompt) = maybe_prompt else {
454 return false;
456 };
457 let mut is_open = true;
458
459 let mut position = prompt.position();
460 position.min.y += toolbar_offset.0 as i32;
461 position.max.y += toolbar_offset.0 as i32;
462 let area = egui::Area::new(egui::Id::new("select-window"))
463 .fixed_pos(egui::pos2(position.min.x as f32, position.max.y as f32));
464
465 let mut selected_option = prompt.selected_option();
466
467 fn display_option(
468 ui: &mut egui::Ui,
469 option: &SelectElementOption,
470 selected_option: &mut Option<usize>,
471 is_open: &mut bool,
472 in_group: bool,
473 ) {
474 let is_checked =
475 selected_option.is_some_and(|selected_index| selected_index == option.id);
476
477 let label_text = if in_group {
479 format!(" {}", option.label)
480 } else {
481 option.label.to_owned()
482 };
483 let label = if option.is_disabled {
484 egui::RichText::new(&label_text).strikethrough()
485 } else {
486 egui::RichText::new(&label_text)
487 };
488 let clickable_area = ui
489 .allocate_ui_with_layout(
490 [ui.available_width(), 0.0].into(),
491 egui::Layout::top_down_justified(egui::Align::LEFT),
492 |ui| ui.selectable_label(is_checked, label),
493 )
494 .inner;
495
496 if clickable_area.clicked() && !option.is_disabled {
497 *selected_option = Some(option.id);
498 *is_open = false;
499 }
500
501 if clickable_area.hovered() && option.is_disabled {
502 ui.ctx().set_cursor_icon(egui::CursorIcon::NotAllowed);
503 }
504
505 if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) {
506 *is_open = false;
507 }
508 }
509
510 let modal = Modal::new("select_element_picker".into()).area(area);
511 let backdrop_response = modal
512 .show(ctx, |ui| {
513 egui::ScrollArea::vertical().show(ui, |ui| {
514 for option_or_optgroup in prompt.options() {
515 match &option_or_optgroup {
516 SelectElementOptionOrOptgroup::Option(option) => {
517 display_option(
518 ui,
519 option,
520 &mut selected_option,
521 &mut is_open,
522 false,
523 );
524 },
525 SelectElementOptionOrOptgroup::Optgroup { label, options } => {
526 ui.label(egui::RichText::new(label).strong());
527
528 for option in options {
529 display_option(
530 ui,
531 option,
532 &mut selected_option,
533 &mut is_open,
534 true,
535 );
536 }
537 },
538 }
539 }
540 });
541 })
542 .backdrop_response;
543
544 if backdrop_response.clicked() {
546 is_open = false;
547 }
548
549 prompt.select(selected_option);
550
551 if !is_open {
552 maybe_prompt.take().unwrap().submit();
553 }
554
555 is_open
556 },
557 Dialog::ColorPicker {
558 current_color,
559 maybe_prompt,
560 toolbar_offset,
561 } => {
562 let Some(prompt) = maybe_prompt else {
563 return false;
565 };
566 let mut is_open = true;
567
568 let mut position = prompt.position();
569 position.min.y += toolbar_offset.0 as i32;
570 position.max.y += toolbar_offset.0 as i32;
571 let area = egui::Area::new(egui::Id::new("select-window"))
572 .fixed_pos(egui::pos2(position.min.x as f32, position.max.y as f32));
573
574 let modal = Modal::new("select_element_picker".into()).area(area);
575 let backdrop_response = modal
576 .show(ctx, |ui| {
577 egui::widgets::color_picker::color_picker_color32(
578 ui,
579 current_color,
580 egui::widgets::color_picker::Alpha::Opaque,
581 );
582
583 ui.add_space(10.);
584
585 if ui.button("Dismiss").clicked() ||
586 ui.input(|i| i.key_pressed(egui::Key::Escape))
587 {
588 is_open = false;
589 prompt.select(None);
590 }
591 if ui.button("Select").clicked() {
592 is_open = false;
593 let selected_color = RgbColor {
594 red: current_color.r(),
595 green: current_color.g(),
596 blue: current_color.b(),
597 };
598 prompt.select(Some(selected_color));
599 }
600 })
601 .backdrop_response;
602
603 if backdrop_response.clicked() {
605 is_open = false;
606 }
607
608 is_open
609 },
610 }
611 }
612
613 pub fn webdriver_diaglog_type(&self) -> WebDriverUserPrompt {
614 match self {
615 Dialog::File { .. } => WebDriverUserPrompt::File,
616 Dialog::SimpleDialog(SimpleDialog::Alert { .. }) => WebDriverUserPrompt::Alert,
617 Dialog::SimpleDialog(SimpleDialog::Confirm { .. }) => WebDriverUserPrompt::Confirm,
618 Dialog::SimpleDialog(SimpleDialog::Prompt { .. }) => WebDriverUserPrompt::Prompt,
619 _ => WebDriverUserPrompt::Default,
620 }
621 }
622}
623
624fn make_dialog_label(message: &str, ui: &mut egui::Ui, input_text: Option<&mut String>) {
625 let mut frame = egui::Frame::default().inner_margin(10.0).begin(ui);
626 frame.content_ui.set_min_width(150.0);
627 frame.content_ui.label(message);
628 if let Some(input_text) = input_text {
629 frame.content_ui.text_edit_singleline(input_text);
630 }
631 frame.end(ui);
632}