From 2778beeb7a623e0150e5084da14afa884b76f656 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Tue, 15 Aug 2023 08:08:50 +0000 Subject: winit: initial minibrowser (#29976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * winit: add minibrowser feature that depends on egui{,-winit} * winit: carve out some space at the top of headed windows * winit: minimal toolbar and egui/winit integration (but no painting) * winit: try to paint with egui_glow (doesn’t work yet) * winit: add comment about toolbar size * Add framebuffer object, set it as glow's target * compositing: clear only the viewport, not the whole framebuffer * plumb the actual size of the egui toolbar to webrender * fix formatting * winit: fix crash when fbo is zero * winit: don’t bother binding the framebuffer object * winit: remove unsafe and get toolbar_height * winit: location field should reflect the current top-level url * [NFC] winit: move Minibrowser out of App::run * winit: clean up toolbar height code * winit: make App own the Minibrowser if any * winit: make the go button work * winit:make the location field reflect the current top-level url * winit: allow enabling minibrowser from command line * winit: tell compositor to repaint WR and flush when we repaint * winit: fix bug where location field edits would get overridden * winit: borrow the minibrowser once in App::handle_events * winit: address todo about viewport origin coordinates * winit: fix some minor problems with comments and errors * winit: update location field once per HistoryChanged event * winit: rename Window::set_toolbar_size to set_toolbar_height * winit: take toolbar height into account in hit testing * winit: pass egui only relevant CursorMoved events * winit: scratch that, coalesce minibrowser updates instead * ensure both minibrowser and WR are repainted on every frame * compositing: only skip framebuffer clear in external present mode * winit: destroy egui glow Painter when shutting down * winit: clean up and fix license lint * fix duplicate versions lint by downgrading bytemuck_derive was egui_glow ^0.22.0 (0.22.0) → egui/bytemuck ^0.22.0 (0.22.0) → epaint/bytemuck ^0.22.0 (0.22.0) → bytemuck ^1.7.2 (1.13.1) → bytemuck_derive ^1.4 (1.4.1) → syn ^2.0.1 (2.0.28) now lock has bytemuck_derive 1.4.0 → syn ^1.0.99 (1.0.103) * fix duplicate versions lint by disabling egui-winit/links (we don’t need support for hyperlinks in our use of egui) * squelch duplicate versions lint by excluding clipboard-win * winit: fix compile warnings * winit: make gleam an optional dependency under /minibrowser * winit: remove cargo feature, since it’s not really optional * winit: extract Minibrowser and related code to separate module * winit: remove unnecessary trailing comma * winit: simplify the ServoUrl serialisation optimisation --------- Co-authored-by: atbrakhi --- Cargo.lock | 224 +++++++++++++++++++++++++++++- components/compositing/compositor.rs | 47 ++++++- components/compositing/lib.rs | 3 + components/constellation/constellation.rs | 6 + components/embedder_traits/lib.rs | 3 + components/servo/lib.rs | 8 ++ ports/winit/Cargo.toml | 5 + ports/winit/app.rs | 202 ++++++++++++++++++++------- ports/winit/browser.rs | 21 ++- ports/winit/egui_glue.rs | 129 +++++++++++++++++ ports/winit/headed_window.rs | 25 +++- ports/winit/headless_window.rs | 8 ++ ports/winit/main2.rs | 2 + ports/winit/minibrowser.rs | 110 +++++++++++++++ ports/winit/window_trait.rs | 2 + servo-tidy.toml | 3 + 16 files changed, 737 insertions(+), 61 deletions(-) create mode 100644 ports/winit/egui_glue.rs create mode 100644 ports/winit/minibrowser.rs diff --git a/Cargo.lock b/Cargo.lock index 2ee570a3e95..910e8e4c197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -145,6 +156,24 @@ dependencies = [ "serde", ] +[[package]] +name = "arboard" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" +dependencies = [ + "clipboard-win 4.5.0", + "log", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "parking_lot 0.12.0", + "thiserror", + "winapi", + "x11rb", +] + [[package]] name = "array-init" version = "0.1.1" @@ -555,6 +584,20 @@ name = "bytemuck" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aca418a974d83d40a0c1f0c5cba6ff4bc28d8df099109ca459a2118d40b6322" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -784,7 +827,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" dependencies = [ - "clipboard-win", + "clipboard-win 2.2.0", "objc", "objc-foundation", "objc_id", @@ -800,6 +843,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "cmake" version = "0.1.50" @@ -1453,12 +1507,73 @@ dependencies = [ "wio", ] +[[package]] +name = "ecolor" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e479a7fa3f23d4e794f8b2f8b3568dd4e47886ad1b12c9c095e141cb591eb63" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "egui" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aef8ec3ae1b772f340170c65bf27d5b8c28f543a0116c844d2ac08d01123e7" +dependencies = [ + "ahash", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-winit" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a49155fd4a0a4fb21224407a91de0030847972ef90fc64edb63621caea61cb2" +dependencies = [ + "arboard", + "egui", + "instant", + "log", + "raw-window-handle 0.5.0", + "smithay-clipboard", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8c2752cdf1b0ef5fcda59a898cacabad974d4f5880e92a420b2c917022da64" +dependencies = [ + "bytemuck", + "egui", + "egui-winit", + "glow", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "emath" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3857d743a6e0741cdd60b622a74c7a36ea75f5f8f11b793b41d905d2c9721a4b" +dependencies = [ + "bytemuck", +] + [[package]] name = "embedder_traits" version = "0.0.1" @@ -1537,6 +1652,23 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epaint" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09333964d4d57f40a85338ba3ca5ed4716070ab184dcfed966b35491c5c64f3b" +dependencies = [ + "ab_glyph", + "ahash", + "atomic_refcell", + "bytemuck", + "ecolor", + "emath", + "log", + "nohash-hasher", + "parking_lot 0.12.0", +] + [[package]] name = "errno" version = "0.3.0" @@ -1558,6 +1690,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "euclid" version = "0.22.7" @@ -1892,6 +2034,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getopts" version = "0.2.17" @@ -2201,6 +2353,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glslopt" version = "0.1.9" @@ -3958,6 +4122,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -5284,8 +5454,13 @@ dependencies = [ "backtrace", "cc", "clipboard", + "egui", + "egui-winit", + "egui_glow", "euclid", "getopts", + "gleam", + "glow", "image 0.24.6", "keyboard-types", "lazy_static", @@ -5790,6 +5965,16 @@ dependencies = [ "wayland-protocols", ] +[[package]] +name = "smithay-clipboard" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] + [[package]] name = "socket2" version = "0.4.9" @@ -5873,6 +6058,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strict-num" version = "0.1.0" @@ -7155,6 +7346,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -7336,6 +7536,28 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname", + "nix 0.24.3", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", +] + [[package]] name = "xcb" version = "0.8.2" diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 67cecd6a4a0..8eed1ed3f66 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -230,6 +230,13 @@ pub struct IOCompositor { /// taken before the render is complete will not reflect the /// most up to date rendering. waiting_on_pending_frame: bool, + + /// Whether to send a ReadyToPresent message to the constellation after rendering a new frame, + /// allowing external code to draw to the framebuffer and decide when to present the frame. + external_present: bool, + + /// Waiting for external code to call present. + waiting_on_present: bool, } #[derive(Clone, Copy)] @@ -382,6 +389,8 @@ impl IOCompositor { exit_after_load, convert_mouse_to_touch, waiting_on_pending_frame: false, + external_present: false, + waiting_on_present: false, } } @@ -1525,6 +1534,12 @@ impl IOCompositor { target: CompositeTarget, rect: Option>, ) -> Result, UnableToComposite> { + if self.waiting_on_present { + return Err(UnableToComposite::NotReadyToPaintImage( + NotReadyToPaint::WaitingOnConstellation, + )); + } + let size = self.embedder_coordinates.framebuffer.to_u32(); if let Err(err) = self.webrender_surfman.make_gl_context_current() { @@ -1703,8 +1718,15 @@ impl IOCompositor { }; // Perform the page flip. This will likely block for a while. - if let Err(err) = self.webrender_surfman.present() { - warn!("Failed to present surface: {:?}", err); + if self.external_present { + self.waiting_on_present = true; + let msg = + ConstellationMsg::ReadyToPresent(self.root_pipeline.top_level_browsing_context_id); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } + } else { + self.present(); } self.composition_request = CompositionRequest::NoCompositingNecessary; @@ -1715,6 +1737,13 @@ impl IOCompositor { Ok(rv) } + pub fn present(&mut self) { + if let Err(err) = self.webrender_surfman.present() { + warn!("Failed to present surface: {:?}", err); + } + self.waiting_on_present = false; + } + fn composite_if_necessary(&mut self, reason: CompositingReason) { if self.composition_request == CompositionRequest::NoCompositingNecessary { if self.is_running_problem_test { @@ -1733,10 +1762,12 @@ impl IOCompositor { let gl = &self.webrender_gl; self.assert_gl_framebuffer_complete(); - // Make framebuffer fully transparent. - gl.clear_color(0.0, 0.0, 0.0, 0.0); - gl.clear(gleam::gl::COLOR_BUFFER_BIT); - self.assert_gl_framebuffer_complete(); + if !self.external_present { + // Make framebuffer fully transparent. + gl.clear_color(0.0, 0.0, 0.0, 0.0); + gl.clear(gleam::gl::COLOR_BUFFER_BIT); + self.assert_gl_framebuffer_complete(); + } // Make the viewport white. let viewport = self.embedder_coordinates.get_flipped_viewport(); @@ -1919,6 +1950,10 @@ impl IOCompositor { None => eprintln!("Unable to locate path to save captures"), } } + + pub fn set_external_present(&mut self, value: bool) { + self.external_present = value; + } } /// Why we performed a composite. This is used for debugging. diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index 4f29cfc1662..554f36df833 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -109,6 +109,8 @@ pub enum ConstellationMsg { ChangeBrowserVisibility(TopLevelBrowsingContextId, bool), /// Virtual keyboard was dismissed IMEDismissed, + /// Compositing done, but external code needs to present. + ReadyToPresent(TopLevelBrowsingContextId), } impl fmt::Debug for ConstellationMsg { @@ -142,6 +144,7 @@ impl fmt::Debug for ConstellationMsg { ChangeBrowserVisibility(..) => "ChangeBrowserVisibility", IMEDismissed => "IMEDismissed", ClearCache => "ClearCache", + ReadyToPresent(..) => "ReadyToPresent", }; write!(formatter, "ConstellationMsg::{}", variant) } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 5982f5ce76d..cf4ca3eb41b 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -1551,6 +1551,12 @@ where FromCompositorMsg::ChangeBrowserVisibility(top_level_browsing_context_id, visible) => { self.handle_change_browser_visibility(top_level_browsing_context_id, visible); }, + FromCompositorMsg::ReadyToPresent(top_level_browsing_context_id) => { + self.embedder_proxy.send(( + Some(top_level_browsing_context_id), + EmbedderMsg::ReadyToPresent, + )); + }, } } diff --git a/components/embedder_traits/lib.rs b/components/embedder_traits/lib.rs index 8654732735d..da117823340 100644 --- a/components/embedder_traits/lib.rs +++ b/components/embedder_traits/lib.rs @@ -212,6 +212,8 @@ pub enum EmbedderMsg { MediaSessionEvent(MediaSessionEvent), /// Report the status of Devtools Server with a token that can be used to bypass the permission prompt. OnDevtoolsStarted(Result, String), + /// Compositing done, but external code needs to present. + ReadyToPresent, } impl Debug for EmbedderMsg { @@ -248,6 +250,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::MediaSessionEvent(..) => write!(f, "MediaSessionEvent"), EmbedderMsg::OnDevtoolsStarted(..) => write!(f, "OnDevtoolsStarted"), EmbedderMsg::ShowContextMenu(..) => write!(f, "ShowContextMenu"), + EmbedderMsg::ReadyToPresent => write!(f, "ReadyToPresent"), } } } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 51df1f0c991..91a263d450e 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -801,6 +801,14 @@ where pub fn deinit(self) { self.compositor.deinit(); } + + pub fn set_external_present(&mut self, value: bool) { + self.compositor.set_external_present(value) + } + + pub fn present(&mut self) { + self.compositor.present(); + } } fn create_embedder_channel( diff --git a/ports/winit/Cargo.toml b/ports/winit/Cargo.toml index 4f85ffb61f4..06978d3ae69 100644 --- a/ports/winit/Cargo.toml +++ b/ports/winit/Cargo.toml @@ -48,8 +48,13 @@ xr-profile = ["libservo/xr-profile"] [target.'cfg(not(target_os = "android"))'.dependencies] backtrace = { workspace = true } clipboard = "0.5" +egui = "0.22.0" +egui_glow = { version = "0.22.0", features = ["winit"] } +egui-winit = { version = "0.22.0", default-features = false, features = ["clipboard", "wayland"] } euclid = { workspace = true } getopts = { workspace = true } +gleam = "0.12" +glow = "0.12.2" keyboard-types = { workspace = true } lazy_static = { workspace = true } libc = { workspace = true } diff --git a/ports/winit/app.rs b/ports/winit/app.rs index fe6e8fa36ef..290ca0bc38a 100644 --- a/ports/winit/app.rs +++ b/ports/winit/app.rs @@ -7,8 +7,10 @@ use crate::browser::Browser; use crate::embedder::EmbedderCallbacks; use crate::events_loop::{EventsLoop, WakerEvent}; +use crate::minibrowser::Minibrowser; use crate::window_trait::WindowPortsMethods; use crate::{headed_window, headless_window}; +use gleam::gl; use winit::window::WindowId; use winit::event_loop::EventLoopWindowTarget; use servo::compositing::windowing::EmbedderEvent; @@ -16,11 +18,12 @@ use servo::config::opts::{self, parse_url_or_filename}; use servo::servo_config::pref; use servo::servo_url::ServoUrl; use servo::Servo; -use std::cell::{Cell, RefCell}; +use std::cell::{Cell, RefCell, RefMut}; use std::collections::HashMap; use std::env; use std::rc::Rc; +use surfman::GLApi; use webxr::glwindow::GlWindowDiscovery; pub struct App { @@ -29,6 +32,13 @@ pub struct App { event_queue: RefCell>, suspended: Cell, windows: HashMap>, + minibrowser: Option>, +} + +/// Action to be taken by the caller of [`App::handle_events`]. +enum PumpResult { + Shutdown, + Present, } impl App { @@ -60,51 +70,96 @@ impl App { servo: None, suspended: Cell::new(false), windows: HashMap::new(), + minibrowser: None, }; + if opts::get().minibrowser && window.winit_window().is_some() { + // Make sure the gl context is made current. + let webrender_surfman = window.webrender_surfman(); + let webrender_gl = match webrender_surfman.connection().gl_api() { + GLApi::GL => unsafe { gl::GlFns::load_with(|s| webrender_surfman.get_proc_address(s)) }, + GLApi::GLES => unsafe { + gl::GlesFns::load_with(|s| webrender_surfman.get_proc_address(s)) + }, + }; + webrender_surfman.make_gl_context_current().unwrap(); + debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR); + + // Set up egui context for minibrowser ui + // Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs + app.minibrowser = Some(Minibrowser::new(&webrender_surfman, &events_loop).into()); + } + + if let Some(mut minibrowser) = app.minibrowser() { + minibrowser.update(window.winit_window().unwrap()); + window.set_toolbar_height(minibrowser.toolbar_height.get()); + } + let ev_waker = events_loop.create_event_loop_waker(); events_loop.run_forever(move |e, w, control_flow| { - if let winit::event::Event::NewEvents(winit::event::StartCause::Init) = e { - let surfman = window.webrender_surfman(); + match e { + winit::event::Event::NewEvents(winit::event::StartCause::Init) => { + let surfman = window.webrender_surfman(); + + let xr_discovery = if pref!(dom.webxr.glwindow.enabled) && ! opts::get().headless { + let window = window.clone(); + // This should be safe because run_forever does, in fact, + // run forever. The event loop window target doesn't get + // moved, and does outlast this closure, and we won't + // ever try to make use of it once shutdown begins and + // it stops being valid. + let w = unsafe { + std::mem::transmute::< + &EventLoopWindowTarget, + &'static EventLoopWindowTarget + >(w.unwrap()) + }; + let factory = Box::new(move || Ok(window.new_glwindow(w))); + Some(GlWindowDiscovery::new( + surfman.connection(), + surfman.adapter(), + surfman.context_attributes(), + factory, + )) + } else { + None + }; - let xr_discovery = if pref!(dom.webxr.glwindow.enabled) && ! opts::get().headless { let window = window.clone(); - // This should be safe because run_forever does, in fact, - // run forever. The event loop window target doesn't get - // moved, and does outlast this closure, and we won't - // ever try to make use of it once shutdown begins and - // it stops being valid. - let w = unsafe { - std::mem::transmute::< - &EventLoopWindowTarget, - &'static EventLoopWindowTarget - >(w.unwrap()) - }; - let factory = Box::new(move || Ok(window.new_glwindow(w))); - Some(GlWindowDiscovery::new( - surfman.connection(), - surfman.adapter(), - surfman.context_attributes(), - factory, - )) - } else { - None - }; - - let window = window.clone(); - // Implements embedder methods, used by libservo and constellation. - let embedder = Box::new(EmbedderCallbacks::new( - ev_waker.clone(), - xr_discovery, - )); - - let servo_data = Servo::new(embedder, window.clone(), user_agent.clone()); - let mut servo = servo_data.servo; - servo.handle_events(vec![EmbedderEvent::NewBrowser(get_default_url(), servo_data.browser_id)]); - servo.setup_logging(); - - app.windows.insert(window.id(), window.clone()); - app.servo = Some(servo); + // Implements embedder methods, used by libservo and constellation. + let embedder = Box::new(EmbedderCallbacks::new( + ev_waker.clone(), + xr_discovery, + )); + + let servo_data = Servo::new(embedder, window.clone(), user_agent.clone()); + let mut servo = servo_data.servo; + + // If we have a minibrowser, ask the compositor to notify us when a new frame + // is ready to present, so that we can paint the minibrowser then present. + servo.set_external_present(app.minibrowser.is_some()); + + servo.handle_events(vec![EmbedderEvent::NewBrowser(get_default_url(), servo_data.browser_id)]); + servo.setup_logging(); + + app.windows.insert(window.id(), window.clone()); + app.servo = Some(servo); + } + + // TODO does windows still need this workaround? + // https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs#L203 + // winit::event::Event::RedrawEventsCleared => todo!(), + winit::event::Event::RedrawRequested(_) => { + if let Some(mut minibrowser) = app.minibrowser() { + minibrowser.update(window.winit_window().unwrap()); + + // Tell Servo to repaint, which will in turn allow us to repaint the + // minibrowser and present a complete frame without partial updates. + app.event_queue.borrow_mut().push(EmbedderEvent::Refresh); + } + } + + _ => {} } // If self.servo is None here, it means that we're in the process of shutting down, @@ -114,7 +169,23 @@ impl App { } // Handle the event - app.queue_embedder_events_for_winit_event(e); + let mut consumed = false; + if let Some(mut minibrowser) = app.minibrowser() { + if let winit::event::Event::WindowEvent { ref event, .. } = e { + let response = minibrowser.context.on_event(&event); + if response.repaint { + // Request a redraw event that will in turn trigger a minibrowser update. + // This allows us to coalesce minibrowser updates across multiple events. + window.winit_window().unwrap().request_redraw(); + } + + // TODO how do we handle the tab key? (see doc for consumed) + consumed = response.consumed; + } + } + if !consumed { + app.queue_embedder_events_for_winit_event(e); + } let animating = app.is_animating(); @@ -125,10 +196,33 @@ impl App { *control_flow = winit::event_loop::ControlFlow::Poll; } - let stop = app.handle_events(); - if stop { - *control_flow = winit::event_loop::ControlFlow::Exit; - app.servo.take().unwrap().deinit(); + // Consume and handle any events from the Minibrowser. + if let Some(mut minibrowser) = app.minibrowser() { + let browser = &mut app.browser.borrow_mut(); + let app_event_queue = &mut app.event_queue.borrow_mut(); + minibrowser.queue_embedder_events_for_minibrowser_events(browser, app_event_queue); + if minibrowser.update_location_in_toolbar(browser) { + // Update the minibrowser immediately. While we could update by requesting a + // redraw, doing so would delay the location update by two frames. + minibrowser.update(window.winit_window().unwrap()); + } + } + + match app.handle_events() { + Some(PumpResult::Shutdown) => { + *control_flow = winit::event_loop::ControlFlow::Exit; + app.servo.take().unwrap().deinit(); + if let Some(mut minibrowser) = app.minibrowser() { + minibrowser.context.destroy(); + } + }, + Some(PumpResult::Present) => { + if let Some(mut minibrowser) = app.minibrowser() { + minibrowser.paint(window.winit_window().unwrap()); + } + app.servo.as_mut().unwrap().present(); + }, + None => {}, } }); } @@ -190,7 +284,7 @@ impl App { /// Window queues to the Browser queue, and from the Browser queue to Servo. We receive and /// collect embedder messages from the various Servo components, then take them out of the /// Servo interface so that the Browser can handle them. - fn handle_events(&mut self) -> bool { + fn handle_events(&mut self) -> Option { let mut browser = self.browser.borrow_mut(); // FIXME: @@ -212,16 +306,17 @@ impl App { // Take any new embedder messages from Servo itself. let mut embedder_messages = self.servo.as_mut().unwrap().get_events(); let mut need_resize = false; + let mut need_present = false; loop { // Consume and handle those embedder messages. - browser.handle_servo_events(embedder_messages); + need_present |= browser.handle_servo_events(embedder_messages); // Route embedder events from the Browser to the relevant Servo components, // receives and collects embedder messages from various Servo components, // and runs the compositor. need_resize |= self.servo.as_mut().unwrap().handle_events(browser.get_events()); if browser.shutdown_requested() { - return true; + return Some(PumpResult::Shutdown); } // Take any new embedder messages from Servo itself. @@ -233,8 +328,17 @@ impl App { if need_resize { self.servo.as_mut().unwrap().repaint_synchronously(); + need_present = true; + } + if need_present { + Some(PumpResult::Present) + } else { + None } - false + } + + fn minibrowser(&self) -> Option> { + self.minibrowser.as_ref().map(|x| x.borrow_mut()) } } diff --git a/ports/winit/browser.rs b/ports/winit/browser.rs index d29b47a3ab0..e46c51e5e9f 100644 --- a/ports/winit/browser.rs +++ b/ports/winit/browser.rs @@ -31,6 +31,8 @@ use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo}; pub struct Browser { current_url: Option, + current_url_string: Option, + /// id of the top level browsing context. It is unique as tabs /// are not supported yet. None until created. browser_id: Option, @@ -57,6 +59,7 @@ where Browser { title: None, current_url: None, + current_url_string: None, browser_id: None, browsers: Vec::new(), window, @@ -72,6 +75,14 @@ where } } + pub fn browser_id(&self) -> Option { + self.browser_id + } + + pub fn current_url_string(&self) -> Option<&str> { + self.current_url_string.as_deref() + } + pub fn get_events(&mut self) -> Vec { std::mem::take(&mut self.event_queue) } @@ -264,7 +275,9 @@ where self.event_queue.push(event); } - pub fn handle_servo_events(&mut self, events: Vec<(Option, EmbedderMsg)>) { + /// Returns true iff the caller needs to manually present a new frame. + pub fn handle_servo_events(&mut self, events: Vec<(Option, EmbedderMsg)>) -> bool { + let mut need_present = false; for (browser_id, msg) in events { match msg { EmbedderMsg::Status(_status) => { @@ -437,6 +450,7 @@ where }, EmbedderMsg::HistoryChanged(urls, current) => { self.current_url = Some(urls[current].clone()); + self.current_url_string = Some(urls[current].clone().into_string()); }, EmbedderMsg::SetFullscreenState(state) => { self.window.set_fullscreen(state); @@ -511,8 +525,13 @@ where EmbedderMsg::ShowContextMenu(sender, ..) => { let _ = sender.send(ContextMenuResult::Ignored); }, + EmbedderMsg::ReadyToPresent => { + need_present = true; + }, } } + + need_present } } diff --git a/ports/winit/egui_glue.rs b/ports/winit/egui_glue.rs new file mode 100644 index 00000000000..0e7f61a6b20 --- /dev/null +++ b/ports/winit/egui_glue.rs @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A modified version of EguiGlow [from egui_glow 0.22.0][0] that retains its shapes, +//! allowing [`EguiGlow::paint`] to be called multiple times. +//! +//! [0]: https://github.com/emilk/egui/blob/0.22.0/crates/egui_glow/src/winit.rs + +// Copyright (c) 2018-2021 Emil Ernerfeldt +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use egui_glow::ShaderVersion; +pub use egui_winit; +use egui_winit::winit; +pub use egui_winit::EventResponse; + +/// Use [`egui`] from a [`glow`] app based on [`winit`]. +pub struct EguiGlow { + pub egui_ctx: egui::Context, + pub egui_winit: egui_winit::State, + pub painter: egui_glow::Painter, + + shapes: Vec, + textures_delta: egui::TexturesDelta, +} + +impl EguiGlow { + /// For automatic shader version detection set `shader_version` to `None`. + pub fn new( + event_loop: &winit::event_loop::EventLoopWindowTarget, + gl: std::sync::Arc, + shader_version: Option, + ) -> Self { + let painter = egui_glow::Painter::new(gl, "", shader_version) + .map_err(|err| { + log::error!("error occurred in initializing painter:\n{err}"); + }) + .unwrap(); + + Self { + egui_ctx: Default::default(), + egui_winit: egui_winit::State::new(event_loop), + painter, + shapes: Default::default(), + textures_delta: Default::default(), + } + } + + pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) -> EventResponse { + self.egui_winit.on_event(&self.egui_ctx, event) + } + + /// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events. + /// + /// Call [`Self::paint`] later to paint. + pub fn run( + &mut self, + window: &winit::window::Window, + run_ui: impl FnMut(&egui::Context), + ) -> std::time::Duration { + let raw_input = self.egui_winit.take_egui_input(window); + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = self.egui_ctx.run(raw_input, run_ui); + + self.egui_winit + .handle_platform_output(window, &self.egui_ctx, platform_output); + + self.shapes = shapes; + self.textures_delta.append(textures_delta); + repaint_after + } + + /// Paint the results of the last call to [`Self::run`]. + pub fn paint(&mut self, window: &winit::window::Window) { + /////// let shapes = std::mem::take(&mut self.shapes); + let shapes = &self.shapes; + let mut textures_delta = std::mem::take(&mut self.textures_delta); + + for (id, image_delta) in textures_delta.set { + self.painter.set_texture(id, &image_delta); + } + + /////// let clipped_primitives = self.egui_ctx.tessellate(shapes); + let clipped_primitives = self.egui_ctx.tessellate(shapes.clone()); + let dimensions: [u32; 2] = window.inner_size().into(); + self.painter.paint_primitives( + dimensions, + self.egui_ctx.pixels_per_point(), + &clipped_primitives, + ); + + for id in textures_delta.free.drain(..) { + self.painter.free_texture(id); + } + } + + /// Call to release the allocated graphics resources. + pub fn destroy(&mut self) { + self.painter.destroy(); + } +} diff --git a/ports/winit/headed_window.rs b/ports/winit/headed_window.rs index c39218bc5d7..e57e0400389 100644 --- a/ports/winit/headed_window.rs +++ b/ports/winit/headed_window.rs @@ -51,6 +51,7 @@ pub struct Window { webrender_surfman: WebrenderSurfman, screen_size: Size2D, inner_size: Cell>, + toolbar_height: Cell, mouse_down_button: Cell>, mouse_down_point: Cell>, primary_monitor: winit::monitor::MonitorHandle, @@ -156,6 +157,7 @@ impl Window { device_pixels_per_px, xr_window_poses: RefCell::new(vec![]), modifiers_state: Cell::new(ModifiersState::empty()), + toolbar_height: Cell::new(0.0), } } @@ -405,8 +407,9 @@ impl WindowPortsMethods for Window { } }, winit::event::WindowEvent::CursorMoved { position, .. } => { - let (x, y): (i32, i32) = position.into(); - self.mouse_pos.set(Point2D::new(x, y)); + let (x, y): (f64, f64) = position.into(); + let y = y - f64::from(self.toolbar_height.get()); + self.mouse_pos.set(Point2D::new(x, y).to_i32()); self.event_queue .borrow_mut() .push(EmbedderEvent::MouseWindowMoveEventClass(Point2D::new( @@ -504,6 +507,14 @@ impl WindowPortsMethods for Window { self.xr_window_poses.borrow_mut().push(pose.clone()); Box::new(XRWindow { winit_window, pose }) } + + fn winit_window(&self) -> Option<&winit::window::Window> { + Some(&self.winit_window) + } + + fn set_toolbar_height(&self, height: f32) { + self.toolbar_height.set(height); + } } impl WindowMethods for Window { @@ -525,8 +536,14 @@ impl WindowMethods for Window { let PhysicalSize { width, height } = self .winit_window .inner_size(); - let inner_size = (Size2D::new(width as f32, height as f32) * dpr).to_i32(); - let viewport = DeviceIntRect::new(Point2D::zero(), inner_size); + + // Subtract the minibrowser toolbar height if any + let toolbar_height = self.toolbar_height.get(); + let inner_size = Size2D::new(width as f32, height as f32) * dpr; + let viewport_size = inner_size - Size2D::new(0f32, toolbar_height); + let viewport_origin = DeviceIntPoint::zero(); // bottom left + let viewport = DeviceIntRect::new(viewport_origin, viewport_size.to_i32()); + let framebuffer = DeviceIntSize::from_untyped(viewport.size.to_untyped()); EmbedderCoordinates { viewport, diff --git a/ports/winit/headless_window.rs b/ports/winit/headless_window.rs index 0347ae47216..ad8d29011fd 100644 --- a/ports/winit/headless_window.rs +++ b/ports/winit/headless_window.rs @@ -108,6 +108,14 @@ impl WindowPortsMethods for Window { ) -> Box { unimplemented!() } + + fn winit_window(&self) -> Option<&winit::window::Window> { + None + } + + fn set_toolbar_height(&self, _height: f32) { + unimplemented!("headless Window only") + } } impl WindowMethods for Window { diff --git a/ports/winit/main2.rs b/ports/winit/main2.rs index 51997ab2c5b..07d931c21ab 100644 --- a/ports/winit/main2.rs +++ b/ports/winit/main2.rs @@ -14,11 +14,13 @@ mod app; mod backtrace; mod browser; mod crash_handler; +mod egui_glue; mod embedder; mod events_loop; mod headed_window; mod headless_window; mod keyutils; +mod minibrowser; mod prefs; mod resources; mod window_trait; diff --git a/ports/winit/minibrowser.rs b/ports/winit/minibrowser.rs new file mode 100644 index 00000000000..48afc9d5936 --- /dev/null +++ b/ports/winit/minibrowser.rs @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + + use std::{cell::{RefCell, Cell}, sync::Arc}; + +use egui::TopBottomPanel; +use servo::{servo_url::ServoUrl, compositing::windowing::EmbedderEvent}; +use servo::webrender_surfman::WebrenderSurfman; + +use crate::{egui_glue::EguiGlow, events_loop::EventsLoop, browser::Browser, window_trait::WindowPortsMethods}; + +pub struct Minibrowser { + pub context: EguiGlow, + pub event_queue: RefCell>, + pub toolbar_height: Cell, + location: RefCell, + + /// Whether the location has been edited by the user without clicking Go. + location_dirty: Cell, +} + +pub enum MinibrowserEvent { + /// Go button clicked. + Go, +} + +impl Minibrowser { + pub fn new(webrender_surfman: &WebrenderSurfman, events_loop: &EventsLoop) -> Self { + let gl = unsafe { + glow::Context::from_loader_function(|s| { + webrender_surfman.get_proc_address(s) + }) + }; + + Self { + context: EguiGlow::new(events_loop.as_winit(), Arc::new(gl), None), + event_queue: RefCell::new(vec![]), + toolbar_height: 0f32.into(), + location: RefCell::new(String::default()), + location_dirty: false.into(), + } + } + + /// Update the minibrowser, but don’t paint. + pub fn update(&mut self, window: &winit::window::Window) { + let Self { context, event_queue, location, location_dirty, toolbar_height } = self; + let _duration = context.run(window, |ctx| { + TopBottomPanel::top("toolbar").show(ctx, |ui| { + ui.allocate_ui_with_layout( + ui.available_size(), + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + if ui.button("go").clicked() { + event_queue.borrow_mut().push(MinibrowserEvent::Go); + location_dirty.set(false); + } + if ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut *location.borrow_mut()), + ).changed() { + location_dirty.set(true); + } + }, + ); + }); + + toolbar_height.set(ctx.used_rect().height()); + }); + } + + /// Paint the minibrowser, as of the last update. + pub fn paint(&mut self, window: &winit::window::Window) { + self.context.paint(window); + } + + /// Takes any outstanding events from the [Minibrowser], converting them to [EmbedderEvent] and + /// routing those to the App event queue. + pub fn queue_embedder_events_for_minibrowser_events( + &self, browser: &Browser, + app_event_queue: &mut Vec, + ) { + for event in self.event_queue.borrow_mut().drain(..) { + match event { + MinibrowserEvent::Go => { + let browser_id = browser.browser_id().unwrap(); + let location = self.location.borrow(); + let Ok(url) = ServoUrl::parse(&location) else { + warn!("failed to parse location"); + break; + }; + app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url)); + }, + } + } + } + + /// Updates the location field when the [Browser] says it has changed, unless the user has + /// started editing it without clicking Go. + pub fn update_location_in_toolbar(&mut self, browser: &mut Browser) -> bool { + if !self.location_dirty.get() { + if let Some(location) = browser.current_url_string() { + self.location = RefCell::new(location.to_owned()); + return true; + } + } + + false + } +} diff --git a/ports/winit/window_trait.rs b/ports/winit/window_trait.rs index 26aea605454..8f780735c23 100644 --- a/ports/winit/window_trait.rs +++ b/ports/winit/window_trait.rs @@ -31,4 +31,6 @@ pub trait WindowPortsMethods: WindowMethods { &self, events_loop: &winit::event_loop::EventLoopWindowTarget ) -> Box; + fn winit_window(&self) -> Option<&winit::window::Window>; + fn set_toolbar_height(&self, height: f32); } diff --git a/servo-tidy.toml b/servo-tidy.toml index 14905fee3b9..a2d4f10c724 100644 --- a/servo-tidy.toml +++ b/servo-tidy.toml @@ -79,6 +79,9 @@ packages = [ # Temporarily duplicated until gleam can be upgrded. "uuid", + + # winit port minibrowser (servo/servo#30049) + "clipboard-win", ] # Files that are ignored for all tidy and lint checks. files = [ -- cgit v1.2.3