diff --git a/sixtyfps_runtime/corelib/eventloop.rs b/sixtyfps_runtime/corelib/eventloop.rs index 948c65495..2b3b1c111 100644 --- a/sixtyfps_runtime/corelib/eventloop.rs +++ b/sixtyfps_runtime/corelib/eventloop.rs @@ -27,13 +27,13 @@ thread_local! { static ALL_WINDOWS: RefCell>> = RefCell::new(std::collections::HashMap::new()); } -pub(crate) fn register_window(id: winit::window::WindowId, window: Rc) { +pub fn register_window(id: winit::window::WindowId, window: Rc) { ALL_WINDOWS.with(|windows| { windows.borrow_mut().insert(id, Rc::downgrade(&window)); }) } -pub(crate) fn unregister_window(id: winit::window::WindowId) { +pub fn unregister_window(id: winit::window::WindowId) { ALL_WINDOWS.with(|windows| { windows.borrow_mut().remove(&id); }) diff --git a/sixtyfps_runtime/corelib/graphics.rs b/sixtyfps_runtime/corelib/graphics.rs index f88bc156f..d0ec6f15e 100644 --- a/sixtyfps_runtime/corelib/graphics.rs +++ b/sixtyfps_runtime/corelib/graphics.rs @@ -20,24 +20,15 @@ LICENSE END */ created by the backend in a type-erased manner. */ extern crate alloc; -use crate::component::{ComponentRc, ComponentWeak}; -use crate::input::{KeyEvent, KeyboardModifiers, MouseEvent, MouseEventType}; use crate::item_rendering::CachedRenderingData; -use crate::items::{ItemRc, ItemRef, ItemWeak}; -use crate::properties::{InterpolatedPropertyValue, Property, PropertyTracker}; +use crate::properties::InterpolatedPropertyValue; #[cfg(feature = "rtti")] use crate::rtti::{BuiltinItem, FieldInfo, PropertyInfo, ValueType}; -use crate::slice::Slice; -use crate::window::{ComponentWindow, GenericWindow}; -#[cfg(feature = "rtti")] -use crate::Callback; -use crate::SharedString; - +use crate::window::ComponentWindow; +use crate::{Callback, SharedString}; use auto_enums::auto_enum; use const_field_offset::FieldOffsets; -use core::pin::Pin; use sixtyfps_corelib_macros::*; -use std::cell::{Cell, RefCell}; use std::rc::Rc; /// 2D Rectangle @@ -336,517 +327,6 @@ pub trait GraphicsBackend: Sized { fn window(&self) -> &winit::window::Window; } -type WindowFactoryFn = - dyn Fn(&crate::eventloop::EventLoop, winit::window::WindowBuilder) -> Backend; - -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.), - } - } -} - -/// 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: std::cell::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(), - }) - } - - /// Returns the window id of the window if it is mapped, None otherwise. - pub fn id(&self) -> Option { - Some(self.map_state.borrow().as_mapped().backend.borrow().window().id()) - } - - fn apply_geometry_constraint(&self, constraints: crate::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<&crate::items::Window>) { - match &*self.map_state.borrow() { - GraphicsWindowBackendState::Unmapped => {} - GraphicsWindowBackendState::Mapped(window) => { - let backend = window.backend.borrow(); - backend.window().set_title( - crate::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 [`crate::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) { - crate::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 = - crate::items::Window::FIELD_OFFSETS.width.apply_pin(window_item).get(); - if width > 0. { - new_size.width = width as _; - } - let height = - crate::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 = - crate::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 = - crate::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) { - crate::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); - crate::item_rendering::render_component_items::( - &component_rc, - &mut renderer, - Point::default(), - ); - if let Some(popup) = &*self.active_popup.borrow() { - crate::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(crate::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); - match &*self.map_state.borrow() { - GraphicsWindowBackendState::Unmapped => {} - GraphicsWindowBackendState::Mapped(window) => { - window.backend.borrow_mut().refresh_window_scale_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) -> crate::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) => { - crate::item_rendering::free_item_rendering_data(items, &window.backend) - } - } - } - - fn set_cursor_blink_binding(&self, prop: &crate::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(&crate::input::FocusEvent::FocusOut, &window); - } - - *self.as_ref().focus_item.borrow_mut() = focus_item.downgrade(); - - focus_item.borrow().as_ref().focus_event(&crate::input::FocusEvent::FocusIn, &window); - } - - fn set_focus(self: Rc, have_focus: bool) { - let window = ComponentWindow::new(self.clone()); - let event = if have_focus { - crate::input::FocusEvent::WindowReceivedFocus - } else { - crate::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: crate::graphics::FontRequest) -> Option> { - match &*self.map_state.borrow() { - GraphicsWindowBackendState::Unmapped => None, - GraphicsWindowBackendState::Mapped(window) => { - Some(window.backend.borrow_mut().font(request)) - } - } - } -} - #[repr(C)] #[derive(FieldOffsets, Default, BuiltinItem, Clone, Debug, PartialEq)] #[pin] @@ -1211,66 +691,6 @@ pub(crate) mod ffi { } } -/// 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: crate::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: &crate::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( - crate::timers::TimerMode::Repeated, - std::time::Duration::from_millis(500), - toggle_cursor, - ); - } - } - - fn stop(&self) { - self.cursor_blink_timer.stop() - } -} - /// This function can be used to register a custom TrueType font with SixtyFPS, /// for use with the `font-family` property. The provided slice must be a valid TrueType /// font. diff --git a/sixtyfps_runtime/corelib/item_rendering.rs b/sixtyfps_runtime/corelib/item_rendering.rs index e98167bf6..231238f7c 100644 --- a/sixtyfps_runtime/corelib/item_rendering.rs +++ b/sixtyfps_runtime/corelib/item_rendering.rs @@ -58,7 +58,7 @@ impl CachedRenderingData { } } -pub(crate) fn render_component_items( +pub fn render_component_items( component: &ComponentRc, renderer: &mut Backend::ItemRenderer, origin: crate::graphics::Point, @@ -86,7 +86,7 @@ pub(crate) fn render_component_items( ); } -pub(crate) fn free_item_rendering_data<'a, Backend: GraphicsBackend>( +pub fn free_item_rendering_data<'a, Backend: GraphicsBackend>( items: &Slice<'a, core::pin::Pin>>, renderer: &RefCell, ) { diff --git a/sixtyfps_runtime/rendering_backends/gl/Cargo.toml b/sixtyfps_runtime/rendering_backends/gl/Cargo.toml index 4ff7c04aa..09df09608 100644 --- a/sixtyfps_runtime/rendering_backends/gl/Cargo.toml +++ b/sixtyfps_runtime/rendering_backends/gl/Cargo.toml @@ -19,6 +19,7 @@ default = ["x11"] [dependencies] sixtyfps-corelib = { version = "=0.0.4", path = "../../corelib", features = ["femtovg_backend"] } +const-field-offset = { version = "0.1", path = "../../../helper_crates/const-field-offset" } lyon = { version = "0.16" } image = { version = "0.23.12", default-features = false } rgb = "0.8" @@ -32,6 +33,7 @@ smallvec = "1.4.1" by_address = "1.0.4" femtovg = { git = "https://github.com/femtovg/femtovg", branch = "master" } euclid = "0.22.1" +pin-weak = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] web_sys = { version = "0.3", package = "web-sys", features=["console", "WebGlContextAttributes"] } diff --git a/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs b/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs new file mode 100644 index 000000000..57c5c948b --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/gl/graphics_window.rs @@ -0,0 +1,607 @@ +/* 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(&corelib::eventloop::EventLoop, winit::window::WindowBuilder) -> Backend; + +/// GraphicsWindow is an implementation of the [GenericWindow][`corelib::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(&corelib::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(), + }) + } + + /// Returns the window id of the window if it is mapped, None otherwise. + pub fn id(&self) -> Option { + Some(self.map_state.borrow().as_mapped().backend.borrow().window().id()) + } + + 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: &corelib::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 + }; + + corelib::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) => { + corelib::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); + match &*self.map_state.borrow() { + GraphicsWindowBackendState::Unmapped => {} + GraphicsWindowBackendState::Mapped(window) => { + window.backend.borrow_mut().refresh_window_scale_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 = corelib::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() + } +} diff --git a/sixtyfps_runtime/rendering_backends/gl/lib.rs b/sixtyfps_runtime/rendering_backends/gl/lib.rs index f441ba455..cf32041ab 100644 --- a/sixtyfps_runtime/rendering_backends/gl/lib.rs +++ b/sixtyfps_runtime/rendering_backends/gl/lib.rs @@ -11,14 +11,16 @@ LICENSE END */ use std::{cell::RefCell, collections::HashMap, rc::Rc}; use sixtyfps_corelib::graphics::{ - Color, Font, FontRequest, GraphicsBackend, GraphicsWindow, Point, Rect, RenderingCache, - Resource, + Color, Font, FontRequest, GraphicsBackend, Point, Rect, RenderingCache, Resource, }; use sixtyfps_corelib::item_rendering::{CachedRenderingData, ItemRenderer}; use sixtyfps_corelib::items::Item; use sixtyfps_corelib::window::ComponentWindow; use sixtyfps_corelib::{Property, SharedString, SharedVector}; +mod graphics_window; +use graphics_window::*; + type CanvasRc = Rc>>; type RenderingCacheRc = Rc>>>;