1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use crate::common::{
5 self, component_catalog, rename_component, ComponentInformation, ElementRcNode,
6 PreviewComponent, PreviewConfig, PreviewToLspMessage, SourceFileVersion,
7};
8use crate::preview::element_selection::ElementSelection;
9use crate::util;
10use i_slint_compiler::object_tree::ElementRc;
11use i_slint_compiler::parser::{syntax_nodes, TextSize};
12use i_slint_compiler::{diagnostics, EmbedResourcesKind};
13use i_slint_core::component_factory::FactoryContext;
14use i_slint_core::lengths::{LogicalPoint, LogicalRect, LogicalSize};
15use lsp_types::Url;
16use slint::PlatformError;
17use slint_interpreter::{ComponentDefinition, ComponentHandle, ComponentInstance};
18use std::borrow::BorrowMut;
19use std::cell::RefCell;
20use std::collections::{HashMap, HashSet};
21use std::path::{Path, PathBuf};
22use std::rc::Rc;
23use std::sync::Mutex;
24
25#[cfg(target_arch = "wasm32")]
26use crate::wasm_prelude::*;
27
28mod debug;
29mod drop_location;
30mod element_selection;
31mod ext;
32mod preview_data;
33use ext::ElementRcNodeExt;
34mod properties;
35pub mod ui;
36#[cfg(all(target_arch = "wasm32", feature = "preview-external"))]
37mod wasm;
38#[cfg(all(target_arch = "wasm32", feature = "preview-external"))]
39pub use wasm::*;
40#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))]
41mod native;
42#[cfg(all(not(target_arch = "wasm32"), feature = "preview-builtin"))]
43pub use native::*;
44
45/// The state of the preview engine:
46///
47/// ```text
48/// ┌─────────────┐
49/// ┌──│ NeedsReload │◄─┐
50/// │ └─────────────┘ │
51/// ▼ │
52/// ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
53/// │ Pending │────►│ PreLoading │────►│ Loading │
54/// └─────────────┘ └─────────────┘ └─────────────┘
55/// ▲ │
56/// │ │
57/// └───────────────────────────────────────┘
58/// ```
59#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
60enum PreviewFutureState {
61 /// The preview future is currently no running
62 #[default]
63 Pending,
64 /// The preview future has been started, but we haven't started compiling
65 PreLoading,
66 /// The preview future is currently loading the preview
67 Loading,
68 /// The preview future is currently loading an outdated preview, we should abort loading and restart loading again
69 NeedsReload,
70}
71
72#[derive(Clone, Debug)]
73struct SourceCodeCacheEntry {
74 // None when read from disk!
75 version: SourceFileVersion,
76 code: String,
77}
78type SourceCodeCache = HashMap<Url, SourceCodeCacheEntry>;
79
80#[derive(Default)]
81struct ContentCache {
82 source_code: SourceCodeCache,
83 resources: HashSet<Url>,
84 dependencies: HashSet<Url>,
85 config: PreviewConfig,
86 current_previewed_component: Option<PreviewComponent>,
87 current_load_behavior: Option<LoadBehavior>,
88 loading_state: PreviewFutureState,
89 ui_is_visible: bool,
90}
91
92static CONTENT_CACHE: std::sync::OnceLock<Mutex<ContentCache>> = std::sync::OnceLock::new();
93
94impl ContentCache {
95 pub fn current_component(&self) -> Option<PreviewComponent> {
96 self.current_previewed_component.clone()
97 }
98
99 pub fn set_current_component(&mut self, component: PreviewComponent) {
100 self.current_previewed_component = Some(component);
101 }
102
103 pub fn clear_style_of_component(&mut self) {
104 if let Some(pc: &mut PreviewComponent) = &mut self.current_previewed_component {
105 pc.style = String::new();
106 }
107 }
108
109 pub fn rename_current_component(&mut self, url: &Url, old_name: &str, new_name: &str) {
110 if let Some(pc: &mut PreviewComponent) = &mut self.current_previewed_component {
111 if pc.url == *url && pc.component.as_deref() == Some(old_name) {
112 pc.component = Some(new_name.to_string());
113 }
114 }
115 }
116}
117
118#[derive(Default)]
119struct PreviewState {
120 ui: Option<ui::PreviewUi>,
121 property_range_declarations: Option<ui::PropertyDeclarations>,
122 handle: Rc<RefCell<Option<slint_interpreter::ComponentInstance>>>,
123 document_cache: Rc<RefCell<Option<Rc<common::DocumentCache>>>>,
124 selected: Option<element_selection::ElementSelection>,
125 notify_editor_about_selection_after_update: bool,
126 workspace_edit_sent: bool,
127 known_components: Vec<ComponentInformation>,
128 preview_loading_delay_timer: Option<slint::Timer>,
129 initial_live_data: preview_data::PreviewDataMap,
130 current_live_data: preview_data::PreviewDataMap,
131}
132
133impl PreviewState {
134 fn component_instance(&self) -> Option<ComponentInstance> {
135 self.handle.borrow().as_ref().map(|ci: &ComponentInstance| ci.clone_strong())
136 }
137}
138thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
139
140pub fn poll_once<F: std::future::Future>(future: F) -> Option<F::Output> {
141 struct DummyWaker();
142 impl std::task::Wake for DummyWaker {
143 fn wake(self: std::sync::Arc<Self>) {}
144 }
145
146 let waker: Waker = std::sync::Arc::new(data:DummyWaker()).into();
147 let mut ctx: Context<'_> = std::task::Context::from_waker(&waker);
148
149 let future: Pin<&mut F> = std::pin::pin!(future);
150
151 match future.poll(&mut ctx) {
152 std::task::Poll::Ready(result: ::Output) => Some(result),
153 std::task::Poll::Pending => None,
154 }
155}
156
157// Just mark the cache as "read from disk" by setting the version to None.
158// Do not reset the code: We can check once the LSP has re-read it from disk
159// whether we need to refresh the preview or not.
160fn invalidate_contents(url: &lsp_types::Url) {
161 let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
162
163 if let Some(cache_entry: &mut SourceCodeCacheEntry) = cache.source_code.get_mut(url) {
164 cache_entry.version = None;
165 }
166}
167
168fn delete_document(url: &lsp_types::Url) {
169 let (current: Option, url_is_used: bool, ui_is_visible: bool) = {
170 let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
171 cache.source_code.remove(url);
172 (
173 cache.current_previewed_component.clone(),
174 cache.dependencies.contains(url),
175 cache.ui_is_visible,
176 )
177 };
178
179 if let Some(current: PreviewComponent) = current {
180 if (&current.url == url || url_is_used) && ui_is_visible {
181 // Trigger a compile error now!
182 load_preview(preview_component:current, behavior:LoadBehavior::Reload);
183 }
184 }
185}
186
187fn set_current_live_data(mut result: preview_data::PreviewDataMap) {
188 PREVIEW_STATE.with(|preview_state: &RefCell| {
189 let mut preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut();
190 preview_state.current_live_data.append(&mut result);
191 })
192}
193
194fn apply_live_preview_data() {
195 let Some(instance) = component_instance() else {
196 return;
197 };
198
199 let new_initial_data = preview_data::query_preview_data_properties_and_callbacks(&instance);
200
201 let (mut previous_initial, mut previous_current) = PREVIEW_STATE.with(|preview_state| {
202 let mut preview_state = preview_state.borrow_mut();
203 (
204 std::mem::replace(&mut preview_state.initial_live_data, new_initial_data),
205 std::mem::take(&mut preview_state.current_live_data),
206 )
207 });
208
209 while let Some((kc, vc)) = previous_current.pop_last() {
210 let prev = previous_initial.pop_last();
211
212 let vc = vc.value.unwrap_or_default();
213
214 if matches!(vc, slint_interpreter::Value::Void) {
215 continue;
216 }
217
218 if let Some((ki, vi)) = prev {
219 let vi = vi.value.unwrap_or_default();
220
221 if ki == kc && vi == vc {
222 continue;
223 }
224 }
225
226 let _ = preview_data::set_preview_data(&instance, &kc.container, &kc.property_name, vc);
227 }
228}
229
230fn set_contents(url: &common::VersionedUrl, content: String) {
231 let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
232 let old: Option = cache.source_code.insert(
233 k:url.url().clone(),
234 v:SourceCodeCacheEntry { version: *url.version(), code: content.clone() },
235 );
236
237 if Some(content) == old.map(|o: SourceCodeCacheEntry| o.code) {
238 return;
239 }
240
241 if cache.dependencies.contains(url.url()) {
242 let ui_is_visible: bool = cache.ui_is_visible;
243 let Some(current: PreviewComponent) = cache.current_component() else {
244 return;
245 };
246
247 drop(cache);
248
249 if ui_is_visible {
250 load_preview(preview_component:current, behavior:LoadBehavior::Reload);
251 }
252 }
253}
254
255/// Try to find the parent of element `child` below `root`.
256fn search_for_parent_element(root: &ElementRc, child: &ElementRc) -> Option<ElementRc> {
257 for c: &Rc> in &root.borrow().children {
258 if std::rc::Rc::ptr_eq(this:c, other:child) {
259 return Some(root.clone());
260 }
261
262 if let Some(parent: Rc>) = search_for_parent_element(root:c, child) {
263 return Some(parent);
264 }
265 }
266 None
267}
268
269// triggered from the UI, running in UI thread
270fn property_declaration_ranges(name: slint::SharedString) -> ui::PropertyDeclaration {
271 let name: String = name.to_string();
272 PREVIEW_STATE
273 .with(|preview_state: &RefCell| {
274 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
275
276 preview_stateOption<&HashMap>
277 .property_range_declarations
278 .as_ref()
279 .and_then(|d: &HashMap| d.get(name.as_str()).cloned())
280 })
281 .unwrap_or_default()
282}
283
284// triggered from the UI, running in UI thread
285fn add_new_component() {
286 fn find_component_name() -> Option<String> {
287 PREVIEW_STATE.with(|preview_state| {
288 let preview_state = preview_state.borrow();
289
290 for i in 0..preview_state.known_components.len() {
291 let name =
292 format!("MyComponent{}", if i == 0 { "".to_string() } else { i.to_string() });
293
294 if preview_state
295 .known_components
296 .binary_search_by_key(&name.as_str(), |ci| ci.name.as_str())
297 .is_err()
298 {
299 return Some(name);
300 }
301 }
302 None
303 })
304 }
305
306 let Some(document_cache) = document_cache() else {
307 return;
308 };
309
310 let preview_component = {
311 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
312 cache.current_component()
313 };
314
315 let Some(preview_component) = preview_component else {
316 return;
317 };
318
319 let Some(component_name) = find_component_name() else {
320 return;
321 };
322
323 let Some(document) = document_cache.get_document(&preview_component.url) else {
324 return;
325 };
326
327 let Some(document) = &document.node else {
328 return;
329 };
330
331 if let Some((edit, drop_data)) =
332 drop_location::add_new_component(&document_cache, &component_name, document)
333 {
334 element_selection::select_element_at_source_code_position(
335 drop_data.path,
336 drop_data.selection_offset,
337 None,
338 SelectionNotification::AfterUpdate,
339 );
340
341 {
342 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
343 cache.set_current_component(PreviewComponent {
344 url: preview_component.url.clone(),
345 component: Some(component_name.clone()),
346 style: preview_component.style.clone(),
347 })
348 }
349
350 send_workspace_edit(format!("Add {component_name}"), edit, true);
351 }
352}
353
354/// Find the identifier that belongs to a component of the given `name` in the `document`
355fn find_component_identifiers(
356 document: &syntax_nodes::Document,
357 name: &str,
358) -> Vec<syntax_nodes::DeclaredIdentifier> {
359 let name: Option = Some(i_slint_compiler::parser::normalize_identifier(ident:name));
360
361 let mut result: Vec = vec![];
362 for el: ExportsList in document.ExportsList() {
363 if let Some(component: Component) = el.Component() {
364 let identifier: DeclaredIdentifier = component.DeclaredIdentifier();
365 if i_slint_compiler::parser::identifier_text(&identifier) == name {
366 result.push(identifier);
367 }
368 }
369 }
370
371 for component: Component in document.Component() {
372 let identifier: DeclaredIdentifier = component.DeclaredIdentifier();
373 if i_slint_compiler::parser::identifier_text(&identifier) == name {
374 result.push(identifier);
375 }
376 }
377
378 result.sort_by_key(|i: &DeclaredIdentifier| i.text_range().start());
379 result
380}
381
382/// Find the last component in the `document`
383pub fn find_last_component_identifier(
384 document: &syntax_nodes::Document,
385) -> Option<syntax_nodes::DeclaredIdentifier> {
386 let last_identifier: Option = {
387 let mut tmp: Option = None;
388 for el: ExportsList in document.ExportsList() {
389 if let Some(component: Component) = el.Component() {
390 tmp = Some(component.DeclaredIdentifier());
391 }
392 }
393 tmp
394 };
395
396 if let Some(component: Component) = document.Component().last() {
397 let identifier: DeclaredIdentifier = component.DeclaredIdentifier();
398 if identifier.text_range().start()
399 > last_identifier.as_ref().map(|i: &DeclaredIdentifier| i.text_range().start()).unwrap_or_default()
400 {
401 return Some(identifier);
402 }
403 }
404
405 last_identifier
406}
407
408// triggered from the UI, running in UI thread
409fn rename_component(
410 old_name: slint::SharedString,
411 old_url: slint::SharedString,
412 new_name: slint::SharedString,
413) {
414 let old_name = old_name.to_string();
415 let Ok(old_url) = lsp_types::Url::parse(old_url.as_ref()) else {
416 return;
417 };
418 let new_name = new_name.to_string();
419
420 let Some(document_cache) = document_cache() else {
421 return;
422 };
423 let Some(document) = document_cache.get_document(&old_url) else {
424 return;
425 };
426 let Some(document) = document.node.as_ref() else {
427 return;
428 };
429
430 let identifiers = find_component_identifiers(document, &old_name);
431 if identifiers.is_empty() {
432 return;
433 };
434
435 if let Ok(edit) = rename_component::find_declaration_node(
436 &document_cache,
437 &identifiers
438 .first()
439 .unwrap()
440 .child_token(i_slint_compiler::parser::SyntaxKind::Identifier)
441 .unwrap(),
442 )
443 .unwrap()
444 .rename(&document_cache, &new_name)
445 {
446 // Update which component to show after refresh from the editor.
447 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
448 cache.rename_current_component(&old_url, &old_name, &new_name);
449
450 if let Some(current) = &mut cache.current_component() {
451 if current.url == old_url {
452 if let Some(component) = &current.component {
453 if component == &old_name {
454 current.component = Some(new_name.clone());
455 }
456 }
457 }
458 }
459
460 send_workspace_edit(format!("Rename component {old_name} to {new_name}"), edit, true);
461 }
462}
463
464fn evaluate_binding(
465 element_url: slint::SharedString,
466 element_version: i32,
467 element_offset: i32,
468 property_name: slint::SharedString,
469 property_value: String,
470) -> Option<lsp_types::WorkspaceEdit> {
471 let element_url: Url = Url::parse(input:element_url.as_ref()).ok()?;
472 let element_version: Option = if element_version < 0 { None } else { Some(element_version) };
473 let element_offset: TextSize = u32::try_from(element_offset).ok()?.into();
474 let property_name: String = property_name.to_string();
475
476 let document_cache: Rc = document_cache()?;
477 let element: ElementRcNode = document_cache.element_at_offset(&element_url, element_offset)?;
478
479 if property_value.is_empty() {
480 properties::remove_binding(uri:element_url, element_version, &element, &property_name).ok()
481 } else {
482 properties::set_binding(
483 uri:element_url,
484 element_version,
485 &element,
486 &property_name,
487 new_expression:property_value,
488 )
489 }
490}
491
492// triggered from the UI, running in UI thread
493fn test_code_binding(
494 element_url: slint::SharedString,
495 element_version: i32,
496 element_offset: i32,
497 property_name: slint::SharedString,
498 property_value: slint::SharedString,
499) -> bool {
500 test_binding(
501 element_url,
502 element_version,
503 element_offset,
504 property_name,
505 property_value.to_string(),
506 )
507}
508
509// Backend function called by `test_*_binding`
510fn test_binding(
511 element_url: slint::SharedString,
512 element_version: i32,
513 element_offset: i32,
514 property_name: slint::SharedString,
515 property_value: String,
516) -> bool {
517 let Some(edit: WorkspaceEdit) = evaluate_binding(
518 element_url,
519 element_version,
520 element_offset,
521 property_name,
522 property_value,
523 ) else {
524 return false;
525 };
526
527 let Some(document_cache: Rc) = document_cache() else {
528 return false;
529 };
530
531 drop_location::workspace_edit_compiles(&document_cache, &edit) != CompilationResult::ChangeFails
532}
533
534fn set_code_binding(
535 element_url: slint::SharedString,
536 element_version: i32,
537 element_offset: i32,
538 property_name: slint::SharedString,
539 property_value: slint::SharedString,
540) {
541 set_binding(
542 element_url,
543 element_version,
544 element_offset,
545 property_name,
546 property_value.to_string(),
547 )
548}
549
550fn set_color_binding(
551 element_url: slint::SharedString,
552 element_version: i32,
553 element_offset: i32,
554 property_name: slint::SharedString,
555 value: slint::Color,
556) {
557 // We need a CSS value which is rgba, color converts to a argb only :-/
558 let rgba: slint::RgbaColor<u8> = value.into();
559 let value: u32 = ((rgba.red as u32) << 24)
560 + ((rgba.green as u32) << 16)
561 + ((rgba.blue as u32) << 8)
562 + (rgba.alpha as u32);
563
564 set_binding(
565 element_url,
566 element_version,
567 element_offset,
568 property_name,
569 property_value:format!("#{value:08x}"),
570 )
571}
572
573/// Internal function called by all the `set_*_binding` functions
574fn set_binding(
575 element_url: slint::SharedString,
576 element_version: i32,
577 element_offset: i32,
578 property_name: slint::SharedString,
579 property_value: String,
580) {
581 if let Some(edit: WorkspaceEdit) = evaluate_binding(
582 element_url,
583 element_version,
584 element_offset,
585 property_name,
586 property_value,
587 ) {
588 send_workspace_edit(label:"Edit property".to_string(), edit, test_edit:true);
589 }
590}
591
592// triggered from the UI, running in UI thread
593fn show_component(name: slint::SharedString, url: slint::SharedString) {
594 let name = name.to_string();
595 let Ok(url) = Url::parse(url.as_ref()) else {
596 return;
597 };
598
599 let Ok(file) = url.to_file_path() else {
600 return;
601 };
602
603 let Some(document_cache) = document_cache() else {
604 return;
605 };
606 let Some(document) = document_cache.get_document(&url) else {
607 return;
608 };
609 let Some(document) = document.node.as_ref() else {
610 return;
611 };
612
613 let Some(identifier) = find_component_identifiers(document, &name).last().cloned() else {
614 return;
615 };
616
617 let start =
618 util::text_size_to_lsp_position(&identifier.source_file, identifier.text_range().start());
619 ask_editor_to_show_document(&file.to_string_lossy(), lsp_types::Range::new(start, start), false)
620}
621
622// triggered from the UI, running in UI thread
623fn show_document_offset_range(url: slint::SharedString, start: i32, end: i32, take_focus: bool) {
624 fn internal(
625 url: slint::SharedString,
626 start: i32,
627 end: i32,
628 ) -> Option<(PathBuf, lsp_types::Position, lsp_types::Position)> {
629 let url = Url::parse(url.as_ref()).ok()?;
630 let file = url.to_file_path().ok()?;
631
632 let start = u32::try_from(start).ok()?;
633 let end = u32::try_from(end).ok()?;
634
635 let document_cache = document_cache()?;
636 let document = document_cache.get_document(&url)?;
637 let document = document.node.as_ref()?;
638
639 let start = util::text_size_to_lsp_position(&document.source_file, start.into());
640 let end = util::text_size_to_lsp_position(&document.source_file, end.into());
641
642 Some((file, start, end))
643 }
644
645 if let Some((f, s, e)) = internal(url, start, end) {
646 ask_editor_to_show_document(&f.to_string_lossy(), lsp_types::Range::new(s, e), take_focus);
647 }
648}
649
650// triggered from the UI, running in UI thread
651fn show_preview_for(name: slint::SharedString, url: slint::SharedString) {
652 let name: String = name.to_string();
653 let Ok(url: Url) = Url::parse(input:url.as_ref()) else {
654 return;
655 };
656
657 let current: PreviewComponent = PreviewComponent { url, component: Some(name), style: String::new() };
658
659 load_preview(preview_component:current, behavior:LoadBehavior::Load);
660}
661
662// triggered from the UI, running in UI thread
663fn can_drop_component(component_index: i32, x: f32, y: f32, on_drop_area: bool) -> bool {
664 if !on_drop_area {
665 set_drop_mark(&None);
666 return false;
667 }
668
669 let Some(document_cache: Rc) = document_cache() else {
670 return false;
671 };
672
673 let position: Point2D = LogicalPoint::new(x, y);
674
675 PREVIEW_STATE.with(|preview_state: &RefCell| {
676 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
677
678 if let Some(component: &ComponentInformation) = preview_state.known_components.get(component_index as usize) {
679 drop_location::can_drop_at(&document_cache, position, component)
680 } else {
681 false
682 }
683 })
684}
685
686// triggered from the UI, running in UI thread
687fn drop_component(component_index: i32, x: f32, y: f32) {
688 let Some(document_cache) = document_cache() else {
689 return;
690 };
691
692 let position = LogicalPoint::new(x, y);
693
694 let drop_result = PREVIEW_STATE.with(|preview_state| {
695 let preview_state = preview_state.borrow();
696
697 let component = preview_state.known_components.get(component_index as usize)?;
698
699 drop_location::drop_at(&document_cache, position, component)
700 .map(|(e, d)| (e, d, component.name.clone()))
701 });
702
703 if let Some((edit, drop_data, component_name)) = drop_result {
704 element_selection::select_element_at_source_code_position(
705 drop_data.path,
706 drop_data.selection_offset,
707 None,
708 SelectionNotification::AfterUpdate,
709 );
710
711 send_workspace_edit(format!("Add element {component_name}"), edit, false);
712 };
713}
714
715fn placeholder_node_text(selected: &common::ElementRcNode) -> String {
716 let Some(parent: ElementRcNode) = selected.parent() else {
717 return Default::default();
718 };
719
720 if parent.layout_kind() != ui::LayoutKind::None && parent.children().len() == 1 {
721 return format!("Rectangle {{ /* {} */ }}", common::NODE_IGNORE_COMMENT);
722 }
723
724 Default::default()
725}
726
727// triggered from the UI, running in UI thread
728fn delete_selected_element() {
729 let Some(selected) = selected_element() else {
730 return;
731 };
732
733 let Ok(url) = Url::from_file_path(&selected.path) else {
734 return;
735 };
736
737 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
738 let Some(cache_entry) = cache.source_code.get(&url) else {
739 return;
740 };
741
742 let Some(selected_node) = selected.as_element_node() else {
743 return;
744 };
745
746 let range = selected_node.with_decorated_node(|n| util::node_to_lsp_range(&n));
747
748 // Insert a placeholder node into layouts if those end up empty:
749 let new_text = placeholder_node_text(&selected_node);
750
751 let edit = common::create_workspace_edit(
752 url,
753 cache_entry.version,
754 vec![lsp_types::TextEdit { range, new_text }],
755 );
756
757 send_workspace_edit("Delete element".to_string(), edit, true);
758}
759
760// triggered from the UI, running in UI thread
761fn resize_selected_element(x: f32, y: f32, width: f32, height: f32) {
762 let Some(element_selection: &ElementSelection) = &selected_element() else {
763 return;
764 };
765 let Some(element_node: ElementRcNode) = element_selection.as_element_node() else {
766 return;
767 };
768
769 let Some((edit: WorkspaceEdit, label: String)) = resize_selected_element_impl(
770 &element_node,
771 element_selection.instance_index,
772 rect:LogicalRect::new(origin:LogicalPoint::new(x, y), size:LogicalSize::new(width, height)),
773 ) else {
774 return;
775 };
776
777 send_workspace_edit(label, edit, test_edit:true);
778}
779
780fn resize_selected_element_impl(
781 element_node: &ElementRcNode,
782 instance_index: usize,
783 rect: LogicalRect,
784) -> Option<(lsp_types::WorkspaceEdit, String)> {
785 let component_instance = component_instance()?;
786
787 // They all have the same size anyway:
788 let (path, offset) = element_node.path_and_offset();
789 let geometry = element_node.geometries(&component_instance).get(instance_index).cloned()?;
790
791 let position = rect.origin;
792 let root_element = element_selection::root_element(&component_instance);
793
794 let parent = search_for_parent_element(&root_element, &element_node.element)
795 .and_then(|parent_element| {
796 component_instance
797 .element_positions(&parent_element)
798 .iter()
799 .find(|g| g.contains(position))
800 .map(|g| g.origin)
801 })
802 .unwrap_or_default();
803
804 let (properties, op) = {
805 let mut p = Vec::with_capacity(4);
806 let mut op = "";
807 if geometry.origin.x != position.x && position.x.is_finite() {
808 p.push(common::PropertyChange::new(
809 "x",
810 format!("{}px", (position.x - parent.x).round()),
811 ));
812 op = "Moving";
813 }
814 if geometry.origin.y != position.y && position.y.is_finite() {
815 p.push(common::PropertyChange::new(
816 "y",
817 format!("{}px", (position.y - parent.y).round()),
818 ));
819 op = "Moving";
820 }
821 if geometry.size.width != rect.size.width && rect.size.width.is_finite() {
822 p.push(common::PropertyChange::new("width", format!("{}px", rect.size.width.round())));
823 op = "Resizing";
824 }
825 if geometry.size.height != rect.size.height && rect.size.height.is_finite() {
826 p.push(common::PropertyChange::new(
827 "height",
828 format!("{}px", rect.size.height.round()),
829 ));
830 op = "Resizing";
831 }
832 (p, op)
833 };
834
835 if properties.is_empty() {
836 return None;
837 }
838
839 let url = Url::from_file_path(&path).ok()?;
840 let document_cache = document_cache()?;
841
842 let version = document_cache.document_version(&url);
843
844 properties::update_element_properties(
845 &document_cache,
846 common::VersionedPosition::new(common::VersionedUrl::new(url, version), offset),
847 properties,
848 )
849 .map(|edit| (edit, format!("{op} element")))
850}
851
852// triggered from the UI, running in UI thread
853fn can_move_selected_element(x: f32, y: f32, mouse_x: f32, mouse_y: f32) -> bool {
854 let position: Point2D = LogicalPoint::new(x, y);
855 let mouse_position: Point2D = LogicalPoint::new(mouse_x, mouse_y);
856 let Some(selected: ElementSelection) = selected_element() else {
857 return false;
858 };
859 let Some(selected_element_node: ElementRcNode) = selected.as_element_node() else {
860 return false;
861 };
862 let Some(document_cache: Rc) = document_cache() else {
863 return false;
864 };
865
866 drop_location::can_move_to(
867 &document_cache,
868 position,
869 mouse_position,
870 selected_element_node,
871 selected.instance_index,
872 )
873}
874
875// triggered from the UI, running in UI thread
876fn move_selected_element(x: f32, y: f32, mouse_x: f32, mouse_y: f32) {
877 let position = LogicalPoint::new(x, y);
878 let mouse_position = LogicalPoint::new(mouse_x, mouse_y);
879 let Some(selected) = selected_element() else {
880 return;
881 };
882 let Some(selected_element_node) = selected.as_element_node() else {
883 return;
884 };
885 let Some(document_cache) = document_cache() else {
886 return;
887 };
888
889 if let Some((edit, drop_data)) = drop_location::move_element_to(
890 &document_cache,
891 selected_element_node,
892 selected.instance_index,
893 position,
894 mouse_position,
895 ) {
896 element_selection::select_element_at_source_code_position(
897 drop_data.path,
898 drop_data.selection_offset,
899 None,
900 SelectionNotification::AfterUpdate,
901 );
902
903 send_workspace_edit("Move element".to_string(), edit, false);
904 } else {
905 element_selection::reselect_element();
906 }
907}
908
909#[derive(Debug, Clone, Eq, PartialEq)]
910enum CompilationResult {
911 ChangeCompiles,
912 ChangeFails,
913 NoChange,
914}
915
916fn test_workspace_edit(edit: &lsp_types::WorkspaceEdit) -> CompilationResult {
917 let Some(document_cache: Rc) = document_cache() else {
918 return CompilationResult::ChangeFails;
919 };
920 drop_location::workspace_edit_compiles(&document_cache, edit)
921}
922
923fn send_workspace_edit(label: String, edit: lsp_types::WorkspaceEdit, test_edit: bool) -> bool {
924 if test_edit {
925 let test_result: CompilationResult = test_workspace_edit(&edit);
926 match test_result {
927 CompilationResult::ChangeCompiles => {}
928 CompilationResult::ChangeFails => return false,
929 CompilationResult::NoChange => return true,
930 }
931 }
932
933 let workspace_edit_sent: bool = PREVIEW_STATE.with(|preview_state: &RefCell| {
934 let mut ps: RefMut<'_, PreviewState> = preview_state.borrow_mut();
935 let result: bool = ps.workspace_edit_sent;
936 ps.workspace_edit_sent = true;
937 result
938 });
939
940 if !workspace_edit_sent {
941 send_message_to_lsp(message:PreviewToLspMessage::SendWorkspaceEdit { label: Some(label), edit });
942 return true;
943 }
944 false
945}
946
947fn change_style() {
948 let cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
949 let ui_is_visible: bool = cache.ui_is_visible;
950 let Some(current: PreviewComponent) = cache.current_component() else {
951 return;
952 };
953
954 drop(cache);
955
956 if ui_is_visible {
957 load_preview(preview_component:current, behavior:LoadBehavior::Reload);
958 }
959}
960
961fn start_parsing() {
962 set_status_text("Updating Preview...");
963 PREVIEW_STATE.with(|preview_state: &RefCell| {
964 let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut();
965
966 if let Some(ui: &PreviewUi) = &preview_state.ui {
967 ui::set_diagnostics(ui, &[]);
968 }
969 });
970}
971
972fn extract_resources(
973 dependencies: &HashSet<Url>,
974 component_instance: &ComponentInstance,
975) -> HashSet<Url> {
976 let tl: Rc = component_instance.definition().type_loader();
977
978 let mut result: HashSet<Url> = Default::default();
979
980 for d: &Url in dependencies {
981 let Ok(path: PathBuf) = d.to_file_path() else {
982 continue;
983 };
984 let Some(doc: &Document) = tl.get_document(&path) else {
985 continue;
986 };
987
988 result.extend(
989 iter:doc.embedded_file_resources
990 .borrow()
991 .keys()
992 .filter_map(|fp: &SmolStr| Url::from_file_path(fp).ok()),
993 );
994 }
995
996 result
997}
998
999fn finish_parsing(preview_url: &Url, previewed_component: Option<String>, success: bool) {
1000 set_status_text("");
1001
1002 if !success {
1003 // No need to update everything...
1004 return;
1005 }
1006
1007 let (previewed_url, component, source_code) = {
1008 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1009 let pc = cache.current_component();
1010 (
1011 pc.as_ref().map(|pc| pc.url.clone()),
1012 pc.as_ref().and_then(|pc| pc.component.clone()),
1013 cache.source_code.clone(),
1014 )
1015 };
1016
1017 if let Some(document_cache) = document_cache() {
1018 let mut document_cache = document_cache.snapshot().unwrap();
1019
1020 for (url, cache_entry) in &source_code {
1021 let mut diag = diagnostics::BuildDiagnostics::default();
1022 if document_cache.get_document(url).is_none() {
1023 poll_once(document_cache.load_url(
1024 url,
1025 cache_entry.version,
1026 cache_entry.code.clone(),
1027 &mut diag,
1028 ));
1029 }
1030 }
1031
1032 let uses_widgets = document_cache.uses_widgets(preview_url);
1033
1034 let mut components = Vec::new();
1035 component_catalog::builtin_components(&document_cache, &mut components);
1036 component_catalog::all_exported_components(
1037 &document_cache,
1038 &mut |ci| !ci.is_global,
1039 &mut components,
1040 );
1041
1042 for url in document_cache.all_urls().filter(|u| u.scheme() != "builtin") {
1043 component_catalog::file_local_components(&document_cache, &url, &mut components);
1044 }
1045
1046 let index = if let Some(component) = component {
1047 components
1048 .iter()
1049 .position(|ci| {
1050 ci.name == component
1051 && ci.defined_at.as_ref().map(|da| da.url()) == previewed_url.as_ref()
1052 })
1053 .unwrap_or(usize::MAX)
1054 } else {
1055 usize::MAX
1056 };
1057
1058 apply_live_preview_data();
1059
1060 PREVIEW_STATE.with(|preview_state| {
1061 let mut preview_state = preview_state.borrow_mut();
1062 preview_state.known_components = components;
1063
1064 preview_state.document_cache.borrow_mut().replace(Some(Rc::new(document_cache)));
1065
1066 let preview_data = preview_state
1067 .component_instance()
1068 .map(|component_instance| {
1069 preview_data::query_preview_data_properties_and_callbacks(&component_instance)
1070 })
1071 .unwrap_or_default();
1072
1073 if let Some(ui) = &preview_state.ui {
1074 ui::ui_set_uses_widgets(ui, uses_widgets);
1075 ui::ui_set_known_components(ui, &preview_state.known_components, index);
1076 ui::ui_set_preview_data(ui, preview_data, previewed_component);
1077 }
1078 });
1079 }
1080
1081 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1082 if let Some(component_instance) = component_instance() {
1083 cache.resources = extract_resources(&cache.dependencies, &component_instance);
1084 } else {
1085 cache.resources.clear();
1086 }
1087}
1088
1089fn config_changed(config: PreviewConfig) {
1090 let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1091
1092 if cache.config != config {
1093 cache.config = config.clone();
1094
1095 let current: Option = cache.current_component();
1096 let ui_is_visible: bool = cache.ui_is_visible;
1097 let hide_ui: Option = cache.config.hide_ui;
1098
1099 drop(cache);
1100
1101 if ui_is_visible {
1102 if let Some(hide_ui: bool) = hide_ui {
1103 set_show_preview_ui(!hide_ui);
1104 }
1105 if let Some(current: PreviewComponent) = current {
1106 load_preview(preview_component:current, behavior:LoadBehavior::Reload);
1107 }
1108 }
1109 }
1110}
1111
1112/// If the file is in the cache, returns it.
1113///
1114/// If the file is not known, the return an empty string marked as "from disk". This is fine:
1115/// The LSP side will load the file and inform us about it soon.
1116///
1117/// In any way, register it as a dependency
1118fn get_url_from_cache(url: &Url) -> (SourceFileVersion, String) {
1119 let mut cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1120 cache.dependencies.insert(url.to_owned());
1121
1122 cache.source_code.get(url).map(|r: &SourceCodeCacheEntry| (r.version, r.code.clone())).unwrap_or_default().clone()
1123}
1124
1125fn get_path_from_cache(path: &Path) -> std::io::Result<(SourceFileVersion, String)> {
1126 let url: Url = Url::from_file_path(path).map_err(|()| {
1127 std::io::Error::new(kind:std::io::ErrorKind::NotFound, error:"Failed to convert path to URL")
1128 })?;
1129 Ok(get_url_from_cache(&url))
1130}
1131
1132#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1133pub enum LoadBehavior {
1134 /// We reload the preview, most likely because a file has changed
1135 Reload,
1136 /// Load the preview and make the window visible if it wasn't already.
1137 Load,
1138 /// We show the preview because the user asked for it. The UI should become visible and focused if it wasn't already
1139 BringWindowToFront,
1140}
1141
1142pub fn reload_preview() {
1143 let pc: Option = {
1144 let cache: MutexGuard<'_, ContentCache> = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1145 cache.current_previewed_component.clone()
1146 };
1147
1148 let Some(pc: PreviewComponent) = pc else {
1149 return;
1150 };
1151
1152 load_preview(preview_component:pc, behavior:LoadBehavior::Load);
1153}
1154
1155async fn reload_timer_function() {
1156 let (selected, notify_editor) = PREVIEW_STATE.with(|preview_state| {
1157 let mut preview_state = preview_state.borrow_mut();
1158 let notify_editor = preview_state.notify_editor_about_selection_after_update;
1159 preview_state.notify_editor_about_selection_after_update = false;
1160 (preview_state.selected.take(), notify_editor)
1161 });
1162
1163 loop {
1164 let (preview_component, config, behavior) = {
1165 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1166 let Some(behavior) = cache.current_load_behavior.take() else { return };
1167
1168 let Some(preview_component) = cache.current_component() else {
1169 return;
1170 };
1171 cache.clear_style_of_component();
1172
1173 assert_eq!(cache.loading_state, PreviewFutureState::PreLoading);
1174
1175 if !cache.ui_is_visible && behavior == LoadBehavior::Reload {
1176 cache.loading_state = PreviewFutureState::Pending;
1177 return;
1178 }
1179 cache.loading_state = PreviewFutureState::Loading;
1180 cache.dependencies.clear();
1181 (preview_component, cache.config.clone(), behavior)
1182 };
1183 let style = if preview_component.style.is_empty() {
1184 get_current_style()
1185 } else {
1186 set_current_style(preview_component.style.clone());
1187 preview_component.style.clone()
1188 };
1189
1190 match reload_preview_impl(preview_component, behavior, style, config).await {
1191 Ok(()) => {}
1192 Err(e) => {
1193 CONTENT_CACHE.get_or_init(Default::default).lock().unwrap().loading_state =
1194 PreviewFutureState::Pending;
1195 send_platform_error_notification(&e.to_string());
1196 return;
1197 }
1198 }
1199
1200 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1201 match cache.loading_state {
1202 PreviewFutureState::Loading => {
1203 cache.loading_state = PreviewFutureState::Pending;
1204 break;
1205 }
1206 PreviewFutureState::NeedsReload => {
1207 cache.loading_state = PreviewFutureState::PreLoading;
1208 continue;
1209 }
1210 PreviewFutureState::Pending | PreviewFutureState::PreLoading => unreachable!(),
1211 };
1212 }
1213
1214 if let Some(se) = selected {
1215 element_selection::select_element_at_source_code_position(
1216 se.path.clone(),
1217 se.offset,
1218 None,
1219 SelectionNotification::Never,
1220 );
1221
1222 if notify_editor {
1223 if let Some(component_instance) = component_instance() {
1224 if let Some((element, debug_index)) = component_instance
1225 .element_node_at_source_code_position(&se.path, se.offset.into())
1226 .first()
1227 {
1228 let Some(element_node) = ElementRcNode::new(element.clone(), *debug_index)
1229 else {
1230 return;
1231 };
1232 let (path, pos) = element_node.with_element_node(|node| {
1233 let sf = &node.source_file;
1234 (sf.path().to_owned(), util::text_size_to_lsp_position(sf, se.offset))
1235 });
1236 ask_editor_to_show_document(
1237 &path.to_string_lossy(),
1238 lsp_types::Range::new(pos, pos),
1239 false,
1240 );
1241 }
1242 }
1243 }
1244 }
1245}
1246
1247pub fn load_preview(preview_component: PreviewComponent, behavior: LoadBehavior) {
1248 {
1249 let mut cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1250
1251 match behavior {
1252 LoadBehavior::Reload => {
1253 if !cache.ui_is_visible {
1254 return;
1255 }
1256 }
1257 LoadBehavior::Load | LoadBehavior::BringWindowToFront => {
1258 cache.set_current_component(preview_component)
1259 }
1260 }
1261
1262 cache.current_load_behavior = Some(behavior);
1263
1264 match cache.loading_state {
1265 PreviewFutureState::Pending => {}
1266 PreviewFutureState::Loading => {
1267 cache.loading_state = PreviewFutureState::NeedsReload;
1268 return;
1269 }
1270 PreviewFutureState::NeedsReload | PreviewFutureState::PreLoading => {
1271 return;
1272 }
1273 }
1274 cache.loading_state = PreviewFutureState::PreLoading;
1275 };
1276
1277 if let Err(e) = run_in_ui_thread(move || async move {
1278 PREVIEW_STATE.with(|preview_state| {
1279 preview_state
1280 .borrow_mut()
1281 .preview_loading_delay_timer
1282 .get_or_insert_with(|| {
1283 let timer = slint::Timer::default();
1284 timer.start(
1285 slint::TimerMode::SingleShot,
1286 core::time::Duration::from_millis(50),
1287 || {
1288 let _ = slint::spawn_local(reload_timer_function());
1289 },
1290 );
1291 timer
1292 })
1293 .restart();
1294 });
1295 }) {
1296 send_platform_error_notification(&e);
1297 }
1298}
1299
1300async fn parse_source(
1301 include_paths: Vec<PathBuf>,
1302 library_paths: HashMap<String, PathBuf>,
1303 path: PathBuf,
1304 version: common::SourceFileVersion,
1305 source_code: String,
1306 style: String,
1307 component: Option<String>,
1308 file_loader_fallback: impl Fn(
1309 String,
1310 ) -> core::pin::Pin<
1311 Box<
1312 dyn core::future::Future<
1313 Output = Option<std::io::Result<(common::SourceFileVersion, String)>>,
1314 >,
1315 >,
1316 > + 'static,
1317) -> (
1318 Vec<diagnostics::Diagnostic>,
1319 Option<ComponentDefinition>,
1320 common::document_cache::OpenImportFallback,
1321 Rc<RefCell<common::document_cache::SourceFileVersionMap>>,
1322) {
1323 let mut builder = slint_interpreter::Compiler::default();
1324
1325 let cc = builder.compiler_configuration(i_slint_core::InternalToken);
1326 cc.components_to_generate = if let Some(name) = component {
1327 i_slint_compiler::ComponentSelection::Named(name)
1328 } else {
1329 i_slint_compiler::ComponentSelection::LastExported
1330 };
1331 #[cfg(target_arch = "wasm32")]
1332 {
1333 cc.resource_url_mapper = resource_url_mapper();
1334 }
1335 cc.embed_resources = EmbedResourcesKind::ListAllResources;
1336 cc.no_native_menu = true;
1337
1338 if !style.is_empty() {
1339 cc.style = Some(style);
1340 }
1341 cc.include_paths = include_paths;
1342 cc.library_paths = library_paths;
1343
1344 let (open_file_fallback, source_file_versions) =
1345 common::document_cache::document_cache_parts_setup(
1346 cc,
1347 Some(Rc::new(file_loader_fallback)),
1348 common::document_cache::SourceFileVersionMap::from([(path.clone(), version)]),
1349 );
1350
1351 let result = builder.build_from_source(source_code, path).await;
1352
1353 let compiled = result.components().next();
1354 (result.diagnostics().collect(), compiled, open_file_fallback, source_file_versions)
1355}
1356
1357// Must be inside the thread running the slint event loop
1358async fn reload_preview_impl(
1359 component: PreviewComponent,
1360 behavior: LoadBehavior,
1361 style: String,
1362 config: PreviewConfig,
1363) -> Result<(), PlatformError> {
1364 start_parsing();
1365
1366 if let Some(component_instance) = component_instance() {
1367 let live_preview_data =
1368 preview_data::query_preview_data_properties_and_callbacks(&component_instance);
1369 set_current_live_data(live_preview_data);
1370 }
1371
1372 let path = component.url.to_file_path().unwrap_or(PathBuf::from(&component.url.to_string()));
1373 let (version, source) = get_url_from_cache(&component.url);
1374
1375 let (diagnostics, compiled, open_import_fallback, source_file_versions) = parse_source(
1376 config.include_paths,
1377 config.library_paths,
1378 path,
1379 version,
1380 source,
1381 style,
1382 component.component.clone(),
1383 move |path| {
1384 let path = path.to_owned();
1385 Box::pin(async move {
1386 let path = PathBuf::from(&path);
1387 // Always return Some to stop the compiler from trying to load itself...
1388 // All loading is done by the LSP for us!
1389 Some(get_path_from_cache(&path))
1390 })
1391 },
1392 )
1393 .await;
1394
1395 let success = compiled.is_some();
1396
1397 let loaded_component_name = compiled.as_ref().map(|c| c.name().to_string());
1398
1399 {
1400 PREVIEW_STATE.with(|preview_state| {
1401 let preview_state = preview_state.borrow_mut();
1402
1403 if let Some(ui) = &preview_state.ui {
1404 ui::set_diagnostics(ui, &diagnostics);
1405 }
1406 });
1407 let diags = convert_diagnostics(&diagnostics, &source_file_versions.borrow());
1408 notify_diagnostics(diags);
1409 }
1410
1411 update_preview_area(compiled, behavior, open_import_fallback, source_file_versions)?;
1412
1413 finish_parsing(&component.url, loaded_component_name, success);
1414 Ok(())
1415}
1416
1417/// Sends a notification back to the editor when the preview fails to load because of a slint::PlatformError.
1418fn send_platform_error_notification(platform_error_str: &str) {
1419 let message: String = format!("Error displaying the Slint preview window: {platform_error_str}");
1420 // Also output the message in the console in case the user missed the notification in the editor
1421 eprintln!("{message}");
1422 send_message_to_lsp(message:PreviewToLspMessage::SendShowMessage {
1423 message: lsp_types::ShowMessageParams { typ: lsp_types::MessageType::ERROR, message },
1424 })
1425}
1426
1427/// This sets up the preview area to show the ComponentInstance
1428///
1429/// This must be run in the UI thread.
1430fn set_preview_factory(
1431 ui: &ui::PreviewUi,
1432 compiled: ComponentDefinition,
1433 callback: Box<dyn Fn(ComponentInstance)>,
1434 behavior: LoadBehavior,
1435) {
1436 // Ensure that any popups are closed as they are related to the old factory
1437 i_slint_core::window::WindowInner::from_pub(ui.window()).close_all_popups();
1438
1439 let factory: ComponentFactory = slint::ComponentFactory::new(factory:move |ctx: FactoryContext| {
1440 let instance: ComponentInstance = compiled.create_embedded(ctx).unwrap();
1441
1442 callback(instance.clone_strong());
1443
1444 Some(instance)
1445 });
1446
1447 let api: Api<'_> = ui.global::<ui::Api>();
1448 api.set_preview_area(factory);
1449 api.set_resize_to_preferred_size(behavior != LoadBehavior::Reload);
1450}
1451
1452/// Highlight the element pointed at the offset in the path.
1453/// When path is None, remove the highlight.
1454pub fn highlight(url: Option<Url>, offset: TextSize) {
1455 let Some(path) = url.as_ref().and_then(|u| Url::to_file_path(u).ok()) else {
1456 return;
1457 };
1458
1459 let selected = selected_element();
1460
1461 if let Some(selected) = &selected {
1462 if selected.path == path && selected.offset == offset {
1463 return;
1464 }
1465 }
1466
1467 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1468 if url.as_ref().is_none_or(|url| cache.dependencies.contains(url)) {
1469 let _ = run_in_ui_thread(move || async move {
1470 if Some((path.clone(), offset)) == selected.map(|s| (s.path, s.offset)) {
1471 // Already selected!
1472 return;
1473 }
1474 element_selection::select_element_at_source_code_position(
1475 path,
1476 offset,
1477 None,
1478 SelectionNotification::Never,
1479 );
1480 });
1481 }
1482}
1483
1484pub fn get_component_info(component_type: &str) -> Option<ComponentInformation> {
1485 PREVIEW_STATE.with(|preview_state: &RefCell| {
1486 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
1487 let index: usize = preview_stateResult
1488 .known_components
1489 .binary_search_by(|ci: &ComponentInformation| ci.name.as_str().cmp(component_type))
1490 .ok()?;
1491 preview_state.known_components.get(index).cloned()
1492 })
1493}
1494
1495fn convert_diagnostics(
1496 diagnostics: &[slint_interpreter::Diagnostic],
1497 file_versions: &common::document_cache::SourceFileVersionMap,
1498) -> HashMap<Url, (SourceFileVersion, Vec<lsp_types::Diagnostic>)> {
1499 let mut result: HashMap<Url, (SourceFileVersion, Vec<lsp_types::Diagnostic>)> =
1500 Default::default();
1501
1502 fn path_to_url(path: &Path) -> Url {
1503 Url::from_file_path(path).ok().unwrap_or_else(|| Url::parse("file:/unknown").unwrap())
1504 }
1505
1506 // Pre-fill version info and an empty diagnostics to reset the state for the url
1507 for (path, version) in file_versions.iter() {
1508 result.insert(path_to_url(path), (*version, Vec::new()));
1509 }
1510
1511 {
1512 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1513
1514 // Fill in actual diagnostics now
1515 for d in diagnostics {
1516 if d.source_file().is_none_or(|f| !i_slint_compiler::pathutils::is_absolute(f)) {
1517 continue;
1518 }
1519 let uri = path_to_url(d.source_file().unwrap());
1520 let new_version = cache.source_code.get(&uri).and_then(|e| e.version);
1521 if let Some(data) = result.get_mut(&uri) {
1522 if data.0.is_some() && new_version.is_some() && data.0 != new_version {
1523 continue;
1524 }
1525 data.1.push(crate::util::to_lsp_diag(d));
1526 }
1527 }
1528 }
1529
1530 result
1531}
1532
1533fn reset_selections(ui: &ui::PreviewUi) {
1534 let model: Rc> = Rc::new(slint::VecModel::from(Vec::new()));
1535 let api: Api<'_> = ui.global::<ui::Api>();
1536 api.set_selections(slint::ModelRc::from(model));
1537}
1538
1539fn set_selections(
1540 ui: Option<&ui::PreviewUi>,
1541 main_index: usize,
1542 layout_kind: ui::LayoutKind,
1543 is_interactive: bool,
1544 is_moveable: bool,
1545 is_resizable: bool,
1546 positions: &[i_slint_core::lengths::LogicalRect],
1547) {
1548 let Some(ui) = ui else {
1549 return;
1550 };
1551
1552 let values = positions
1553 .iter()
1554 .enumerate()
1555 .map(|(i, g)| ui::Selection {
1556 geometry: ui::SelectionRectangle {
1557 width: g.size.width,
1558 height: g.size.height,
1559 x: g.origin.x,
1560 y: g.origin.y,
1561 },
1562 layout_data: layout_kind,
1563 is_primary: i == main_index,
1564 is_interactive,
1565 is_moveable,
1566 is_resizable,
1567 })
1568 .collect::<Vec<_>>();
1569 let model = Rc::new(slint::VecModel::from(values));
1570 let api = ui.global::<ui::Api>();
1571 api.set_selections(slint::ModelRc::from(model));
1572}
1573
1574fn set_drop_mark(mark: &Option<drop_location::DropMark>) {
1575 PREVIEW_STATE.with(move |preview_state: &RefCell| {
1576 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
1577
1578 let Some(ui: &PreviewUi) = &preview_state.ui else {
1579 return;
1580 };
1581
1582 let api: Api<'_> = ui.global::<ui::Api>();
1583 if let Some(m: &DropMark) = mark {
1584 api.set_drop_mark(ui::DropMark {
1585 x1: m.start.x,
1586 y1: m.start.y,
1587 x2: m.end.x,
1588 y2: m.end.y,
1589 });
1590 } else {
1591 api.set_drop_mark(ui::DropMark { x1: -1.0, y1: -1.0, x2: -1.0, y2: -1.0 });
1592 }
1593 })
1594}
1595
1596#[derive(Debug, PartialEq)]
1597pub enum SelectionNotification {
1598 Never,
1599 Now,
1600 AfterUpdate,
1601}
1602
1603fn set_selected_element(
1604 selection: Option<element_selection::ElementSelection>,
1605 positions: &[i_slint_core::lengths::LogicalRect],
1606 editor_notification: SelectionNotification,
1607) {
1608 let (layout_kind, parent_layout_kind, type_name) = {
1609 let selection_node = selection.as_ref().and_then(|s| s.as_element_node());
1610 let (layout_kind, parent_layout_kind) = selection_node
1611 .as_ref()
1612 .map(|en| (en.layout_kind(), element_selection::parent_layout_kind(en)))
1613 .unwrap_or((ui::LayoutKind::None, ui::LayoutKind::None));
1614 let type_name = selection_node
1615 .and_then(|n| {
1616 // This is an approximation, I hope it is good enough. The ElementRc was lowered, so there is nothing to see there anymore
1617 n.with_element_node(|n| {
1618 n.QualifiedName().map(|qn| qn.text().to_string().trim().to_string())
1619 })
1620 })
1621 .unwrap_or_default();
1622
1623 (layout_kind, parent_layout_kind, type_name)
1624 };
1625
1626 set_drop_mark(&None);
1627
1628 let element_node = selection.as_ref().and_then(|s| s.as_element_node());
1629 let notify_editor_about_selection_after_update =
1630 editor_notification == SelectionNotification::AfterUpdate;
1631 PREVIEW_STATE.with(move |preview_state| {
1632 let mut preview_state = preview_state.borrow_mut();
1633
1634 let is_in_layout = parent_layout_kind != ui::LayoutKind::None;
1635 let is_layout = layout_kind != ui::LayoutKind::None;
1636 let is_interactive = {
1637 let index = preview_state
1638 .known_components
1639 .iter()
1640 .position(|ci| ci.name.as_str() == type_name.as_str());
1641
1642 index
1643 .and_then(|idx| preview_state.known_components.get(idx))
1644 .map(|kc| kc.is_interactive)
1645 .unwrap_or_default()
1646 };
1647
1648 set_selections(
1649 preview_state.ui.as_ref(),
1650 selection.as_ref().map(|s| s.instance_index).unwrap_or_default(),
1651 layout_kind,
1652 is_interactive,
1653 true,
1654 !is_in_layout && !is_layout,
1655 positions,
1656 );
1657
1658 if let Some(ui) = &preview_state.ui {
1659 if let Some(document_cache) = document_cache_from(&preview_state) {
1660 if let Some((uri, version, selection)) = selection
1661 .clone()
1662 .or_else(|| {
1663 let current = {
1664 let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
1665 cache.current_component()
1666 }?;
1667
1668 let document = document_cache.get_document(&current.url)?;
1669 let document = document.node.as_ref()?;
1670
1671 let identifier = if let Some(name) = &current.component {
1672 find_component_identifiers(document, name).last().cloned()
1673 } else {
1674 find_last_component_identifier(document)
1675 }?;
1676
1677 let path = identifier.source_file.path().to_path_buf();
1678 let offset = identifier.text_range().start();
1679
1680 Some(ElementSelection { path, offset, instance_index: 0 })
1681 })
1682 .as_ref()
1683 .and_then(|selection| {
1684 let url = Url::from_file_path(&selection.path).ok()?;
1685 let version = document_cache.document_version(&url);
1686 Some((
1687 url.clone(),
1688 version,
1689 document_cache.element_at_offset(&url, selection.offset)?,
1690 ))
1691 })
1692 {
1693 let in_layout = match parent_layout_kind {
1694 ui::LayoutKind::None => properties::LayoutKind::None,
1695 ui::LayoutKind::Horizontal => properties::LayoutKind::HorizontalBox,
1696 ui::LayoutKind::Vertical => properties::LayoutKind::VerticalBox,
1697 ui::LayoutKind::Grid => properties::LayoutKind::GridLayout,
1698 };
1699 preview_state.property_range_declarations = Some(ui::ui_set_properties(
1700 ui,
1701 &document_cache,
1702 properties::query_properties(&uri, version, &selection, in_layout).ok(),
1703 ));
1704 }
1705 }
1706 }
1707
1708 preview_state.selected = selection;
1709 preview_state.notify_editor_about_selection_after_update =
1710 notify_editor_about_selection_after_update;
1711 });
1712
1713 if editor_notification == SelectionNotification::Now {
1714 if let Some(element_node) = element_node {
1715 let (path, pos) = element_node.with_element_node(|node| {
1716 let sf = &node.source_file;
1717 (
1718 sf.path().to_owned(),
1719 util::text_size_to_lsp_position(sf, node.text_range().start()),
1720 )
1721 });
1722 ask_editor_to_show_document(
1723 &path.to_string_lossy(),
1724 lsp_types::Range::new(pos, pos),
1725 false,
1726 );
1727 }
1728 }
1729}
1730
1731fn selected_element() -> Option<ElementSelection> {
1732 PREVIEW_STATE.with(move |preview_state: &RefCell| {
1733 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
1734 preview_state.selected.clone()
1735 })
1736}
1737
1738fn component_instance() -> Option<ComponentInstance> {
1739 PREVIEW_STATE.with(move |preview_state: &RefCell| preview_state.borrow().component_instance())
1740}
1741
1742/// This is a *read-only* snapshot of the raw type loader, use this when you
1743/// need to know the exact state the compiled resources were in.
1744fn document_cache() -> Option<Rc<common::DocumentCache>> {
1745 PREVIEW_STATE.with(move |preview_state: &RefCell| document_cache_from(&preview_state.borrow()))
1746}
1747
1748/// This is a *read-only* snapshot of the raw type loader, use this when you
1749/// need to know the exact state the compiled resources were in.
1750fn document_cache_from(preview_state: &PreviewState) -> Option<Rc<common::DocumentCache>> {
1751 preview_state.document_cache.borrow().as_ref().map(|dc: &Rc| dc.clone())
1752}
1753
1754fn set_show_preview_ui(show_preview_ui: bool) {
1755 let _ = run_in_ui_thread(create_future:move || async move {
1756 PREVIEW_STATE.with(|preview_state: &RefCell| {
1757 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
1758 if let Some(ui: &PreviewUi) = &preview_state.ui {
1759 let api: Api<'_> = ui.global::<ui::Api>();
1760 api.set_show_preview_ui(show_preview_ui)
1761 }
1762 })
1763 });
1764}
1765
1766fn set_current_style(style: String) {
1767 PREVIEW_STATE.with(move |preview_state: &RefCell| {
1768 let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut();
1769 if let Some(ui: &PreviewUi) = &preview_state.ui {
1770 let api: Api<'_> = ui.global::<ui::Api>();
1771 api.set_current_style(style.into())
1772 }
1773 });
1774}
1775
1776fn get_current_style() -> String {
1777 PREVIEW_STATE.with(|preview_state: &RefCell| -> String {
1778 let preview_state: Ref<'_, PreviewState> = preview_state.borrow();
1779 if let Some(ui: &PreviewUi) = &preview_state.ui {
1780 let api: Api<'_> = ui.global::<ui::Api>();
1781 api.get_current_style().as_str().to_string()
1782 } else {
1783 String::new()
1784 }
1785 })
1786}
1787
1788fn set_status_text(text: &str) {
1789 let text: String = text.to_string();
1790
1791 i_slint_coreResult<(), EventLoopError>::api::invoke_from_event_loop(func:move || {
1792 PREVIEW_STATE.with(|preview_state: &RefCell| {
1793 let preview_state: RefMut<'_, PreviewState> = preview_state.borrow_mut();
1794 if let Some(ui: &PreviewUi) = &preview_state.ui {
1795 let api: Api<'_> = ui.global::<ui::Api>();
1796 api.set_status_text(text.into());
1797 }
1798 });
1799 })
1800 .unwrap();
1801}
1802
1803/// This ensure that the preview window is visible and runs `set_preview_factory`
1804fn update_preview_area(
1805 compiled: Option<ComponentDefinition>,
1806 behavior: LoadBehavior,
1807 open_import_fallback: common::document_cache::OpenImportFallback,
1808 source_file_versions: Rc<RefCell<common::document_cache::SourceFileVersionMap>>,
1809) -> Result<(), PlatformError> {
1810 PREVIEW_STATE.with(move |preview_state| {
1811 let mut preview_state = preview_state.borrow_mut();
1812 preview_state.workspace_edit_sent = false;
1813
1814 #[cfg(not(target_arch = "wasm32"))]
1815 native::open_ui_impl(&mut preview_state)?;
1816
1817 let ui = preview_state.ui.as_ref().unwrap();
1818 let shared_handle = preview_state.handle.clone();
1819 let shared_document_cache = preview_state.document_cache.clone();
1820
1821 if let Some(compiled) = compiled {
1822 let api = ui.global::<ui::Api>();
1823 api.set_focus_previewed_element(behavior == LoadBehavior::BringWindowToFront);
1824
1825 set_preview_factory(
1826 ui,
1827 compiled,
1828 Box::new(move |instance| {
1829 if let Some(rtl) = instance.definition().raw_type_loader() {
1830 shared_document_cache.replace(Some(Rc::new(
1831 common::DocumentCache::new_from_raw_parts(
1832 rtl,
1833 open_import_fallback.clone(),
1834 source_file_versions.clone(),
1835 ),
1836 )));
1837 }
1838
1839 shared_handle.replace(Some(instance));
1840 }),
1841 behavior,
1842 );
1843 reset_selections(ui);
1844 }
1845
1846 ui.show().and_then(|_| {
1847 if matches!(behavior, LoadBehavior::BringWindowToFront) {
1848 let window_inner = i_slint_core::window::WindowInner::from_pub(ui.window());
1849 if let Some(window_adapter_internal) =
1850 window_inner.window_adapter().internal(i_slint_core::InternalToken)
1851 {
1852 window_adapter_internal.bring_to_front()?;
1853 }
1854 }
1855
1856 Ok(())
1857 })
1858 })?;
1859
1860 element_selection::reselect_element();
1861 Ok(())
1862}
1863
1864pub fn lsp_to_preview_message(message: crate::common::LspToPreviewMessage) {
1865 use crate::common::LspToPreviewMessage as M;
1866 match message {
1867 M::InvalidateContents { url: Url } => invalidate_contents(&url),
1868 M::ForgetFile { url: Url } => delete_document(&url),
1869 M::SetContents { url: VersionedUrl, contents: String } => {
1870 set_contents(&url, content:contents);
1871 }
1872 M::SetConfiguration { config: PreviewConfig } => {
1873 config_changed(config);
1874 }
1875 M::ShowPreview(pc: PreviewComponent) => {
1876 load_preview(preview_component:pc, behavior:LoadBehavior::BringWindowToFront);
1877 }
1878 M::HighlightFromEditor { url: Option, offset: u32 } => {
1879 highlight(url, offset.into());
1880 }
1881 }
1882}
1883
1884#[cfg(test)]
1885pub mod test {
1886 use std::{collections::HashMap, path::PathBuf, rc::Rc};
1887
1888 use slint_interpreter::ComponentInstance;
1889
1890 use crate::common::test::main_test_file_name;
1891
1892 #[track_caller]
1893 pub fn interpret_test_with_sources(
1894 style: &str,
1895 code: HashMap<PathBuf, String>,
1896 ) -> ComponentInstance {
1897 i_slint_backend_testing::init_no_event_loop();
1898 reinterpret_test_with_sources(style, code)
1899 }
1900
1901 #[track_caller]
1902 pub fn reinterpret_test_with_sources(
1903 style: &str,
1904 code: HashMap<PathBuf, String>,
1905 ) -> ComponentInstance {
1906 let code = Rc::new(code);
1907
1908 let path = main_test_file_name();
1909 let source_code = code.get(&path).unwrap().clone();
1910 let (diagnostics, component_definition, _, _) = spin_on::spin_on(super::parse_source(
1911 vec![],
1912 std::collections::HashMap::new(),
1913 path,
1914 Some(24),
1915 source_code.to_string(),
1916 style.to_string(),
1917 None,
1918 move |path| {
1919 let code = code.clone();
1920 let path = PathBuf::from(&path);
1921
1922 Box::pin(async move {
1923 let Some(source) = code.get(&path) else {
1924 return Some(Result::Err(std::io::Error::new(
1925 std::io::ErrorKind::NotFound,
1926 "path not found",
1927 )));
1928 };
1929 Some(Ok((Some(24), source.clone())))
1930 })
1931 },
1932 ));
1933
1934 assert!(diagnostics.is_empty());
1935
1936 component_definition.unwrap().create().unwrap()
1937 }
1938
1939 #[track_caller]
1940 pub fn interpret_test(style: &str, source_code: &str) -> ComponentInstance {
1941 let code = HashMap::from([(main_test_file_name(), source_code.to_string())]);
1942 interpret_test_with_sources(style, code)
1943 }
1944}
1945