/* 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 glutin window implementation. use crate::app; use crate::context::GlContext; use crate::keyutils::keyboard_event_from_winit; use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT}; use euclid::{TypedPoint2D, TypedScale, TypedSize2D, TypedVector2D}; use gleam::gl; use glutin::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; #[cfg(target_os = "macos")] use glutin::os::macos::{ActivationPolicy, WindowBuilderExt}; #[cfg(any(target_os = "linux", target_os = "windows"))] use glutin::Icon; use glutin::Api; use glutin::{ElementState, KeyboardInput, MouseButton, MouseScrollDelta, TouchPhase}; #[cfg(any(target_os = "linux", target_os = "windows"))] use image; use keyboard_types::{Key, KeyState, KeyboardEvent}; use servo::compositing::windowing::{AnimationState, MouseWindowEvent, WindowEvent}; use servo::compositing::windowing::{EmbedderCoordinates, WindowMethods}; use servo::embedder_traits::Cursor; use servo::script_traits::TouchEventType; use servo::servo_config::opts; use servo::servo_geometry::DeviceIndependentPixel; use servo::style_traits::DevicePixel; use servo::webrender_api::{ DeviceIntPoint, DeviceIntRect, DeviceIntSize, FramebufferIntSize, ScrollLocation, }; use std::cell::{Cell, RefCell}; use std::mem; use std::rc::Rc; #[cfg(target_os = "windows")] use winapi; const MULTISAMPLES: u16 = 16; #[cfg(target_os = "macos")] fn builder_with_platform_options(mut builder: glutin::WindowBuilder) -> glutin::WindowBuilder { if opts::get().output_file.is_some() { // Prevent the window from showing in Dock.app, stealing focus, // when generating an output file. builder = builder.with_activation_policy(ActivationPolicy::Prohibited) } builder } #[cfg(not(target_os = "macos"))] fn builder_with_platform_options(builder: glutin::WindowBuilder) -> glutin::WindowBuilder { builder } pub struct Window { gl_context: RefCell, screen_size: TypedSize2D, inner_size: Cell>, mouse_down_button: Cell>, mouse_down_point: Cell>, primary_monitor: glutin::MonitorId, event_queue: RefCell>, mouse_pos: Cell>, last_pressed: Cell>, animation_state: Cell, fullscreen: Cell, gl: Rc, } #[cfg(not(target_os = "windows"))] fn window_creation_scale_factor() -> TypedScale { TypedScale::new(1.0) } #[cfg(target_os = "windows")] fn window_creation_scale_factor() -> TypedScale { let hdc = unsafe { winapi::um::winuser::GetDC(::std::ptr::null_mut()) }; let ppi = unsafe { winapi::um::wingdi::GetDeviceCaps(hdc, winapi::um::wingdi::LOGPIXELSY) }; TypedScale::new(ppi as f32 / 96.0) } impl Window { pub fn new( win_size: TypedSize2D, events_loop: &glutin::EventsLoop, ) -> Rc { let opts = opts::get(); // If there's no chrome, start off with the window invisible. It will be set to visible in // `load_end()`. This avoids an ugly flash of unstyled content (especially important since // unstyled content is white and chrome often has a transparent background). See issue // #9996. let visible = opts.output_file.is_none() && !opts.no_native_titlebar; let win_size: DeviceIntSize = (win_size.to_f32() * window_creation_scale_factor()).to_i32(); let width = win_size.to_untyped().width; let height = win_size.to_untyped().height; let mut window_builder = glutin::WindowBuilder::new() .with_title("Servo".to_string()) .with_decorations(!opts.no_native_titlebar) .with_transparency(opts.no_native_titlebar) .with_dimensions(LogicalSize::new(width as f64, height as f64)) .with_visibility(visible) .with_multitouch(); window_builder = builder_with_platform_options(window_builder); let mut context_builder = glutin::ContextBuilder::new() .with_gl(app::gl_version()) .with_vsync(opts.enable_vsync); if opts.use_msaa { context_builder = context_builder.with_multisampling(MULTISAMPLES) } let context = context_builder .build_windowed(window_builder, &events_loop) .expect("Failed to create window."); #[cfg(any(target_os = "linux", target_os = "windows"))] { let icon_bytes = include_bytes!("../../resources/servo64.png"); context.window().set_window_icon(Some(load_icon(icon_bytes))); } let context = unsafe { context.make_current().expect("Couldn't make window current") }; let primary_monitor = events_loop.get_primary_monitor(); let PhysicalSize { width: screen_width, height: screen_height, } = primary_monitor.get_dimensions(); let screen_size = TypedSize2D::new(screen_width as u32, screen_height as u32); // TODO(ajeffrey): can this fail? let LogicalSize { width, height } = context .window() .get_inner_size() .expect("Failed to get window inner size."); let inner_size = TypedSize2D::new(width as u32, height as u32); context.window().show(); let gl = match context.get_api() { Api::OpenGl => unsafe { gl::GlFns::load_with(|s| context.get_proc_address(s) as *const _) }, Api::OpenGlEs => unsafe { gl::GlesFns::load_with(|s| context.get_proc_address(s) as *const _) }, Api::WebGl => unreachable!("webgl is unsupported"), }; gl.clear_color(0.6, 0.6, 0.6, 1.0); gl.clear(gl::COLOR_BUFFER_BIT); gl.finish(); let mut context = GlContext::Current(context); context.make_not_current(); let window = Window { gl_context: RefCell::new(context), event_queue: RefCell::new(vec![]), mouse_down_button: Cell::new(None), mouse_down_point: Cell::new(TypedPoint2D::new(0, 0)), mouse_pos: Cell::new(TypedPoint2D::new(0, 0)), last_pressed: Cell::new(None), gl: gl.clone(), animation_state: Cell::new(AnimationState::Idle), fullscreen: Cell::new(false), inner_size: Cell::new(inner_size), primary_monitor, screen_size, }; window.present(); Rc::new(window) } fn handle_received_character(&self, mut ch: char) { info!("winit received character: {:?}", ch); if ch.is_control() { if ch as u8 >= 32 { return; } // shift ASCII control characters to lowercase ch = (ch as u8 + 96) as char; } let mut event = if let Some(event) = self.last_pressed.replace(None) { event } else if ch.is_ascii() { // Some keys like Backspace emit a control character in winit // but they are already dealt with in handle_keyboard_input // so just ignore the character. return; } else { // For combined characters like the letter e with an acute accent // no keyboard event is emitted. A dummy event is created in this case. KeyboardEvent::default() }; event.key = Key::Character(ch.to_string()); self.event_queue .borrow_mut() .push(WindowEvent::Keyboard(event)); } fn handle_keyboard_input(&self, input: KeyboardInput) { let event = keyboard_event_from_winit(input); if event.state == KeyState::Down && event.key == Key::Unidentified { // If pressed and probably printable, we expect a ReceivedCharacter event. self.last_pressed.set(Some(event)); } else if event.key != Key::Unidentified { self.last_pressed.set(None); self.event_queue .borrow_mut() .push(WindowEvent::Keyboard(event)); } } /// Helper function to handle a click fn handle_mouse( &self, button: glutin::MouseButton, action: glutin::ElementState, coords: TypedPoint2D, ) { use servo::script_traits::MouseButton; let max_pixel_dist = 10.0 * self.servo_hidpi_factor().get(); let event = match action { ElementState::Pressed => { self.mouse_down_point.set(coords); self.mouse_down_button.set(Some(button)); MouseWindowEvent::MouseDown(MouseButton::Left, coords.to_f32()) }, ElementState::Released => { let mouse_up_event = MouseWindowEvent::MouseUp(MouseButton::Left, coords.to_f32()); match self.mouse_down_button.get() { None => mouse_up_event, Some(but) if button == but => { let pixel_dist = self.mouse_down_point.get() - coords; let pixel_dist = ((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32) .sqrt(); if pixel_dist < max_pixel_dist { self.event_queue .borrow_mut() .push(WindowEvent::MouseWindowEventClass(mouse_up_event)); MouseWindowEvent::Click(MouseButton::Left, coords.to_f32()) } else { mouse_up_event } }, Some(_) => mouse_up_event, } }, }; self.event_queue .borrow_mut() .push(WindowEvent::MouseWindowEventClass(event)); } fn device_hidpi_factor(&self) -> TypedScale { TypedScale::new(self.gl_context.borrow().window().get_hidpi_factor() as f32) } fn servo_hidpi_factor(&self) -> TypedScale { match opts::get().device_pixels_per_px { Some(device_pixels_per_px) => TypedScale::new(device_pixels_per_px), _ => match opts::get().output_file { Some(_) => TypedScale::new(1.0), None => self.device_hidpi_factor(), }, } } } impl WindowPortsMethods for Window { fn get_events(&self) -> Vec { mem::replace(&mut *self.event_queue.borrow_mut(), Vec::new()) } fn has_events(&self) -> bool { !self.event_queue.borrow().is_empty() } fn page_height(&self) -> f32 { let dpr = self.servo_hidpi_factor(); let size = self .gl_context .borrow() .window() .get_inner_size() .expect("Failed to get window inner size."); size.height as f32 * dpr.get() } fn set_title(&self, title: &str) { self.gl_context.borrow().window().set_title(title); } fn set_inner_size(&self, size: DeviceIntSize) { let size = size.to_f32() / self.device_hidpi_factor(); self.gl_context.borrow_mut().window() .set_inner_size(LogicalSize::new(size.width.into(), size.height.into())) } fn set_position(&self, point: DeviceIntPoint) { let point = point.to_f32() / self.device_hidpi_factor(); self.gl_context.borrow_mut().window() .set_position(LogicalPosition::new(point.x.into(), point.y.into())) } fn set_fullscreen(&self, state: bool) { if self.fullscreen.get() != state { self.gl_context.borrow_mut().window() .set_fullscreen(Some(self.primary_monitor.clone())); } self.fullscreen.set(state); } fn get_fullscreen(&self) -> bool { return self.fullscreen.get(); } fn set_cursor(&self, cursor: Cursor) { use glutin::MouseCursor; let winit_cursor = match cursor { Cursor::Default => MouseCursor::Default, Cursor::Pointer => MouseCursor::Hand, Cursor::ContextMenu => MouseCursor::ContextMenu, Cursor::Help => MouseCursor::Help, Cursor::Progress => MouseCursor::Progress, Cursor::Wait => MouseCursor::Wait, Cursor::Cell => MouseCursor::Cell, Cursor::Crosshair => MouseCursor::Crosshair, Cursor::Text => MouseCursor::Text, Cursor::VerticalText => MouseCursor::VerticalText, Cursor::Alias => MouseCursor::Alias, Cursor::Copy => MouseCursor::Copy, Cursor::Move => MouseCursor::Move, Cursor::NoDrop => MouseCursor::NoDrop, Cursor::NotAllowed => MouseCursor::NotAllowed, Cursor::Grab => MouseCursor::Grab, Cursor::Grabbing => MouseCursor::Grabbing, Cursor::EResize => MouseCursor::EResize, Cursor::NResize => MouseCursor::NResize, Cursor::NeResize => MouseCursor::NeResize, Cursor::NwResize => MouseCursor::NwResize, Cursor::SResize => MouseCursor::SResize, Cursor::SeResize => MouseCursor::SeResize, Cursor::SwResize => MouseCursor::SwResize, Cursor::WResize => MouseCursor::WResize, Cursor::EwResize => MouseCursor::EwResize, Cursor::NsResize => MouseCursor::NsResize, Cursor::NeswResize => MouseCursor::NeswResize, Cursor::NwseResize => MouseCursor::NwseResize, Cursor::ColResize => MouseCursor::ColResize, Cursor::RowResize => MouseCursor::RowResize, Cursor::AllScroll => MouseCursor::AllScroll, Cursor::ZoomIn => MouseCursor::ZoomIn, Cursor::ZoomOut => MouseCursor::ZoomOut, _ => MouseCursor::Default, }; self.gl_context.borrow_mut().window().set_cursor(winit_cursor); } fn is_animating(&self) -> bool { self.animation_state.get() == AnimationState::Animating } fn id(&self) -> Option { Some(self.gl_context.borrow().window().id()) } fn winit_event_to_servo_event(&self, event: glutin::WindowEvent) { match event { glutin::WindowEvent::ReceivedCharacter(ch) => self.handle_received_character(ch), glutin::WindowEvent::KeyboardInput { input, .. } => self.handle_keyboard_input(input), glutin::WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left || button == MouseButton::Right { self.handle_mouse(button, state, self.mouse_pos.get()); } }, glutin::WindowEvent::CursorMoved { position, .. } => { let pos = position.to_physical(self.device_hidpi_factor().get() as f64); let (x, y): (i32, i32) = pos.into(); self.mouse_pos.set(TypedPoint2D::new(x, y)); self.event_queue .borrow_mut() .push(WindowEvent::MouseWindowMoveEventClass(TypedPoint2D::new( x as f32, y as f32, ))); }, glutin::WindowEvent::MouseWheel { delta, phase, .. } => { let (mut dx, mut dy) = match delta { MouseScrollDelta::LineDelta(dx, dy) => (dx, dy * LINE_HEIGHT), MouseScrollDelta::PixelDelta(position) => { let position = position.to_physical(self.device_hidpi_factor().get() as f64); (position.x as f32, position.y as f32) }, }; // Scroll events snap to the major axis of movement, with vertical // preferred over horizontal. if dy.abs() >= dx.abs() { dx = 0.0; } else { dy = 0.0; } let scroll_location = ScrollLocation::Delta(TypedVector2D::new(dx, dy)); let phase = winit_phase_to_touch_event_type(phase); let event = WindowEvent::Scroll(scroll_location, self.mouse_pos.get(), phase); self.event_queue.borrow_mut().push(event); }, glutin::WindowEvent::Touch(touch) => { use servo::script_traits::TouchId; let phase = winit_phase_to_touch_event_type(touch.phase); let id = TouchId(touch.id as i32); let position = touch .location .to_physical(self.device_hidpi_factor().get() as f64); let point = TypedPoint2D::new(position.x as f32, position.y as f32); self.event_queue .borrow_mut() .push(WindowEvent::Touch(phase, id, point)); }, glutin::WindowEvent::Refresh => { self.event_queue.borrow_mut().push(WindowEvent::Refresh); }, glutin::WindowEvent::CloseRequested => { self.event_queue.borrow_mut().push(WindowEvent::Quit); }, glutin::WindowEvent::Resized(size) => { self.gl_context.borrow_mut().window().set_inner_size(size); // window.set_inner_size() takes DeviceIndependentPixel. let (width, height) = size.into(); let new_size = TypedSize2D::new(width, height); if self.inner_size.get() != new_size { self.inner_size.set(new_size); self.event_queue.borrow_mut().push(WindowEvent::Resize); } }, _ => {}, } } } impl WindowMethods for Window { fn gl(&self) -> Rc { self.gl.clone() } fn get_coordinates(&self) -> EmbedderCoordinates { // TODO(ajeffrey): can this fail? let dpr = self.device_hidpi_factor(); let LogicalSize { width, height } = self .gl_context .borrow() .window() .get_outer_size() .expect("Failed to get window outer size."); let LogicalPosition { x, y } = self .gl_context .borrow() .window() .get_position() .unwrap_or(LogicalPosition::new(0., 0.)); let win_size = (TypedSize2D::new(width as f32, height as f32) * dpr).to_i32(); let win_origin = (TypedPoint2D::new(x as f32, y as f32) * dpr).to_i32(); let screen = (self.screen_size.to_f32() * dpr).to_i32(); let LogicalSize { width, height } = self .gl_context .borrow() .window() .get_inner_size() .expect("Failed to get window inner size."); let inner_size = (TypedSize2D::new(width as f32, height as f32) * dpr).to_i32(); let viewport = DeviceIntRect::new(TypedPoint2D::zero(), inner_size); let framebuffer = FramebufferIntSize::from_untyped(&viewport.size.to_untyped()); EmbedderCoordinates { viewport, framebuffer, window: (win_size, win_origin), screen: screen, // FIXME: Glutin doesn't have API for available size. Fallback to screen size screen_avail: screen, hidpi_factor: self.servo_hidpi_factor(), } } fn present(&self) { self.gl_context.borrow().swap_buffers(); self.gl_context.borrow_mut().make_not_current(); } fn set_animation_state(&self, state: AnimationState) { self.animation_state.set(state); } fn prepare_for_composite(&self) -> bool { self.gl_context.borrow_mut().make_current(); true } } fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType { match phase { TouchPhase::Started => TouchEventType::Down, TouchPhase::Moved => TouchEventType::Move, TouchPhase::Ended => TouchEventType::Up, TouchPhase::Cancelled => TouchEventType::Cancel, } } #[cfg(any(target_os = "linux", target_os = "windows"))] fn load_icon(icon_bytes: &[u8]) -> Icon { let (icon_rgba, icon_width, icon_height) = { use image::{GenericImageView, Pixel}; let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");; let (width, height) = image.dimensions(); let mut rgba = Vec::with_capacity((width * height) as usize * 4); for (_, _, pixel) in image.pixels() { rgba.extend_from_slice(&pixel.to_rgba().data); } (rgba, width, height) }; Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon") }