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