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