// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial //! This module contains the GraphicsWindow that used to be within corelib. // cspell:ignore borderless corelib nesw webgl winit winsys xlib use core::cell::{Cell, RefCell}; use core::pin::Pin; use std::rc::{Rc, Weak}; use crate::event_loop::WinitWindow; use crate::renderer::{WinitCompatibleCanvas, WinitCompatibleRenderer}; use const_field_offset::FieldOffsets; use corelib::component::ComponentRc; use corelib::input::KeyboardModifiers; use corelib::items::{ItemRef, MouseCursor}; use corelib::layout::Orientation; use corelib::window::{WindowAdapter, WindowAdapterSealed, WindowHandleAccess}; use corelib::Property; use corelib::{graphics::*, Coord}; use i_slint_core as corelib; use winit::dpi::LogicalSize; /// GraphicsWindow is an implementation of the [WindowAdapter][`crate::eventloop::WindowAdapter`] trait. This is /// typically instantiated by entry factory functions of the different graphics back ends. pub(crate) struct GLWindow { window: corelib::api::Window, self_weak: Weak, map_state: RefCell>, keyboard_modifiers: std::cell::Cell, currently_pressed_key_code: std::cell::Cell>, renderer: Renderer, #[cfg(target_arch = "wasm32")] virtual_keyboard_helper: RefCell>, } 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(#[cfg(target_arch = "wasm32")] canvas_id: String) -> Rc { let self_rc = Rc::new_cyclic(|self_weak| Self { window: corelib::api::Window::new(self_weak.clone() as _), self_weak: self_weak.clone(), map_state: RefCell::new(GraphicsWindowBackendState::Unmapped { requested_position: None, requested_size: None, }), keyboard_modifiers: Default::default(), currently_pressed_key_code: Default::default(), renderer: Renderer::new( &(self_weak.clone() as _), #[cfg(target_arch = "wasm32")] canvas_id, ), #[cfg(target_arch = "wasm32")] virtual_keyboard_helper: Default::default(), }); self_rc as _ } fn is_mapped(&self) -> bool { matches!(&*self.map_state.borrow(), GraphicsWindowBackendState::Mapped { .. }) } fn borrow_mapped_window(&self) -> Option>> { 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 unmap(&self) { let old_mapped = match self.map_state.replace(GraphicsWindowBackendState::Unmapped { requested_position: None, requested_size: None, }) { GraphicsWindowBackendState::Unmapped { .. } => return, GraphicsWindowBackendState::Mapped(old_mapped) => old_mapped, }; old_mapped.canvas.with_window_handle(|winit_window| { crate::event_loop::unregister_window(winit_window.id()); }); self.renderer.release_canvas(old_mapped.canvas); } fn show_impl(&self) { let (requested_position, requested_size) = match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped { requested_position, requested_size } => { (requested_position.clone(), requested_size.clone()) } GraphicsWindowBackendState::Mapped(_) => return, }; let runtime_window = self.window().window_handle(); let component_rc = runtime_window.component(); let component = ComponentRc::borrow_pin(&component_rc); let (window_title, no_frame, is_resizable) = if let Some(window_item) = runtime_window.window_item().as_ref().map(|i| i.as_pin_ref()) { ( window_item.title().to_string(), window_item.no_frame(), window_item.height() <= 0 as _ && window_item.width() <= 0 as _, ) } else { ("Slint Window".to_string(), false, true) }; let mut window_builder = winit::window::WindowBuilder::new() .with_title(window_title) .with_resizable(is_resizable); let scale_factor_override = runtime_window.scale_factor(); // If the scale factor was already set programmatically, use that // else, use the SLINT_SCALE_FACTOR if set, otherwise use the one from winit let scale_factor_override = if scale_factor_override > 1. { Some(scale_factor_override as f64) } else { std::env::var("SLINT_SCALE_FACTOR") .ok() .and_then(|x| x.parse::().ok()) .filter(|f| *f > 0.) }; let into_size = |s: winit::dpi::LogicalSize| -> winit::dpi::Size { if let Some(f) = scale_factor_override { s.to_physical::(f).into() } else { s.into() } }; 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()); let window_builder = if std::env::var("SLINT_FULLSCREEN").is_ok() { window_builder.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) } else { if layout_info_h.min >= 1. || layout_info_v.min >= 1. { window_builder = window_builder.with_min_inner_size(into_size( LogicalSize::new(layout_info_h.min, layout_info_v.min), )) } if layout_info_h.max < f32::MAX || layout_info_v.max < f32::MAX { window_builder = window_builder.with_max_inner_size(into_size( LogicalSize::new(layout_info_h.max, layout_info_v.max), )) } if let Some(requested_size) = requested_size { // It would be nice to bound this with our constraints, but those are in logical coordinates // and we don't know the scale factor yet... window_builder.with_inner_size(winit::dpi::Size::new( winit::dpi::PhysicalSize::new(requested_size.width, requested_size.height), )) } else if s.width > 0 as Coord && s.height > 0 as Coord { // 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(into_size(s)) } else { window_builder } }; let window_builder = if no_frame { window_builder.with_decorations(false) } else { window_builder }; let window_builder = if let Some(requested_position) = requested_position { window_builder.with_position(winit::dpi::Position::new( winit::dpi::PhysicalPosition::new(requested_position.x, requested_position.y), )) } else { window_builder }; let canvas = self.renderer.create_canvas(window_builder); let id = canvas.with_window_handle(|winit_window| { self.window.window_handle().set_scale_factor( scale_factor_override.unwrap_or_else(|| winit_window.scale_factor()) as _, ); // On wasm, with_inner_size on the WindowBuilder don't have effect, so apply manually #[cfg(target_arch = "wasm32")] if (s.width > 0 as Coord && s.height > 0 as Coord) { winit_window.set_inner_size(s); } winit_window.id() }); self.map_state.replace(GraphicsWindowBackendState::Mapped(MappedWindow { canvas, constraints: Default::default(), })); crate::event_loop::register_window(id, self.self_weak.upgrade().unwrap()); } } impl WinitWindow for GLWindow { fn currently_pressed_key_code(&self) -> &Cell> { &self.currently_pressed_key_code } fn current_keyboard_modifiers(&self) -> &Cell { &self.keyboard_modifiers } /// Draw the items of the specified `component` in the given window. fn draw(&self) { let window = match self.borrow_mapped_window() { Some(window) => window, None => return, // caller bug, doesn't make sense to call draw() when not mapped }; self.renderer.render(&window.canvas, self); } fn with_window_handle(&self, callback: &mut dyn FnMut(&winit::window::Window)) { if let Some(mapped_window) = self.borrow_mapped_window() { mapped_window.canvas.with_window_handle(callback); } } 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_icon(&self, icon: corelib::graphics::Image) { let image_inner: &ImageInner = (&icon).into(); let pixel_buffer = match image_inner { ImageInner::EmbeddedImage { buffer, .. } => buffer.clone(), _ => return, }; // This could become a method in SharedPixelBuffer... let rgba_pixels: Vec = match &pixel_buffer { SharedImageBuffer::RGB8(pixels) => pixels .as_bytes() .chunks(3) .flat_map(|rgb| IntoIterator::into_iter([rgb[0], rgb[1], rgb[2], 255])) .collect(), SharedImageBuffer::RGBA8(pixels) => pixels.as_bytes().to_vec(), SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels .as_bytes() .chunks(4) .flat_map(|rgba| { let alpha = rgba[3] as u32; IntoIterator::into_iter(rgba) .take(3) .map(move |component| (*component as u32 * alpha / 255) as u8) .chain(std::iter::once(alpha as u8)) }) .collect(), }; self.with_window_handle(&mut |window| { window.set_window_icon( winit::window::Icon::from_rgba( rgba_pixels.clone(), // FIXME: if the closure were FnOnce we could move rgba_pixels pixel_buffer.width(), pixel_buffer.height(), ) .ok(), ); }) } #[cfg(target_arch = "wasm32")] fn input_method_focused(&self) -> bool { match self.virtual_keyboard_helper.try_borrow() { Ok(vkh) => vkh.as_ref().map_or(false, |h| h.has_focus()), // the only location in which the virtual_keyboard_helper is mutably borrowed is from // show_virtual_keyboard, which means we have the focus Err(_) => true, } } fn resize_event(&self, size: winit::dpi::PhysicalSize) { if let Some(mapped_window) = self.borrow_mapped_window() { self.window().set_size(corelib::api::PhysicalSize::new(size.width, size.height)); mapped_window.canvas.resize_event() } } fn inner_size(&self) -> corelib::api::PhysicalSize { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped { requested_size, .. } => { requested_size.unwrap_or_default() } GraphicsWindowBackendState::Mapped(mapped_window) => { mapped_window.canvas.with_window_handle(|winit_window| { let size = winit_window.inner_size(); corelib::api::PhysicalSize::new(size.width, size.height) }) } } } } impl WindowAdapter for GLWindow { fn window(&self) -> &corelib::api::Window { &self.window } } impl WindowAdapterSealed for GLWindow { fn request_redraw(&self) { self.with_window_handle(&mut |window| window.request_redraw()) } fn unregister_component<'a>( &self, component: corelib::component::ComponentRef, _items: &mut dyn Iterator>>, ) { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped { .. } => {} GraphicsWindowBackendState::Mapped(mapped_window) => { mapped_window.canvas.component_destroyed(component) } } } fn request_window_properties_update(&self) { self.with_window_handle(&mut |window| { let window_id = 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<&i_slint_core::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) { // Creating a new winit::Window requires access to the running event loop (`&EventLoopWindowTarget`), which // is only possible from within the event loop handler with wasm, as `run()` consumes the event loop. Work // around by invoking `show()` from within the event loop handler. #[cfg(target_arch = "wasm32")] corelib::api::invoke_from_event_loop({ let self_weak = send_wrapper::SendWrapper::new(self.self_weak.clone()); move || { if let Some(this) = self_weak.take().upgrade() { this.show_impl(); } } }); #[cfg(not(target_arch = "wasm32"))] self.show_impl(); } fn hide(&self) { self.unmap(); /* FIXME: if let Some(existing_blinker) = self.cursor_blinker.borrow().upgrade() { existing_blinker.stop(); }*/ crate::event_loop::with_window_target(|event_loop| { event_loop.event_loop_proxy().send_event(crate::event_loop::CustomEvent::WindowHidden) }) .unwrap(); } 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::Move => winit::window::CursorIcon::Move, MouseCursor::NoDrop => winit::window::CursorIcon::NoDrop, MouseCursor::NotAllowed => winit::window::CursorIcon::NotAllowed, MouseCursor::Grab => winit::window::CursorIcon::Grab, MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing, MouseCursor::ColResize => winit::window::CursorIcon::ColResize, MouseCursor::RowResize => winit::window::CursorIcon::RowResize, MouseCursor::NResize => winit::window::CursorIcon::NResize, MouseCursor::EResize => winit::window::CursorIcon::EResize, MouseCursor::SResize => winit::window::CursorIcon::SResize, MouseCursor::WResize => winit::window::CursorIcon::WResize, MouseCursor::NeResize => winit::window::CursorIcon::NeResize, MouseCursor::NwResize => winit::window::CursorIcon::NwResize, MouseCursor::SeResize => winit::window::CursorIcon::SeResize, MouseCursor::SwResize => winit::window::CursorIcon::SwResize, MouseCursor::EwResize => winit::window::CursorIcon::EwResize, MouseCursor::NsResize => winit::window::CursorIcon::NsResize, MouseCursor::NeswResize => winit::window::CursorIcon::NeswResize, MouseCursor::NwseResize => 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 renderer(&self) -> &dyn i_slint_core::renderer::Renderer { &self.renderer } #[cfg(target_arch = "wasm32")] fn show_virtual_keyboard(&self, _it: corelib::items::InputType) { let mut vkh = self.virtual_keyboard_helper.borrow_mut(); let h = vkh.get_or_insert_with(|| { let canvas = self.borrow_mapped_window().unwrap().canvas.html_canvas_element().clone(); super::wasm_input_helper::WasmInputHelper::new(self.self_weak.clone(), canvas) }); h.show(); } #[cfg(target_arch = "wasm32")] fn hide_virtual_keyboard(&self) { if let Some(h) = &*self.virtual_keyboard_helper.borrow() { h.hide() } } fn as_any(&self) -> &dyn std::any::Any { self } fn position(&self) -> corelib::api::PhysicalPosition { match &*self.map_state.borrow() { GraphicsWindowBackendState::Unmapped { requested_position, .. } => { requested_position.unwrap_or_default() } GraphicsWindowBackendState::Mapped(mapped_window) => mapped_window .canvas .with_window_handle(|winit_window| match winit_window.outer_position() { Ok(outer_position) => { corelib::api::PhysicalPosition::new(outer_position.x, outer_position.y) } Err(_) => Default::default(), }), } } fn set_position(&self, position: corelib::api::PhysicalPosition) { match &mut *self.map_state.borrow_mut() { GraphicsWindowBackendState::Unmapped { requested_position, .. } => { *requested_position = Some(position) } GraphicsWindowBackendState::Mapped(mapped_window) => { mapped_window.canvas.with_window_handle(|winit_window| { winit_window.set_outer_position(winit::dpi::Position::new( winit::dpi::PhysicalPosition::new(position.x, position.y), )) }) } } } fn set_inner_size(&self, size: corelib::api::PhysicalSize) { if let Ok(mut map_state) = self.map_state.try_borrow_mut() { // otherwise we are called from the resize event match &mut *map_state { GraphicsWindowBackendState::Unmapped { requested_size, .. } => { *requested_size = Some(size) } GraphicsWindowBackendState::Mapped(mapped_window) => { mapped_window.canvas.with_window_handle(|winit_window| { winit_window .set_inner_size(winit::dpi::PhysicalSize::new(size.width, size.height)); }); } } } } } impl Drop for GLWindow { fn drop(&mut self) { self.unmap(); } } struct MappedWindow { canvas: Renderer::Canvas, constraints: Cell<(corelib::layout::LayoutInfo, corelib::layout::LayoutInfo)>, } enum GraphicsWindowBackendState { Unmapped { requested_position: Option, requested_size: Option, }, Mapped(MappedWindow), } #[derive(FieldOffsets)] #[repr(C)] #[pin] struct WindowProperties { scale_factor: Property, } impl Default for WindowProperties { fn default() -> Self { Self { scale_factor: Property::new(1.0) } } }