1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | use std::{path::PathBuf, rc::Rc}; |
5 | |
6 | use i_slint_compiler::diagnostics::SourceFile; |
7 | use i_slint_compiler::object_tree::{Component, ElementRc}; |
8 | use i_slint_core::lengths::{LogicalLength, LogicalPoint}; |
9 | use rowan::TextRange; |
10 | use slint_interpreter::{highlight::ComponentPositions, ComponentInstance}; |
11 | |
12 | use crate::common::ElementRcNode; |
13 | |
14 | #[derive (Clone, Debug)] |
15 | pub struct ElementSelection { |
16 | pub path: PathBuf, |
17 | pub offset: u32, |
18 | pub instance_index: usize, |
19 | pub is_layout: bool, |
20 | } |
21 | |
22 | impl 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() |
46 | fn 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 | |
57 | fn 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 | |
75 | fn 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 | |
86 | pub fn unselect_element() { |
87 | super::set_selected_element(selection:None, positions:ComponentPositions::default(), notify_editor_about_selection_after_update:false); |
88 | } |
89 | |
90 | pub 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 | |
110 | fn 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 | |
133 | fn 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 | |
154 | fn 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 |
165 | pub 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)] |
175 | pub 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 | |
182 | impl 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 | |
196 | impl 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 |
206 | fn 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 | |
256 | pub 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 | |
274 | pub 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 | |
293 | pub 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 | |
308 | pub 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 | |
342 | pub 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 | |
365 | pub 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! |
417 | pub 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 | |