// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial use std::cell::{Cell, RefCell}; use std::rc::{Rc, Weak}; use embedded_graphics::prelude::*; use embedded_graphics_simulator::SimulatorDisplay; use i_slint_core::component::ComponentRc; use i_slint_core::graphics::{Image, ImageInner, StaticTextures}; use i_slint_core::input::KeyboardModifiers; use i_slint_core::item_rendering::DirtyRegion; use i_slint_core::items::{Item, ItemRef, WindowItem}; use i_slint_core::layout::Orientation; use i_slint_core::window::{PlatformWindow, Window}; use i_slint_core::{Color, Coord}; use rgb::FromSlice; use self::event_loop::WinitWindow; type Canvas = femtovg::Canvas; type CanvasRc = Rc>; pub mod event_loop; mod glcontext; use glcontext::*; pub struct SimulatorWindow { self_weak: Weak, keyboard_modifiers: std::cell::Cell, currently_pressed_key_code: std::cell::Cell>, canvas: CanvasRc, opengl_context: OpenGLContext, constraints: Cell<(i_slint_core::layout::LayoutInfo, i_slint_core::layout::LayoutInfo)>, visible: Cell, background_color: Cell, frame_buffer: RefCell>>, initial_dirty_region_for_next_frame: Cell, } impl SimulatorWindow { pub(crate) fn new(window_weak: &Weak) -> Rc { let window_builder = winit::window::WindowBuilder::new().with_visible(false); #[cfg(target_arch = "wasm32")] let (opengl_context, renderer) = OpenGLContext::new_context_and_renderer(window_builder, &self.canvas_id); #[cfg(not(target_arch = "wasm32"))] let (opengl_context, renderer) = OpenGLContext::new_context_and_renderer(window_builder); let canvas = femtovg::Canvas::new(renderer).unwrap(); opengl_context.make_not_current(); let canvas = Rc::new(RefCell::new(canvas)); let window_rc = Rc::new(Self { self_weak: window_weak.clone(), keyboard_modifiers: Default::default(), currently_pressed_key_code: Default::default(), canvas, opengl_context, constraints: Default::default(), visible: Default::default(), background_color: Color::from_rgb_u8(0, 0, 0).into(), frame_buffer: RefCell::default(), initial_dirty_region_for_next_frame: Default::default(), }); let runtime_window = window_weak.upgrade().unwrap(); runtime_window.set_scale_factor(window_rc.opengl_context.window().scale_factor() as _); window_rc } } impl Drop for SimulatorWindow { fn drop(&mut self) { crate::event_loop::unregister_window(self.opengl_context.window().id()); } } impl PlatformWindow for SimulatorWindow { fn show(self: Rc) { if self.visible.get() { return; } self.visible.set(true); 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 platform_window = self.opengl_context.window(); if let Some(window_item) = ItemRef::downcast_pin::(root_item) { platform_window.set_title(&window_item.title()); platform_window.set_decorations(!window_item.no_frame()); }; if std::env::var("SLINT_FULLSCREEN").is_ok() { platform_window.set_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 = winit::dpi::LogicalSize::new( layout_info_h.preferred_bounded(), layout_info_v.preferred_bounded(), ); 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); platform_window.set_inner_size(s) } }; platform_window.set_visible(true); let id = platform_window.id(); drop(platform_window); crate::event_loop::register_window(id, self); } fn hide(self: Rc) { self.opengl_context.window().set_visible(false); self.visible.set(false); crate::event_loop::unregister_window(self.opengl_context.window().id()); } fn request_redraw(&self) { if self.visible.get() { self.opengl_context.window().request_redraw(); } } fn free_graphics_resources<'a>( &self, _: i_slint_core::component::ComponentRef, items: &mut dyn Iterator>>, ) { super::LINE_RENDERER.with(|cache| { cache.borrow().free_graphics_resources(items); }); } fn show_popup( &self, popup: &i_slint_core::component::ComponentRc, position: i_slint_core::graphics::Point, ) { let runtime_window = self.self_weak.upgrade().unwrap(); let size = runtime_window.set_active_popup(i_slint_core::window::PopupWindow { location: i_slint_core::window::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 = i_slint_core::items::WindowItem::FIELD_OFFSETS.width.apply_pin(window_item); let height_property = i_slint_core::items::WindowItem::FIELD_OFFSETS.height.apply_pin(window_item); width_property.set(size.width); height_property.set(size.height); } } fn close_popup(&self, popup: &i_slint_core::window::PopupWindow) { match popup.location { i_slint_core::window::PopupWindowLocation::TopLevel(_) => {} i_slint_core::window::PopupWindowLocation::ChildWindow(offset) => { let popup_component = ComponentRc::borrow_pin(&popup.component); let popup_root = popup_component.as_ref().get_item_ref(0); if let Some(window_item) = ItemRef::downcast_pin::(popup_root) { let popup_region = i_slint_core::properties::evaluate_no_tracking(|| window_item.geometry()) .translate(offset.to_vector()); if !popup_region.is_empty() { self.initial_dirty_region_for_next_frame.set( self.initial_dirty_region_for_next_frame .get() .union(&popup_region.to_box2d()), ); } } } } } fn request_window_properties_update(&self) { let window_id = self.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: std::pin::Pin<&i_slint_core::items::WindowItem>, ) { WinitWindow::apply_window_properties(self as &dyn WinitWindow, window_item); } fn apply_geometry_constraint( &self, constraints_horizontal: i_slint_core::layout::LayoutInfo, constraints_vertical: i_slint_core::layout::LayoutInfo, ) { self.apply_constraints(constraints_horizontal, constraints_vertical) } fn set_mouse_cursor(&self, _cursor: i_slint_core::items::MouseCursor) {} fn text_size( &self, font_request: i_slint_core::graphics::FontRequest, text: &str, max_width: Option, ) -> i_slint_core::graphics::Size { let runtime_window = self.self_weak.upgrade().unwrap(); crate::renderer::fonts::text_size( font_request.merge(&self.self_weak.upgrade().unwrap().default_font_properties()), text, max_width, crate::ScaleFactor::new(runtime_window.scale_factor()), ) .to_untyped() } fn text_input_byte_offset_for_position( &self, _text_input: std::pin::Pin<&i_slint_core::items::TextInput>, _pos: i_slint_core::graphics::Point, ) -> usize { todo!() } fn text_input_cursor_rect_for_byte_offset( &self, _text_input: std::pin::Pin<&i_slint_core::items::TextInput>, _byte_offset: usize, ) -> i_slint_core::graphics::Rect { todo!() } fn as_any(&self) -> &dyn core::any::Any { self } } impl WinitWindow for SimulatorWindow { fn runtime_window(&self) -> Rc { self.self_weak.upgrade().unwrap() } fn currently_pressed_key_code(&self) -> &Cell> { &self.currently_pressed_key_code } fn current_keyboard_modifiers(&self) -> &Cell { &self.keyboard_modifiers } fn draw(self: Rc) { let runtime_window = self.self_weak.upgrade().unwrap(); let size = self.opengl_context.window().inner_size(); self.opengl_context.with_current_context(|opengl_context| { opengl_context.ensure_resized(); { let mut canvas = self.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); } let mut frame_buffer = self.frame_buffer.borrow_mut(); let display = match frame_buffer.as_mut() { Some(buffer) if buffer.size().width == size.width && buffer.size().height == size.height => { buffer } _ => { let buffer = frame_buffer.insert(SimulatorDisplay::new(Size { width: size.width, height: size.height, })); super::LINE_RENDERER.with(|cache| { *cache.borrow_mut() = Default::default(); }); buffer } }; struct BufferProvider<'a> { devices: &'a mut dyn crate::Devices, line_buffer: Vec, } impl crate::renderer::LineBufferProvider for BufferProvider<'_> { type TargetPixel = crate::TargetPixel; fn process_line( &mut self, line: crate::PhysicalLength, render_fn: impl FnOnce(&mut [super::TargetPixel]), ) { render_fn(&mut self.line_buffer); self.devices.fill_region( euclid::rect(0, line.get(), self.line_buffer.len() as _, 1), &self.line_buffer, ); } } super::LINE_RENDERER.with(|renderer| { renderer.borrow().render( runtime_window, self.initial_dirty_region_for_next_frame.take(), BufferProvider { devices: display, line_buffer: vec![Default::default(); size.width as usize], }, ) }); let output_image = display .to_rgb_output_image(&embedded_graphics_simulator::OutputSettings::default()); let image_buffer = output_image.as_image_buffer(); let image_ref: imgref::ImgRef = imgref::ImgRef::new( image_buffer.as_rgb(), image_buffer.width() as usize, image_buffer.height() as usize, ) .into(); let mut canvas = self.canvas.borrow_mut(); let image_id = canvas.create_image(image_ref, femtovg::ImageFlags::empty()).unwrap(); let mut path = femtovg::Path::new(); path.rect(0., 0., image_ref.width() as _, image_ref.height() as _); let fill_paint = femtovg::Paint::image( image_id, 0., 0., image_ref.width() as _, image_ref.height() as _, 0.0, 1.0, ); canvas.fill_path(&mut path, fill_paint); canvas.flush(); canvas.delete_image(image_id); opengl_context.swap_buffers(); }); } fn with_window_handle(&self, callback: &mut dyn FnMut(&winit::window::Window)) { callback(&*self.opengl_context.window()) } fn constraints(&self) -> (i_slint_core::layout::LayoutInfo, i_slint_core::layout::LayoutInfo) { self.constraints.get() } fn set_constraints( &self, constraints: (i_slint_core::layout::LayoutInfo, i_slint_core::layout::LayoutInfo), ) { self.constraints.set(constraints) } fn set_background_color(&self, color: Color) { self.background_color.set(color); } fn set_icon(&self, _icon: i_slint_core::graphics::Image) {} } pub struct SimulatorBackend; impl i_slint_core::backend::Backend for SimulatorBackend { fn create_window(&'static self) -> Rc { i_slint_core::window::Window::new(|window| SimulatorWindow::new(window)) } fn run_event_loop(&'static self, behavior: i_slint_core::backend::EventLoopQuitBehavior) { event_loop::run(behavior); std::process::exit(0); } fn quit_event_loop(&'static self) { self::event_loop::with_window_target(|event_loop| { event_loop.event_loop_proxy().send_event(self::event_loop::CustomEvent::Exit).ok(); }) } fn register_font_from_memory( &'static self, _data: &'static [u8], ) -> Result<(), Box> { //TODO Err("Not implemented".into()) } fn register_font_from_path( &'static self, _path: &std::path::Path, ) -> Result<(), Box> { unimplemented!() } fn register_bitmap_font(&'static self, font_data: &'static i_slint_core::graphics::BitmapFont) { crate::renderer::fonts::register_bitmap_font(font_data); } fn set_clipboard_text(&'static self, _text: String) { unimplemented!() } fn clipboard_text(&'static self) -> Option { unimplemented!() } fn post_event(&'static self, event: Box) { self::event_loop::GLOBAL_PROXY .get_or_init(Default::default) .lock() .unwrap() .send_event(self::event_loop::CustomEvent::UserEvent(event)); } fn image_size(&'static self, image: &Image) -> i_slint_core::graphics::IntSize { let inner: &ImageInner = image.into(); match inner { ImageInner::None => Default::default(), ImageInner::AbsoluteFilePath(_) | ImageInner::EmbeddedData { .. } => unimplemented!(), ImageInner::EmbeddedImage(buffer) => buffer.size(), ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size, } } }