// 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::graphics::euclid::num::Zero; use corelib::input::KeyboardModifiers; use corelib::items::{ItemRef, MouseCursor}; use corelib::layout::Orientation; use corelib::lengths::{LogicalLength, LogicalPoint, LogicalSize}; use corelib::window::{WindowAdapter, WindowAdapterSealed, WindowInner}; use corelib::Property; use corelib::{graphics::*, Coord}; use i_slint_core as corelib; fn position_to_winit(pos: &corelib::api::WindowPosition) -> winit::dpi::Position { match pos { corelib::api::WindowPosition::Logical(pos) => { winit::dpi::Position::new(winit::dpi::LogicalPosition::new(pos.x, pos.y)) } corelib::api::WindowPosition::Physical(pos) => { winit::dpi::Position::new(winit::dpi::PhysicalPosition::new(pos.x, pos.y)) } } } fn size_to_winit(pos: &corelib::api::WindowSize) -> winit::dpi::Size { match pos { corelib::api::WindowSize::Logical(size) => { winit::dpi::Size::new(winit::dpi::LogicalSize::new(size.width, size.height)) } corelib::api::WindowSize::Physical(size) => { winit::dpi::Size::new(winit::dpi::PhysicalSize::new(size.width, size.height)) } } } fn icon_to_winit(icon: corelib::graphics::Image) -> Option { let image_inner: &ImageInner = (&icon).into(); let pixel_buffer = match image_inner { ImageInner::EmbeddedImage { buffer, .. } => buffer.clone(), _ => return None, }; // 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(), }; winit::window::Icon::from_rgba(rgba_pixels, pixel_buffer.width(), pixel_buffer.height()).ok() } /// 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>, pending_redraw: 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(), pending_redraw: Cell::new(false), 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 call_with_event_loop(&self, callback: fn(&Self)) { // With wasm, winit's `run()` consumes the event loop and access to it from outside the event handler yields // loop and thus ends up trying to create a new event loop instance, which panics in winit. Instead, forward // the call to be invoked from within the event loop #[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() { callback(&this) } } }) .unwrap(); #[cfg(not(target_arch = "wasm32"))] callback(self) } } impl WinitWindow for GLWindow { fn take_pending_redraw(&self) -> bool { self.pending_redraw.take() } 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) -> bool { let window = match self.borrow_mapped_window() { Some(window) => window, None => return false, // caller bug, doesn't make sense to call draw() when not mapped }; self.pending_redraw.set(false); self.renderer.render(&window.canvas, self); self.pending_redraw.get() } 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); } } #[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() } } } impl WindowAdapter for GLWindow { fn window(&self) -> &corelib::api::Window { &self.window } } impl WindowAdapterSealed for GLWindow { fn request_redraw(&self) { self.pending_redraw.set(true); 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.call_with_event_loop(|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; } let mut width = window_item.width().get() as f32; let mut height = window_item.height().get() as f32; let mut must_resize = false; self.with_window_handle(&mut |winit_window| { winit_window.set_window_icon(icon_to_winit(window_item.icon())); winit_window.set_title(&window_item.title()); winit_window .set_decorations(!window_item.no_frame() || winit_window.fullscreen().is_some()); if width <= 0. || height <= 0. { must_resize = true; let winit_size = winit_window.inner_size().to_logical(self.window().scale_factor() as f64); if width <= 0. { width = winit_size.width; } if height <= 0. { height = winit_size.height; } } let existing_size: winit::dpi::LogicalSize = winit_window.inner_size().to_logical(self.window().scale_factor().into()); if (existing_size.width - width).abs() > 1. || (existing_size.height - height).abs() > 1. { // If we're in fullscreen state, don't try to resize the window but maintain the surface // size we've been assigned to from the windowing system. Weston/Wayland don't like it // when we create a surface that's bigger than the screen due to constraints (#532). if winit_window.fullscreen().is_none() { winit_window.set_inner_size(winit::dpi::LogicalSize::new(width, height)); } } }); if must_resize { let win = self.window(); win.set_size(i_slint_core::api::LogicalSize::new(width, height)); } } fn apply_geometry_constraint( &self, constraints_horizontal: corelib::layout::LayoutInfo, constraints_vertical: corelib::layout::LayoutInfo, ) { self.with_window_handle(&mut |winit_window| { // If we're in fullscreen state, don't try to resize the window but maintain the surface // size we've been assigned to from the windowing system. Weston/Wayland don't like it // when we create a surface that's bigger than the screen due to constraints (#532). if winit_window.fullscreen().is_some() { return; } if (constraints_horizontal, constraints_vertical) != self.constraints() { let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32; let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32; let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32; let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32; let sf = self.window().scale_factor(); winit_window.set_resizable(true); winit_window.set_min_inner_size(if min_width > 0. || min_height > 0. { Some(winit::dpi::PhysicalSize::new(min_width * sf, min_height * sf)) } else { None }); winit_window.set_max_inner_size( if max_width < i32::MAX as f32 || max_height < i32::MAX as f32 { Some(winit::dpi::PhysicalSize::new( (max_width * sf).min(65535.), (max_height * sf).min(65535.), )) } else { None }, ); self.set_constraints((constraints_horizontal, constraints_vertical)); winit_window.set_resizable(min_width < max_width || min_height < max_height); #[cfg(target_arch = "wasm32")] { // set_max_inner_size / set_min_inner_size don't work on wasm, so apply the size manually let existing_size: winit::dpi::LogicalSize = winit_window.inner_size().to_logical(sf as f64); if !(min_width..=max_width).contains(&(existing_size.width)) || !(min_height..=max_height).contains(&(existing_size.height)) { let new_size = winit::dpi::LogicalSize::new( existing_size.width.min(max_width).max(min_width), existing_size.height.min(max_height).max(min_height), ); winit_window.set_inner_size(new_size); } } } }); } fn show(&self) { self.call_with_event_loop(|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 mut window_builder = winit::window::WindowBuilder::new(); let runtime_window = WindowInner::from_pub(self_.window()); let component_rc = runtime_window.component(); let component = ComponentRc::borrow_pin(&component_rc); window_builder = if let Some(window_item) = runtime_window.window_item().as_ref().map(|i| i.as_pin_ref()) { window_builder .with_title(window_item.title().to_string()) .with_resizable( window_item.height() <= LogicalLength::zero() || window_item.width() <= LogicalLength::zero(), ) .with_decorations(!window_item.no_frame()) .with_window_icon(icon_to_winit(window_item.icon())) } else { window_builder.with_title("Slint Window".to_string()) }; 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); if let Some(window_item) = runtime_window.window_item() { // Setting the width to its preferred size before querying the vertical layout info // is important in case the height depends on the width window_item.width.set(LogicalLength::new(layout_info_h.preferred_bounded())); } let layout_info_v = component.as_ref().layout_info(Orientation::Vertical); let s = winit::dpi::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( winit::dpi::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( winit::dpi::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... if let Some(sf) = scale_factor_override { let physical_size = requested_size.to_physical(sf as f32); window_builder.with_inner_size(winit::dpi::Size::new( winit::dpi::PhysicalSize::new( physical_size.width, physical_size.height, ), )) } else { window_builder.with_inner_size(size_to_winit(requested_size)) } } 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(LogicalSize::new(s.width, s.height)); window_builder.with_inner_size(into_size(s)) } else { window_builder } }; let window_builder = if let Some(pos) = &requested_position { window_builder.with_position(position_to_winit(pos)) } else { window_builder }; let canvas = self_.renderer.create_canvas(window_builder); let id = canvas.with_window_handle(|winit_window| { WindowInner::from_pub(&self_.window).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()); }); } fn hide(&self) { self.call_with_event_loop(|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 } fn enable_input_method(&self, _it: corelib::items::InputType) { #[cfg(target_arch = "wasm32")] { 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(not(target_arch = "wasm32"))] self.with_window_handle(&mut |winit_window| { winit_window.set_ime_allowed(matches!(_it, corelib::items::InputType::Text)) }); } fn disable_input_method(&self) { #[cfg(target_arch = "wasm32")] if let Some(h) = &*self.virtual_keyboard_helper.borrow() { h.hide() } #[cfg(not(target_arch = "wasm32"))] self.with_window_handle(&mut |winit_window| winit_window.set_ime_allowed(false)); } fn set_ime_position(&self, ime_pos: LogicalPoint) { self.with_window_handle(&mut |winit_window| { winit_window.set_ime_position(winit::dpi::LogicalPosition::new(ime_pos.x, ime_pos.y)) }) } 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 .as_ref() .map(|p| p.to_physical(self.window().scale_factor())) .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::WindowPosition) { 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(position_to_winit(&position)) }) } } } fn set_size(&self, size: corelib::api::WindowSize) { 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(size_to_winit(&size)); }); } } } } fn dark_color_scheme(&self) -> bool { dark_light::detect() == dark_light::Mode::Dark } fn is_visible(&self) -> bool { if let Some(mapped_window) = self.borrow_mapped_window() { mapped_window .canvas .with_window_handle(&mut |win: &winit::window::Window| win.is_visible()) .unwrap_or(true) } else { false } } } 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) } } }