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
4use crate::common::{self, ComponentInformation, PreviewComponent, PreviewConfig};
5use crate::lsp_ext::Health;
6use crate::preview::element_selection::ElementSelection;
7use crate::util;
8use i_slint_compiler::object_tree::ElementRc;
9use i_slint_compiler::parser::{syntax_nodes::Element, SyntaxKind};
10use i_slint_core::component_factory::FactoryContext;
11use i_slint_core::lengths::{LogicalLength, LogicalPoint};
12use i_slint_core::model::VecModel;
13use lsp_types::Url;
14use slint_interpreter::highlight::ComponentPositions;
15use slint_interpreter::{ComponentDefinition, ComponentHandle, ComponentInstance};
16use std::cell::RefCell;
17use std::collections::{HashMap, HashSet};
18use std::path::{Path, PathBuf};
19use std::rc::Rc;
20use std::sync::Mutex;
21
22#[cfg(target_arch = "wasm32")]
23use crate::wasm_prelude::*;
24
25mod debug;
26mod drop_location;
27mod element_selection;
28mod ui;
29#[cfg(all(target_arch = "wasm32", feature = "preview-external"))]
30mod wasm;
31#[cfg(all(target_arch = "wasm32", feature = "preview-external"))]
32pub use wasm::*;
33#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))]
34mod native;
35#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))]
36pub use native::*;
37
38#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
39enum 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)]
52struct 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
62static CONTENT_CACHE: std::sync::OnceLock<Mutex<ContentCache>> = std::sync::OnceLock::new();
63
64#[derive(Default)]
65struct 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}
72thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
73
74pub 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`.
97fn 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
111fn 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
131fn 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
162fn 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
202fn 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
284fn 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
297fn start_parsing() {
298 set_status_text("Updating Preview...");
299 set_diagnostics(&[]);
300 send_status(message:"Loading Preview…", Health::Ok);
301}
302
303fn 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
312pub 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
337fn 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
344fn 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
349pub 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
449async 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.
501fn 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.
529pub 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
565pub 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
583fn 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
599fn 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
604fn 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
648fn 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
677fn 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
684fn 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
690fn 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
701fn 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
710fn 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
721fn 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
735fn 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
751fn 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
777pub 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.
805const 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`
809fn 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