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 std::{path::PathBuf, rc::Rc};
5
6use i_slint_compiler::diagnostics::SourceFile;
7use i_slint_compiler::object_tree::{Component, ElementRc};
8use i_slint_core::lengths::{LogicalLength, LogicalPoint};
9use rowan::TextRange;
10use slint_interpreter::{highlight::ComponentPositions, ComponentInstance};
11
12use crate::common::ElementRcNode;
13
14#[derive(Clone, Debug)]
15pub struct ElementSelection {
16 pub path: PathBuf,
17 pub offset: u32,
18 pub instance_index: usize,
19 pub is_layout: bool,
20}
21
22impl ElementSelection {
23 pub fn as_element(&self) -> Option<ElementRc> {
24 let component_instance: ComponentInstance = super::component_instance()?;
25
26 let elements: Vec>> = component_instance.element_at_source_code_position(&self.path, self.offset);
27 elements.get(self.instance_index).or_else(|| elements.first()).cloned()
28 }
29
30 pub fn as_element_node(&self) -> Option<ElementRcNode> {
31 let element: Rc> = self.as_element()?;
32
33 let debug_index: Option = {
34 let e: Ref<'_, Element> = element.borrow();
35 e.debug.iter().position(|(n: &Element, _)| {
36 n.source_file.path() == self.path
37 && u32::from(n.text_range().start()) == self.offset
38 })
39 };
40
41 debug_index.map(|i: usize| ElementRcNode { element, debug_index: i })
42 }
43}
44
45// Look at an element and if it is a sub component, jump to its root_element()
46fn self_or_embedded_component_root(element: &ElementRc) -> ElementRc {
47 let elem: Ref<'_, Element> = element.borrow();
48 if elem.repeated.is_some() {
49 if let i_slint_compiler::langtype::ElementType::Component(base: &Rc) = &elem.base_type {
50 return base.root_element.clone();
51 }
52 }
53
54 element.clone()
55}
56
57fn lsp_element_node_position(element: &ElementRcNode) -> Option<(String, lsp_types::Range)> {
58 let location = element.with_element_node(|n: &Element| {
59 n.parent()
60 .filter(|p| p.kind() == i_slint_compiler::parser::SyntaxKind::SubElement)
61 .map_or_else(
62 || Some(n.source_file.text_size_to_file_line_column(n.text_range().start())),
63 |p| Some(p.source_file.text_size_to_file_line_column(p.text_range().start())),
64 )
65 });
66 location.map(|(f, sl: u32, sc: u32, el: u32, ec: u32)| {
67 use lsp_types::{Position, Range};
68 let start: Position = Position::new((sl as u32).saturating_sub(1), (sc as u32).saturating_sub(1));
69 let end: Position = Position::new((el as u32).saturating_sub(1), (ec as u32).saturating_sub(1));
70
71 (f, Range::new(start, end))
72 })
73}
74
75fn element_covers_point(
76 x: f32,
77 y: f32,
78 component_instance: &ComponentInstance,
79 selected_element: &ElementRc,
80) -> bool {
81 let click_position: Point2D = LogicalPoint::from_lengths(x:LogicalLength::new(x), y:LogicalLength::new(y));
82
83 component_instance.element_position(selected_element).iter().any(|p: &Rect| p.contains(click_position))
84}
85
86pub fn unselect_element() {
87 super::set_selected_element(selection:None, positions:ComponentPositions::default(), notify_editor_about_selection_after_update:false);
88}
89
90pub fn select_element_at_source_code_position(
91 path: PathBuf,
92 offset: u32,
93 is_layout: bool,
94 position: Option<LogicalPoint>,
95 notify_editor_about_selection_after_update: bool,
96) {
97 let Some(component_instance: ComponentInstance) = super::component_instance() else {
98 return;
99 };
100 select_element_at_source_code_position_impl(
101 &component_instance,
102 path,
103 offset,
104 is_layout,
105 position,
106 notify_editor_about_selection_after_update,
107 )
108}
109
110fn select_element_at_source_code_position_impl(
111 component_instance: &ComponentInstance,
112 path: PathBuf,
113 offset: u32,
114 is_layout: bool,
115 position: Option<LogicalPoint>,
116 notify_editor_about_selection_after_update: bool,
117) {
118 let positions: ComponentPositions = component_instance.component_positions(&path, offset);
119
120 let instance_index: usize = positionOption
121 .and_then(|p: Point2D| {
122 positions.geometries.iter().enumerate().find_map(|(i: usize, g: &Rect)| g.contains(p).then_some(i))
123 })
124 .unwrap_or_default();
125
126 super::set_selected_element(
127 selection:Some(ElementSelection { path, offset, instance_index, is_layout }),
128 positions,
129 notify_editor_about_selection_after_update,
130 );
131}
132
133fn select_element_node(
134 component_instance: &ComponentInstance,
135 selected_element: &ElementRcNode,
136 position: Option<LogicalPoint>,
137) {
138 let (path: PathBuf, offset: u32) = selected_element.path_and_offset();
139
140 select_element_at_source_code_position_impl(
141 component_instance,
142 path,
143 offset,
144 selected_element.is_layout(),
145 position,
146 notify_editor_about_selection_after_update:false, // We update directly;-)
147 );
148
149 if let Some(document_position: (String, Range)) = lsp_element_node_position(selected_element) {
150 super::ask_editor_to_show_document(&document_position.0, selection:document_position.1);
151 }
152}
153
154fn element_node_source_range(
155 element: &ElementRc,
156 debug_index: usize,
157) -> Option<(SourceFile, TextRange)> {
158 let node: Element = element.borrow().debug.get(debug_index)?.0.clone();
159 let source_file: Rc = node.source_file.clone();
160 let range: TextRange = node.text_range();
161 Some((source_file, range))
162}
163
164// Return the real root element, skipping any WindowElement that got added
165pub fn root_element(component_instance: &ComponentInstance) -> ElementRc {
166 let root_element: Rc> = component_instance.definition().root_component().root_element.clone();
167 if !root_element.borrow().debug.is_empty() {
168 return root_element;
169 }
170 let child: Option>> = root_element.borrow().children.first().cloned();
171 child.unwrap_or(default:root_element)
172}
173
174#[derive(Clone)]
175pub struct SelectionCandidate {
176 pub component_stack: Vec<Rc<Component>>,
177 pub element: ElementRc,
178 pub debug_index: usize,
179 pub text_range: Option<(SourceFile, TextRange)>,
180}
181
182impl SelectionCandidate {
183 pub fn is_selected_element_node(&self, selection: &ElementSelection) -> bool {
184 let Some((sf: &Rc, r: &TextRange)) = self.text_range.as_ref() else {
185 return false;
186 };
187 sf.path() == selection.path && u32::from(r.start()) == selection.offset
188 }
189
190 pub fn as_element_node(&self) -> Option<ElementRcNode> {
191 let (sf: &Rc, range: &TextRange) = self.text_range.as_ref()?;
192 ElementRcNode::find_in(self.element.clone(), sf.path(), offset:u32::from(range.start()))
193 }
194}
195
196impl std::fmt::Debug for SelectionCandidate {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 let tmp: Vec = self.component_stack.iter().map(|c: &Rc| c.id.clone()).collect::<Vec<_>>();
199 let component: String = format!("{:?}", tmp);
200 write!(f, "{}({}) in {component}", self.element.borrow().id, self.debug_index)
201 }
202}
203
204// Traverse the element tree in reverse render order and collect information on
205// all elements that "render" at the given x and y coordinates
206fn collect_all_element_nodes_covering_impl(
207 x: f32,
208 y: f32,
209 component_instance: &ComponentInstance,
210 current_element: &ElementRc,
211 component_stack: &Vec<Rc<Component>>,
212 result: &mut Vec<SelectionCandidate>,
213) {
214 let ce = self_or_embedded_component_root(current_element);
215 let Some(component) = ce.borrow().enclosing_component.upgrade() else {
216 return;
217 };
218 let component_root_element = component.root_element.clone();
219
220 let mut tmp;
221 let children_component_stack = {
222 if Rc::ptr_eq(&component_root_element, &ce) {
223 tmp = component_stack.clone();
224 tmp.push(component.clone());
225 &tmp
226 } else {
227 component_stack
228 }
229 };
230
231 for c in ce.borrow().children.iter().rev() {
232 collect_all_element_nodes_covering_impl(
233 x,
234 y,
235 component_instance,
236 c,
237 children_component_stack,
238 result,
239 );
240 }
241
242 if element_covers_point(x, y, component_instance, &ce) {
243 for (i, _) in ce.borrow().debug.iter().enumerate().rev() {
244 // All nodes have the same geometry
245 let text_range = element_node_source_range(&ce, i);
246 result.push(SelectionCandidate {
247 element: ce.clone(),
248 debug_index: i,
249 component_stack: component_stack.clone(),
250 text_range,
251 });
252 }
253 }
254}
255
256pub fn collect_all_element_nodes_covering(
257 x: f32,
258 y: f32,
259 component_instance: &ComponentInstance,
260) -> Vec<SelectionCandidate> {
261 let root_element: Rc> = root_element(component_instance);
262 let mut elements: Vec = Vec::new();
263 collect_all_element_nodes_covering_impl(
264 x,
265 y,
266 component_instance,
267 &root_element,
268 &vec![],
269 &mut elements,
270 );
271 elements
272}
273
274pub fn is_root_element_node(
275 component_instance: &ComponentInstance,
276 element_node: &ElementRcNode,
277) -> bool {
278 let root_element: Rc> = root_element(component_instance);
279 let Some((root_path, root_offset: u32)) = root_elementOption<&(Element, Option<…>)>
280 .borrow()
281 .debug
282 .iter()
283 .find(|(n: &Element, _)| !super::is_element_node_ignored(node:n))
284 .map(|(n: &Element, _)| (n.source_file.path().to_owned(), u32::from(n.text_range().start())))
285 else {
286 return false;
287 };
288
289 let (path: PathBuf, offset: u32) = element_node.path_and_offset();
290 path == root_path && offset == root_offset
291}
292
293pub fn is_same_file_as_root_node(
294 component_instance: &ComponentInstance,
295 element_node: &ElementRcNode,
296) -> bool {
297 let root_element: Rc> = root_element(component_instance);
298 let Some(root_path) =
299 root_element.borrow().debug.first().map(|(n: &Element, _)| n.source_file.path().to_owned())
300 else {
301 return false;
302 };
303
304 let (path: PathBuf, _) = element_node.path_and_offset();
305 path == root_path
306}
307
308pub fn select_element_at(x: f32, y: f32, enter_component: bool) {
309 let Some(component_instance) = super::component_instance() else {
310 return;
311 };
312
313 if let Some(se) = super::selected_element() {
314 if let Some(element) = se.as_element() {
315 if element_covers_point(x, y, &component_instance, &element) {
316 // We clicked on the already selected element: Do nothing!
317 return;
318 }
319 }
320 }
321
322 for sc in &collect_all_element_nodes_covering(x, y, &component_instance) {
323 let Some(en) = sc.as_element_node() else {
324 continue;
325 };
326
327 if en.with_element_node(super::is_element_node_ignored) {
328 continue;
329 }
330 if !enter_component && !is_same_file_as_root_node(&component_instance, &en) {
331 continue;
332 }
333 if is_root_element_node(&component_instance, &en) {
334 continue;
335 }
336
337 select_element_node(&component_instance, &en, Some(LogicalPoint::new(x, y)));
338 break;
339 }
340}
341
342pub fn is_element_node_in_layout(element: &ElementRcNode) -> bool {
343 if element.debug_index > 0 {
344 // If we are not the first node, then we might have been inlined right
345 // after a layout managing us
346 elementOption
347 .element
348 .borrow()
349 .debug
350 .get(index:element.debug_index - 1)
351 .map(|d: &(Element, Option)| d.1.is_some())
352 .unwrap_or_default()
353 } else {
354 // If we are the first node, then we might be a child of a layout stored
355 // in the last node of our parent element.
356 let Some(parent: Rc>) = i_slint_compiler::object_tree::find_parent_element(&element.element)
357 else {
358 return false;
359 };
360 let r: bool = parent.borrow().debug.last().map(|d: &(Element, Option)| d.1.is_some()).unwrap_or_default();
361 r
362 }
363}
364
365pub fn select_element_behind(x: f32, y: f32, enter_component: bool, reverse: bool) {
366 let Some(component_instance) = super::component_instance() else {
367 return;
368 };
369 let elements = collect_all_element_nodes_covering(x, y, &component_instance);
370
371 let Some(selected_element_data) = super::selected_element() else {
372 return;
373 };
374
375 let Some(current_selection_position) =
376 elements.iter().position(|sc| sc.is_selected_element_node(&selected_element_data))
377 else {
378 return;
379 };
380
381 let target_range = if reverse {
382 if current_selection_position == 0 {
383 return;
384 }
385 (current_selection_position - 1)..=0
386 } else {
387 if current_selection_position == elements.len() - 1 {
388 return;
389 }
390 (current_selection_position + 1)..=elements.len() - 1
391 };
392
393 for i in target_range {
394 let sc = elements.get(i).unwrap();
395 let Some(en) = sc.as_element_node() else {
396 continue;
397 };
398
399 if en.with_element_node(super::is_element_node_ignored) {
400 continue;
401 }
402
403 if !enter_component && !is_same_file_as_root_node(&component_instance, &en) {
404 continue;
405 }
406
407 if is_root_element_node(&component_instance, &en) {
408 continue;
409 }
410
411 select_element_node(&component_instance, &en, Some(LogicalPoint::new(x, y)));
412 break;
413 }
414}
415
416// Called from UI thread!
417pub fn reselect_element() {
418 let Some(selected: ElementSelection) = super::selected_element() else {
419 return;
420 };
421 let Some(component_instance: ComponentInstance) = super::component_instance() else {
422 return;
423 };
424 let positions: ComponentPositions = component_instance.component_positions(&selected.path, selected.offset);
425
426 super::set_selected_element(selection:Some(selected), positions, notify_editor_about_selection_after_update:false);
427}
428