slint_interpreter/
live_preview.rs

1// Copyright © SixtyFPS GmbH <[email protected]>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This is an internal module that contains the [`LiveReloadingComponent`] struct.
5
6use crate::dynamic_item_tree::WindowOptions;
7use core::cell::RefCell;
8use core::task::Waker;
9use i_slint_core::api::{ComponentHandle, PlatformError};
10use std::collections::{HashMap, HashSet};
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13use std::sync::{Arc, Mutex};
14
15//re-export for the generated code:
16pub use crate::{Compiler, ComponentInstance, Value};
17
18/// This struct is used to compile and instantiate a component from a .slint file on disk.
19/// The file is watched for changes and the component is recompiled and instantiated
20pub struct LiveReloadingComponent {
21    // because new_cyclic cannot return error, we need to initialize the instance after
22    instance: Option<ComponentInstance>,
23    compiler: Compiler,
24    file_name: PathBuf,
25    component_name: String,
26    properties: HashMap<String, Value>,
27    callbacks: HashMap<String, Rc<dyn Fn(&[Value]) -> Value + 'static>>,
28}
29
30impl LiveReloadingComponent {
31    /// Compile and instantiate a component from the specified .slint file and component.
32    pub fn new(
33        mut compiler: Compiler,
34        file_name: PathBuf,
35        component_name: String,
36    ) -> Result<Rc<RefCell<Self>>, PlatformError> {
37        let self_rc = Rc::<RefCell<Self>>::new_cyclic(move |self_weak| {
38            let watcher = Watcher::new(self_weak.clone());
39            if watcher.lock().unwrap().watcher.is_some() {
40                let watcher_clone = watcher.clone();
41                compiler.set_file_loader(move |path| {
42                    Watcher::watch(&watcher_clone, path);
43                    Box::pin(async { None })
44                });
45                Watcher::watch(&watcher, &file_name);
46            }
47            RefCell::new(Self {
48                instance: None,
49                compiler,
50                file_name,
51                component_name,
52                properties: Default::default(),
53                callbacks: Default::default(),
54            })
55        });
56
57        let mut self_mut = self_rc.borrow_mut();
58        let result = {
59            let mut future =
60                core::pin::pin!(self_mut.compiler.build_from_path(&self_mut.file_name));
61            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
62            let std::task::Poll::Ready(result) =
63                std::future::Future::poll(future.as_mut(), &mut cx)
64            else {
65                unreachable!("Compiler returned Pending")
66            };
67            result
68        };
69        #[cfg(feature = "display-diagnostics")]
70        result.print_diagnostics();
71        assert!(
72            !result.has_errors(),
73            "Was not able to compile the file {}. \n{:?}",
74            self_mut.file_name.display(),
75            result.diagnostics
76        );
77        let definition = result.component(&self_mut.component_name).expect("Cannot open component");
78        let instance = definition.create()?;
79        eprintln!(
80            "Loaded component {} from {}",
81            self_mut.component_name,
82            self_mut.file_name.display()
83        );
84        self_mut.instance = Some(instance);
85        drop(self_mut);
86        Ok(self_rc)
87    }
88
89    /// Reload the component from the .slint file.
90    /// If there is an error, it won't actually reload.
91    /// Return false in case of errors
92    pub fn reload(&mut self) -> bool {
93        let result = {
94            let mut future = core::pin::pin!(self.compiler.build_from_path(&self.file_name));
95            let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
96            let std::task::Poll::Ready(result) =
97                std::future::Future::poll(future.as_mut(), &mut cx)
98            else {
99                unreachable!("Compiler returned Pending")
100            };
101            result
102        };
103        #[cfg(feature = "display-diagnostics")]
104        result.print_diagnostics();
105        if result.has_errors() {
106            return false;
107        }
108
109        if let Some(definition) = result.component(&self.component_name) {
110            let window_adapter =
111                i_slint_core::window::WindowInner::from_pub(self.instance().window())
112                    .window_adapter();
113            match definition.create_with_options(WindowOptions::UseExistingWindow(window_adapter)) {
114                Ok(instance) => {
115                    self.instance = Some(instance);
116                }
117                Err(e) => {
118                    eprintln!("Error while creating the component: {e}");
119                    return false;
120                }
121            }
122        } else {
123            eprintln!("Component {} not found", self.component_name);
124            return false;
125        }
126
127        // Set the properties
128        for (name, value) in self.properties.iter() {
129            if let Some((global, prop)) = name.split_once('.') {
130                self.instance()
131                    .set_global_property(global, prop, value.clone())
132                    .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"));
133            } else {
134                self.instance()
135                    .set_property(name, value.clone())
136                    .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"));
137            }
138        }
139        for (name, callback) in self.callbacks.iter() {
140            let callback = callback.clone();
141            if let Some((global, prop)) = name.split_once('.') {
142                self.instance()
143                    .set_global_callback(global, prop, move |args| callback(args))
144                    .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
145            } else {
146                self.instance()
147                    .set_callback(name, move |args| callback(args))
148                    .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
149            }
150        }
151
152        eprintln!("Reloaded component {} from {}", self.component_name, self.file_name.display());
153
154        true
155    }
156
157    /// Return the instance
158    pub fn instance(&self) -> &ComponentInstance {
159        &self.instance.as_ref().expect("always set after Self is created from Rc::new_cyclic")
160    }
161
162    /// Set a property and remember its value for when the component is reloaded
163    pub fn set_property(&mut self, name: &str, value: Value) {
164        self.properties.insert(name.into(), value.clone());
165        self.instance()
166            .set_property(&name, value)
167            .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"))
168    }
169
170    /// Forward to get_property
171    pub fn get_property(&self, name: &str) -> Value {
172        self.instance()
173            .get_property(&name)
174            .unwrap_or_else(|e| panic!("Cannot get property {name}: {e}"))
175    }
176
177    /// Forward to invoke
178    pub fn invoke(&self, name: &str, args: &[Value]) -> Value {
179        self.instance()
180            .invoke(name, args)
181            .unwrap_or_else(|e| panic!("Cannot invoke callback {name}: {e}"))
182    }
183
184    /// Forward to set_callback
185    pub fn set_callback(&mut self, name: &str, callback: Rc<dyn Fn(&[Value]) -> Value + 'static>) {
186        self.callbacks.insert(name.into(), callback.clone());
187        self.instance()
188            .set_callback(&name, move |args| callback(args))
189            .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
190    }
191
192    /// forward to set_global_property
193    pub fn set_global_property(&mut self, global_name: &str, name: &str, value: Value) {
194        self.properties.insert(format!("{global_name}.{name}"), value.clone());
195        self.instance()
196            .set_global_property(global_name, name, value)
197            .unwrap_or_else(|e| panic!("Cannot set property {global_name}::{name}: {e}"))
198    }
199
200    /// forward to get_global_property
201    pub fn get_global_property(&self, global_name: &str, name: &str) -> Value {
202        self.instance()
203            .get_global_property(global_name, name)
204            .unwrap_or_else(|e| panic!("Cannot get property {global_name}::{name}: {e}"))
205    }
206
207    /// Forward to invoke_global
208    pub fn invoke_global(&self, global_name: &str, name: &str, args: &[Value]) -> Value {
209        self.instance()
210            .invoke_global(global_name, name, args)
211            .unwrap_or_else(|e| panic!("Cannot invoke callback {global_name}::{name}: {e}"))
212    }
213
214    /// Forward to set_global_callback
215    pub fn set_global_callback(
216        &mut self,
217        global_name: &str,
218        name: &str,
219        callback: Rc<dyn Fn(&[Value]) -> Value + 'static>,
220    ) {
221        self.callbacks.insert(format!("{global_name}.{name}"), callback.clone());
222        self.instance()
223            .set_global_callback(global_name, name, move |args| callback(args))
224            .unwrap_or_else(|e| panic!("Cannot set callback {global_name}::{name}: {e}"));
225    }
226}
227
228enum WatcherState {
229    Starting,
230    /// The file system watcher notified the main thread of a change
231    Changed,
232    /// The main thread is waiting for the next event
233    Waiting(Waker),
234}
235
236struct Watcher {
237    // (wouldn't need to be an option if new_cyclic() could return errors)
238    watcher: Option<notify::RecommendedWatcher>,
239    state: WatcherState,
240    files: HashSet<PathBuf>,
241}
242
243impl Watcher {
244    fn new(component_weak: std::rc::Weak<RefCell<LiveReloadingComponent>>) -> Arc<Mutex<Self>> {
245        let arc = Arc::new(Mutex::new(Self {
246            state: WatcherState::Starting,
247            watcher: None,
248            files: Default::default(),
249        }));
250
251        let watcher_weak = Arc::downgrade(&arc);
252        let result = crate::spawn_local(std::future::poll_fn(move |cx| {
253            let (Some(instance), Some(watcher)) =
254                (component_weak.upgrade(), watcher_weak.upgrade())
255            else {
256                // When the instance is dropped, we can stop this future
257                return std::task::Poll::Ready(());
258            };
259            let state = std::mem::replace(
260                &mut watcher.lock().unwrap().state,
261                WatcherState::Waiting(cx.waker().clone()),
262            );
263            if matches!(state, WatcherState::Changed) {
264                instance.borrow_mut().reload();
265            };
266            std::task::Poll::Pending
267        }));
268
269        // no event loop, no need to start a watcher
270        if !result.is_ok() {
271            return arc;
272        }
273
274        let watcher_weak = Arc::downgrade(&arc);
275        arc.lock().unwrap().watcher =
276            notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
277                use notify::{event::ModifyKind as M, EventKind as K};
278                let Ok(event) = event else { return };
279                let Some(watcher) = watcher_weak.upgrade() else { return };
280                if matches!(event.kind, K::Modify(M::Data(_) | M::Any) | K::Create(_))
281                    && watcher.lock().is_ok_and(|w| event.paths.iter().any(|p| w.files.contains(p)))
282                {
283                    if let WatcherState::Waiting(waker) =
284                        std::mem::replace(&mut watcher.lock().unwrap().state, WatcherState::Changed)
285                    {
286                        // Wait a bit to let the time to write multiple files
287                        std::thread::sleep(std::time::Duration::from_millis(15));
288                        waker.wake();
289                    }
290                }
291            })
292            .ok();
293        arc
294    }
295
296    fn watch(self_: &Mutex<Self>, path: &Path) {
297        let Some(parent) = path.parent() else { return };
298
299        let mut locked = self_.lock().unwrap();
300        let Some(mut watcher) = locked.watcher.take() else { return };
301        locked.files.insert(path.into());
302        // Don't call the notify api while holding the mutex
303        drop(locked);
304        notify::Watcher::watch(
305            &mut watcher,
306            parent,
307            // on macOS, notify only delivers us events for changes within a directory when using
308            // the recursive mode. On the upside, fsevents works already recursively anyway.
309            if cfg!(target_vendor = "apple") {
310                notify::RecursiveMode::Recursive
311            } else {
312                notify::RecursiveMode::NonRecursive
313            },
314        )
315        .unwrap_or_else(|err| {
316            eprintln!("Warning: error while watching {}: {:?}", path.display(), err)
317        });
318        self_.lock().unwrap().watcher = Some(watcher);
319    }
320}
321
322#[cfg(feature = "ffi")]
323mod ffi {
324    use super::*;
325    use core::ffi::c_void;
326    use i_slint_core::window::WindowAdapter;
327    use i_slint_core::{slice::Slice, SharedString, SharedVector};
328    type LiveReloadingComponentInner = RefCell<LiveReloadingComponent>;
329
330    #[unsafe(no_mangle)]
331    /// LibraryPath is an array of string that have in the form `lib=...`
332    pub extern "C" fn slint_live_preview_new(
333        file_name: Slice<u8>,
334        component_name: Slice<u8>,
335        include_paths: &SharedVector<SharedString>,
336        library_paths: &SharedVector<SharedString>,
337        style: Slice<u8>,
338    ) -> *const LiveReloadingComponentInner {
339        let mut compiler = Compiler::default();
340        compiler.set_include_paths(
341            include_paths.iter().map(|path| PathBuf::from(path.as_str())).collect(),
342        );
343        compiler.set_library_paths(
344            library_paths
345                .iter()
346                .map(|path| path.as_str().split_once('=').expect("library path must have an '='"))
347                .map(|(lib, path)| (lib.into(), PathBuf::from(path)))
348                .collect(),
349        );
350        if !style.is_empty() {
351            compiler.set_style(std::str::from_utf8(&style).unwrap().into());
352        }
353        Rc::into_raw(
354            LiveReloadingComponent::new(
355                compiler,
356                std::path::PathBuf::from(std::str::from_utf8(&file_name).unwrap()),
357                std::str::from_utf8(&component_name).unwrap().into(),
358            )
359            .expect("Creating the component failed"),
360        )
361    }
362
363    #[unsafe(no_mangle)]
364    pub unsafe extern "C" fn slint_live_preview_clone(
365        component: *const LiveReloadingComponentInner,
366    ) {
367        Rc::increment_strong_count(component);
368    }
369
370    #[unsafe(no_mangle)]
371    pub unsafe extern "C" fn slint_live_preview_drop(
372        component: *const LiveReloadingComponentInner,
373    ) {
374        Rc::decrement_strong_count(component);
375    }
376
377    #[unsafe(no_mangle)]
378    pub extern "C" fn slint_live_preview_set_property(
379        component: &LiveReloadingComponentInner,
380        property: Slice<u8>,
381        value: &Value,
382    ) {
383        let property = std::str::from_utf8(&property).unwrap();
384        if let Some((global, prop)) = property.split_once('.') {
385            component.borrow_mut().set_global_property(global, prop, value.clone());
386        } else {
387            component.borrow_mut().set_property(property, value.clone());
388        }
389    }
390
391    #[unsafe(no_mangle)]
392    pub extern "C" fn slint_live_preview_get_property(
393        component: &LiveReloadingComponentInner,
394        property: Slice<u8>,
395    ) -> *mut Value {
396        let property = std::str::from_utf8(&property).unwrap();
397        let val = if let Some((global, prop)) = property.split_once('.') {
398            component.borrow().get_global_property(global, prop)
399        } else {
400            component.borrow().get_property(property)
401        };
402        Box::into_raw(Box::new(val))
403    }
404
405    #[unsafe(no_mangle)]
406    pub extern "C" fn slint_live_preview_invoke(
407        component: &LiveReloadingComponentInner,
408        callback: Slice<u8>,
409        args: Slice<Box<Value>>,
410    ) -> *mut Value {
411        let callback = std::str::from_utf8(&callback).unwrap();
412        let args = args.iter().map(|vb| vb.as_ref().clone()).collect::<Vec<_>>();
413        let val = if let Some((global, prop)) = callback.split_once('.') {
414            component.borrow().invoke_global(global, prop, &args)
415        } else {
416            component.borrow().invoke(callback, &args)
417        };
418        Box::into_raw(Box::new(val))
419    }
420
421    #[unsafe(no_mangle)]
422    pub unsafe extern "C" fn slint_live_preview_set_callback(
423        component: &LiveReloadingComponentInner,
424        callback: Slice<u8>,
425        callback_handler: extern "C" fn(
426            user_data: *mut c_void,
427            arg: Slice<Box<Value>>,
428        ) -> Box<Value>,
429        user_data: *mut c_void,
430        drop_user_data: Option<extern "C" fn(*mut c_void)>,
431    ) {
432        let ud = crate::ffi::CallbackUserData::new(user_data, drop_user_data, callback_handler);
433        let handler = Rc::new(move |args: &[Value]| ud.call(args));
434        let callback = std::str::from_utf8(&callback).unwrap();
435        if let Some((global, prop)) = callback.split_once('.') {
436            component.borrow_mut().set_global_callback(global, prop, handler);
437        } else {
438            component.borrow_mut().set_callback(callback, handler);
439        }
440    }
441
442    /// Same precondition as slint_interpreter_component_instance_window
443    #[unsafe(no_mangle)]
444    pub unsafe extern "C" fn slint_live_preview_window(
445        component: &LiveReloadingComponentInner,
446        out: *mut *const i_slint_core::window::ffi::WindowAdapterRcOpaque,
447    ) {
448        assert_eq!(
449            core::mem::size_of::<Rc<dyn WindowAdapter>>(),
450            core::mem::size_of::<i_slint_core::window::ffi::WindowAdapterRcOpaque>()
451        );
452        let borrow = component.borrow();
453        let adapter = borrow.instance().inner.window_adapter_ref().unwrap();
454        core::ptr::write(out as *mut *const Rc<dyn WindowAdapter>, adapter as *const _)
455    }
456}