servo/ports/winit/app.rs
Delan Azabani 2778beeb7a
winit: initial minibrowser (#29976)
* 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 <atbrakhi@igalia.com>
2023-08-15 08:08:50 +00:00

357 lines
15 KiB
Rust

/* 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/. */
//! Application entry point, runs the event loop.
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;
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, RefMut};
use std::collections::HashMap;
use std::env;
use std::rc::Rc;
use surfman::GLApi;
use webxr::glwindow::GlWindowDiscovery;
pub struct App {
servo: Option<Servo<dyn WindowPortsMethods>>,
browser: RefCell<Browser<dyn WindowPortsMethods>>,
event_queue: RefCell<Vec<EmbedderEvent>>,
suspended: Cell<bool>,
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
minibrowser: Option<RefCell<Minibrowser>>,
}
/// Action to be taken by the caller of [`App::handle_events`].
enum PumpResult {
Shutdown,
Present,
}
impl App {
pub fn run(
no_native_titlebar: bool,
device_pixels_per_px: Option<f32>,
user_agent: Option<String>,
) {
let events_loop = EventsLoop::new(opts::get().headless, opts::get().output_file.is_some());
// Implements window methods, used by compositor.
let window = if opts::get().headless {
headless_window::Window::new(opts::get().initial_window_size, device_pixels_per_px)
} else {
Rc::new(headed_window::Window::new(
opts::get().initial_window_size,
&events_loop,
no_native_titlebar,
device_pixels_per_px,
))
};
// Handle browser state.
let browser = Browser::new(window.clone());
let mut app = App {
event_queue: RefCell::new(vec![]),
browser: RefCell::new(browser),
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| {
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<WakerEvent>,
&'static EventLoopWindowTarget<WakerEvent>
>(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;
// 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,
// let's ignore events.
if app.servo.is_none() {
return;
}
// Handle the event
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();
// Block until the window gets an event
if !animating || app.suspended.get() {
*control_flow = winit::event_loop::ControlFlow::Wait;
} else {
*control_flow = winit::event_loop::ControlFlow::Poll;
}
// 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 => {},
}
});
}
fn is_animating(&self) -> bool {
self.windows.iter().any(|(_, window)| window.is_animating())
}
fn get_events(&self) -> Vec<EmbedderEvent> {
std::mem::take(&mut *self.event_queue.borrow_mut())
}
/// Processes the given winit Event, possibly converting it to an [EmbedderEvent] and
/// routing that to the App or relevant Window event queues.
fn queue_embedder_events_for_winit_event(&self, event: winit::event::Event<'_, WakerEvent>) {
match event {
// App level events
winit::event::Event::Suspended => {
self.suspended.set(true);
},
winit::event::Event::Resumed => {
self.suspended.set(false);
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
},
winit::event::Event::UserEvent(_) => {
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
},
winit::event::Event::DeviceEvent { .. } => {},
winit::event::Event::RedrawRequested(_) => {
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
},
// Window level events
winit::event::Event::WindowEvent {
window_id, event, ..
} => {
match self.windows.get(&window_id) {
None => {
warn!("Got an event from unknown window");
},
Some(window) => {
window.queue_embedder_events_for_winit_event(event);
},
}
},
winit::event::Event::LoopDestroyed |
winit::event::Event::NewEvents(..) |
winit::event::Event::MainEventsCleared |
winit::event::Event::RedrawEventsCleared => {},
}
}
/// Pumps events and messages between the embedder and Servo, where embedder events flow
/// towards Servo and embedder messages flow away from Servo, and also runs the compositor.
///
/// As the embedder, we push embedder events through our event queues, from the App queue and
/// 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) -> Option<PumpResult> {
let mut browser = self.browser.borrow_mut();
// FIXME:
// As of now, we support only one browser (self.browser)
// but have multiple windows (dom.webxr.glwindow). We forward
// the events of all the windows combined to that single
// browser instance. Pressing the "a" key on the glwindow
// will send a key event to the servo window.
// Take any outstanding embedder events from the App and its Windows.
let mut embedder_events = self.get_events();
for (_win_id, window) in &self.windows {
embedder_events.extend(window.get_events());
}
// Catch some keyboard events, and push the rest onto the Browser event queue.
browser.handle_window_events(embedder_events);
// 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.
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 Some(PumpResult::Shutdown);
}
// Take any new embedder messages from Servo itself.
embedder_messages = self.servo.as_mut().unwrap().get_events();
if embedder_messages.is_empty() {
break;
}
}
if need_resize {
self.servo.as_mut().unwrap().repaint_synchronously();
need_present = true;
}
if need_present {
Some(PumpResult::Present)
} else {
None
}
}
fn minibrowser(&self) -> Option<RefMut<Minibrowser>> {
self.minibrowser.as_ref().map(|x| x.borrow_mut())
}
}
fn get_default_url() -> ServoUrl {
// If the url is not provided, we fallback to the homepage in prefs,
// or a blank page in case the homepage is not set either.
let cwd = env::current_dir().unwrap();
let cmdline_url = opts::get().url.clone();
let pref_url = {
let homepage_url = pref!(shell.homepage);
parse_url_or_filename(&cwd, &homepage_url).ok()
};
let blank_url = ServoUrl::parse("about:blank").ok();
cmdline_url.or(pref_url).or(blank_url).unwrap()
}