forked from slint-ui/slint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpreview.rs
350 lines (309 loc) · 12.3 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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// 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<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(|_| 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<String>,
/// The list of include paths
pub include_paths: Vec<std::path::PathBuf>,
/// The style name for the preview
pub style: String,
}
#[derive(Default)]
struct ContentCache {
source_code: HashMap<PathBuf, String>,
dependency: HashSet<PathBuf>,
current: PreviewComponent,
sender: Option<crate::ServerNotifier>,
highlight: Option<(PathBuf, u32)>,
}
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);
}
}
}
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<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
}
#[derive(Default)]
struct PreviewState {
handle: Option<slint_interpreter::ComponentInstance>,
}
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = 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<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));
}
/// Highlight the element pointed at the offset in the path.
/// When path is None, remove the highlight.
pub fn highlight(path: Option<PathBuf>, 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);
}
}
})
}))
}
}