Skip to main content

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, DefaultTranslationContext, 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: RefCell<HashMap<String, Value>>,
27    callbacks: RefCell<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        true
127    }
128
129    /// Reload the properties and callbacks after a reload()
130    pub fn reload_properties_and_callbacks(&self) {
131        // Set the properties
132        for (name, value) in self.properties.borrow_mut().iter() {
133            if let Some((global, prop)) = name.split_once('.') {
134                self.instance()
135                    .set_global_property(global, prop, value.clone())
136                    .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"));
137            } else {
138                self.instance()
139                    .set_property(name, value.clone())
140                    .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"));
141            }
142        }
143        for (name, callback) in self.callbacks.borrow_mut().iter() {
144            let callback = callback.clone();
145            if let Some((global, prop)) = name.split_once('.') {
146                self.instance()
147                    .set_global_callback(global, prop, move |args| callback(args))
148                    .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
149            } else {
150                self.instance()
151                    .set_callback(name, move |args| callback(args))
152                    .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
153            }
154        }
155
156        eprintln!("Reloaded component {} from {}", self.component_name, self.file_name.display());
157    }
158
159    /// Return the instance
160    pub fn instance(&self) -> &ComponentInstance {
161        &self.instance.as_ref().expect("always set after Self is created from Rc::new_cyclic")
162    }
163
164    /// Set a property and remember its value for when the component is reloaded
165    pub fn set_property(&self, name: &str, value: Value) {
166        self.properties.borrow_mut().insert(name.into(), value.clone());
167        self.instance()
168            .set_property(&name, value)
169            .unwrap_or_else(|e| panic!("Cannot set property {name}: {e}"))
170    }
171
172    /// Forward to get_property
173    pub fn get_property(&self, name: &str) -> Value {
174        self.instance()
175            .get_property(&name)
176            .unwrap_or_else(|e| panic!("Cannot get property {name}: {e}"))
177    }
178
179    /// Forward to invoke
180    pub fn invoke(&self, name: &str, args: &[Value]) -> Value {
181        self.instance()
182            .invoke(name, args)
183            .unwrap_or_else(|e| panic!("Cannot invoke callback {name}: {e}"))
184    }
185
186    /// Forward to set_callback
187    pub fn set_callback(&self, name: &str, callback: Rc<dyn Fn(&[Value]) -> Value + 'static>) {
188        self.callbacks.borrow_mut().insert(name.into(), callback.clone());
189        self.instance()
190            .set_callback(&name, move |args| callback(args))
191            .unwrap_or_else(|e| panic!("Cannot set callback {name}: {e}"));
192    }
193
194    /// forward to set_global_property
195    pub fn set_global_property(&self, global_name: &str, name: &str, value: Value) {
196        self.properties.borrow_mut().insert(format!("{global_name}.{name}"), value.clone());
197        self.instance()
198            .set_global_property(global_name, name, value)
199            .unwrap_or_else(|e| panic!("Cannot set property {global_name}::{name}: {e}"))
200    }
201
202    /// forward to get_global_property
203    pub fn get_global_property(&self, global_name: &str, name: &str) -> Value {
204        self.instance()
205            .get_global_property(global_name, name)
206            .unwrap_or_else(|e| panic!("Cannot get property {global_name}::{name}: {e}"))
207    }
208
209    /// Forward to invoke_global
210    pub fn invoke_global(&self, global_name: &str, name: &str, args: &[Value]) -> Value {
211        self.instance()
212            .invoke_global(global_name, name, args)
213            .unwrap_or_else(|e| panic!("Cannot invoke callback {global_name}::{name}: {e}"))
214    }
215
216    /// Forward to set_global_callback
217    pub fn set_global_callback(
218        &self,
219        global_name: &str,
220        name: &str,
221        callback: Rc<dyn Fn(&[Value]) -> Value + 'static>,
222    ) {
223        self.callbacks.borrow_mut().insert(format!("{global_name}.{name}"), callback.clone());
224        self.instance()
225            .set_global_callback(global_name, name, move |args| callback(args))
226            .unwrap_or_else(|e| panic!("Cannot set callback {global_name}::{name}: {e}"));
227    }
228}
229
230enum WatcherState {
231    Starting,
232    /// The file system watcher notified the main thread of a change
233    Changed,
234    /// The main thread is waiting for the next event
235    Waiting(Waker),
236}
237
238struct Watcher {
239    // (wouldn't need to be an option if new_cyclic() could return errors)
240    watcher: Option<notify::RecommendedWatcher>,
241    state: WatcherState,
242    files: HashSet<PathBuf>,
243}
244
245impl Watcher {
246    fn new(component_weak: std::rc::Weak<RefCell<LiveReloadingComponent>>) -> Arc<Mutex<Self>> {
247        let arc = Arc::new(Mutex::new(Self {
248            state: WatcherState::Starting,
249            watcher: None,
250            files: Default::default(),
251        }));
252
253        let watcher_weak = Arc::downgrade(&arc);
254        let result = crate::spawn_local(std::future::poll_fn(move |cx| {
255            let (Some(instance), Some(watcher)) =
256                (component_weak.upgrade(), watcher_weak.upgrade())
257            else {
258                // When the instance is dropped, we can stop this future
259                return std::task::Poll::Ready(());
260            };
261            let state = std::mem::replace(
262                &mut watcher.lock().unwrap().state,
263                WatcherState::Waiting(cx.waker().clone()),
264            );
265            if matches!(state, WatcherState::Changed) {
266                let success = instance.borrow_mut().reload();
267                if success {
268                    instance.borrow().reload_properties_and_callbacks();
269                };
270            };
271            std::task::Poll::Pending
272        }));
273
274        // no event loop, no need to start a watcher
275        if !result.is_ok() {
276            return arc;
277        }
278
279        let watcher_weak = Arc::downgrade(&arc);
280        arc.lock().unwrap().watcher =
281            notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
282                use notify::{EventKind as K, event::ModifyKind as M};
283                let Ok(event) = event else { return };
284                let Some(watcher) = watcher_weak.upgrade() else { return };
285                if matches!(event.kind, K::Modify(M::Data(_) | M::Any) | K::Create(_))
286                    && watcher.lock().is_ok_and(|w| event.paths.iter().any(|p| w.files.contains(p)))
287                {
288                    if let WatcherState::Waiting(waker) =
289                        std::mem::replace(&mut watcher.lock().unwrap().state, WatcherState::Changed)
290                    {
291                        // Wait a bit to let the time to write multiple files
292                        std::thread::sleep(std::time::Duration::from_millis(15));
293                        waker.wake();
294                    }
295                }
296            })
297            .ok();
298        arc
299    }
300
301    fn watch(self_: &Mutex<Self>, path: &Path) {
302        let Some(parent) = path.parent() else { return };
303
304        let mut locked = self_.lock().unwrap();
305        let Some(mut watcher) = locked.watcher.take() else { return };
306        locked.files.insert(path.into());
307        // Don't call the notify api while holding the mutex
308        drop(locked);
309        notify::Watcher::watch(
310            &mut watcher,
311            parent,
312            // on macOS, notify only delivers us events for changes within a directory when using
313            // the recursive mode. On the upside, fsevents works already recursively anyway.
314            if cfg!(target_vendor = "apple") {
315                notify::RecursiveMode::Recursive
316            } else {
317                notify::RecursiveMode::NonRecursive
318            },
319        )
320        .unwrap_or_else(|err| {
321            eprintln!("Warning: error while watching {}: {:?}", path.display(), err)
322        });
323        self_.lock().unwrap().watcher = Some(watcher);
324    }
325}
326
327#[cfg(feature = "ffi")]
328mod ffi {
329    use super::*;
330    use core::ffi::c_void;
331    use i_slint_core::window::WindowAdapter;
332    use i_slint_core::{SharedString, SharedVector, slice::Slice};
333    type LiveReloadingComponentInner = RefCell<LiveReloadingComponent>;
334
335    #[unsafe(no_mangle)]
336    /// LibraryPath is an array of string that have in the form `lib=...`
337    pub extern "C" fn slint_live_preview_new(
338        file_name: Slice<u8>,
339        component_name: Slice<u8>,
340        include_paths: &SharedVector<SharedString>,
341        library_paths: &SharedVector<SharedString>,
342        style: Slice<u8>,
343        translation_domain: Slice<u8>,
344        no_default_translation_context: bool,
345    ) -> *const LiveReloadingComponentInner {
346        let mut compiler = Compiler::default();
347        compiler.set_include_paths(
348            include_paths.iter().map(|path| PathBuf::from(path.as_str())).collect(),
349        );
350        compiler.set_library_paths(
351            library_paths
352                .iter()
353                .map(|path| path.as_str().split_once('=').expect("library path must have an '='"))
354                .map(|(lib, path)| (lib.into(), PathBuf::from(path)))
355                .collect(),
356        );
357        if !style.is_empty() {
358            compiler.set_style(std::str::from_utf8(&style).unwrap().into());
359        }
360        if !translation_domain.is_empty() {
361            compiler
362                .set_translation_domain(std::str::from_utf8(&translation_domain).unwrap().into());
363        }
364        if no_default_translation_context {
365            compiler.set_default_translation_context(crate::DefaultTranslationContext::None);
366        }
367        Rc::into_raw(
368            LiveReloadingComponent::new(
369                compiler,
370                std::path::PathBuf::from(std::str::from_utf8(&file_name).unwrap()),
371                std::str::from_utf8(&component_name).unwrap().into(),
372            )
373            .expect("Creating the component failed"),
374        )
375    }
376
377    #[unsafe(no_mangle)]
378    pub unsafe extern "C" fn slint_live_preview_clone(
379        component: *const LiveReloadingComponentInner,
380    ) {
381        unsafe { Rc::increment_strong_count(component) };
382    }
383
384    #[unsafe(no_mangle)]
385    pub unsafe extern "C" fn slint_live_preview_drop(
386        component: *const LiveReloadingComponentInner,
387    ) {
388        unsafe { Rc::decrement_strong_count(component) };
389    }
390
391    #[unsafe(no_mangle)]
392    pub extern "C" fn slint_live_preview_set_property(
393        component: &LiveReloadingComponentInner,
394        property: Slice<u8>,
395        value: &Value,
396    ) {
397        let property = std::str::from_utf8(&property).unwrap();
398        if let Some((global, prop)) = property.split_once('.') {
399            component.borrow_mut().set_global_property(global, prop, value.clone());
400        } else {
401            component.borrow_mut().set_property(property, value.clone());
402        }
403    }
404
405    #[unsafe(no_mangle)]
406    pub extern "C" fn slint_live_preview_get_property(
407        component: &LiveReloadingComponentInner,
408        property: Slice<u8>,
409    ) -> *mut Value {
410        let property = std::str::from_utf8(&property).unwrap();
411        let val = if let Some((global, prop)) = property.split_once('.') {
412            component.borrow().get_global_property(global, prop)
413        } else {
414            component.borrow().get_property(property)
415        };
416        Box::into_raw(Box::new(val))
417    }
418
419    #[unsafe(no_mangle)]
420    pub extern "C" fn slint_live_preview_invoke(
421        component: &LiveReloadingComponentInner,
422        callback: Slice<u8>,
423        args: Slice<Box<Value>>,
424    ) -> *mut Value {
425        let callback = std::str::from_utf8(&callback).unwrap();
426        let args = args.iter().map(|vb| vb.as_ref().clone()).collect::<Vec<_>>();
427        let val = if let Some((global, prop)) = callback.split_once('.') {
428            component.borrow().invoke_global(global, prop, &args)
429        } else {
430            component.borrow().invoke(callback, &args)
431        };
432        Box::into_raw(Box::new(val))
433    }
434
435    #[unsafe(no_mangle)]
436    pub unsafe extern "C" fn slint_live_preview_set_callback(
437        component: &LiveReloadingComponentInner,
438        callback: Slice<u8>,
439        callback_handler: extern "C" fn(
440            user_data: *mut c_void,
441            arg: Slice<Box<Value>>,
442        ) -> Box<Value>,
443        user_data: *mut c_void,
444        drop_user_data: Option<extern "C" fn(*mut c_void)>,
445    ) {
446        let ud = unsafe {
447            crate::ffi::CallbackUserData::new(user_data, drop_user_data, callback_handler)
448        };
449        let handler = Rc::new(move |args: &[Value]| ud.call(args));
450        let callback = std::str::from_utf8(&callback).unwrap();
451        if let Some((global, prop)) = callback.split_once('.') {
452            component.borrow_mut().set_global_callback(global, prop, handler);
453        } else {
454            component.borrow_mut().set_callback(callback, handler);
455        }
456    }
457
458    /// Same precondition as slint_interpreter_component_instance_window
459    #[unsafe(no_mangle)]
460    pub unsafe extern "C" fn slint_live_preview_window(
461        component: &LiveReloadingComponentInner,
462        out: *mut *const i_slint_core::window::ffi::WindowAdapterRcOpaque,
463    ) {
464        assert_eq!(
465            core::mem::size_of::<Rc<dyn WindowAdapter>>(),
466            core::mem::size_of::<i_slint_core::window::ffi::WindowAdapterRcOpaque>()
467        );
468        let borrow = component.borrow();
469        let adapter = borrow.instance().inner.window_adapter_ref().unwrap();
470        unsafe { core::ptr::write(out as *mut *const Rc<dyn WindowAdapter>, adapter as *const _) };
471    }
472}