Skip to main content

servoshell/desktop/
gui.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6#[cfg(any(
7    target_os = "windows",
8    target_os = "linux",
9    target_os = "freebsd",
10    target_os = "macos"
11))]
12use std::fs;
13#[cfg(any(
14    target_os = "windows",
15    target_os = "linux",
16    target_os = "freebsd",
17    target_os = "macos"
18))]
19use std::path::Path;
20use std::rc::Rc;
21use std::sync::Arc;
22
23use accesskit::Affine;
24use dpi::PhysicalSize;
25use egui::text::{CCursor, CCursorRange};
26use egui::text_edit::TextEditState;
27use egui::{
28    Button, FontDefinitions, Id, Key, Label, LayerId, Modifiers, Order, PaintCallback, Panel, Vec2,
29    WidgetInfo, WidgetType, pos2,
30};
31#[cfg(any(
32    target_os = "windows",
33    target_os = "linux",
34    target_os = "freebsd",
35    target_os = "macos"
36))]
37use egui::{FontData, FontFamily};
38use egui_glow::{CallbackFn, EguiGlow};
39use egui_winit::EventResponse;
40use euclid::{Length, Point2D, Rect, Scale, Size2D};
41#[cfg(any(
42    target_os = "windows",
43    target_os = "linux",
44    target_os = "freebsd",
45    target_os = "macos"
46))]
47use log::info;
48use log::warn;
49use servo::{
50    DeviceIndependentPixel, DevicePixel, Image, LoadStatus, OffscreenRenderingContext, PixelFormat,
51    RenderingContext, WebView, WebViewId,
52};
53use url::Url;
54use winit::event::WindowEvent;
55use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
56use winit::window::Window;
57
58use crate::desktop::event_loop::AppEvent;
59use crate::desktop::headed_window;
60use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
61use crate::window::{ServoShellWindow, TopLevelWebViewCreationRequest};
62
63/// The user interface of a headed servoshell. Currently this is implemented via
64/// egui.
65pub struct Gui {
66    rendering_context: Rc<OffscreenRenderingContext>,
67    context: EguiGlow,
68    toolbar_height: Length<f32, DeviceIndependentPixel>,
69
70    location: String,
71
72    /// Whether the location has been edited by the user without clicking Go.
73    location_dirty: bool,
74
75    /// The [`LoadStatus`] of the active `WebView`.
76    load_status: LoadStatus,
77
78    /// The text to display in the status bar on the bottom of the window.
79    status_text: Option<String>,
80
81    /// Whether or not the current `WebView` can navigate backward.
82    can_go_back: bool,
83
84    /// Whether or not the current `WebView` can navigate forward.
85    can_go_forward: bool,
86
87    /// Handle to the GPU texture of the favicon.
88    ///
89    /// These need to be cached across egui draw calls.
90    favicon_textures: HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
91
92    /// AccessKit tree updates pending the next egui tick.
93    /// This allows us to ensure that graft nodes are sent before the subtrees they graft.
94    pending_accesskit_updates: Vec<accesskit::TreeUpdate>,
95}
96
97fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
98    if input.chars().count() > max_length {
99        let truncated: String = input.chars().take(max_length.saturating_sub(1)).collect();
100        format!("{}…", truncated)
101    } else {
102        input.to_string()
103    }
104}
105
106#[cfg(any(
107    target_os = "windows",
108    target_os = "linux",
109    target_os = "freebsd",
110    target_os = "macos"
111))]
112fn load_cjk_fonts(font_candidates: &[(&str, &str)]) -> FontDefinitions {
113    let mut fonts = FontDefinitions::default();
114    let mut loaded_font_names = Vec::new();
115
116    for (path_str, font_name) in font_candidates.iter() {
117        let font_path = Path::new(path_str);
118        if font_path.exists() {
119            match fs::read(font_path) {
120                Ok(bytes) => {
121                    if !fonts.font_data.contains_key(*font_name) {
122                        fonts
123                            .font_data
124                            .insert(font_name.to_string(), Arc::new(FontData::from_owned(bytes)));
125                        loaded_font_names.push(font_name.to_string());
126                        info!("Loaded font: {}", font_name);
127                    }
128                },
129                Err(error) => {
130                    info!("Failed to read font {}: {}", font_name, error);
131                },
132            }
133        }
134    }
135
136    if !loaded_font_names.is_empty() {
137        let proportional = fonts.families.get_mut(&FontFamily::Proportional).unwrap();
138        for font_name in loaded_font_names.iter() {
139            proportional.insert(0, font_name.clone());
140        }
141    }
142
143    fonts
144}
145
146#[cfg(target_os = "windows")]
147fn configure_fonts() -> FontDefinitions {
148    load_cjk_fonts(&[
149        (r"C:\Windows\Fonts\malgun.ttf", "Malgun Gothic"), // Korean
150        (r"C:\Windows\Fonts\msyh.ttc", "Microsoft YaHei"), // Chinese + Japanese
151    ])
152}
153
154#[cfg(any(target_os = "linux", target_os = "freebsd"))]
155fn configure_fonts() -> FontDefinitions {
156    load_cjk_fonts(&[
157        (
158            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
159            "Noto Sans CJK",
160        ), // Ubuntu/Debian
161        (
162            "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
163            "Noto Sans CJK",
164        ), // Fedora/Arch
165        // FreeBSD splits the Noto CJK fonts into regional subsets
166        (
167            "/usr/local/share/fonts/noto/NotoSansCJKhk-Regular.otf",
168            "Noto Sans CJK HK",
169        ),
170        (
171            "/usr/local/share/fonts/noto/NotoSansCJKjp-Regular.otf",
172            "Noto Sans CJK JP",
173        ),
174        (
175            "/usr/local/share/fonts/noto/NotoSansCJKkr-Regular.otf",
176            "Noto Sans CJK KR",
177        ),
178        (
179            "/usr/local/share/fonts/noto/NotoSansCJKsc-Regular.otf",
180            "Noto Sans CJK SC",
181        ),
182        (
183            "/usr/local/share/fonts/noto/NotoSansCJKtc-Regular.otf",
184            "Noto Sans CJK TC",
185        ),
186        (
187            "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
188            "WenQuanYi Micro Hei",
189        ), // common fallback
190        (
191            "/usr/local/share/fonts/wqy/wqy-microhei.ttc",
192            "WenQuanYi Micro Hei",
193        ), // FreeBSD
194    ])
195}
196
197#[cfg(target_os = "macos")]
198fn configure_fonts() -> FontDefinitions {
199    load_cjk_fonts(&[
200        (
201            "/System/Library/Fonts/AppleSDGothicNeo.ttc",
202            "Apple SD Gothic Neo",
203        ), // Korean
204        ("/System/Library/Fonts/STHeiti Medium.ttc", "STHeiti Medium"), // Chinese + Japanese
205    ])
206}
207
208impl Drop for Gui {
209    fn drop(&mut self) {
210        self.rendering_context
211            .make_current()
212            .expect("Could not make window RenderingContext current");
213        self.context.destroy();
214    }
215}
216
217impl Gui {
218    pub(crate) fn new(
219        winit_window: &Window,
220        event_loop: &ActiveEventLoop,
221        event_loop_proxy: EventLoopProxy<AppEvent>,
222        rendering_context: Rc<OffscreenRenderingContext>,
223        initial_url: Url,
224    ) -> Self {
225        rendering_context
226            .make_current()
227            .expect("Could not make window RenderingContext current");
228        let mut context = EguiGlow::new(
229            event_loop,
230            rendering_context.glow_gl_api(),
231            None,
232            None,
233            false,
234        );
235
236        let font_definitions = configure_fonts();
237        context.egui_ctx.set_fonts(font_definitions);
238
239        context
240            .egui_winit
241            .init_accesskit(event_loop, winit_window, event_loop_proxy);
242        winit_window.set_visible(true);
243
244        context.egui_ctx.options_mut(|options| {
245            // Disable the builtin egui handlers for the Ctrl+Plus, Ctrl+Minus and Ctrl+0
246            // shortcuts as they don't work well with servoshell's `device-pixel-ratio` CLI argument.
247            options.zoom_with_keyboard = false;
248
249            // On platforms where winit fails to obtain a system theme, fall back to a light theme
250            // since it is the more common default.
251            options.fallback_theme = egui::Theme::Light;
252        });
253
254        Self {
255            rendering_context,
256            context,
257            toolbar_height: Default::default(),
258            location: initial_url.to_string(),
259            location_dirty: false,
260            load_status: LoadStatus::Complete,
261            status_text: None,
262            can_go_back: false,
263            can_go_forward: false,
264            favicon_textures: Default::default(),
265            pending_accesskit_updates: vec![],
266        }
267    }
268
269    pub(crate) fn has_keyboard_focus(&self) -> bool {
270        self.context
271            .egui_ctx
272            .memory(|memory| memory.focused().is_some())
273    }
274
275    pub(crate) fn surrender_focus(&self) {
276        self.context.egui_ctx.memory_mut(|memory| {
277            if let Some(focused) = memory.focused() {
278                memory.surrender_focus(focused);
279            }
280        });
281    }
282
283    pub(crate) fn on_window_event(
284        &mut self,
285        winit_window: &Window,
286        event: &WindowEvent,
287    ) -> EventResponse {
288        self.context.on_window_event(winit_window, event)
289    }
290
291    /// The height of the top toolbar of this user inteface ie the distance from the top of the
292    /// window to the position of the `WebView`.
293    pub(crate) fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
294        self.toolbar_height
295    }
296
297    /// Return true iff the given position is over the egui toolbar.
298    pub(crate) fn is_in_egui_toolbar_rect(
299        &self,
300        position: Point2D<f32, DeviceIndependentPixel>,
301    ) -> bool {
302        position.y < self.toolbar_height.get()
303    }
304
305    /// Create a frameless button with square sizing, as used in the toolbar.
306    fn toolbar_button(text: &str) -> egui::Button<'_> {
307        egui::Button::new(text)
308            .frame(false)
309            .min_size(Vec2 { x: 20.0, y: 20.0 })
310    }
311
312    /// Draws a browser tab, checking for clicks and queues appropriate [`UserInterfaceCommand`]s.
313    /// Using a custom widget here would've been nice, but it doesn't seem as though egui
314    /// supports that, so we arrange multiple Widgets in a way that they look connected.
315    fn browser_tab(
316        ui: &mut egui::Ui,
317        window: &ServoShellWindow,
318        webview: WebView,
319        favicon_texture: Option<egui::load::SizedTexture>,
320    ) {
321        let label = match (webview.page_title(), webview.url()) {
322            (Some(title), _) if !title.is_empty() => title,
323            (_, Some(url)) => url.to_string(),
324            _ => "New Tab".into(),
325        };
326
327        let inactive_bg_color = ui.visuals().window_fill;
328        let active_bg_color = ui.visuals().widgets.active.weak_bg_fill;
329        let active = window.active_webview().map(|webview| webview.id()) == Some(webview.id());
330
331        // Setup a tab frame that will contain the favicon, title and close button
332        let mut tab_frame = egui::Frame::NONE.corner_radius(4).begin(ui);
333        {
334            tab_frame.content_ui.add_space(5.0);
335
336            let visuals = tab_frame.content_ui.visuals_mut();
337            // Remove the stroke so we don't see the border between the close button and the label
338            visuals.widgets.active.bg_stroke.width = 0.0;
339            visuals.widgets.hovered.bg_stroke.width = 0.0;
340            // Now we make sure the fill color is always the same, irrespective of state, that way
341            // we can make sure that both the label and close button have the same background color
342            visuals.widgets.noninteractive.weak_bg_fill = inactive_bg_color;
343            visuals.widgets.inactive.weak_bg_fill = inactive_bg_color;
344            visuals.widgets.hovered.weak_bg_fill = active_bg_color;
345            visuals.widgets.active.weak_bg_fill = active_bg_color;
346            visuals.selection.bg_fill = active_bg_color;
347            visuals.selection.stroke.color = visuals.widgets.active.fg_stroke.color;
348            visuals.widgets.hovered.fg_stroke.color = visuals.widgets.active.fg_stroke.color;
349
350            // Expansion would also show that they are 2 separate widgets
351            visuals.widgets.active.expansion = 0.0;
352            visuals.widgets.hovered.expansion = 0.0;
353
354            if let Some(favicon) = favicon_texture {
355                tab_frame.content_ui.add(
356                    egui::Image::from_texture(favicon)
357                        .fit_to_exact_size(egui::vec2(16.0, 16.0))
358                        .bg_fill(egui::Color32::TRANSPARENT),
359                );
360            }
361
362            let tab = tab_frame
363                .content_ui
364                .add(Button::selectable(
365                    active,
366                    truncate_with_ellipsis(&label, 20),
367                ))
368                .on_hover_ui(|ui| {
369                    ui.label(&label);
370                });
371
372            let close_button = tab_frame
373                .content_ui
374                .add(egui::Button::new("X").fill(egui::Color32::TRANSPARENT));
375            close_button.widget_info(|| {
376                let mut info = WidgetInfo::new(WidgetType::Button);
377                info.label = Some("Close".into());
378                info
379            });
380            if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
381                window
382                    .queue_user_interface_command(UserInterfaceCommand::CloseWebView(webview.id()));
383            } else if !active && tab.clicked() {
384                window.activate_webview(webview.id());
385            }
386        }
387
388        let response = tab_frame.allocate_space(ui);
389        let fill_color = if active || response.hovered() {
390            active_bg_color
391        } else {
392            inactive_bg_color
393        };
394        tab_frame.frame.fill = fill_color;
395        tab_frame.end(ui);
396    }
397
398    /// Update the user interface, but do not paint the updated state.
399    pub(crate) fn update(
400        &mut self,
401        state: &RunningAppState,
402        window: &ServoShellWindow,
403        headed_window: &headed_window::HeadedWindow,
404    ) {
405        self.rendering_context
406            .make_current()
407            .expect("Could not make RenderingContext current");
408        let Self {
409            rendering_context,
410            context,
411            toolbar_height,
412            location,
413            location_dirty,
414            favicon_textures,
415            ..
416        } = self;
417
418        let winit_window = headed_window.winit_window();
419        context.run(winit_window, |ctx| {
420            load_pending_favicons(ctx, window, favicon_textures);
421
422            // TODO: While in fullscreen add some way to mitigate the increased phishing risk
423            // when not displaying the URL bar: https://github.com/servo/servo/issues/32443
424            // Show toolbar unless fullscreen is from document (web API)
425            if !headed_window.is_fullscreen_from_document() {
426                let frame = egui::Frame::default()
427                    .fill(ctx.style().visuals.window_fill)
428                    .inner_margin(4.0);
429                Panel::top("toolbar").frame(frame).show_inside(ctx, |ui| {
430                    ui.allocate_ui_with_layout(
431                        ui.available_size(),
432                        egui::Layout::left_to_right(egui::Align::Center),
433                        |ui| {
434                            let back_button =
435                                ui.add_enabled(self.can_go_back, Gui::toolbar_button("⏴"));
436                            back_button.widget_info(|| {
437                                let mut info = WidgetInfo::new(WidgetType::Button);
438                                info.label = Some("Back".into());
439                                info
440                            });
441                            if back_button.clicked() {
442                                *location_dirty = false;
443                                window.queue_user_interface_command(UserInterfaceCommand::Back);
444                            }
445
446                            let forward_button =
447                                ui.add_enabled(self.can_go_forward, Gui::toolbar_button("⏵"));
448                            forward_button.widget_info(|| {
449                                let mut info = WidgetInfo::new(WidgetType::Button);
450                                info.label = Some("Forward".into());
451                                info
452                            });
453                            if forward_button.clicked() {
454                                *location_dirty = false;
455                                window.queue_user_interface_command(UserInterfaceCommand::Forward);
456                            }
457
458                            match self.load_status {
459                                LoadStatus::Started | LoadStatus::HeadParsed => {
460                                    let stop_button = ui.add(Gui::toolbar_button("X"));
461                                    stop_button.widget_info(|| {
462                                        let mut info = WidgetInfo::new(WidgetType::Button);
463                                        info.label = Some("Stop".into());
464                                        info
465                                    });
466                                    if stop_button.clicked() {
467                                        warn!("Do not support stop yet.");
468                                    }
469                                },
470                                LoadStatus::Complete => {
471                                    let reload_button = ui.add(Gui::toolbar_button("↻"));
472                                    reload_button.widget_info(|| {
473                                        let mut info = WidgetInfo::new(WidgetType::Button);
474                                        info.label = Some("Reload".into());
475                                        info
476                                    });
477                                    if reload_button.clicked() {
478                                        *location_dirty = false;
479                                        window.queue_user_interface_command(
480                                            UserInterfaceCommand::Reload,
481                                        );
482                                    }
483                                },
484                            }
485                            ui.add_space(2.0);
486
487                            ui.allocate_ui_with_layout(
488                                ui.available_size(),
489                                egui::Layout::right_to_left(egui::Align::Center),
490                                |ui| {
491                                    let mut experimental_preferences_enabled =
492                                        state.experimental_preferences_enabled();
493                                    let prefs_toggle = ui
494                                        .toggle_value(&mut experimental_preferences_enabled, "☢")
495                                        .on_hover_text("Enable experimental prefs");
496                                    prefs_toggle.widget_info(|| {
497                                        let mut info = WidgetInfo::new(WidgetType::Button);
498                                        info.label = Some("Enable experimental preferences".into());
499                                        info.selected = Some(experimental_preferences_enabled);
500                                        info
501                                    });
502                                    if prefs_toggle.clicked() {
503                                        state.set_experimental_preferences_enabled(
504                                            experimental_preferences_enabled,
505                                        );
506                                        *location_dirty = false;
507                                        window.queue_user_interface_command(
508                                            UserInterfaceCommand::ReloadAll,
509                                        );
510                                    }
511
512                                    let location_id = egui::Id::new("location_input");
513                                    let location_field = ui.add_sized(
514                                        ui.available_size(),
515                                        egui::TextEdit::singleline(location)
516                                            .id(location_id)
517                                            .hint_text("Search or enter address"),
518                                    );
519
520                                    if location_field.changed() {
521                                        *location_dirty = true;
522                                    }
523                                    // Handle adddress bar shortcut.
524                                    if ui.input(|i| {
525                                        if cfg!(target_os = "macos") {
526                                            i.clone().consume_key(Modifiers::COMMAND, Key::L)
527                                        } else {
528                                            i.clone().consume_key(Modifiers::COMMAND, Key::L) ||
529                                                i.clone().consume_key(Modifiers::ALT, Key::D)
530                                        }
531                                    }) {
532                                        // The focus request immediately makes gained_focus return true.
533                                        location_field.request_focus();
534                                    }
535                                    // Select address bar text when it's focused (click or shortcut).
536                                    if location_field.gained_focus() &&
537                                        let Some(mut state) =
538                                            TextEditState::load(ui.ctx(), location_id)
539                                    {
540                                        // Select the whole input.
541                                        state.cursor.set_char_range(Some(CCursorRange::two(
542                                            CCursor::new(0),
543                                            CCursor::new(location.len()),
544                                        )));
545                                        state.store(ui.ctx(), location_id);
546                                    }
547                                    // Navigate to address when enter is pressed in the address bar.
548                                    if location_field.lost_focus() &&
549                                        ui.input(|i| i.clone().key_pressed(Key::Enter))
550                                    {
551                                        window.queue_user_interface_command(
552                                            UserInterfaceCommand::Go(location.clone()),
553                                        );
554                                    }
555                                },
556                            );
557                        },
558                    );
559                });
560
561                // A simple Tab header strip
562                let outer = Panel::top("tabs").show_inside(ctx, |ui| {
563                    // Add scroll for overflowing tabs
564                    egui::ScrollArea::horizontal()
565                        .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
566                        .show(ui, |ui| {
567                            ui.allocate_ui_with_layout(
568                                ui.available_size(),
569                                egui::Layout::left_to_right(egui::Align::Center),
570                                |ui| {
571                                    for (id, webview) in window.webviews().into_iter() {
572                                        let favicon = favicon_textures
573                                            .get(&id)
574                                            .map(|(_, favicon)| favicon)
575                                            .copied();
576                                        Self::browser_tab(ui, window, webview, favicon);
577                                    }
578
579                                    let new_tab_button = ui.add(Gui::toolbar_button("+"));
580                                    new_tab_button.widget_info(|| {
581                                        let mut info = WidgetInfo::new(WidgetType::Button);
582                                        info.label = Some("New tab".into());
583                                        info
584                                    });
585                                    if new_tab_button.clicked() {
586                                        window.queue_user_interface_command(
587                                            UserInterfaceCommand::NewWebView,
588                                        );
589                                    }
590
591                                    let new_window_button = ui.add(Gui::toolbar_button("⊞"));
592                                    new_window_button.widget_info(|| {
593                                        let mut info = WidgetInfo::new(WidgetType::Button);
594                                        info.label = Some("New window".into());
595                                        info
596                                    });
597                                    if new_window_button.clicked() {
598                                        let url = Url::parse("servo:newtab").expect("Should be able to unconditionally parse 'servo:newtab' as URL");
599                                        window.queue_user_interface_command(
600                                            UserInterfaceCommand::NewWindow(TopLevelWebViewCreationRequest::WithUrl(url)),
601                                        );
602                                    }
603                                },
604                            );
605                        })
606                });
607
608                *toolbar_height = Length::new(outer.response.rect.max.y);
609            } else {
610                *toolbar_height = Length::default();
611            }
612
613            let scale =
614                Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
615
616            headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx));
617
618            // If the top parts of the GUI changed size, then update the size of the WebView and also
619            // the size of its RenderingContext.
620            let available_rect = ctx.available_rect_before_wrap();
621
622            // Build a graft node for each WebView.
623            let affine = {
624                // The grafted WebView tree reports bounds in device pixels relative to the
625                // WebView's own origin, so this node supplies the offset of the WebView within
626                // the window, and scales it to the same scale as the rest of the nodes in egui's
627                // AccessKit tree.
628                let scale = (1.0 / window.platform_window().hidpi_scale_factor().get()) as f64;
629                let x = available_rect.min.x as f64;
630                let y = available_rect.min.y as f64;
631                Affine::new([scale, 0.0, 0.0, scale, x, y])
632            };
633            for (webview_id, webview) in window.webviews() {
634                if let Some(tree_id) = webview.accesskit_tree_id() {
635                    let id = egui::Id::new(webview_id);
636                    ctx.accesskit_node_builder(id, |node| {
637                        node.set_tree_id(tree_id);
638                        // Only the transform is set: AccessKit consumers exclude graft nodes from
639                        // the presented tree, so bounds on this node would never be read.
640                        node.set_transform(affine);
641                    });
642                }
643            }
644            let size = Size2D::new(available_rect.width(), available_rect.height()) * scale;
645            if let Some(webview) = window.active_webview() &&
646                size != webview.size()
647            {
648                // `rect` is sized to just the WebView viewport, which is required by
649                // `OffscreenRenderingContext` See:
650                // <https://github.com/servo/servo/issues/38369#issuecomment-3138378527>
651                webview.resize(PhysicalSize::new(size.width as u32, size.height as u32))
652            }
653
654            if let Some(status_text) = &self.status_text {
655                egui::Tooltip::always_open(
656                    ctx.clone(),
657                    LayerId::new(Order::Tooltip, Id::new("tooltip")),
658                    "tooltip layer".into(),
659                    pos2(0.0, available_rect.max.y),
660                )
661                .show(|ui| ui.add(Label::new(status_text.clone()).extend()));
662            }
663
664            window.repaint_webviews();
665
666            if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
667                ctx.layer_painter(LayerId::background()).add(PaintCallback {
668                    rect: available_rect,
669                    callback: Arc::new(CallbackFn::new(move |info, painter| {
670                        let clip = info.viewport_in_pixels();
671                        let rect_in_parent = Rect::new(
672                            Point2D::new(clip.left_px, clip.from_bottom_px),
673                            Size2D::new(clip.width_px, clip.height_px),
674                        );
675                        render_to_parent(painter.gl(), rect_in_parent)
676                    })),
677                });
678            }
679        });
680
681        // If any egui widget requested a repaint, also request a repaint for our
682        // containing window. This allows egui widget to animate on their own.
683        if self.context.egui_ctx.has_requested_repaint() {
684            window.set_needs_repaint();
685        }
686
687        let adapter = self
688            .context
689            .egui_winit
690            .accesskit
691            .as_mut()
692            .expect("guaranteed by Gui::new()");
693        for tree_update in self.pending_accesskit_updates.drain(..) {
694            adapter.update_if_active(|| tree_update);
695        }
696    }
697
698    /// Paint the GUI, as of the last update.
699    pub(crate) fn paint(&mut self, window: &Window) {
700        self.rendering_context
701            .make_current()
702            .expect("Could not make RenderingContext current");
703        self.rendering_context
704            .parent_context()
705            .prepare_for_rendering();
706        self.context.paint(window);
707        self.rendering_context.parent_context().present();
708    }
709
710    /// Updates the location field from the given [`RunningAppState`], unless the user has started
711    /// editing it without clicking Go, returning true iff it has changed (needing an egui update).
712    fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool {
713        // User edited without clicking Go?
714        if self.location_dirty {
715            return false;
716        }
717
718        let current_url_string = window
719            .active_webview()
720            .and_then(|webview| Some(webview.url()?.to_string()));
721        match current_url_string {
722            Some(location) if location != self.location => {
723                self.location = location;
724                true
725            },
726            _ => false,
727        }
728    }
729
730    fn update_load_status(&mut self, window: &ServoShellWindow) -> bool {
731        let state_status = window
732            .active_webview()
733            .map(|webview| webview.load_status())
734            .unwrap_or(LoadStatus::Complete);
735        let old_status = std::mem::replace(&mut self.load_status, state_status);
736        let status_changed = old_status != self.load_status;
737
738        // When the load status changes, we want the new changes to the URL to start
739        // being reflected in the location bar.
740        if status_changed {
741            self.location_dirty = false;
742        }
743
744        status_changed
745    }
746
747    fn update_status_text(&mut self, window: &ServoShellWindow) -> bool {
748        let state_status = window
749            .active_webview()
750            .and_then(|webview| webview.status_text());
751        let old_status = std::mem::replace(&mut self.status_text, state_status);
752        old_status != self.status_text
753    }
754
755    fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool {
756        let (can_go_back, can_go_forward) = window
757            .active_webview()
758            .map(|webview| (webview.can_go_back(), webview.can_go_forward()))
759            .unwrap_or((false, false));
760        let old_can_go_back = std::mem::replace(&mut self.can_go_back, can_go_back);
761        let old_can_go_forward = std::mem::replace(&mut self.can_go_forward, can_go_forward);
762        old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward
763    }
764
765    /// Updates all fields taken from the given [`ServoShellWindow`], such as the location field.
766    /// Returns true iff the egui needs an update.
767    pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool {
768        // Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
769        //       because logical OR would short-circuit if any of the functions return true.
770        //       We want to ensure that all functions are called. The "bitwise OR" operator
771        //       does not short-circuit.
772        self.update_load_status(window) |
773            self.update_location_in_toolbar(window) |
774            self.update_status_text(window) |
775            self.update_can_go_back_and_forward(window)
776    }
777
778    /// Returns true if a redraw is required after handling the provided event.
779    pub(crate) fn handle_accesskit_event(
780        &mut self,
781        event: &egui_winit::accesskit_winit::WindowEvent,
782    ) -> bool {
783        match event {
784            egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
785                self.context.egui_ctx.enable_accesskit();
786                true
787            },
788            egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
789                self.context
790                    .egui_winit
791                    .on_accesskit_action_request(req.clone());
792                true
793            },
794            egui_winit::accesskit_winit::WindowEvent::AccessibilityDeactivated => {
795                self.context.egui_ctx.disable_accesskit();
796                false
797            },
798        }
799    }
800
801    pub(crate) fn set_zoom_factor(&self, factor: f32) {
802        self.context.egui_ctx.set_zoom_factor(factor);
803    }
804
805    pub(crate) fn notify_accessibility_tree_update(&mut self, tree_update: accesskit::TreeUpdate) {
806        self.pending_accesskit_updates.push(tree_update);
807    }
808}
809
810fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {
811    let width = image.width as usize;
812    let height = image.height as usize;
813
814    match image.format {
815        PixelFormat::K8 => egui::ColorImage::from_gray([width, height], image.data()),
816        PixelFormat::KA8 => {
817            // Convert to rgba
818            let data: Vec<u8> = image
819                .data()
820                .chunks_exact(2)
821                .flat_map(|pixel| [pixel[0], pixel[0], pixel[0], pixel[1]])
822                .collect();
823            egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
824        },
825        PixelFormat::RGB8 => egui::ColorImage::from_rgb([width, height], image.data()),
826        PixelFormat::RGBA8 => {
827            egui::ColorImage::from_rgba_unmultiplied([width, height], image.data())
828        },
829        PixelFormat::BGRA8 => {
830            // Convert from BGRA to RGBA
831            let data: Vec<u8> = image
832                .data()
833                .chunks_exact(4)
834                .flat_map(|chunk| [chunk[2], chunk[1], chunk[0], chunk[3]])
835                .collect();
836            egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
837        },
838    }
839}
840
841/// Uploads all favicons that have not yet been processed to the GPU.
842fn load_pending_favicons(
843    ctx: &egui::Context,
844    window: &ServoShellWindow,
845    texture_cache: &mut HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
846) {
847    for id in window.take_pending_favicon_loads() {
848        let Some(webview) = window.webview_by_id(id) else {
849            continue;
850        };
851        let Some(favicon) = webview.favicon() else {
852            continue;
853        };
854
855        let egui_image = embedder_image_to_egui_image(&favicon);
856        let handle = ctx.load_texture(format!("favicon-{id:?}"), egui_image, Default::default());
857        let texture = egui::load::SizedTexture::new(
858            handle.id(),
859            egui::vec2(favicon.width as f32, favicon.height as f32),
860        );
861
862        // We don't need the handle anymore but we can't drop it either since that would cause
863        // the texture to be freed.
864        texture_cache.insert(id, (handle, texture));
865    }
866}