Skip to main content

slint_interpreter/
live_preview.rs

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