// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! This is an internal module that contains the [`LiveReloadingComponent`] struct. use crate::dynamic_item_tree::WindowOptions; use core::cell::RefCell; use core::task::Waker; use i_slint_core::api::{ComponentHandle, PlatformError}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::{Arc, Mutex}; //re-export for the generated code: pub use crate::{Compiler, ComponentInstance, Value}; /// This struct is used to compile and instantiate a component from a .slint file on disk. /// The file is watched for changes and the component is recompiled and instantiated pub struct LiveReloadingComponent { // because new_cyclic cannot return error, we need to initialize the instance after instance: Option, compiler: Compiler, file_name: PathBuf, component_name: String, properties: HashMap, callbacks: HashMap Value + 'static>>, } impl LiveReloadingComponent { /// Compile and instantiate a component from the specified .slint file and component. pub fn new( mut compiler: Compiler, file_name: PathBuf, component_name: String, ) -> Result>, PlatformError> { let self_rc = Rc::>::new_cyclic(move |self_weak| { let watcher = Watcher::new(self_weak.clone()); if watcher.lock().unwrap().watcher.is_some() { let watcher_clone = watcher.clone(); compiler.set_file_loader(move |path| { Watcher::watch(&watcher_clone, path); Box::pin(async { None }) }); Watcher::watch(&watcher, &file_name); } RefCell::new(Self { instance: None, compiler, file_name, component_name, properties: Default::default(), callbacks: Default::default(), }) }); let mut self_mut = self_rc.borrow_mut(); let result = { let mut future = core::pin::pin!(self_mut.compiler.build_from_path(&self_mut.file_name)); let mut cx = std::task::Context::from_waker(std::task::Waker::noop()); let std::task::Poll::Ready(result) = std::future::Future::poll(future.as_mut(), &mut cx) else { unreachable!("Compiler returned Pending") }; result }; #[cfg(feature = "display-diagnostics")] result.print_diagnostics(); assert!( !result.has_errors(), "Was not able to compile the file {}. \n{:?}", self_mut.file_name.display(), result.diagnostics ); let definition = result.component(&self_mut.component_name).expect("Cannot open component"); let instance = definition.create()?; eprintln!( "Loaded component {} from {}", self_mut.component_name, self_mut.file_name.display() ); self_mut.instance = Some(instance); drop(self_mut); Ok(self_rc) } /// Reload the component from the .slint file. /// If there is an error, it won't actually reload. /// Return false in case of errors pub fn reload(&mut self) -> bool { let result = { let mut future = core::pin::pin!(self.compiler.build_from_path(&self.file_name)); let mut cx = std::task::Context::from_waker(std::task::Waker::noop()); let std::task::Poll::Ready(result) = std::future::Future::poll(future.as_mut(), &mut cx) else { unreachable!("Compiler returned Pending") }; result }; #[cfg(feature = "display-diagnostics")] result.print_diagnostics(); if result.has_errors() { return false; } if let Some(definition) = result.component(&self.component_name) { let window_adapter = i_slint_core::window::WindowInner::from_pub(self.instance().window()) .window_adapter(); match definition.create_with_options(WindowOptions::UseExistingWindow(window_adapter)) { Ok(instance) => { self.instance = Some(instance); } Err(e) => { eprintln!("Error while creating the component: {e}"); return false; } } } else { eprintln!("Component {} not found", self.component_name); return false; } // Set the properties for (name, value) in self.properties.iter() { if let Some((global, prop)) = name.split_once('.') { self.instance() .set_global_property(global, prop, value.clone()) .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}")); } else { self.instance() .set_property(name, value.clone()) .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}")); } } for (name, callback) in self.callbacks.iter() { let callback = callback.clone(); if let Some((global, prop)) = name.split_once('.') { self.instance() .set_global_callback(global, prop, move |args| callback(args)) .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}")); } else { self.instance() .set_callback(name, move |args| callback(args)) .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}")); } } eprintln!("Reloaded component {} from {}", self.component_name, self.file_name.display()); true } /// Return the instance pub fn instance(&self) -> &ComponentInstance { &self.instance.as_ref().expect("always set after Self is created from Rc::new_cyclic") } /// Set a property and remember its value for when the component is reloaded pub fn set_property(&mut self, name: &str, value: Value) { self.properties.insert(name.into(), value.clone()); self.instance() .set_property(&name, value) .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}")) } /// Forward to get_property pub fn get_property(&self, name: &str) -> Value { self.instance() .get_property(&name) .unwrap_or_else(|e| panic!("Cannot get property {name}: {e}")) } /// Forward to invoke pub fn invoke(&self, name: &str, args: &[Value]) -> Value { self.instance() .invoke(name, args) .unwrap_or_else(|e| panic!("Cannot invoke callback {name}: {e}")) } /// Forward to set_callback pub fn set_callback(&mut self, name: &str, callback: Rc Value + 'static>) { self.callbacks.insert(name.into(), callback.clone()); self.instance() .set_callback(&name, move |args| callback(args)) .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}")); } /// forward to set_global_property pub fn set_global_property(&mut self, global_name: &str, name: &str, value: Value) { self.properties.insert(format!("{global_name}.{name}"), value.clone()); self.instance() .set_global_property(global_name, name, value) .unwrap_or_else(|e| panic!("Cannot set property {global_name}::{name}: {e}")) } /// forward to get_global_property pub fn get_global_property(&self, global_name: &str, name: &str) -> Value { self.instance() .get_global_property(global_name, name) .unwrap_or_else(|e| panic!("Cannot get property {global_name}::{name}: {e}")) } /// Forward to invoke_global pub fn invoke_global(&self, global_name: &str, name: &str, args: &[Value]) -> Value { self.instance() .invoke_global(global_name, name, args) .unwrap_or_else(|e| panic!("Cannot invoke callback {global_name}::{name}: {e}")) } /// Forward to set_global_callback pub fn set_global_callback( &mut self, global_name: &str, name: &str, callback: Rc Value + 'static>, ) { self.callbacks.insert(format!("{global_name}.{name}"), callback.clone()); self.instance() .set_global_callback(global_name, name, move |args| callback(args)) .unwrap_or_else(|e| panic!("Cannot set callback {global_name}::{name}: {e}")); } } enum WatcherState { Starting, /// The file system watcher notified the main thread of a change Changed, /// The main thread is waiting for the next event Waiting(Waker), } struct Watcher { // (wouldn't need to be an option if new_cyclic() could return errors) watcher: Option, state: WatcherState, files: HashSet, } impl Watcher { fn new(component_weak: std::rc::Weak>) -> Arc> { let arc = Arc::new(Mutex::new(Self { state: WatcherState::Starting, watcher: None, files: Default::default(), })); let watcher_weak = Arc::downgrade(&arc); let result = crate::spawn_local(std::future::poll_fn(move |cx| { let (Some(instance), Some(watcher)) = (component_weak.upgrade(), watcher_weak.upgrade()) else { // When the instance is dropped, we can stop this future return std::task::Poll::Ready(()); }; let state = std::mem::replace( &mut watcher.lock().unwrap().state, WatcherState::Waiting(cx.waker().clone()), ); if matches!(state, WatcherState::Changed) { instance.borrow_mut().reload(); }; std::task::Poll::Pending })); // no event loop, no need to start a watcher if !result.is_ok() { return arc; } let watcher_weak = Arc::downgrade(&arc); arc.lock().unwrap().watcher = notify::recommended_watcher(move |event: notify::Result| { use notify::{event::ModifyKind as M, EventKind as K}; let Ok(event) = event else { return }; let Some(watcher) = watcher_weak.upgrade() else { return }; if matches!(event.kind, K::Modify(M::Data(_) | M::Any) | K::Create(_)) && watcher.lock().is_ok_and(|w| event.paths.iter().any(|p| w.files.contains(p))) { if let WatcherState::Waiting(waker) = std::mem::replace(&mut watcher.lock().unwrap().state, WatcherState::Changed) { // Wait a bit to let the time to write multiple files std::thread::sleep(std::time::Duration::from_millis(15)); waker.wake(); } } }) .ok(); arc } fn watch(self_: &Mutex, path: &Path) { let Some(parent) = path.parent() else { return }; let mut locked = self_.lock().unwrap(); let Some(mut watcher) = locked.watcher.take() else { return }; locked.files.insert(path.into()); // Don't call the notify api while holding the mutex drop(locked); notify::Watcher::watch( &mut watcher, parent, // on macOS, notify only delivers us events for changes within a directory when using // the recursive mode. On the upside, fsevents works already recursively anyway. if cfg!(target_vendor = "apple") { notify::RecursiveMode::Recursive } else { notify::RecursiveMode::NonRecursive }, ) .unwrap_or_else(|err| { eprintln!("Warning: error while watching {}: {:?}", path.display(), err) }); self_.lock().unwrap().watcher = Some(watcher); } } #[cfg(feature = "ffi")] mod ffi { use super::*; use core::ffi::c_void; use i_slint_core::window::WindowAdapter; use i_slint_core::{slice::Slice, SharedString, SharedVector}; type LiveReloadingComponentInner = RefCell; #[unsafe(no_mangle)] /// LibraryPath is an array of string that have in the form `lib=...` pub extern "C" fn slint_live_preview_new( file_name: Slice, component_name: Slice, include_paths: &SharedVector, library_paths: &SharedVector, style: Slice, ) -> *const LiveReloadingComponentInner { let mut compiler = Compiler::default(); compiler.set_include_paths( include_paths.iter().map(|path| PathBuf::from(path.as_str())).collect(), ); compiler.set_library_paths( library_paths .iter() .map(|path| path.as_str().split_once('=').expect("library path must have an '='")) .map(|(lib, path)| (lib.into(), PathBuf::from(path))) .collect(), ); if !style.is_empty() { compiler.set_style(std::str::from_utf8(&style).unwrap().into()); } Rc::into_raw( LiveReloadingComponent::new( compiler, std::path::PathBuf::from(std::str::from_utf8(&file_name).unwrap()), std::str::from_utf8(&component_name).unwrap().into(), ) .expect("Creating the component failed"), ) } #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_live_preview_clone( component: *const LiveReloadingComponentInner, ) { Rc::increment_strong_count(component); } #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_live_preview_drop( component: *const LiveReloadingComponentInner, ) { Rc::decrement_strong_count(component); } #[unsafe(no_mangle)] pub extern "C" fn slint_live_preview_set_property( component: &LiveReloadingComponentInner, property: Slice, value: &Value, ) { let property = std::str::from_utf8(&property).unwrap(); if let Some((global, prop)) = property.split_once('.') { component.borrow_mut().set_global_property(global, prop, value.clone()); } else { component.borrow_mut().set_property(property, value.clone()); } } #[unsafe(no_mangle)] pub extern "C" fn slint_live_preview_get_property( component: &LiveReloadingComponentInner, property: Slice, ) -> *mut Value { let property = std::str::from_utf8(&property).unwrap(); let val = if let Some((global, prop)) = property.split_once('.') { component.borrow().get_global_property(global, prop) } else { component.borrow().get_property(property) }; Box::into_raw(Box::new(val)) } #[unsafe(no_mangle)] pub extern "C" fn slint_live_preview_invoke( component: &LiveReloadingComponentInner, callback: Slice, args: Slice>, ) -> *mut Value { let callback = std::str::from_utf8(&callback).unwrap(); let args = args.iter().map(|vb| vb.as_ref().clone()).collect::>(); let val = if let Some((global, prop)) = callback.split_once('.') { component.borrow().invoke_global(global, prop, &args) } else { component.borrow().invoke(callback, &args) }; Box::into_raw(Box::new(val)) } #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_live_preview_set_callback( component: &LiveReloadingComponentInner, callback: Slice, callback_handler: extern "C" fn( user_data: *mut c_void, arg: Slice>, ) -> Box, user_data: *mut c_void, drop_user_data: Option, ) { let ud = crate::ffi::CallbackUserData::new(user_data, drop_user_data, callback_handler); let handler = Rc::new(move |args: &[Value]| ud.call(args)); let callback = std::str::from_utf8(&callback).unwrap(); if let Some((global, prop)) = callback.split_once('.') { component.borrow_mut().set_global_callback(global, prop, handler); } else { component.borrow_mut().set_callback(callback, handler); } } /// Same precondition as slint_interpreter_component_instance_window #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_live_preview_window( component: &LiveReloadingComponentInner, out: *mut *const i_slint_core::window::ffi::WindowAdapterRcOpaque, ) { assert_eq!( core::mem::size_of::>(), core::mem::size_of::() ); let borrow = component.borrow(); let adapter = borrow.instance().inner.window_adapter_ref().unwrap(); core::ptr::write(out as *mut *const Rc, adapter as *const _) } }