diff --git a/Cargo.toml b/Cargo.toml index 5a98c3e03..d352a5a31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ 'sixtyfps_runtime/interpreter', 'sixtyfps_runtime/rendering_backends/gl', 'sixtyfps_runtime/rendering_backends/qt', + 'sixtyfps_runtime/rendering_backends/mcu', 'sixtyfps_runtime/rendering_backends/default', 'sixtyfps_runtime/rendering_backends/testing', 'sixtyfps_compiler', @@ -47,6 +48,7 @@ default-members = [ 'sixtyfps_runtime/interpreter', 'sixtyfps_runtime/rendering_backends/gl', 'sixtyfps_runtime/rendering_backends/qt', + 'sixtyfps_runtime/rendering_backends/mcu', 'sixtyfps_runtime/rendering_backends/default', 'sixtyfps_compiler', 'api/sixtyfps-rs', diff --git a/sixtyfps_runtime/rendering_backends/mcu/Cargo.toml b/sixtyfps_runtime/rendering_backends/mcu/Cargo.toml new file mode 100644 index 000000000..c215974e1 --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sixtyfps-rendering-backend-mcu" +version = "0.1.5" +authors = ["SixtyFPS "] +edition = "2018" +license = "GPL-3.0-only" +description = "Rendering backend for SixtyFPS for use on Microcontrollers" +repository = "https://github.com/sixtyfpsui/sixtyfps" +homepage = "https://sixtyfps.io" + +[lib] +path = "lib.rs" + +[features] +simulator = ["winit", "glutin", "femtovg", "embedded-graphics-simulator"] +default = ["simulator"] + +[dependencies] +sixtyfps-corelib = { version = "=0.1.5", path = "../../corelib" } +const-field-offset = { version = "0.1", path = "../../../helper_crates/const-field-offset" } +rgb = "0.8.27" +imgref = "1.6.1" +vtable = { version = "0.1", path = "../../../helper_crates/vtable" } +by_address = "1.0.4" +euclid = "0.22.1" +pin-weak = "1" +scoped-tls-hkt = "0.1" +smallvec = "1.7" +once_cell = "1.5" +derive_more = "0.99.5" +winit = { version = "0.25", default-features = false, optional = true } +glutin = { version = "0.27", default-features = false, optional = true } +femtovg = { version = "0.2.8", optional = true } +embedded-graphics = "0.7.1" +embedded-graphics-simulator = { version = "0.3.0", optional = true, default-features = false } + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = { version = "0.24.0" } diff --git a/sixtyfps_runtime/rendering_backends/mcu/lib.rs b/sixtyfps_runtime/rendering_backends/mcu/lib.rs new file mode 100644 index 000000000..f01e2dc7a --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/lib.rs @@ -0,0 +1,102 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2021 Olivier Goffart + Copyright (c) 2021 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 */ +/*! + +**NOTE**: This library is an **internal** crate for the [SixtyFPS project](https://sixtyfps.io). +This crate should **not be used directly** by applications using SixtyFPS. +You should use the `sixtyfps` crate instead. + +**WARNING**: This crate does not follow the semver convention for versioning and can +only be used with `version = "=x.y.z"` in Cargo.toml. + +*/ +#![doc(html_logo_url = "https://sixtyfps.io/resources/logo.drawio.svg")] + +use sixtyfps_corelib::{ + graphics::{Image, Size}, + window::Window, + ImageInner, +}; +use std::rc::Rc; + +#[cfg(feature = "simulator")] +mod simulator; + +#[cfg(feature = "simulator")] +use simulator::*; + +mod renderer; + +pub struct Backend; + +impl sixtyfps_corelib::backend::Backend for Backend { + fn create_window(&'static self) -> Rc { + sixtyfps_corelib::window::Window::new(|window| SimulatorWindow::new(window)) + } + + fn run_event_loop(&'static self, behavior: sixtyfps_corelib::backend::EventLoopQuitBehavior) { + simulator::event_loop::run(behavior); + } + + fn quit_event_loop(&'static self) { + crate::event_loop::with_window_target(|event_loop| { + event_loop.event_loop_proxy().send_event(simulator::event_loop::CustomEvent::Exit).ok(); + }) + } + + fn register_font_from_memory( + &'static self, + _data: &'static [u8], + ) -> Result<(), Box> { + unimplemented!() + } + + fn register_font_from_path( + &'static self, + _path: &std::path::Path, + ) -> Result<(), Box> { + unimplemented!() + } + + fn set_clipboard_text(&'static self, _text: String) { + unimplemented!() + } + + fn clipboard_text(&'static self) -> Option { + unimplemented!() + } + + fn post_event(&'static self, event: Box) { + let e = crate::event_loop::CustomEvent::UserEvent(event); + crate::event_loop::GLOBAL_PROXY.get_or_init(Default::default).lock().unwrap().send_event(e); + } + + fn image_size(&'static self, image: &Image) -> Size { + let inner: &ImageInner = image.into(); + match inner { + ImageInner::None => Default::default(), + ImageInner::AbsoluteFilePath(_) => todo!(), + ImageInner::EmbeddedData { .. } => todo!(), + ImageInner::EmbeddedImage(buffer) => { + [buffer.width() as f32, buffer.height() as f32].into() + } + } + } +} + +pub type NativeWidgets = (); +pub type NativeGlobals = (); +pub mod native_widgets {} +pub const HAS_NATIVE_STYLE: bool = false; +pub const IS_AVAILABLE: bool = true; + +pub fn init() { + sixtyfps_corelib::backend::instance_or_init(|| Box::new(Backend)); +} diff --git a/sixtyfps_runtime/rendering_backends/mcu/renderer.rs b/sixtyfps_runtime/rendering_backends/mcu/renderer.rs new file mode 100644 index 000000000..71fe5006c --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/renderer.rs @@ -0,0 +1,112 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2021 Olivier Goffart + Copyright (c) 2021 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 */ + +use std::rc::Rc; + +pub struct SoftwareRenderer<'a, Target: embedded_graphics::draw_target::DrawTarget + 'static> { + pub draw_target: &'a mut Target, + pub window: Rc, +} + +impl + sixtyfps_corelib::item_rendering::ItemRenderer for SoftwareRenderer<'_, Target> +{ + fn draw_rectangle(&mut self, _rect: std::pin::Pin<&sixtyfps_corelib::items::Rectangle>) { + // TODO + } + + fn draw_border_rectangle( + &mut self, + _rect: std::pin::Pin<&sixtyfps_corelib::items::BorderRectangle>, + ) { + // TODO + } + + fn draw_image(&mut self, _image: std::pin::Pin<&sixtyfps_corelib::items::ImageItem>) { + // TODO + } + + fn draw_clipped_image( + &mut self, + _image: std::pin::Pin<&sixtyfps_corelib::items::ClippedImage>, + ) { + // TODO + } + + fn draw_text(&mut self, _text: std::pin::Pin<&sixtyfps_corelib::items::Text>) { + // TODO + } + + fn draw_text_input(&mut self, _text_input: std::pin::Pin<&sixtyfps_corelib::items::TextInput>) { + // TODO + } + + fn draw_path(&mut self, _path: std::pin::Pin<&sixtyfps_corelib::items::Path>) { + // TODO + } + + fn draw_box_shadow(&mut self, _box_shadow: std::pin::Pin<&sixtyfps_corelib::items::BoxShadow>) { + // TODO + } + + fn combine_clip( + &mut self, + _rect: sixtyfps_corelib::graphics::Rect, + _radius: f32, + _border_width: f32, + ) { + // TODO + } + + fn get_current_clip(&self) -> sixtyfps_corelib::graphics::Rect { + Default::default() + } + + fn translate(&mut self, _x: f32, _y: f32) { + // TODO + } + + fn rotate(&mut self, _angle_in_degrees: f32) { + // TODO + } + + fn apply_opacity(&mut self, _opacity: f32) { + // TODO + } + + fn save_state(&mut self) { + // TODO + } + + fn restore_state(&mut self) { + // TODO + } + + fn scale_factor(&self) -> f32 { + // TODO + 1.0 + } + + fn draw_cached_pixmap( + &mut self, + _item_cache: &sixtyfps_corelib::item_rendering::CachedRenderingData, + _update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])), + ) { + todo!() + } + + fn window(&self) -> sixtyfps_corelib::window::WindowRc { + self.window.clone() + } + + fn as_any(&mut self) -> &mut dyn core::any::Any { + self.draw_target + } +} diff --git a/sixtyfps_runtime/rendering_backends/mcu/simulator.rs b/sixtyfps_runtime/rendering_backends/mcu/simulator.rs new file mode 100644 index 000000000..fec29637e --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/simulator.rs @@ -0,0 +1,330 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2021 Olivier Goffart + Copyright (c) 2021 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 */ + +use std::cell::{Cell, RefCell}; +use std::rc::{Rc, Weak}; + +use embedded_graphics::prelude::*; +use embedded_graphics_simulator::SimulatorDisplay; +use rgb::FromSlice; +use sixtyfps_corelib::component::ComponentRc; +use sixtyfps_corelib::input::KeyboardModifiers; +use sixtyfps_corelib::items::ItemRef; +use sixtyfps_corelib::layout::Orientation; +use sixtyfps_corelib::window::PlatformWindow; +use sixtyfps_corelib::Color; + +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<(sixtyfps_corelib::layout::LayoutInfo, sixtyfps_corelib::layout::LayoutInfo)>, + visible: 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(), + }); + + 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("SIXTYFPS_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. && 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); + 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, + _items: &mut dyn Iterator>>, + ) { + // Nothing to do until we start caching stuff that needs freeing + } + + fn show_popup( + &self, + _popup: &sixtyfps_corelib::component::ComponentRc, + _position: sixtyfps_corelib::graphics::Point, + ) { + todo!() + } + + 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<&sixtyfps_corelib::items::WindowItem>, + ) { + WinitWindow::apply_window_properties(self as &dyn WinitWindow, window_item); + } + + fn apply_geometry_constraint( + &self, + constraints_horizontal: sixtyfps_corelib::layout::LayoutInfo, + constraints_vertical: sixtyfps_corelib::layout::LayoutInfo, + ) { + self.apply_constraints(constraints_horizontal, constraints_vertical) + } + + fn text_size( + &self, + _font_request: sixtyfps_corelib::graphics::FontRequest, + _text: &str, + _max_width: Option, + ) -> sixtyfps_corelib::graphics::Size { + // TODO + Default::default() + } + + fn text_input_byte_offset_for_position( + &self, + _text_input: std::pin::Pin<&sixtyfps_corelib::items::TextInput>, + _pos: sixtyfps_corelib::graphics::Point, + ) -> usize { + todo!() + } + + fn text_input_position_for_byte_offset( + &self, + _text_input: std::pin::Pin<&sixtyfps_corelib::items::TextInput>, + _byte_offset: usize, + ) -> sixtyfps_corelib::graphics::Point { + 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(); + runtime_window.clone().draw_contents(|components| { + let size = self.opengl_context.window().inner_size(); + + self.opengl_context.with_current_context(|| { + self.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 display: SimulatorDisplay = + SimulatorDisplay::new(Size { width: size.width, height: size.height }); + + // Debug + { + use embedded_graphics::{ + pixelcolor::Rgb888, + prelude::*, + primitives::{PrimitiveStyleBuilder, Rectangle}, + }; + + let style = PrimitiveStyleBuilder::new() + .stroke_color(Rgb888::RED) + .stroke_width(3) + .fill_color(Rgb888::GREEN) + .build(); + + Rectangle::new(Point::new(30, 20), Size::new(10, 15)) + .into_styled(style) + .draw(&mut display) + .unwrap(); + } + + let mut renderer = crate::renderer::SoftwareRenderer { + draw_target: &mut display, + window: runtime_window.clone(), + }; + + for (component, origin) in components { + sixtyfps_corelib::item_rendering::render_component_items( + &component, + &mut renderer, + origin.clone(), + ); + } + + 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); + + self.opengl_context.swap_buffers(); + }); + }); + } + + fn with_window_handle(&self, callback: &mut dyn FnMut(&winit::window::Window)) { + callback(&*self.opengl_context.window()) + } + + fn constraints( + &self, + ) -> (sixtyfps_corelib::layout::LayoutInfo, sixtyfps_corelib::layout::LayoutInfo) { + self.constraints.get() + } + fn set_constraints( + &self, + constraints: (sixtyfps_corelib::layout::LayoutInfo, sixtyfps_corelib::layout::LayoutInfo), + ) { + self.constraints.set(constraints) + } + + fn set_background_color(&self, _color: Color) {} + fn set_icon(&self, _icon: sixtyfps_corelib::graphics::Image) {} +} diff --git a/sixtyfps_runtime/rendering_backends/mcu/simulator/event_loop.rs b/sixtyfps_runtime/rendering_backends/mcu/simulator/event_loop.rs new file mode 120000 index 000000000..a6bf69ffa --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/simulator/event_loop.rs @@ -0,0 +1 @@ +../../gl/event_loop.rs \ No newline at end of file diff --git a/sixtyfps_runtime/rendering_backends/mcu/simulator/glcontext.rs b/sixtyfps_runtime/rendering_backends/mcu/simulator/glcontext.rs new file mode 120000 index 000000000..18ae23946 --- /dev/null +++ b/sixtyfps_runtime/rendering_backends/mcu/simulator/glcontext.rs @@ -0,0 +1 @@ +../../gl/glcontext.rs \ No newline at end of file