// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams}; use i_slint_compiler::CompilerConfiguration; use lsp_types::notification::Notification; use once_cell::sync::Lazy; use slint_interpreter::ComponentHandle; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::{Arc, Condvar, Mutex}; use std::task::Wake; #[derive(PartialEq)] enum RequestedGuiEventLoopState { /// The UI event loop hasn't been started yet because no preview has been requested Uninitialized, /// The LSP thread requested the UI loop to start because a preview was requested, /// But the loop hasn't been started yet StartLoop, /// The Loop is now started so the LSP thread can start posting events LoopStated, /// The LSP thread requested the application to be terminated QuitLoop, } static GUI_EVENT_LOOP_NOTIFIER: Lazy = Lazy::new(Condvar::new); static GUI_EVENT_LOOP_STATE_REQUEST: Lazy> = Lazy::new(|| Mutex::new(RequestedGuiEventLoopState::Uninitialized)); struct FutureRunner { fut: Mutex>>>>, } /// Safety: the future is only going to be run in the UI thread unsafe impl Send for FutureRunner {} /// Safety: the future is only going to be run in the UI thread unsafe impl Sync for FutureRunner {} impl Wake for FutureRunner { fn wake(self: Arc) { i_slint_core::api::invoke_from_event_loop(move || { let waker = self.clone().into(); let mut cx = std::task::Context::from_waker(&waker); let mut fut_opt = self.fut.lock().unwrap(); if let Some(fut) = &mut *fut_opt { match fut.as_mut().poll(&mut cx) { std::task::Poll::Ready(_) => *fut_opt = None, std::task::Poll::Pending => {} } } }) .unwrap(); } } fn run_in_ui_thread(fut: Pin>>) { // Wake up the main thread to start the event loop, if possible { let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); if *state_request == RequestedGuiEventLoopState::Uninitialized { *state_request = RequestedGuiEventLoopState::StartLoop; GUI_EVENT_LOOP_NOTIFIER.notify_one(); } // We don't want to call post_event before the loop is properly initialized while *state_request == RequestedGuiEventLoopState::StartLoop { state_request = GUI_EVENT_LOOP_NOTIFIER.wait(state_request).unwrap(); } } Arc::new(FutureRunner { fut: Mutex::new(Some(fut)) }).wake() } pub fn start_ui_event_loop() { { let mut state_requested = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); while *state_requested == RequestedGuiEventLoopState::Uninitialized { state_requested = GUI_EVENT_LOOP_NOTIFIER.wait(state_requested).unwrap(); } if *state_requested == RequestedGuiEventLoopState::QuitLoop { return; } if *state_requested == RequestedGuiEventLoopState::StartLoop { // make sure the backend is initialized i_slint_backend_selector::with_platform(|_| Ok(())).unwrap(); // Send an event so that once the loop is started, we notify the LSP thread that it can send more events i_slint_core::api::invoke_from_event_loop(|| { let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); if *state_request == RequestedGuiEventLoopState::StartLoop { *state_request = RequestedGuiEventLoopState::LoopStated; GUI_EVENT_LOOP_NOTIFIER.notify_one(); } }) .unwrap(); } } i_slint_backend_selector::with_platform(|b| { b.set_event_loop_quit_on_last_window_closed(false); b.run_event_loop() }) .unwrap(); } pub fn quit_ui_event_loop() { // Wake up the main thread, in case it wasn't woken up earlier. If it wasn't, then don't request // a start of the event loop. { let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); *state_request = RequestedGuiEventLoopState::QuitLoop; GUI_EVENT_LOOP_NOTIFIER.notify_one(); } let _ = i_slint_core::api::quit_event_loop(); // Make sure then sender channel gets dropped if let Some(cache) = CONTENT_CACHE.get() { let mut cache = cache.lock().unwrap(); cache.sender = None; }; } pub enum PostLoadBehavior { ShowAfterLoad, DoNothing, } pub fn load_preview( sender: crate::ServerNotifier, component: PreviewComponent, post_load_behavior: PostLoadBehavior, ) { use std::sync::atomic::{AtomicU32, Ordering}; static PENDING_EVENTS: AtomicU32 = AtomicU32::new(0); if PENDING_EVENTS.load(Ordering::SeqCst) > 0 { return; } PENDING_EVENTS.fetch_add(1, Ordering::SeqCst); run_in_ui_thread(Box::pin(async move { PENDING_EVENTS.fetch_sub(1, Ordering::SeqCst); reload_preview(sender, component, post_load_behavior).await })); } #[derive(Default, Clone)] pub struct PreviewComponent { /// The file name to preview pub path: PathBuf, /// The name of the component within that file. /// If None, then the last component is going to be shown. pub component: Option, /// The list of include paths pub include_paths: Vec, /// The style name for the preview pub style: String, } #[derive(Default)] struct ContentCache { source_code: HashMap, dependency: HashSet, current: PreviewComponent, sender: Option, highlight: Option<(PathBuf, u32)>, } static CONTENT_CACHE: once_cell::sync::OnceCell> = once_cell::sync::OnceCell::new(); pub fn set_contents(path: &Path, content: String) { let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); cache.source_code.insert(path.to_owned(), content); if cache.dependency.contains(path) { let current = cache.current.clone(); let sender = cache.sender.clone(); drop(cache); if let Some(sender) = sender { load_preview(sender, current, PostLoadBehavior::DoNothing); } } } pub fn config_changed(config: &CompilerConfiguration) { if let Some(cache) = CONTENT_CACHE.get() { let mut cache = cache.lock().unwrap(); let style = config.style.clone().unwrap_or_default(); if cache.current.style != style || cache.current.include_paths != config.include_paths { cache.current.style = style; cache.current.include_paths = config.include_paths.clone(); let current = cache.current.clone(); let sender = cache.sender.clone(); drop(cache); if let Some(sender) = sender { load_preview(sender, current, PostLoadBehavior::DoNothing); } } }; } /// If the file is in the cache, returns it. /// In any was, register it as a dependency fn get_file_from_cache(path: PathBuf) -> Option { let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); let r = cache.source_code.get(&path).cloned(); cache.dependency.insert(path); r } #[derive(Default)] struct PreviewState { handle: Option, } thread_local! {static PREVIEW_STATE: std::cell::RefCell = Default::default();} async fn reload_preview( sender: crate::ServerNotifier, preview_component: PreviewComponent, post_load_behavior: PostLoadBehavior, ) { send_notification(&sender, "Loading Preview…", Health::Ok); { let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); cache.dependency.clear(); cache.current = preview_component.clone(); } let mut builder = slint_interpreter::ComponentCompiler::default(); if !preview_component.style.is_empty() { builder.set_style(preview_component.style); } builder.set_include_paths(preview_component.include_paths); builder.set_file_loader(|path| { let path = path.to_owned(); Box::pin(async move { get_file_from_cache(path).map(Result::Ok) }) }); let compiled = if let Some(mut from_cache) = get_file_from_cache(preview_component.path.clone()) { if let Some(component) = &preview_component.component { from_cache = format!("{}\nexport component _Preview inherits {} {{ }}\n", from_cache, component); } builder.build_from_source(from_cache, preview_component.path).await } else { builder.build_from_path(preview_component.path).await }; notify_diagnostics(builder.diagnostics(), &sender); if let Some(compiled) = compiled { PREVIEW_STATE.with(|preview_state| { let mut preview_state = preview_state.borrow_mut(); let handle = if let Some(handle) = preview_state.handle.take() { let window = handle.window(); let handle = compiled.create_with_existing_window(window); match post_load_behavior { PostLoadBehavior::ShowAfterLoad => handle.show().unwrap(), PostLoadBehavior::DoNothing => {} } handle } else { let handle = compiled.create().unwrap(); handle.show().unwrap(); handle }; if let Some((path, offset)) = CONTENT_CACHE.get().and_then(|c| c.lock().unwrap().highlight.clone()) { handle.highlight(path, offset); } preview_state.handle = Some(handle); }); send_notification(&sender, "Preview Loaded", Health::Ok); } else { send_notification(&sender, "Preview not updated", Health::Error); } CONTENT_CACHE.get_or_init(Default::default).lock().unwrap().sender.replace(sender); } fn notify_diagnostics( diagnostics: &[slint_interpreter::Diagnostic], sender: &crate::ServerNotifier, ) -> Option<()> { let mut lsp_diags: HashMap> = Default::default(); for d in diagnostics { if d.source_file().map_or(true, |f| f.is_relative()) { continue; } let uri = lsp_types::Url::from_file_path(d.source_file().unwrap()).unwrap(); lsp_diags.entry(uri).or_default().push(crate::util::to_lsp_diag(d)); } for (uri, diagnostics) in lsp_diags { sender .send_notification( "textDocument/publishDiagnostics".into(), lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None }, ) .ok()?; } Some(()) } fn send_notification(sender: &crate::ServerNotifier, arg: &str, health: Health) { sender .send_notification( ServerStatusNotification::METHOD.into(), ServerStatusParams { health, quiescent: false, message: Some(arg.into()) }, ) .unwrap_or_else(|e| eprintln!("Error sending notification: {:?}", e)); } /// Highlight the element pointed at the offset in the path. /// When path is None, remove the highlight. pub fn highlight(path: Option, offset: u32) { let highlight = path.map(|x| (x, offset)); let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); if cache.highlight == highlight { return; } cache.highlight = highlight; if cache.highlight.as_ref().map_or(true, |(path, _)| cache.dependency.contains(path)) { run_in_ui_thread(Box::pin(async move { PREVIEW_STATE.with(|preview_state| { let preview_state = preview_state.borrow(); if let (Some(cache), Some(handle)) = (CONTENT_CACHE.get(), preview_state.handle.as_ref()) { if let Some((path, offset)) = cache.lock().unwrap().highlight.clone() { handle.highlight(path, offset); } else { handle.highlight(PathBuf::default(), 0); } } }) })) } }