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 | use crate::common::{self, ComponentInformation, PreviewComponent, PreviewConfig}; |
5 | use crate::lsp_ext::Health; |
6 | use crate::preview::element_selection::ElementSelection; |
7 | use crate::util; |
8 | use i_slint_compiler::object_tree::ElementRc; |
9 | use i_slint_compiler::parser::{syntax_nodes::Element, SyntaxKind}; |
10 | use i_slint_core::component_factory::FactoryContext; |
11 | use i_slint_core::lengths::{LogicalLength, LogicalPoint}; |
12 | use i_slint_core::model::VecModel; |
13 | use lsp_types::Url; |
14 | use slint_interpreter::highlight::ComponentPositions; |
15 | use slint_interpreter::{ComponentDefinition, ComponentHandle, ComponentInstance}; |
16 | use std::cell::RefCell; |
17 | use std::collections::{HashMap, HashSet}; |
18 | use std::path::{Path, PathBuf}; |
19 | use std::rc::Rc; |
20 | use std::sync::Mutex; |
21 | |
22 | #[cfg (target_arch = "wasm32" )] |
23 | use crate::wasm_prelude::*; |
24 | |
25 | mod debug; |
26 | mod drop_location; |
27 | mod element_selection; |
28 | mod ui; |
29 | #[cfg (all(target_arch = "wasm32" , feature = "preview-external" ))] |
30 | mod wasm; |
31 | #[cfg (all(target_arch = "wasm32" , feature = "preview-external" ))] |
32 | pub use wasm::*; |
33 | #[cfg (all(not(target_arch = "wasm32" ), feature = "preview-builtin" ))] |
34 | mod native; |
35 | #[cfg (all(not(target_arch = "wasm32" ), feature = "preview-builtin" ))] |
36 | pub use native::*; |
37 | |
38 | #[derive (Default, Copy, Clone, PartialEq, Eq, Debug)] |
39 | enum PreviewFutureState { |
40 | /// The preview future is currently no running |
41 | #[default] |
42 | Pending, |
43 | /// The preview future has been started, but we haven't started compiling |
44 | PreLoading, |
45 | /// The preview future is currently loading the preview |
46 | Loading, |
47 | /// The preview future is currently loading an outdated preview, we should abort loading and restart loading again |
48 | NeedsReload, |
49 | } |
50 | |
51 | #[derive (Default)] |
52 | struct ContentCache { |
53 | source_code: HashMap<Url, (common::UrlVersion, String)>, |
54 | dependency: HashSet<Url>, |
55 | current: Option<PreviewComponent>, |
56 | config: PreviewConfig, |
57 | loading_state: PreviewFutureState, |
58 | highlight: Option<(Url, u32)>, |
59 | ui_is_visible: bool, |
60 | } |
61 | |
62 | static CONTENT_CACHE: std::sync::OnceLock<Mutex<ContentCache>> = std::sync::OnceLock::new(); |
63 | |
64 | #[derive (Default)] |
65 | struct PreviewState { |
66 | ui: Option<ui::PreviewUi>, |
67 | handle: Rc<RefCell<Option<slint_interpreter::ComponentInstance>>>, |
68 | selected: Option<element_selection::ElementSelection>, |
69 | notify_editor_about_selection_after_update: bool, |
70 | known_components: Vec<ComponentInformation>, |
71 | } |
72 | thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();} |
73 | |
74 | pub fn set_contents(url: &common::VersionedUrl, content: String) { |
75 | let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
76 | let old: Option<(Option, String)> = cache.source_code.insert(k:url.url().clone(), (*url.version(), content.clone())); |
77 | if cache.dependency.contains(url.url()) { |
78 | if let Some((old_version: Option, old: String)) = old { |
79 | if content == old && old_version == *url.version() { |
80 | return; |
81 | } |
82 | } |
83 | let Some(current: PreviewComponent) = cache.current.clone() else { |
84 | return; |
85 | }; |
86 | let ui_is_visible: bool = cache.ui_is_visible; |
87 | |
88 | drop(cache); |
89 | |
90 | if ui_is_visible { |
91 | load_preview(preview_component:current); |
92 | } |
93 | } |
94 | } |
95 | |
96 | /// Try to find the parent of element `child` below `root`. |
97 | fn search_for_parent_element(root: &ElementRc, child: &ElementRc) -> Option<ElementRc> { |
98 | for c: &Rc> in &root.borrow().children { |
99 | if std::rc::Rc::ptr_eq(this:c, other:child) { |
100 | return Some(root.clone()); |
101 | } |
102 | |
103 | if let Some(parent: Rc>) = search_for_parent_element(root:c, child) { |
104 | return Some(parent); |
105 | } |
106 | } |
107 | None |
108 | } |
109 | |
110 | // triggered from the UI, running in UI thread |
111 | fn can_drop_component(component_type: slint::SharedString, x: f32, y: f32) -> bool { |
112 | let component_type: String = component_type.to_string(); |
113 | |
114 | PREVIEW_STATE.with(move |preview_state: &RefCell| { |
115 | let preview_state: Ref<'_, PreviewState> = preview_state.borrow(); |
116 | |
117 | let component_index: &usize = &preview_state |
118 | .known_components |
119 | .binary_search_by_key(&component_type.as_str(), |ci| ci.name.as_str()) |
120 | .unwrap_or(default:usize::MAX); |
121 | |
122 | let Some(component: &ComponentInformation) = preview_state.known_components.get(*component_index) else { |
123 | return false; |
124 | }; |
125 | |
126 | drop_location::can_drop_at(x, y, component) |
127 | }) |
128 | } |
129 | |
130 | // triggered from the UI, running in UI thread |
131 | fn drop_component(component_type: slint::SharedString, x: f32, y: f32) { |
132 | let component_type = component_type.to_string(); |
133 | |
134 | let drop_result = PREVIEW_STATE.with(|preview_state| { |
135 | let preview_state = preview_state.borrow(); |
136 | |
137 | let component_index = &preview_state |
138 | .known_components |
139 | .binary_search_by_key(&component_type.as_str(), |ci| ci.name.as_str()) |
140 | .unwrap_or(usize::MAX); |
141 | |
142 | drop_location::drop_at(x, y, preview_state.known_components.get(*component_index)?) |
143 | }); |
144 | |
145 | if let Some((edit, drop_data)) = drop_result { |
146 | element_selection::select_element_at_source_code_position( |
147 | drop_data.path, |
148 | drop_data.selection_offset, |
149 | drop_data.is_layout, |
150 | None, |
151 | true, |
152 | ); |
153 | |
154 | send_message_to_lsp(crate::common::PreviewToLspMessage::SendWorkspaceEdit { |
155 | label: Some(format!("Add element {}" , component_type)), |
156 | edit, |
157 | }); |
158 | }; |
159 | } |
160 | |
161 | // triggered from the UI, running in UI thread |
162 | fn delete_selected_element() { |
163 | let Some(selected) = selected_element() else { |
164 | return; |
165 | }; |
166 | |
167 | let Ok(url) = Url::from_file_path(&selected.path) else { |
168 | return; |
169 | }; |
170 | |
171 | let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
172 | let Some((version, _)) = cache.source_code.get(&url).cloned() else { |
173 | return; |
174 | }; |
175 | |
176 | let Some(range) = selected.as_element_node().and_then(|en| { |
177 | en.with_element_node(|n| { |
178 | if let Some(parent) = &n.parent() { |
179 | if parent.kind() == SyntaxKind::SubElement { |
180 | return util::map_node(parent); |
181 | } |
182 | } |
183 | util::map_node(n) |
184 | }) |
185 | }) else { |
186 | return; |
187 | }; |
188 | |
189 | let edit = common::create_workspace_edit( |
190 | url, |
191 | version, |
192 | vec![lsp_types::TextEdit { range, new_text: "" .into() }], |
193 | ); |
194 | |
195 | send_message_to_lsp(crate::common::PreviewToLspMessage::SendWorkspaceEdit { |
196 | label: Some("Delete element" .to_string()), |
197 | edit, |
198 | }); |
199 | } |
200 | |
201 | // triggered from the UI, running in UI thread |
202 | fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) { |
203 | let Some(selected) = selected_element() else { |
204 | return; |
205 | }; |
206 | let Some(selected_element_node) = selected.as_element_node() else { |
207 | return; |
208 | }; |
209 | let Some(component_instance) = component_instance() else { |
210 | return; |
211 | }; |
212 | |
213 | let Some(geometry) = component_instance |
214 | .element_position(&selected_element_node.element) |
215 | .get(selected.instance_index) |
216 | .cloned() |
217 | else { |
218 | return; |
219 | }; |
220 | |
221 | let click_position = LogicalPoint::from_lengths(LogicalLength::new(x), LogicalLength::new(y)); |
222 | let root_element = element_selection::root_element(&component_instance); |
223 | |
224 | let (parent_x, parent_y) = |
225 | search_for_parent_element(&root_element, &selected_element_node.element) |
226 | .and_then(|parent_element| { |
227 | component_instance |
228 | .element_position(&parent_element) |
229 | .iter() |
230 | .find(|g| g.contains(click_position)) |
231 | .map(|g| (g.origin.x, g.origin.y)) |
232 | }) |
233 | .unwrap_or_default(); |
234 | |
235 | let (properties, op) = { |
236 | let mut p = Vec::with_capacity(4); |
237 | let mut op = "" ; |
238 | if geometry.origin.x != x && x.is_finite() { |
239 | p.push(crate::common::PropertyChange::new( |
240 | "x" , |
241 | format!(" {}px" , (x - parent_x).round()), |
242 | )); |
243 | op = "Moving" ; |
244 | } |
245 | if geometry.origin.y != y && y.is_finite() { |
246 | p.push(crate::common::PropertyChange::new( |
247 | "y" , |
248 | format!(" {}px" , (y - parent_y).round()), |
249 | )); |
250 | op = "Moving" ; |
251 | } |
252 | if geometry.size.width != width && width.is_finite() { |
253 | p.push(crate::common::PropertyChange::new("width" , format!(" {}px" , width.round()))); |
254 | op = "Resizing" ; |
255 | } |
256 | if geometry.size.height != height && height.is_finite() { |
257 | p.push(crate::common::PropertyChange::new("height" , format!(" {}px" , height.round()))); |
258 | op = "Resizing" ; |
259 | } |
260 | (p, op) |
261 | }; |
262 | |
263 | if !properties.is_empty() { |
264 | let Ok(url) = Url::from_file_path(&selected.path) else { |
265 | return; |
266 | }; |
267 | |
268 | let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
269 | let Some((version, _)) = cache.source_code.get(&url).cloned() else { |
270 | return; |
271 | }; |
272 | |
273 | send_message_to_lsp(crate::common::PreviewToLspMessage::UpdateElement { |
274 | label: Some(format!(" {op} element" )), |
275 | position: common::VersionedPosition::new( |
276 | common::VersionedUrl::new(url, version), |
277 | selected.offset, |
278 | ), |
279 | properties, |
280 | }); |
281 | } |
282 | } |
283 | |
284 | fn change_style() { |
285 | let cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
286 | let ui_is_visible: bool = cache.ui_is_visible; |
287 | let Some(current: PreviewComponent) = cache.current.clone() else { |
288 | return; |
289 | }; |
290 | drop(cache); |
291 | |
292 | if ui_is_visible { |
293 | load_preview(preview_component:current); |
294 | } |
295 | } |
296 | |
297 | fn start_parsing() { |
298 | set_status_text("Updating Preview..." ); |
299 | set_diagnostics(&[]); |
300 | send_status(message:"Loading Preview…" , Health::Ok); |
301 | } |
302 | |
303 | fn finish_parsing(ok: bool) { |
304 | set_status_text("" ); |
305 | if ok { |
306 | send_status(message:"Preview Loaded" , Health::Ok); |
307 | } else { |
308 | send_status(message:"Preview not updated" , Health::Error); |
309 | } |
310 | } |
311 | |
312 | pub fn config_changed(config: PreviewConfig) { |
313 | if let Some(cache: &Mutex) = CONTENT_CACHE.get() { |
314 | let mut cache: MutexGuard<'_, ContentCache> = cache.lock().unwrap(); |
315 | if cache.config != config { |
316 | cache.config = config; |
317 | let current: Option = cache.current.clone(); |
318 | let ui_is_visible: bool = cache.ui_is_visible; |
319 | let hide_ui: Option = cache.config.hide_ui; |
320 | |
321 | drop(cache); |
322 | |
323 | if ui_is_visible { |
324 | if let Some(hide_ui: bool) = hide_ui { |
325 | set_show_preview_ui(!hide_ui); |
326 | } |
327 | if let Some(current: PreviewComponent) = current { |
328 | load_preview(preview_component:current); |
329 | } |
330 | } |
331 | } |
332 | }; |
333 | } |
334 | |
335 | /// If the file is in the cache, returns it. |
336 | /// In any way, register it as a dependency |
337 | fn get_url_from_cache(url: &Url) -> Option<(common::UrlVersion, String)> { |
338 | let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
339 | let r: Option<(Option, String)> = cache.source_code.get(url).cloned(); |
340 | cache.dependency.insert(url.to_owned()); |
341 | r |
342 | } |
343 | |
344 | fn get_path_from_cache(path: &Path) -> Option<(common::UrlVersion, String)> { |
345 | let url: Url = Url::from_file_path(path).ok()?; |
346 | get_url_from_cache(&url) |
347 | } |
348 | |
349 | pub fn load_preview(preview_component: PreviewComponent) { |
350 | { |
351 | let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
352 | cache.current = Some(preview_component.clone()); |
353 | if !cache.ui_is_visible { |
354 | return; |
355 | } |
356 | match cache.loading_state { |
357 | PreviewFutureState::Pending => (), |
358 | PreviewFutureState::PreLoading => return, |
359 | PreviewFutureState::Loading => { |
360 | cache.loading_state = PreviewFutureState::NeedsReload; |
361 | return; |
362 | } |
363 | PreviewFutureState::NeedsReload => return, |
364 | } |
365 | cache.loading_state = PreviewFutureState::PreLoading; |
366 | }; |
367 | |
368 | run_in_ui_thread(move || async move { |
369 | let (selected, notify_editor) = PREVIEW_STATE.with(|preview_state| { |
370 | let mut preview_state = preview_state.borrow_mut(); |
371 | let notify_editor = preview_state.notify_editor_about_selection_after_update; |
372 | preview_state.notify_editor_about_selection_after_update = false; |
373 | (preview_state.selected.take(), notify_editor) |
374 | }); |
375 | |
376 | loop { |
377 | let (preview_component, config) = { |
378 | let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
379 | let Some(current) = &mut cache.current else { return }; |
380 | let preview_component = current.clone(); |
381 | current.style.clear(); |
382 | |
383 | assert_eq!(cache.loading_state, PreviewFutureState::PreLoading); |
384 | if !cache.ui_is_visible { |
385 | cache.loading_state = PreviewFutureState::Pending; |
386 | return; |
387 | } |
388 | cache.loading_state = PreviewFutureState::Loading; |
389 | cache.dependency.clear(); |
390 | (preview_component, cache.config.clone()) |
391 | }; |
392 | let style = if preview_component.style.is_empty() { |
393 | get_current_style() |
394 | } else { |
395 | set_current_style(preview_component.style.clone()); |
396 | preview_component.style.clone() |
397 | }; |
398 | |
399 | reload_preview_impl(preview_component, style, config).await; |
400 | |
401 | let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
402 | match cache.loading_state { |
403 | PreviewFutureState::Loading => { |
404 | cache.loading_state = PreviewFutureState::Pending; |
405 | break; |
406 | } |
407 | PreviewFutureState::Pending => unreachable!(), |
408 | PreviewFutureState::PreLoading => unreachable!(), |
409 | PreviewFutureState::NeedsReload => { |
410 | cache.loading_state = PreviewFutureState::PreLoading; |
411 | continue; |
412 | } |
413 | }; |
414 | } |
415 | |
416 | if let Some(se) = selected { |
417 | element_selection::select_element_at_source_code_position( |
418 | se.path.clone(), |
419 | se.offset, |
420 | se.is_layout, |
421 | None, |
422 | false, |
423 | ); |
424 | |
425 | if notify_editor { |
426 | if let Some(component_instance) = component_instance() { |
427 | if let Some(element) = component_instance |
428 | .element_at_source_code_position(&se.path, se.offset) |
429 | .first() |
430 | { |
431 | if let Some((node, _)) = |
432 | element.borrow().debug.iter().find(|n| !is_element_node_ignored(&n.0)) |
433 | { |
434 | let sf = &node.source_file; |
435 | let pos = util::map_position(sf, se.offset.into()); |
436 | ask_editor_to_show_document( |
437 | &se.path.to_string_lossy(), |
438 | lsp_types::Range::new(pos, pos), |
439 | ); |
440 | } |
441 | } |
442 | } |
443 | } |
444 | } |
445 | }); |
446 | } |
447 | |
448 | // Most be inside the thread running the slint event loop |
449 | async fn reload_preview_impl( |
450 | preview_component: PreviewComponent, |
451 | style: String, |
452 | config: PreviewConfig, |
453 | ) { |
454 | let component = PreviewComponent { style: String::new(), ..preview_component }; |
455 | |
456 | start_parsing(); |
457 | |
458 | let mut builder = slint_interpreter::ComponentCompiler::default(); |
459 | |
460 | #[cfg (target_arch = "wasm32" )] |
461 | { |
462 | let cc = builder.compiler_configuration(i_slint_core::InternalToken); |
463 | cc.resource_url_mapper = resource_url_mapper(); |
464 | } |
465 | |
466 | if !style.is_empty() { |
467 | builder.set_style(style.clone()); |
468 | } |
469 | builder.set_include_paths(config.include_paths); |
470 | builder.set_library_paths(config.library_paths); |
471 | |
472 | builder.set_file_loader(|path| { |
473 | let path = path.to_owned(); |
474 | Box::pin(async move { get_path_from_cache(&path).map(|(_, c)| Result::Ok(c)) }) |
475 | }); |
476 | |
477 | // to_file_path on a WASM Url just returns the URL as the path! |
478 | let path = component.url.to_file_path().unwrap_or(PathBuf::from(&component.url.to_string())); |
479 | |
480 | let compiled = if let Some((_, mut from_cache)) = get_url_from_cache(&component.url) { |
481 | if let Some(component_name) = &component.component { |
482 | from_cache = format!( |
483 | " {from_cache}\nexport component _SLINT_LivePreview inherits {component_name} {{ /* {NODE_IGNORE_COMMENT} */ }}\n" , |
484 | ); |
485 | } |
486 | builder.build_from_source(from_cache, path).await |
487 | } else { |
488 | builder.build_from_path(path).await |
489 | }; |
490 | |
491 | notify_diagnostics(builder.diagnostics()); |
492 | |
493 | let success = compiled.is_some(); |
494 | update_preview_area(compiled); |
495 | finish_parsing(success); |
496 | } |
497 | |
498 | /// This sets up the preview area to show the ComponentInstance |
499 | /// |
500 | /// This must be run in the UI thread. |
501 | fn set_preview_factory( |
502 | ui: &ui::PreviewUi, |
503 | compiled: ComponentDefinition, |
504 | callback: Box<dyn Fn(ComponentInstance)>, |
505 | ) { |
506 | // Ensure that the popup is closed as it is related to the old factory |
507 | i_slint_core::window::WindowInner::from_pub(ui.window()).close_popup(); |
508 | |
509 | let factory: ComponentFactory = slint::ComponentFactory::new(factory:move |ctx: FactoryContext| { |
510 | let instance: ComponentInstance = compiled.create_embedded(ctx).unwrap(); |
511 | |
512 | if let Some((url: Url, offset: u32)) = |
513 | CONTENT_CACHE.get().and_then(|c: &Mutex| c.lock().unwrap().highlight.clone()) |
514 | { |
515 | highlight(url:Some(url), offset); |
516 | } else { |
517 | highlight(url:None, offset:0); |
518 | } |
519 | |
520 | callback(instance.clone_strong()); |
521 | |
522 | Some(instance) |
523 | }); |
524 | ui.set_preview_area(factory); |
525 | } |
526 | |
527 | /// Highlight the element pointed at the offset in the path. |
528 | /// When path is None, remove the highlight. |
529 | pub fn highlight(url: Option<Url>, offset: u32) { |
530 | let highlight = url.clone().map(|u| (u, offset)); |
531 | let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap(); |
532 | |
533 | if cache.highlight == highlight { |
534 | return; |
535 | } |
536 | cache.highlight = highlight; |
537 | |
538 | if cache.highlight.as_ref().map_or(true, |(url, _)| cache.dependency.contains(url)) { |
539 | run_in_ui_thread(move || async move { |
540 | let Some(component_instance) = component_instance() else { |
541 | return; |
542 | }; |
543 | let Some(path) = url.and_then(|u| Url::to_file_path(&u).ok()) else { |
544 | return; |
545 | }; |
546 | let elements = component_instance.element_at_source_code_position(&path, offset); |
547 | if let Some(e) = elements.first() { |
548 | let Some(debug_index) = e.borrow().debug.iter().position(|(n, _)| { |
549 | n.text_range().contains(offset.into()) && n.source_file.path() == path |
550 | }) else { |
551 | return; |
552 | }; |
553 | let is_layout = |
554 | e.borrow().debug.get(debug_index).map_or(false, |(_, l)| l.is_some()); |
555 | element_selection::select_element_at_source_code_position( |
556 | path, offset, is_layout, None, false, |
557 | ); |
558 | } else { |
559 | element_selection::unselect_element(); |
560 | } |
561 | }) |
562 | } |
563 | } |
564 | |
565 | pub fn known_components( |
566 | _url: &Option<common::VersionedUrl>, |
567 | mut components: Vec<ComponentInformation>, |
568 | ) { |
569 | components.sort_unstable_by_key(|ci: &ComponentInformation| ci.name.clone()); |
570 | |
571 | run_in_ui_thread(create_future:move || async move { |
572 | PREVIEW_STATE.with(|preview_state: &RefCell| { |
573 | let mut preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
574 | preview_state.known_components = components; |
575 | |
576 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
577 | ui::ui_set_known_components(ui, &preview_state.known_components) |
578 | } |
579 | }) |
580 | }); |
581 | } |
582 | |
583 | fn convert_diagnostics( |
584 | diagnostics: &[slint_interpreter::Diagnostic], |
585 | ) -> HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> { |
586 | let mut result: HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> = Default::default(); |
587 | for d: &Diagnostic in diagnostics { |
588 | if d.source_file().map_or(default:true, |f: &Path| !i_slint_compiler::pathutils::is_absolute(path:f)) { |
589 | continue; |
590 | } |
591 | let uri: Url = lsp_typesOption::Url::from_file_path(d.source_file().unwrap()) |
592 | .ok() |
593 | .unwrap_or_else(|| lsp_types::Url::parse(input:"file:/unknown" ).unwrap()); |
594 | result.entry(key:uri).or_default().push(crate::util::to_lsp_diag(d)); |
595 | } |
596 | result |
597 | } |
598 | |
599 | fn reset_selections(ui: &ui::PreviewUi) { |
600 | let model: Rc> = Rc::new(slint::VecModel::from(Vec::new())); |
601 | ui.set_selections(slint::ModelRc::from(model)); |
602 | } |
603 | |
604 | fn set_selections( |
605 | ui: Option<&ui::PreviewUi>, |
606 | main_index: usize, |
607 | is_layout: bool, |
608 | is_moveable: bool, |
609 | is_resizable: bool, |
610 | positions: ComponentPositions, |
611 | ) { |
612 | let Some(ui) = ui else { |
613 | return; |
614 | }; |
615 | |
616 | let border_color = if is_layout { |
617 | i_slint_core::Color::from_argb_encoded(0xffff0000) |
618 | } else { |
619 | i_slint_core::Color::from_argb_encoded(0xff0000ff) |
620 | }; |
621 | let secondary_border_color = if is_layout { |
622 | i_slint_core::Color::from_argb_encoded(0x80ff0000) |
623 | } else { |
624 | i_slint_core::Color::from_argb_encoded(0x800000ff) |
625 | }; |
626 | |
627 | let values = positions |
628 | .geometries |
629 | .iter() |
630 | .enumerate() |
631 | .map(|(i, g)| ui::Selection { |
632 | geometry: ui::SelectionRectangle { |
633 | width: g.size.width, |
634 | height: g.size.height, |
635 | x: g.origin.x, |
636 | y: g.origin.y, |
637 | }, |
638 | border_color: if i == main_index { border_color } else { secondary_border_color }, |
639 | is_primary: i == main_index, |
640 | is_moveable, |
641 | is_resizable, |
642 | }) |
643 | .collect::<Vec<_>>(); |
644 | let model = Rc::new(slint::VecModel::from(values)); |
645 | ui.set_selections(slint::ModelRc::from(model)); |
646 | } |
647 | |
648 | fn set_selected_element( |
649 | selection: Option<element_selection::ElementSelection>, |
650 | positions: slint_interpreter::highlight::ComponentPositions, |
651 | notify_editor_about_selection_after_update: bool, |
652 | ) { |
653 | let (is_layout: bool, is_in_layout: bool) = selection |
654 | .as_ref() |
655 | .and_then(|s| s.as_element_node()) |
656 | .map(|en| (en.is_layout(), element_selection::is_element_node_in_layout(&en))) |
657 | .unwrap_or((false, false)); |
658 | |
659 | PREVIEW_STATE.with(move |preview_state: &RefCell| { |
660 | let mut preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
661 | |
662 | set_selections( |
663 | ui:preview_state.ui.as_ref(), |
664 | main_index:selection.as_ref().map(|s| s.instance_index).unwrap_or_default(), |
665 | is_layout, |
666 | !is_in_layout && !is_layout, |
667 | !is_in_layout && !is_layout, |
668 | positions, |
669 | ); |
670 | |
671 | preview_state.selected = selection; |
672 | preview_state.notify_editor_about_selection_after_update = |
673 | notify_editor_about_selection_after_update; |
674 | }) |
675 | } |
676 | |
677 | fn selected_element() -> Option<ElementSelection> { |
678 | PREVIEW_STATE.with(move |preview_state: &RefCell| { |
679 | let preview_state: Ref<'_, PreviewState> = preview_state.borrow(); |
680 | preview_state.selected.clone() |
681 | }) |
682 | } |
683 | |
684 | fn component_instance() -> Option<ComponentInstance> { |
685 | PREVIEW_STATE.with(move |preview_state: &RefCell| { |
686 | preview_state.borrow().handle.borrow().as_ref().map(|ci: &ComponentInstance| ci.clone_strong()) |
687 | }) |
688 | } |
689 | |
690 | fn set_show_preview_ui(show_preview_ui: bool) { |
691 | run_in_ui_thread(create_future:move || async move { |
692 | PREVIEW_STATE.with(|preview_state: &RefCell| { |
693 | let preview_state: Ref<'_, PreviewState> = preview_state.borrow(); |
694 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
695 | ui.set_show_preview_ui(show_preview_ui) |
696 | } |
697 | }) |
698 | }); |
699 | } |
700 | |
701 | fn set_current_style(style: String) { |
702 | PREVIEW_STATE.with(move |preview_state: &RefCell| { |
703 | let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
704 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
705 | ui.set_current_style(style.into()) |
706 | } |
707 | }); |
708 | } |
709 | |
710 | fn get_current_style() -> String { |
711 | PREVIEW_STATE.with(|preview_state: &RefCell| -> String { |
712 | let preview_state: Ref<'_, PreviewState> = preview_state.borrow(); |
713 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
714 | ui.get_current_style().as_str().to_string() |
715 | } else { |
716 | String::new() |
717 | } |
718 | }) |
719 | } |
720 | |
721 | fn set_status_text(text: &str) { |
722 | let text: String = text.to_string(); |
723 | |
724 | i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || { |
725 | PREVIEW_STATE.with(|preview_state: &RefCell| { |
726 | let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
727 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
728 | ui.set_status_text(text.into()); |
729 | } |
730 | }); |
731 | }) |
732 | .unwrap(); |
733 | } |
734 | |
735 | fn set_diagnostics(diagnostics: &[slint_interpreter::Diagnostic]) { |
736 | let data: Vec = crate::preview::ui::convert_diagnostics(diagnostics); |
737 | |
738 | i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || { |
739 | PREVIEW_STATE.with(|preview_state: &RefCell| { |
740 | let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut(); |
741 | if let Some(ui: &PreviewUi) = &preview_state.ui { |
742 | let model: VecModel = VecModel::from(data); |
743 | ui.set_diagnostics(Rc::new(model).into()); |
744 | } |
745 | }); |
746 | }) |
747 | .unwrap(); |
748 | } |
749 | |
750 | /// This runs `set_preview_factory` in the UI thread |
751 | fn update_preview_area(compiled: Option<ComponentDefinition>) { |
752 | PREVIEW_STATE.with(|preview_state| { |
753 | #[allow (unused_mut)] |
754 | let mut preview_state = preview_state.borrow_mut(); |
755 | |
756 | #[cfg (not(target_arch = "wasm32" ))] |
757 | native::open_ui_impl(&mut preview_state); |
758 | |
759 | let ui = preview_state.ui.as_ref().unwrap(); |
760 | let shared_handle = preview_state.handle.clone(); |
761 | |
762 | if let Some(compiled) = compiled { |
763 | set_preview_factory( |
764 | ui, |
765 | compiled, |
766 | Box::new(move |instance| { |
767 | shared_handle.replace(Some(instance)); |
768 | }), |
769 | ); |
770 | reset_selections(ui); |
771 | } |
772 | |
773 | ui.show().unwrap(); |
774 | }); |
775 | } |
776 | |
777 | pub fn lsp_to_preview_message( |
778 | message: crate::common::LspToPreviewMessage, |
779 | #[cfg (not(target_arch = "wasm32" ))] sender: &crate::ServerNotifier, |
780 | ) { |
781 | use crate::common::LspToPreviewMessage as M; |
782 | match message { |
783 | M::SetContents { url: VersionedUrl, contents: String } => { |
784 | set_contents(&url, content:contents); |
785 | } |
786 | M::SetConfiguration { config: PreviewConfig } => { |
787 | config_changed(config); |
788 | } |
789 | M::ShowPreview(pc: PreviewComponent) => { |
790 | #[cfg (not(target_arch = "wasm32" ))] |
791 | native::open_ui(sender); |
792 | load_preview(preview_component:pc); |
793 | } |
794 | M::HighlightFromEditor { url: Option, offset: u32 } => { |
795 | highlight(url, offset); |
796 | } |
797 | M::KnownComponents { url: Option, components: Vec } => { |
798 | known_components(&url, components); |
799 | } |
800 | } |
801 | } |
802 | |
803 | /// Use this in nodes you want the language server and preview to |
804 | /// ignore a node for code analysis purposes. |
805 | const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node" ; |
806 | |
807 | /// Check whether a node is marked to be ignored in the LSP/live preview |
808 | /// using a comment containing `@lsp:ignore-node` |
809 | fn is_element_node_ignored(node: &Element) -> bool { |
810 | node.children_with_tokens().any(|nt| { |
811 | nt.as_token() |
812 | .map(|t| t.kind() == SyntaxKind::Comment && t.text().contains(NODE_IGNORE_COMMENT)) |
813 | .unwrap_or(false) |
814 | }) |
815 | } |
816 | |