forked from slint-ui/slint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
preview.rs
291 lines (258 loc) · 10.2 KB
/
preview.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams};
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<Condvar> = Lazy::new(Condvar::new);
static GUI_EVENT_LOOP_STATE_REQUEST: Lazy<Mutex<RequestedGuiEventLoopState>> =
Lazy::new(|| Mutex::new(RequestedGuiEventLoopState::Uninitialized));
struct FutureRunner {
fut: Mutex<Option<Pin<Box<dyn Future<Output = ()>>>>>,
}
/// 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<Self>) {
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<Box<dyn Future<Output = ()>>>) {
// 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(|_| {});
// 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()
});
}
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();
}
i_slint_core::api::quit_event_loop().unwrap();
// 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<String>,
}
#[derive(Default)]
struct ContentCache {
source_code: HashMap<PathBuf, String>,
dependency: HashSet<PathBuf>,
current: PreviewComponent,
sender: Option<crate::ServerNotifier>,
}
static CONTENT_CACHE: once_cell::sync::OnceCell<Mutex<ContentCache>> =
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);
}
}
}
/// 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<String> {
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
}
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();
#[cfg(not(target_arch = "wasm32"))]
{
use clap::Parser;
let cli_args = super::Cli::parse();
if !cli_args.style.is_empty() {
builder.set_style(cli_args.style)
};
builder.set_include_paths(cli_args.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!("{}\n_Preview := {} {{ }}\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 {
#[derive(Default)]
struct PreviewState {
handle: Option<slint_interpreter::ComponentInstance>,
}
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
PREVIEW_STATE.with(|preview_state| {
let mut preview_state = preview_state.borrow_mut();
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(),
PostLoadBehavior::DoNothing => {}
}
preview_state.handle = Some(handle);
} else {
let handle = compiled.create();
handle.show();
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<lsp_types::Url, Vec<lsp_types::Diagnostic>> = 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));
}