/* LICENSE BEGIN This file is part of the SixtyFPS Project -- https://sixtyfps.io Copyright (c) 2020 Olivier Goffart Copyright (c) 2020 Simon Hausmann SPDX-License-Identifier: GPL-3.0-only This file is also available under commercial licensing terms. Please contact info@sixtyfps.io for more information. LICENSE END */ //! This module contains the GraphicsWindow that used to be within corelib. //! FIXME The GraphicsWindow probably does not need to be generic use core::cell::{Cell, RefCell}; use core::pin::Pin; use std::rc::Rc; use const_field_offset::FieldOffsets; use corelib::component::{ComponentRc, ComponentWeak}; use corelib::graphics::*; use corelib::input::{KeyEvent, KeyboardModifiers, MouseEvent, MouseEventType}; use corelib::items::{ItemRc, ItemRef, ItemWeak}; use corelib::properties::PropertyTracker; use corelib::slice::Slice; use corelib::window::{ComponentWindow, GenericWindow}; use corelib::Property; use sixtyfps_corelib as corelib; type WindowFactoryFn = dyn Fn(&crate::eventloop::EventLoop, winit::window::WindowBuilder) -> Backend; /// GraphicsWindow is an implementation of the [GenericWindow][`crate::eventloop::GenericWindow`] trait. This is /// typically instantiated by entry factory functions of the different graphics backends. pub struct GraphicsWindow { window_factory: Box>, map_state: RefCell>, properties: Pin>, cursor_blinker: RefCell>, keyboard_modifiers: std::cell::Cell, component: std::cell::RefCell, /// Gets dirty when the layout restrictions, or some other property of the windows change meta_property_listener: Pin>, focus_item: std::cell::RefCell, mouse_input_state: std::cell::Cell, /// Current popup's component and position /// FIXME: the popup should actually be another window, not just some overlay active_popup: std::cell::RefCell>, } impl GraphicsWindow { /// Creates a new reference-counted instance. /// /// Arguments: /// * `graphics_backend_factory`: The factor function stored in the GraphicsWindow that's called when the state /// of the window changes to mapped. The event loop and window builder parameters can be used to create a /// backing window. pub fn new( graphics_backend_factory: impl Fn(&crate::eventloop::EventLoop, winit::window::WindowBuilder) -> Backend + 'static, ) -> Rc { Rc::new(Self { window_factory: Box::new(graphics_backend_factory), map_state: RefCell::new(GraphicsWindowBackendState::Unmapped), properties: Box::pin(WindowProperties::default()), cursor_blinker: Default::default(), keyboard_modifiers: Default::default(), component: Default::default(), meta_property_listener: Rc::pin(Default::default()), focus_item: Default::default(), mouse_input_state: Default::default(), active_popup: Default::default(), }) } fn apply_geometry_constraint(&self, constraints: corelib::layout::LayoutInfo) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(window) => { if constraints != window.constraints.get() { let min_width = constraints.min_width.min(constraints.max_width); let min_height = constraints.min_height.min(constraints.max_height); let max_width = constraints.max_width.max(constraints.min_width); let max_height = constraints.max_height.max(constraints.min_height); window.backend.borrow().window().set_min_inner_size( if min_width > 0. || min_height > 0. { Some(winit::dpi::PhysicalSize::new(min_width, min_height)) } else { None }, ); window.backend.borrow().window().set_max_inner_size( if max_width < f32::MAX || max_height < f32::MAX { Some(winit::dpi::PhysicalSize::new( max_width.min(65535.), max_height.min(65535.), )) } else { None }, ); window.constraints.set(constraints); } } } } fn apply_window_properties(&self, window_item: Pin<&corelib::items::Window>) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(window) => { let backend = window.backend.borrow(); backend.window().set_title( corelib::items::Window::FIELD_OFFSETS .title .apply_pin(window_item) .get() .as_str(), ); } } } /// Requests for the window to be mapped to the screen. /// /// Arguments: /// * `event_loop`: The event loop used to drive further event handling for this window /// as it will receive events. /// * `component`: The component that holds the root item of the scene. If the item is a [`corelib::items::Window`], then /// the `width` and `height` properties are read and the values are passed to the windowing system as request /// for the initial size of the window. Then bindings are installed on these properties to keep them up-to-date /// with the size as it may be changed by the user or the windowing system in general. fn map_window(self: Rc, event_loop: &crate::eventloop::EventLoop) { if matches!(&*self.map_state.borrow(), GraphicsWindowBackendState::Mapped(..)) { return; } let component = self.component.borrow().upgrade().unwrap(); let component = ComponentRc::borrow_pin(&component); let root_item = component.as_ref().get_item_ref(0); let window_title = if let Some(window_item) = ItemRef::downcast_pin(root_item) { corelib::items::Window::FIELD_OFFSETS.title.apply_pin(window_item).get().to_string() } else { "SixtyFPS Window".to_string() }; let window_builder = winit::window::WindowBuilder::new().with_title(window_title); let id = { let backend = self.window_factory.as_ref()(&event_loop, window_builder); let platform_window = backend.window(); if std::env::var("SIXTYFPS_FULLSCREEN").is_ok() { platform_window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); } let window_id = platform_window.id(); // Ideally we should be passing the initial requested size to the window builder, but those properties // may be specified in logical pixels, relative to the scale factory, which we only know *after* mapping // the window to the screen. So we first map the window then, propagate the scale factory and *then* the // width/height properties should have the correct values calculated via their bindings that multiply with // the scale factor. // We could pass the logical requested size at window builder time, *if* we knew what the values are. { self.properties.as_ref().scale_factor.set(platform_window.scale_factor() as _); let existing_size = platform_window.inner_size(); let mut new_size = existing_size; if let Some(window_item) = ItemRef::downcast_pin(root_item) { let width = corelib::items::Window::FIELD_OFFSETS.width.apply_pin(window_item).get(); if width > 0. { new_size.width = width as _; } let height = corelib::items::Window::FIELD_OFFSETS.height.apply_pin(window_item).get(); if height > 0. { new_size.height = height as _; } { let window = self.clone(); window_item.as_ref().width.set_binding(move || { WindowProperties::FIELD_OFFSETS .width .apply_pin(window.properties.as_ref()) .get() }); } { let window = self.clone(); window_item.as_ref().height.set_binding(move || { WindowProperties::FIELD_OFFSETS .height .apply_pin(window.properties.as_ref()) .get() }); } } if new_size != existing_size { platform_window.set_inner_size(new_size) } self.properties.as_ref().width.set(new_size.width as _); self.properties.as_ref().height.set(new_size.height as _); } self.map_state.replace(GraphicsWindowBackendState::Mapped(MappedWindow { backend: RefCell::new(backend), constraints: Default::default(), })); window_id }; crate::eventloop::register_window(id, self.clone() as Rc); } /// Removes the window from the screen. The window is not destroyed though, it can be show (mapped) again later /// by calling [`GenericWindow::map_window`]. fn unmap_window(self: Rc) { self.map_state.replace(GraphicsWindowBackendState::Unmapped); if let Some(existing_blinker) = self.cursor_blinker.borrow().upgrade() { existing_blinker.stop(); } } } impl Drop for GraphicsWindow { fn drop(&mut self) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(mw) => { crate::eventloop::unregister_window(mw.backend.borrow().window().id()); } } if let Some(existing_blinker) = self.cursor_blinker.borrow().upgrade() { existing_blinker.stop(); } } } impl GenericWindow for GraphicsWindow { fn set_component(self: Rc, component: &ComponentRc) { *self.component.borrow_mut() = vtable::VRc::downgrade(&component) } fn draw(self: Rc) { let component_rc = self.component.borrow().upgrade().unwrap(); let component = ComponentRc::borrow_pin(&component_rc); { if self.meta_property_listener.as_ref().is_dirty() { self.meta_property_listener.as_ref().evaluate(|| { self.apply_geometry_constraint(component.as_ref().layout_info()); component.as_ref().apply_layout(self.get_geometry()); let root_item = component.as_ref().get_item_ref(0); if let Some(window_item) = ItemRef::downcast_pin(root_item) { self.apply_window_properties(window_item); } if let Some((popup, pos)) = &*self.active_popup.borrow() { let popup = ComponentRc::borrow_pin(popup); let popup_root = popup.as_ref().get_item_ref(0); let size = if let Some(window_item) = ItemRef::downcast_pin(popup_root) { let layout_info = popup.as_ref().layout_info(); let width = corelib::items::Window::FIELD_OFFSETS.width.apply_pin(window_item); let mut w = width.get(); if w < layout_info.min_width { w = layout_info.min_width; width.set(w); } let height = corelib::items::Window::FIELD_OFFSETS.height.apply_pin(window_item); let mut h = height.get(); if h < layout_info.min_height { h = layout_info.min_height; height.set(h); } Size::new(h, w) } else { Size::default() }; popup.as_ref().apply_layout(Rect::new(pos.clone(), size)); } }) } } let map_state = self.map_state.borrow(); let window = map_state.as_mapped(); let root_item = component.as_ref().get_item_ref(0); let background_color = if let Some(window_item) = ItemRef::downcast_pin(root_item) { corelib::items::Window::FIELD_OFFSETS.color.apply_pin(window_item).get() } else { RgbaColor { red: 255 as u8, green: 255, blue: 255, alpha: 255 }.into() }; let mut renderer = window.backend.borrow_mut().new_renderer(&background_color); corelib::item_rendering::render_component_items::( &component_rc, &mut renderer, Point::default(), ); if let Some(popup) = &*self.active_popup.borrow() { corelib::item_rendering::render_component_items::( &popup.0, &mut renderer, popup.1, ); } window.backend.borrow_mut().flush_renderer(renderer); } fn process_mouse_input(self: Rc, mut pos: Point, what: MouseEventType) { let active_popup = (*self.active_popup.borrow()).clone(); let component = if let Some(popup) = &active_popup { pos -= popup.1.to_vector(); if what == MouseEventType::MousePressed { // close the popup if one press outside the popup let geom = ComponentRc::borrow_pin(&popup.0).as_ref().get_item_ref(0).as_ref().geometry(); if !geom.contains(pos) { self.close_popup(); return; } } popup.0.clone() } else { self.component.borrow().upgrade().unwrap() }; self.mouse_input_state.set(corelib::input::process_mouse_input( component, MouseEvent { pos, what }, &ComponentWindow::new(self.clone()), self.mouse_input_state.take(), )); if active_popup.is_some() { //FIXME: currently the ComboBox is the only thing that uses the popup, and it should close automatically // on release. But ideally, there would be API to close the popup rather than always closing it on release if what == MouseEventType::MouseReleased { self.close_popup(); } } } fn process_key_input(self: Rc, event: &KeyEvent) { if let Some(focus_item) = self.as_ref().focus_item.borrow().upgrade() { let window = &ComponentWindow::new(self.clone()); focus_item.borrow().as_ref().key_event(event, &window); } } fn request_redraw(&self) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(window) => { window.backend.borrow().window().request_redraw() } } } fn scale_factor(&self) -> f32 { WindowProperties::FIELD_OFFSETS.scale_factor.apply_pin(self.properties.as_ref()).get() } fn set_scale_factor(&self, factor: f32) { self.properties.as_ref().scale_factor.set(factor); } fn refresh_window_scale_factor(&self) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(window) => { let sf = window.backend.borrow().window().scale_factor(); self.set_scale_factor(sf as f32) } } } fn set_width(&self, width: f32) { self.properties.as_ref().width.set(width); } fn set_height(&self, height: f32) { self.properties.as_ref().height.set(height); } fn get_geometry(&self) -> corelib::graphics::Rect { euclid::rect( 0., 0., WindowProperties::FIELD_OFFSETS.width.apply_pin(self.properties.as_ref()).get(), WindowProperties::FIELD_OFFSETS.height.apply_pin(self.properties.as_ref()).get(), ) } fn free_graphics_resources<'a>(self: Rc, items: &Slice<'a, Pin>>) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => {} GraphicsWindowBackendState::Mapped(window) => { corelib::item_rendering::free_item_rendering_data(items, &window.backend) } } } fn set_cursor_blink_binding(&self, prop: &corelib::properties::Property) { let existing_blinker = self.cursor_blinker.borrow().clone(); let blinker = existing_blinker.upgrade().unwrap_or_else(|| { let new_blinker = TextCursorBlinker::new(); *self.cursor_blinker.borrow_mut() = pin_weak::rc::PinWeak::downgrade(new_blinker.clone()); new_blinker }); TextCursorBlinker::set_binding(blinker, prop); } /// Returns the currently active keyboard notifiers. fn current_keyboard_modifiers(&self) -> KeyboardModifiers { self.keyboard_modifiers.get() } /// Sets the currently active keyboard notifiers. This is used only for testing or directly /// from the event loop implementation. fn set_current_keyboard_modifiers(&self, state: KeyboardModifiers) { self.keyboard_modifiers.set(state) } fn set_focus_item(self: Rc, focus_item: &ItemRc) { let window = ComponentWindow::new(self.clone()); if let Some(old_focus_item) = self.as_ref().focus_item.borrow().upgrade() { old_focus_item .borrow() .as_ref() .focus_event(&corelib::input::FocusEvent::FocusOut, &window); } *self.as_ref().focus_item.borrow_mut() = focus_item.downgrade(); focus_item.borrow().as_ref().focus_event(&corelib::input::FocusEvent::FocusIn, &window); } fn set_focus(self: Rc, have_focus: bool) { let window = ComponentWindow::new(self.clone()); let event = if have_focus { corelib::input::FocusEvent::WindowReceivedFocus } else { corelib::input::FocusEvent::WindowLostFocus }; if let Some(focus_item) = self.as_ref().focus_item.borrow().upgrade() { focus_item.borrow().as_ref().focus_event(&event, &window); } } fn show_popup(&self, popup: &ComponentRc, position: Point) { self.meta_property_listener.set_dirty(); *self.active_popup.borrow_mut() = Some((popup.clone(), position)); } fn close_popup(&self) { *self.active_popup.borrow_mut() = None; } fn run(self: Rc) { let event_loop = crate::eventloop::EventLoop::new(); self.clone().map_window(&event_loop); event_loop.run(); self.unmap_window(); } fn font( &self, request: corelib::graphics::FontRequest, ) -> Option> { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped => None, GraphicsWindowBackendState::Mapped(window) => { Some(window.backend.borrow_mut().font(request)) } } } } struct MappedWindow { backend: RefCell, constraints: Cell, } enum GraphicsWindowBackendState { Unmapped, Mapped(MappedWindow), } impl GraphicsWindowBackendState { fn as_mapped(&self) -> &MappedWindow { match self { GraphicsWindowBackendState::Unmapped => panic!( "internal error: tried to access window functions that require a mapped window" ), GraphicsWindowBackendState::Mapped(mw) => &mw, } } } #[derive(FieldOffsets)] #[repr(C)] #[pin] struct WindowProperties { scale_factor: Property, width: Property, height: Property, } impl Default for WindowProperties { fn default() -> Self { Self { scale_factor: Property::new(1.0), width: Property::new(800.), height: Property::new(600.), } } } /// The TextCursorBlinker takes care of providing a toggled boolean property /// that can be used to animate a blinking cursor. It's typically stored in the /// Window using a Weak and set_binding() can be used to set up a binding on a given /// property that'll keep it up-to-date. That binding keeps a strong reference to the /// blinker. If the underlying item that uses it goes away, the binding goes away and /// so does the blinker. #[derive(FieldOffsets)] #[repr(C)] #[pin] struct TextCursorBlinker { cursor_visible: Property, cursor_blink_timer: corelib::timers::Timer, } impl TextCursorBlinker { fn new() -> Pin> { Rc::pin(Self { cursor_visible: Property::new(true), cursor_blink_timer: Default::default(), }) } fn set_binding( instance: Pin>, prop: &corelib::properties::Property, ) { instance.as_ref().cursor_visible.set(true); // Re-start timer, in case. Self::start(&instance); prop.set_binding(move || { TextCursorBlinker::FIELD_OFFSETS.cursor_visible.apply_pin(instance.as_ref()).get() }); } fn start(self: &Pin>) { if self.cursor_blink_timer.running() { self.cursor_blink_timer.restart(); } else { let toggle_cursor = { let weak_blinker = pin_weak::rc::PinWeak::downgrade(self.clone()); move || { if let Some(blinker) = weak_blinker.upgrade() { let visible = TextCursorBlinker::FIELD_OFFSETS .cursor_visible .apply_pin(blinker.as_ref()) .get(); blinker.cursor_visible.set(!visible); } } }; self.cursor_blink_timer.start( corelib::timers::TimerMode::Repeated, std::time::Duration::from_millis(500), toggle_cursor, ); } } fn stop(&self) { self.cursor_blink_timer.stop() } }