1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | // cSpell: ignore condvar |
5 | |
6 | use super::PreviewState; |
7 | use crate::lsp_ext::Health; |
8 | use crate::ServerNotifier; |
9 | use once_cell::sync::Lazy; |
10 | use slint_interpreter::ComponentHandle; |
11 | use std::future::Future; |
12 | use std::sync::{Condvar, Mutex}; |
13 | |
14 | #[derive (PartialEq)] |
15 | enum RequestedGuiEventLoopState { |
16 | /// The UI event loop hasn't been started yet because no preview has been requested |
17 | Uninitialized, |
18 | /// The LSP thread requested the UI loop to start because a preview was requested, |
19 | /// But the loop hasn't been started yet |
20 | StartLoop, |
21 | /// The Loop is now started so the LSP thread can start posting events |
22 | LoopStarted, |
23 | /// The LSP thread requested the application to be terminated |
24 | QuitLoop, |
25 | } |
26 | |
27 | static GUI_EVENT_LOOP_NOTIFIER: Lazy<Condvar> = Lazy::new(Condvar::new); |
28 | static GUI_EVENT_LOOP_STATE_REQUEST: Lazy<Mutex<RequestedGuiEventLoopState>> = |
29 | Lazy::new(|| Mutex::new(RequestedGuiEventLoopState::Uninitialized)); |
30 | |
31 | thread_local! {static CLI_ARGS: std::cell::OnceCell<crate::Cli> = Default::default();} |
32 | |
33 | pub fn run_in_ui_thread<F: Future<Output = ()> + 'static>( |
34 | create_future: impl Send + FnOnce() -> F + 'static, |
35 | ) { |
36 | // Wake up the main thread to start the event loop, if possible |
37 | { |
38 | let mut state_request: MutexGuard<'_, RequestedGuiEventLoopState> = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); |
39 | if *state_request == RequestedGuiEventLoopState::Uninitialized { |
40 | *state_request = RequestedGuiEventLoopState::StartLoop; |
41 | GUI_EVENT_LOOP_NOTIFIER.notify_one(); |
42 | } |
43 | // We don't want to call post_event before the loop is properly initialized |
44 | while *state_request == RequestedGuiEventLoopState::StartLoop { |
45 | state_request = GUI_EVENT_LOOP_NOTIFIER.wait(guard:state_request).unwrap(); |
46 | } |
47 | } |
48 | i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || { |
49 | i_slint_core::future::spawn_local(fut:create_future()).unwrap(); |
50 | }) |
51 | .unwrap(); |
52 | } |
53 | |
54 | pub fn start_ui_event_loop(cli_args: crate::Cli) { |
55 | CLI_ARGS.with(|f| f.set(cli_args).ok()); |
56 | |
57 | { |
58 | let mut state_requested = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); |
59 | |
60 | while *state_requested == RequestedGuiEventLoopState::Uninitialized { |
61 | state_requested = GUI_EVENT_LOOP_NOTIFIER.wait(state_requested).unwrap(); |
62 | } |
63 | |
64 | if *state_requested == RequestedGuiEventLoopState::QuitLoop { |
65 | return; |
66 | } |
67 | |
68 | if *state_requested == RequestedGuiEventLoopState::StartLoop { |
69 | // make sure the backend is initialized |
70 | i_slint_backend_selector::with_platform(|_| Ok(())).unwrap(); |
71 | // Send an event so that once the loop is started, we notify the LSP thread that it can send more events |
72 | i_slint_core::api::invoke_from_event_loop(|| { |
73 | let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); |
74 | if *state_request == RequestedGuiEventLoopState::StartLoop { |
75 | *state_request = RequestedGuiEventLoopState::LoopStarted; |
76 | GUI_EVENT_LOOP_NOTIFIER.notify_one(); |
77 | } |
78 | }) |
79 | .unwrap(); |
80 | } |
81 | } |
82 | |
83 | slint::run_event_loop_until_quit().unwrap(); |
84 | } |
85 | |
86 | pub fn quit_ui_event_loop() { |
87 | // Wake up the main thread, in case it wasn't woken up earlier. If it wasn't, then don't request |
88 | // a start of the event loop. |
89 | { |
90 | let mut state_request: MutexGuard<'_, RequestedGuiEventLoopState> = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); |
91 | *state_request = RequestedGuiEventLoopState::QuitLoop; |
92 | GUI_EVENT_LOOP_NOTIFIER.notify_one(); |
93 | } |
94 | |
95 | close_ui(); |
96 | |
97 | let _ = i_slint_core::api::quit_event_loop(); |
98 | |
99 | // Make sure then sender channel gets dropped. |
100 | if let Some(sender: &Mutex) = SERVER_NOTIFIER.get() { |
101 | let mut sender: MutexGuard<'_, Option> = sender.lock().unwrap(); |
102 | *sender = None; |
103 | }; |
104 | } |
105 | |
106 | pub fn open_ui(sender: &ServerNotifier) { |
107 | // Wake up the main thread to start the event loop, if possible |
108 | { |
109 | let mut state_request = GUI_EVENT_LOOP_STATE_REQUEST.lock().unwrap(); |
110 | if *state_request == RequestedGuiEventLoopState::Uninitialized { |
111 | *state_request = RequestedGuiEventLoopState::StartLoop; |
112 | GUI_EVENT_LOOP_NOTIFIER.notify_one(); |
113 | } |
114 | // We don't want to call post_event before the loop is properly initialized |
115 | while *state_request == RequestedGuiEventLoopState::StartLoop { |
116 | state_request = GUI_EVENT_LOOP_NOTIFIER.wait(state_request).unwrap(); |
117 | } |
118 | } |
119 | |
120 | { |
121 | let mut cache = super::CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
122 | if cache.ui_is_visible { |
123 | return; // UI is already up! |
124 | } |
125 | cache.ui_is_visible = true; |
126 | |
127 | let mut s = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap(); |
128 | *s = Some(sender.clone()); |
129 | }; |
130 | |
131 | i_slint_core::api::invoke_from_event_loop(move || { |
132 | super::PREVIEW_STATE.with(|preview_state| { |
133 | let mut preview_state = preview_state.borrow_mut(); |
134 | |
135 | open_ui_impl(&mut preview_state); |
136 | }); |
137 | }) |
138 | .unwrap(); |
139 | } |
140 | |
141 | pub(super) fn open_ui_impl(preview_state: &mut PreviewState) { |
142 | let (default_style, show_preview_ui, fullscreen) = { |
143 | let cache = super::CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
144 | let style = cache.config.style.clone(); |
145 | let style = if style.is_empty() { |
146 | CLI_ARGS.with(|args| args.get().map(|a| a.style.clone()).unwrap_or_default()) |
147 | } else { |
148 | style |
149 | }; |
150 | let hide_ui = cache |
151 | .config |
152 | .hide_ui |
153 | .or_else(|| CLI_ARGS.with(|args| args.get().map(|a| a.no_toolbar))) |
154 | .unwrap_or(false); |
155 | let fullscreen = CLI_ARGS.with(|args| args.get().map(|a| a.fullscreen).unwrap_or_default()); |
156 | (style, !hide_ui, fullscreen) |
157 | }; |
158 | |
159 | // TODO: Handle Error! |
160 | let experimental = std::env::var_os("SLINT_ENABLE_EXPERIMENTAL_FEATURES" ) |
161 | .map(|s| !s.is_empty() && s != "0" ) |
162 | .unwrap_or(false); |
163 | |
164 | let ui = preview_state |
165 | .ui |
166 | .get_or_insert_with(|| super::ui::create_ui(default_style, experimental).unwrap()); |
167 | ui.set_show_preview_ui(show_preview_ui); |
168 | ui.window().set_fullscreen(fullscreen); |
169 | ui.window().on_close_requested(|| { |
170 | let mut cache = super::CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
171 | cache.ui_is_visible = false; |
172 | |
173 | let mut sender = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap(); |
174 | *sender = None; |
175 | |
176 | slint::CloseRequestResponse::HideWindow |
177 | }); |
178 | } |
179 | |
180 | pub fn close_ui() { |
181 | { |
182 | let mut cache: MutexGuard<'_, ContentCache> = super::CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
183 | if !cache.ui_is_visible { |
184 | return; // UI is already down! |
185 | } |
186 | cache.ui_is_visible = false; |
187 | } |
188 | |
189 | i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || { |
190 | super::PREVIEW_STATE.with(move |preview_state: &RefCell| { |
191 | let mut preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
192 | close_ui_impl(&mut preview_state) |
193 | }); |
194 | }) |
195 | .unwrap(); // TODO: Handle Error |
196 | } |
197 | |
198 | fn close_ui_impl(preview_state: &mut PreviewState) { |
199 | let ui: Option = preview_state.ui.take(); |
200 | if let Some(ui: PreviewUi) = ui { |
201 | ui.hide().unwrap(); |
202 | } |
203 | } |
204 | |
205 | static SERVER_NOTIFIER: std::sync::OnceLock<Mutex<Option<ServerNotifier>>> = |
206 | std::sync::OnceLock::new(); |
207 | |
208 | pub fn notify_diagnostics(diagnostics: &[slint_interpreter::Diagnostic]) -> Option<()> { |
209 | super::set_diagnostics(diagnostics); |
210 | |
211 | let Some(sender: ServerNotifier) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { |
212 | return Some(()); |
213 | }; |
214 | |
215 | let lsp_diags: HashMap> = crate::preview::convert_diagnostics(diagnostics); |
216 | |
217 | for (url: Url, diagnostics: Vec) in lsp_diags { |
218 | crate::common::lsp_to_editor::notify_lsp_diagnostics(&sender, uri:url, diagnostics)?; |
219 | } |
220 | Some(()) |
221 | } |
222 | |
223 | pub fn send_status(message: &str, health: Health) { |
224 | let Some(sender: ServerNotifier) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { |
225 | return; |
226 | }; |
227 | |
228 | crate::common::lsp_to_editor::send_status_notification(&sender, message, health) |
229 | } |
230 | |
231 | pub fn ask_editor_to_show_document(file: &str, selection: lsp_types::Range) { |
232 | let Some(sender: ServerNotifier) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { |
233 | return; |
234 | }; |
235 | let Ok(url: Url) = lsp_types::Url::from_file_path(file) else { return }; |
236 | let fut: impl Future = crate::common::lsp_to_editor::send_show_document_to_editor(sender, file:url, range:selection); |
237 | slint_interpreter::spawn_local(fut).unwrap(); // Fire and forget. |
238 | } |
239 | |
240 | pub fn send_message_to_lsp(message: crate::common::PreviewToLspMessage) { |
241 | let Some(sender: ServerNotifier) = SERVER_NOTIFIER.get_or_init(Default::default).lock().unwrap().clone() else { |
242 | return; |
243 | }; |
244 | sender.send_message_to_lsp(message); |
245 | } |
246 | |