slint/sixtyfps_runtime/rendering_backends/gl/glwindow.rs
2021-12-07 22:41:24 +01:00

609 lines
23 KiB
Rust

/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2021 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2021 Simon Hausmann <simon.hausmann@sixtyfps.io>
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.
// cspell:ignore corelib winit Borderless
use core::cell::{Cell, RefCell};
use core::pin::Pin;
use std::rc::{Rc, Weak};
use super::{ImageCache, ItemGraphicsCache};
use crate::event_loop::WinitWindow;
use const_field_offset::FieldOffsets;
use corelib::component::ComponentRc;
use corelib::graphics::*;
use corelib::input::KeyboardModifiers;
use corelib::items::{ItemRef, MouseCursor};
use corelib::layout::Orientation;
use corelib::window::{PlatformWindow, PopupWindow, PopupWindowLocation};
use corelib::Property;
use sixtyfps_corelib as corelib;
use winit::dpi::LogicalSize;
use crate::CanvasRc;
/// GraphicsWindow is an implementation of the [PlatformWindow][`crate::eventloop::PlatformWindow`] trait. This is
/// typically instantiated by entry factory functions of the different graphics back ends.
pub struct GLWindow {
self_weak: Weak<corelib::window::Window>,
map_state: RefCell<GraphicsWindowBackendState>,
keyboard_modifiers: std::cell::Cell<KeyboardModifiers>,
currently_pressed_key_code: std::cell::Cell<Option<winit::event::VirtualKeyCode>>,
pub(crate) graphics_cache: RefCell<ItemGraphicsCache>,
// This cache only contains textures. The cache for decoded CPU side images is in crate::IMAGE_CACHE.
pub(crate) texture_cache: RefCell<ImageCache>,
#[cfg(target_arch = "wasm32")]
canvas_id: String,
}
impl GLWindow {
/// 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(crate) fn new(
window_weak: &Weak<corelib::window::Window>,
#[cfg(target_arch = "wasm32")] canvas_id: String,
) -> Rc<Self> {
Rc::new(Self {
self_weak: window_weak.clone(),
map_state: RefCell::new(GraphicsWindowBackendState::Unmapped),
keyboard_modifiers: Default::default(),
currently_pressed_key_code: Default::default(),
graphics_cache: Default::default(),
texture_cache: Default::default(),
#[cfg(target_arch = "wasm32")]
canvas_id,
})
}
fn with_current_context<T>(&self, cb: impl FnOnce() -> T) -> T {
match &*self.map_state.borrow() {
GraphicsWindowBackendState::Unmapped => cb(),
GraphicsWindowBackendState::Mapped(window) => {
window.opengl_context.with_current_context(cb)
}
}
}
fn is_mapped(&self) -> bool {
matches!(&*self.map_state.borrow(), GraphicsWindowBackendState::Mapped { .. })
}
fn borrow_mapped_window(&self) -> Option<std::cell::Ref<MappedWindow>> {
if self.is_mapped() {
std::cell::Ref::map(self.map_state.borrow(), |state| match state {
GraphicsWindowBackendState::Unmapped => {
panic!("borrow_mapped_window must be called after checking if the window is mapped")
}
GraphicsWindowBackendState::Mapped(window) => window,
}).into()
} else {
None
}
}
fn borrow_mapped_window_mut(&self) -> Option<std::cell::RefMut<MappedWindow>> {
if self.is_mapped() {
std::cell::RefMut::map(self.map_state.borrow_mut(), |state| match state {
GraphicsWindowBackendState::Unmapped => {
panic!("borrow_mapped_window_mut must be called after checking if the window is mapped")
}
GraphicsWindowBackendState::Mapped(window) => window,
}).into()
} else {
None
}
}
pub fn default_font_properties(&self) -> FontRequest {
self.self_weak.upgrade().unwrap().default_font_properties()
}
}
impl WinitWindow for GLWindow {
fn runtime_window(&self) -> Rc<corelib::window::Window> {
self.self_weak.upgrade().unwrap()
}
fn currently_pressed_key_code(&self) -> &Cell<Option<winit::event::VirtualKeyCode>> {
&self.currently_pressed_key_code
}
fn current_keyboard_modifiers(&self) -> &Cell<KeyboardModifiers> {
&self.keyboard_modifiers
}
/// Draw the items of the specified `component` in the given window.
fn draw(self: Rc<Self>) {
let runtime_window = self.self_weak.upgrade().unwrap();
let scale_factor = runtime_window.scale_factor();
runtime_window.draw_contents(|components| {
let window = match self.borrow_mapped_window() {
Some(window) => window,
None => return, // caller bug, doesn't make sense to call draw() when not mapped
};
let size = window.opengl_context.window().inner_size();
window.opengl_context.make_current();
window.opengl_context.ensure_resized();
{
let mut canvas = window.canvas.borrow_mut();
// We pass 1.0 as dpi / device pixel ratio as femtovg only uses this factor to scale
// text metrics. Since we do the entire translation from logical pixels to physical
// pixels on our end, we don't need femtovg to scale a second time.
canvas.set_size(size.width, size.height, 1.0);
canvas.clear_rect(
0,
0,
size.width,
size.height,
crate::to_femtovg_color(&window.clear_color),
);
}
let mut renderer = crate::GLItemRenderer {
canvas: window.canvas.clone(),
layer_images_to_delete_after_flush: Default::default(),
graphics_window: self.clone(),
scale_factor,
state: vec![crate::State {
scissor: Rect::new(
Point::default(),
Size::new(size.width as _, size.height as _),
),
global_alpha: 1.,
layer: None,
}],
};
for (component, origin) in components {
corelib::item_rendering::render_component_items(
&component,
&mut renderer,
origin.clone(),
);
}
renderer.canvas.borrow_mut().flush();
// Delete any images and layer images (and their FBOs) before making the context not current anymore, to
// avoid GPU memory leaks.
renderer.graphics_window.texture_cache.borrow_mut().drain();
drop(renderer);
window.opengl_context.swap_buffers();
window.opengl_context.make_not_current();
});
}
fn with_window_handle(&self, callback: &mut dyn FnMut(&winit::window::Window)) {
if let Some(mapped_window) = self.borrow_mapped_window() {
callback(&*mapped_window.opengl_context.window())
}
}
fn constraints(&self) -> (corelib::layout::LayoutInfo, corelib::layout::LayoutInfo) {
self.borrow_mapped_window().map(|window| window.constraints.get()).unwrap_or_default()
}
fn set_constraints(
&self,
constraints: (corelib::layout::LayoutInfo, corelib::layout::LayoutInfo),
) {
if let Some(window) = self.borrow_mapped_window() {
window.constraints.set(constraints);
}
}
fn set_background_color(&self, color: Color) {
if let Some(mut window) = self.borrow_mapped_window_mut() {
window.clear_color = color;
}
}
fn set_icon(&self, icon: corelib::graphics::Image) {
if let Some(rgba) = crate::IMAGE_CACHE
.with(|c| c.borrow_mut().load_image_resource((&icon).into()))
.and_then(|i| i.to_rgba())
{
let (width, height) = rgba.dimensions();
if let Some(window) = self.borrow_mapped_window() {
window.opengl_context.window().set_window_icon(
winit::window::Icon::from_rgba(rgba.into_raw(), width, height).ok(),
);
}
};
}
}
impl PlatformWindow for GLWindow {
fn request_redraw(&self) {
match &*self.map_state.borrow() {
GraphicsWindowBackendState::Unmapped => {}
GraphicsWindowBackendState::Mapped(window) => {
window.opengl_context.window().request_redraw()
}
}
}
fn free_graphics_resources<'a>(&self, items: &mut dyn Iterator<Item = Pin<ItemRef<'a>>>) {
match &*self.map_state.borrow() {
GraphicsWindowBackendState::Unmapped => {}
GraphicsWindowBackendState::Mapped(_) => {
let mut cache_entries_to_clear = items
.flat_map(|item| {
let cached_rendering_data = item.cached_rendering_data_offset();
cached_rendering_data.release(&mut *self.graphics_cache.borrow_mut())
})
.peekable();
if cache_entries_to_clear.peek().is_some() {
self.with_current_context(|| {
cache_entries_to_clear.for_each(drop);
});
}
}
}
}
fn show_popup(&self, popup: &ComponentRc, position: Point) {
let runtime_window = self.self_weak.upgrade().unwrap();
let size = runtime_window.set_active_popup(PopupWindow {
location: PopupWindowLocation::ChildWindow(position),
component: popup.clone(),
});
let popup = ComponentRc::borrow_pin(popup);
let popup_root = popup.as_ref().get_item_ref(0);
if let Some(window_item) = ItemRef::downcast_pin(popup_root) {
let width_property =
corelib::items::WindowItem::FIELD_OFFSETS.width.apply_pin(window_item);
let height_property =
corelib::items::WindowItem::FIELD_OFFSETS.height.apply_pin(window_item);
width_property.set(size.width);
height_property.set(size.height);
}
}
fn request_window_properties_update(&self) {
match &*self.map_state.borrow() {
GraphicsWindowBackendState::Unmapped => {
// Nothing to be done if the window isn't visible. When it becomes visible,
// corelib::window::Window::show() calls update_window_properties()
}
GraphicsWindowBackendState::Mapped(window) => {
let window_id = window.opengl_context.window().id();
crate::event_loop::with_window_target(|event_loop| {
event_loop.event_loop_proxy().send_event(
crate::event_loop::CustomEvent::UpdateWindowProperties(window_id),
)
})
.ok();
}
}
}
fn apply_window_properties(&self, window_item: Pin<&sixtyfps_corelib::items::WindowItem>) {
// Make the unwrap() calls on self.borrow_mapped_window*() safe
if !self.is_mapped() {
return;
}
WinitWindow::apply_window_properties(self as &dyn WinitWindow, window_item);
}
fn apply_geometry_constraint(
&self,
constraints_horizontal: corelib::layout::LayoutInfo,
constraints_vertical: corelib::layout::LayoutInfo,
) {
self.apply_constraints(constraints_horizontal, constraints_vertical)
}
fn show(self: Rc<Self>) {
if self.is_mapped() {
return;
}
let runtime_window = self.runtime_window();
let component_rc = runtime_window.component();
let component = ComponentRc::borrow_pin(&component_rc);
let root_item = component.as_ref().get_item_ref(0);
let (window_title, no_frame, is_resizable) = if let Some(window_item) =
ItemRef::downcast_pin::<corelib::items::WindowItem>(root_item)
{
(
window_item.title().to_string(),
window_item.no_frame(),
window_item.height() == 0. && window_item.width() == 0.,
)
} else {
("SixtyFPS Window".to_string(), false, true)
};
let window_builder = winit::window::WindowBuilder::new()
.with_title(window_title)
.with_resizable(is_resizable);
let window_builder = if std::env::var("SIXTYFPS_FULLSCREEN").is_ok() {
window_builder.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)))
} else {
let layout_info_h = component.as_ref().layout_info(Orientation::Horizontal);
let layout_info_v = component.as_ref().layout_info(Orientation::Vertical);
let s = LogicalSize::new(
layout_info_h.preferred_bounded(),
layout_info_v.preferred_bounded(),
);
if s.width > 0. && s.height > 0. {
// Make sure that the window's inner size is in sync with the root window item's
// width/height.
runtime_window.set_window_item_geometry(s.width, s.height);
window_builder.with_inner_size(s)
} else {
window_builder
}
};
let window_builder =
if no_frame { window_builder.with_decorations(false) } else { window_builder };
#[cfg(target_arch = "wasm32")]
let (opengl_context, renderer) =
crate::OpenGLContext::new_context_and_renderer(window_builder, &self.canvas_id);
#[cfg(not(target_arch = "wasm32"))]
let (opengl_context, renderer) =
crate::OpenGLContext::new_context_and_renderer(window_builder);
let canvas = femtovg::Canvas::new_with_text_context(
renderer,
crate::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone()),
)
.unwrap();
opengl_context.make_not_current();
let canvas = Rc::new(RefCell::new(canvas));
let platform_window = opengl_context.window();
let runtime_window = self.self_weak.upgrade().unwrap();
runtime_window.set_scale_factor(platform_window.scale_factor() as _);
let id = platform_window.id();
drop(platform_window);
self.map_state.replace(GraphicsWindowBackendState::Mapped(MappedWindow {
canvas,
opengl_context,
clear_color: RgbaColor { red: 255_u8, green: 255, blue: 255, alpha: 255 }.into(),
constraints: Default::default(),
}));
crate::event_loop::register_window(id, self);
}
fn hide(self: Rc<Self>) {
// Release GL textures and other GPU bound resources.
self.with_current_context(|| {
self.graphics_cache.borrow_mut().clear();
self.texture_cache.borrow_mut().remove_textures();
});
self.map_state.replace(GraphicsWindowBackendState::Unmapped);
/* FIXME:
if let Some(existing_blinker) = self.cursor_blinker.borrow().upgrade() {
existing_blinker.stop();
}*/
}
fn set_mouse_cursor(&self, cursor: MouseCursor) {
let winit_cursor = match cursor {
MouseCursor::default => winit::window::CursorIcon::Default,
MouseCursor::none => winit::window::CursorIcon::Default,
MouseCursor::help => winit::window::CursorIcon::Help,
MouseCursor::pointer => winit::window::CursorIcon::Hand,
MouseCursor::progress => winit::window::CursorIcon::Progress,
MouseCursor::wait => winit::window::CursorIcon::Wait,
MouseCursor::crosshair => winit::window::CursorIcon::Crosshair,
MouseCursor::text => winit::window::CursorIcon::Text,
MouseCursor::alias => winit::window::CursorIcon::Alias,
MouseCursor::copy => winit::window::CursorIcon::Copy,
MouseCursor::r#move => winit::window::CursorIcon::Move,
MouseCursor::no_drop => winit::window::CursorIcon::NoDrop,
MouseCursor::not_allowed => winit::window::CursorIcon::NotAllowed,
MouseCursor::grab => winit::window::CursorIcon::Grab,
MouseCursor::grabbing => winit::window::CursorIcon::Grabbing,
MouseCursor::col_resize => winit::window::CursorIcon::ColResize,
MouseCursor::row_resize => winit::window::CursorIcon::RowResize,
MouseCursor::n_resize => winit::window::CursorIcon::NResize,
MouseCursor::e_resize => winit::window::CursorIcon::EResize,
MouseCursor::s_resize => winit::window::CursorIcon::SResize,
MouseCursor::w_resize => winit::window::CursorIcon::WResize,
MouseCursor::ne_resize => winit::window::CursorIcon::NeResize,
MouseCursor::nw_resize => winit::window::CursorIcon::NwResize,
MouseCursor::se_resize => winit::window::CursorIcon::SeResize,
MouseCursor::sw_resize => winit::window::CursorIcon::SwResize,
MouseCursor::ew_resize => winit::window::CursorIcon::EwResize,
MouseCursor::ns_resize => winit::window::CursorIcon::NsResize,
MouseCursor::nesw_resize => winit::window::CursorIcon::NeswResize,
MouseCursor::nwse_resize => winit::window::CursorIcon::NwseResize,
};
self.with_window_handle(& mut |winit_window| {
winit_window.set_cursor_visible(cursor != MouseCursor::none);
winit_window.set_cursor_icon(winit_cursor);
});
}
fn text_size(
&self,
font_request: corelib::graphics::FontRequest,
text: &str,
max_width: Option<f32>,
) -> Size {
let font_request = font_request.merge(&self.default_font_properties());
crate::fonts::text_size(
&font_request,
self.self_weak.upgrade().unwrap().scale_factor(),
text,
max_width,
)
}
fn text_input_byte_offset_for_position(
&self,
text_input: Pin<&sixtyfps_corelib::items::TextInput>,
pos: Point,
) -> usize {
let scale_factor = self.self_weak.upgrade().unwrap().scale_factor();
let pos = pos * scale_factor;
let text = text_input.text();
let mut result = text.len();
let width = text_input.width() * scale_factor;
let height = text_input.height() * scale_factor;
if width <= 0. || height <= 0. {
return 0;
}
let font = crate::fonts::FONT_CACHE.with(|cache| {
cache.borrow_mut().font(
text_input.unresolved_font_request().merge(&self.default_font_properties()),
scale_factor,
&text_input.text(),
)
});
let paint = font.init_paint(text_input.letter_spacing() * scale_factor, Default::default());
let text_context =
crate::fonts::FONT_CACHE.with(|cache| cache.borrow().text_context.clone());
let font_height = text_context.measure_font(paint).unwrap().height();
crate::fonts::layout_text_lines(
text.as_str(),
&font,
Size::new(width, height),
(text_input.horizontal_alignment(), text_input.vertical_alignment()),
text_input.wrap(),
sixtyfps_corelib::items::TextOverflow::clip,
text_input.single_line(),
paint,
|line_text, line_pos, start, metrics| {
if (line_pos.y..(line_pos.y + font_height)).contains(&pos.y) {
let mut current_x = 0.;
for glyph in &metrics.glyphs {
if line_pos.x + current_x + glyph.advance_x / 2. >= pos.x {
result = start + glyph.byte_index;
return;
}
current_x += glyph.advance_x;
}
result = start + line_text.trim_end().len();
}
},
);
result
}
fn text_input_position_for_byte_offset(
&self,
text_input: Pin<&corelib::items::TextInput>,
byte_offset: usize,
) -> Point {
let scale_factor = self.self_weak.upgrade().unwrap().scale_factor();
let text = text_input.text();
let mut result = Point::default();
let width = text_input.width() * scale_factor;
let height = text_input.height() * scale_factor;
if width <= 0. || height <= 0. {
return result;
}
let font = crate::fonts::FONT_CACHE.with(|cache| {
cache.borrow_mut().font(
text_input.unresolved_font_request().merge(&self.default_font_properties()),
scale_factor,
&text_input.text(),
)
});
let paint = font.init_paint(text_input.letter_spacing() * scale_factor, Default::default());
crate::fonts::layout_text_lines(
text.as_str(),
&font,
Size::new(width, height),
(text_input.horizontal_alignment(), text_input.vertical_alignment()),
text_input.wrap(),
sixtyfps_corelib::items::TextOverflow::clip,
text_input.single_line(),
paint,
|line_text, line_pos, start, metrics| {
if (start..=(start + line_text.len())).contains(&byte_offset) {
for glyph in &metrics.glyphs {
if glyph.byte_index == (byte_offset - start) {
result = line_pos + euclid::vec2(glyph.x, glyph.y);
return;
}
}
if let Some(last) = metrics.glyphs.last() {
result = line_pos + euclid::vec2(last.x + last.advance_x, last.y);
}
}
},
);
result / scale_factor
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
struct MappedWindow {
canvas: CanvasRc,
opengl_context: crate::OpenGLContext,
clear_color: Color,
constraints: Cell<(corelib::layout::LayoutInfo, corelib::layout::LayoutInfo)>,
}
impl Drop for MappedWindow {
fn drop(&mut self) {
crate::event_loop::unregister_window(self.opengl_context.window().id());
}
}
enum GraphicsWindowBackendState {
Unmapped,
Mapped(MappedWindow),
}
#[derive(FieldOffsets)]
#[repr(C)]
#[pin]
struct WindowProperties {
scale_factor: Property<f32>,
}
impl Default for WindowProperties {
fn default() -> Self {
Self { scale_factor: Property::new(1.0) }
}
}