| 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 | |
| 4 | use std::{path::PathBuf, rc::Rc}; |
| 5 | |
| 6 | use i_slint_compiler::{ |
| 7 | object_tree::ElementRc, |
| 8 | parser::{SyntaxKind, TextSize}, |
| 9 | }; |
| 10 | use i_slint_core::lengths::{LogicalPoint, LogicalRect}; |
| 11 | use slint_interpreter::{ComponentHandle, ComponentInstance}; |
| 12 | |
| 13 | use crate::common; |
| 14 | |
| 15 | use crate::preview::{ext::ElementRcNodeExt, ui, SelectionNotification}; |
| 16 | |
| 17 | #[derive (Clone, Debug)] |
| 18 | pub struct ElementSelection { |
| 19 | pub path: PathBuf, |
| 20 | pub offset: TextSize, |
| 21 | pub instance_index: usize, |
| 22 | } |
| 23 | |
| 24 | impl ElementSelection { |
| 25 | pub fn as_element(&self) -> Option<ElementRc> { |
| 26 | let component_instance: ComponentInstance = super::component_instance()?; |
| 27 | |
| 28 | let elements: Vec<(Rc>, …)> = |
| 29 | component_instance.element_node_at_source_code_position(&self.path, self.offset.into()); |
| 30 | elements.get(self.instance_index).or_else(|| elements.first()).map(|(e: &Rc>, _)| e.clone()) |
| 31 | } |
| 32 | |
| 33 | pub fn as_element_node(&self) -> Option<common::ElementRcNode> { |
| 34 | let element: Rc> = self.as_element()?; |
| 35 | |
| 36 | let debug_index: Option = { |
| 37 | let e: Ref<'_, Element> = element.borrow(); |
| 38 | e.debug.iter().position(|d: &ElementDebugInfo| { |
| 39 | d.node.source_file.path() == self.path && d.node.text_range().start() == self.offset |
| 40 | }) |
| 41 | }; |
| 42 | |
| 43 | debug_index.map(|i: usize| common::ElementRcNode { element, debug_index: i }) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // Look at an element and if it is a sub component, jump to its root_element() |
| 48 | fn self_or_embedded_component_root(element: &ElementRc) -> ElementRc { |
| 49 | let elem: Ref<'_, Element> = element.borrow(); |
| 50 | if elem.repeated.is_some() { |
| 51 | if let i_slint_compiler::langtype::ElementType::Component(base: &Rc) = &elem.base_type { |
| 52 | return base.root_element.clone(); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | element.clone() |
| 57 | } |
| 58 | |
| 59 | fn lsp_element_node_position( |
| 60 | element: &common::ElementRcNode, |
| 61 | ) -> Option<(String, lsp_types::Range)> { |
| 62 | let location = element.with_element_node(|n: &Element| { |
| 63 | nFilter<{unknown}, impl FnMut(…) -> …>.parent() |
| 64 | .filter(|p: &{unknown}| p.kind() == i_slint_compiler::parser::SyntaxKind::SubElement) |
| 65 | .map_or_else( |
| 66 | || Some(n.source_file.text_size_to_file_line_column(n.text_range().start())), |
| 67 | |p| Some(p.source_file.text_size_to_file_line_column(p.text_range().start())), |
| 68 | ) |
| 69 | }); |
| 70 | location.map(|(f, sl: u32, sc: u32, el: u32, ec: u32)| { |
| 71 | use lsp_types::{Position, Range}; |
| 72 | let start: Position = Position::new((sl as u32).saturating_sub(1), (sc as u32).saturating_sub(1)); |
| 73 | let end: Position = Position::new((el as u32).saturating_sub(1), (ec as u32).saturating_sub(1)); |
| 74 | |
| 75 | (f, Range::new(start, end)) |
| 76 | }) |
| 77 | } |
| 78 | |
| 79 | fn element_covers_point( |
| 80 | position: LogicalPoint, |
| 81 | component_instance: &ComponentInstance, |
| 82 | selected_element: &ElementRc, |
| 83 | ) -> Option<LogicalRect> { |
| 84 | slint_interpreterOption<&Rect>::highlight::element_positions( |
| 85 | &component_instance.clone_strong().into(), |
| 86 | selected_element, |
| 87 | filter_clipped:slint_interpreter::highlight::ElementPositionFilter::ExcludeClipped, |
| 88 | ) |
| 89 | .iter() |
| 90 | .find(|p: &&Rect| p.contains(position)) |
| 91 | .copied() |
| 92 | } |
| 93 | |
| 94 | pub fn unselect_element() { |
| 95 | super::set_selected_element(selection:None, &[], editor_notification:SelectionNotification::Never); |
| 96 | } |
| 97 | |
| 98 | pub fn select_element_at_source_code_position( |
| 99 | path: PathBuf, |
| 100 | offset: TextSize, |
| 101 | position: Option<LogicalPoint>, |
| 102 | editor_notification: crate::preview::SelectionNotification, |
| 103 | ) { |
| 104 | let Some(component_instance: ComponentInstance) = super::component_instance() else { |
| 105 | return; |
| 106 | }; |
| 107 | select_element_at_source_code_position_impl( |
| 108 | &component_instance, |
| 109 | path, |
| 110 | offset, |
| 111 | position, |
| 112 | editor_notification, |
| 113 | ) |
| 114 | } |
| 115 | |
| 116 | fn select_element_at_source_code_position_impl( |
| 117 | component_instance: &ComponentInstance, |
| 118 | path: PathBuf, |
| 119 | offset: TextSize, |
| 120 | position: Option<LogicalPoint>, |
| 121 | editor_notification: SelectionNotification, |
| 122 | ) { |
| 123 | let positions: Vec> = component_instance.component_positions(&path, offset.into()); |
| 124 | |
| 125 | let instance_index: usize = positionOption |
| 126 | .and_then(|p: Point2D| positions.iter().enumerate().find_map(|(i: usize, g: &Rect)| g.contains(p).then_some(i))) |
| 127 | .unwrap_or_default(); |
| 128 | |
| 129 | super::set_selected_element( |
| 130 | selection:Some(ElementSelection { path, offset, instance_index }), |
| 131 | &positions, |
| 132 | editor_notification, |
| 133 | ); |
| 134 | } |
| 135 | |
| 136 | fn select_element_node( |
| 137 | component_instance: &ComponentInstance, |
| 138 | selected_element: &common::ElementRcNode, |
| 139 | position: Option<LogicalPoint>, |
| 140 | ) { |
| 141 | let (path: PathBuf, offset: TextSize) = selected_element.path_and_offset(); |
| 142 | |
| 143 | select_element_at_source_code_position_impl( |
| 144 | component_instance, |
| 145 | path, |
| 146 | offset, |
| 147 | position, |
| 148 | editor_notification:SelectionNotification::Never, // We update directly;-) |
| 149 | ); |
| 150 | |
| 151 | if let Some(document_position: (String, Range)) = lsp_element_node_position(selected_element) { |
| 152 | super::ask_editor_to_show_document(&document_position.0, selection:document_position.1, take_focus:false); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // Return the real root element, skipping the WindowElement that might got added |
| 157 | pub fn root_element(component_instance: &ComponentInstance) -> ElementRc { |
| 158 | let root_element: Rc> = component_instance.definition().root_component().root_element.clone(); |
| 159 | if root_element.borrow().debug.is_empty() { |
| 160 | // The root element has no debug set if it is a window inserted by the compiler. |
| 161 | // That window will have one child -- the "real root", but it might |
| 162 | // have a few more compiler-generated nodes in front or behind the "real root"! |
| 163 | let child: Option>> = |
| 164 | root_element.borrow().children.iter().find(|c: &&Rc>| !c.borrow().debug.is_empty()).cloned(); |
| 165 | child.unwrap_or(default:root_element) |
| 166 | } else { |
| 167 | root_element |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | #[derive (Clone)] |
| 172 | pub struct SelectionCandidate { |
| 173 | pub element: ElementRc, |
| 174 | pub debug_index: usize, |
| 175 | pub geometry: LogicalRect, |
| 176 | pub is_in_root_component: bool, |
| 177 | } |
| 178 | |
| 179 | impl SelectionCandidate { |
| 180 | pub fn is_selected_element_node(&self, selection: &common::ElementRcNode) -> bool { |
| 181 | self.as_element_node().map(|en: ElementRcNode| en.path_and_offset()) == Some(selection.path_and_offset()) |
| 182 | } |
| 183 | |
| 184 | pub fn as_element_node(&self) -> Option<common::ElementRcNode> { |
| 185 | common::ElementRcNode::new(self.element.clone(), self.debug_index) |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | impl std::fmt::Debug for SelectionCandidate { |
| 190 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 191 | write!(f, "SelectionCandidate {{ {:?} }}@( {:?})" , self.as_element_node(), self.geometry) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | // Traverse the element tree in reverse render order and collect information on |
| 196 | // all elements that "render" at the given x and y coordinates |
| 197 | fn collect_all_element_nodes_covering_impl( |
| 198 | position: LogicalPoint, |
| 199 | component_instance: &ComponentInstance, |
| 200 | current_element: &ElementRc, |
| 201 | result: &mut Vec<SelectionCandidate>, |
| 202 | ) { |
| 203 | let ce: Rc> = self_or_embedded_component_root(current_element); |
| 204 | |
| 205 | for c: &Rc> in ce.borrow().children.iter().rev() { |
| 206 | collect_all_element_nodes_covering_impl(position, component_instance, current_element:c, result); |
| 207 | } |
| 208 | |
| 209 | if let Some(geometry: Rect) = element_covers_point(position, component_instance, selected_element:current_element) { |
| 210 | for (i: usize, d: &ElementDebugInfo) in ce.borrow().debug.iter().enumerate().rev() { |
| 211 | if !common::is_element_node_ignored(&d.node) |
| 212 | && !d.node.source_file.path().starts_with("builtin:/" ) |
| 213 | { |
| 214 | // All nodes have the same geometry |
| 215 | result.push(SelectionCandidate { |
| 216 | element: ce.clone(), |
| 217 | debug_index: i, |
| 218 | is_in_root_component: false, |
| 219 | geometry, |
| 220 | }); |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | fn assign_is_in_root_component(candidates: &mut Vec<SelectionCandidate>) { |
| 227 | let mut root_text_range: Option<i_slint_compiler::parser::TextRange> = None; |
| 228 | for sc: &mut SelectionCandidate in candidates.iter_mut().rev() { |
| 229 | let Some(en: ElementRcNode) = sc.as_element_node() else { |
| 230 | continue; |
| 231 | }; |
| 232 | |
| 233 | let node_text_range: TextRange = en.with_element_node(|n: &Element| n.text_range()); |
| 234 | if let Some(rtr: TextRange) = root_text_range { |
| 235 | sc.is_in_root_component = rtr.contains_range(node_text_range); |
| 236 | } else { |
| 237 | root_text_range = Some(node_text_range); |
| 238 | sc.is_in_root_component = true; |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | pub fn collect_all_element_nodes_covering( |
| 244 | position: LogicalPoint, |
| 245 | component_instance: &ComponentInstance, |
| 246 | ) -> Vec<SelectionCandidate> { |
| 247 | let root_element: Rc> = root_element(component_instance); |
| 248 | let mut elements: Vec = Vec::new(); |
| 249 | collect_all_element_nodes_covering_impl( |
| 250 | position, |
| 251 | component_instance, |
| 252 | &root_element, |
| 253 | &mut elements, |
| 254 | ); |
| 255 | |
| 256 | assign_is_in_root_component(&mut elements); |
| 257 | |
| 258 | elements |
| 259 | } |
| 260 | |
| 261 | fn select_element_at_impl( |
| 262 | component_instance: &ComponentInstance, |
| 263 | position: LogicalPoint, |
| 264 | enter_component: bool, |
| 265 | ) -> Option<common::ElementRcNode> { |
| 266 | for sc: &SelectionCandidate in &collect_all_element_nodes_covering(position, component_instance) { |
| 267 | if let Some(en: ElementRcNode) = filter_nodes_for_selection(selection_candidate:sc, enter_component) { |
| 268 | return Some(en); |
| 269 | } |
| 270 | } |
| 271 | None |
| 272 | } |
| 273 | |
| 274 | pub fn select_element_at(x: f32, y: f32, enter_component: bool) { |
| 275 | let Some(component_instance: ComponentInstance) = super::component_instance() else { |
| 276 | return; |
| 277 | }; |
| 278 | |
| 279 | let position: Point2D = LogicalPoint::new(x, y); |
| 280 | |
| 281 | let Some(en: ElementRcNode) = select_element_at_impl(&component_instance, position, enter_component) else { |
| 282 | return; |
| 283 | }; |
| 284 | |
| 285 | select_element_node(&component_instance, &en, position:Some(position)); |
| 286 | } |
| 287 | |
| 288 | pub fn selection_stack_at( |
| 289 | x: f32, |
| 290 | y: f32, |
| 291 | ) -> slint::ModelRc<crate::preview::ui::SelectionStackFrame> { |
| 292 | let Some(component_instance) = &super::component_instance() else { |
| 293 | return Default::default(); |
| 294 | }; |
| 295 | let root_element = root_element(component_instance); |
| 296 | let Some(root_geometry) = component_instance.element_positions(&root_element).first().cloned() |
| 297 | else { |
| 298 | return Default::default(); |
| 299 | }; |
| 300 | |
| 301 | let position = LogicalPoint::new(x, y); |
| 302 | |
| 303 | let (known_components, mut selected) = crate::preview::PREVIEW_STATE.with(|preview_state| { |
| 304 | let preview_state = preview_state.borrow(); |
| 305 | |
| 306 | let known_components = preview_state.known_components.clone(); |
| 307 | let selected = |
| 308 | preview_state.selected.as_ref().and_then(|s| s.as_element_node()).filter(|en| { |
| 309 | en.geometries(component_instance).iter().any(|gr| gr.contains(position)) |
| 310 | }); |
| 311 | |
| 312 | (known_components, selected) |
| 313 | }); |
| 314 | |
| 315 | let mut longest_path_prefix = PathBuf::new(); |
| 316 | |
| 317 | let mut result = collect_all_element_nodes_covering(position, component_instance) |
| 318 | .iter() |
| 319 | .filter(|sn| filter_nodes_for_selection(sn, true).is_some()) |
| 320 | .map(|sc| { |
| 321 | let (type_name, id, is_layout, is_selected, path, offset) = sc |
| 322 | .as_element_node() |
| 323 | .map(|en| { |
| 324 | let (path, offset) = en.path_and_offset(); |
| 325 | let offset: u32 = offset.into(); |
| 326 | |
| 327 | let is_selected = if selected.is_none() { |
| 328 | select_element_node(component_instance, &en, Some(position)); |
| 329 | selected = Some(en.clone()); |
| 330 | true |
| 331 | } else { |
| 332 | selected.as_ref() == Some(&en) |
| 333 | }; |
| 334 | |
| 335 | let (type_name, id, is_layout) = en.with_element_debug(|di| { |
| 336 | let id = di |
| 337 | .node |
| 338 | .parent() |
| 339 | .and_then(|p| { |
| 340 | if p.kind() == SyntaxKind::SubElement { |
| 341 | p.child_token(SyntaxKind::Identifier) |
| 342 | .map(|t| t.text().to_string()) |
| 343 | } else { |
| 344 | None |
| 345 | } |
| 346 | }) |
| 347 | .unwrap_or_default(); |
| 348 | |
| 349 | let type_name = { |
| 350 | di.node |
| 351 | .parent() |
| 352 | .and_then(|p| { |
| 353 | if p.kind() == SyntaxKind::Component { |
| 354 | p.child_node(SyntaxKind::DeclaredIdentifier) |
| 355 | .map(|t| t.text().to_string()) |
| 356 | } else { |
| 357 | None |
| 358 | } |
| 359 | }) |
| 360 | .or_else(|| { |
| 361 | di.node |
| 362 | .QualifiedName() |
| 363 | .map(|qn| qn.text().to_string().trim().to_string()) |
| 364 | }) |
| 365 | .unwrap_or_default() |
| 366 | .trim() |
| 367 | .to_string() |
| 368 | }; |
| 369 | |
| 370 | (type_name, id, di.layout.is_some()) |
| 371 | }); |
| 372 | |
| 373 | (type_name, id, is_layout, is_selected, path, offset) |
| 374 | }) |
| 375 | .unwrap_or_default(); |
| 376 | |
| 377 | if path.strip_prefix("/@" ).is_err() && path != PathBuf::new() { |
| 378 | if longest_path_prefix == PathBuf::new() { |
| 379 | longest_path_prefix = path.clone(); |
| 380 | } else { |
| 381 | longest_path_prefix = |
| 382 | std::iter::zip(longest_path_prefix.components(), path.components()) |
| 383 | .take_while(|(l, p)| l == p) |
| 384 | .map(|(l, _)| l) |
| 385 | .collect(); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | let width = (sc.geometry.size.width / root_geometry.size.width) * 100.0; |
| 390 | let height = (sc.geometry.size.height / root_geometry.size.height) * 100.0; |
| 391 | let x = ((sc.geometry.origin.x + root_geometry.origin.x) / root_geometry.size.width) |
| 392 | * 100.0; |
| 393 | let y = ((sc.geometry.origin.y + root_geometry.origin.y) / root_geometry.size.height) |
| 394 | * 100.0; |
| 395 | |
| 396 | let is_interactive = known_components |
| 397 | .iter() |
| 398 | .position(|kc| kc.name.as_str() == type_name.as_str()) |
| 399 | .map(|index| known_components.get(index).unwrap().is_interactive) |
| 400 | .unwrap_or_default(); |
| 401 | |
| 402 | crate::preview::ui::SelectionStackFrame { |
| 403 | width, |
| 404 | height, |
| 405 | x, |
| 406 | y, |
| 407 | is_in_root_component: sc.is_in_root_component, |
| 408 | is_selected, |
| 409 | is_layout, |
| 410 | is_interactive, |
| 411 | type_name: type_name.into(), |
| 412 | file_name: path.to_string_lossy().to_string().into(), |
| 413 | element_path: path.to_string_lossy().to_string().into(), |
| 414 | element_offset: offset as i32, |
| 415 | id: id.into(), |
| 416 | } |
| 417 | }) |
| 418 | .collect::<Vec<_>>(); |
| 419 | |
| 420 | for frame in result.iter_mut() { |
| 421 | let file_name = PathBuf::from(frame.file_name.to_string()); |
| 422 | let new_file_name = { |
| 423 | if let Some(library) = file_name.to_string_lossy().strip_prefix("/@" ) { |
| 424 | format!("@ {library:?}" ) |
| 425 | } else if file_name == longest_path_prefix { |
| 426 | file_name.file_name().unwrap_or_default().to_string_lossy().to_string() |
| 427 | } else { |
| 428 | file_name |
| 429 | .strip_prefix(&longest_path_prefix) |
| 430 | .unwrap_or(&file_name) |
| 431 | .to_string_lossy() |
| 432 | .to_string() |
| 433 | } |
| 434 | }; |
| 435 | frame.file_name = new_file_name.into(); |
| 436 | } |
| 437 | |
| 438 | Rc::new(slint::VecModel::from(result)).into() |
| 439 | } |
| 440 | |
| 441 | pub fn filter_sort_selection_stack( |
| 442 | model: slint::ModelRc<crate::preview::ui::SelectionStackFrame>, |
| 443 | filter_text: slint::SharedString, |
| 444 | filter: crate::preview::ui::SelectionStackFilter, |
| 445 | ) -> slint::ModelRc<crate::preview::ui::SelectionStackFrame> { |
| 446 | use crate::preview::ui::{SelectionStackFilter, SelectionStackFrame}; |
| 447 | use slint::ModelExt; |
| 448 | |
| 449 | fn filter_fn(frame: &SelectionStackFrame, filter: SelectionStackFilter) -> bool { |
| 450 | match filter { |
| 451 | SelectionStackFilter::Nothing => false, |
| 452 | SelectionStackFilter::Layouts => frame.is_layout, |
| 453 | SelectionStackFilter::Interactive => frame.is_interactive, |
| 454 | SelectionStackFilter::Others => !frame.is_interactive && !frame.is_layout, |
| 455 | SelectionStackFilter::LayoutsAndInteractive => frame.is_layout || frame.is_interactive, |
| 456 | SelectionStackFilter::LayoutsAndOthers => { |
| 457 | frame.is_layout || (!frame.is_layout && !frame.is_interactive) |
| 458 | } |
| 459 | SelectionStackFilter::InteractiveAndOthers => { |
| 460 | frame.is_interactive || (!frame.is_layout && !frame.is_interactive) |
| 461 | } |
| 462 | SelectionStackFilter::Everything => true, |
| 463 | } |
| 464 | } |
| 465 | |
| 466 | let filter_text = filter_text.to_string(); |
| 467 | |
| 468 | if filter_text.is_empty() && filter == SelectionStackFilter::Everything { |
| 469 | model |
| 470 | } else if filter_text.as_str().chars().any(|c| !c.is_lowercase()) { |
| 471 | Rc::new(model.filter(move |frame| { |
| 472 | filter_fn(frame, filter) |
| 473 | && (frame.id.contains(&filter_text) |
| 474 | || frame.type_name.contains(&filter_text) |
| 475 | || frame.file_name.contains(&filter_text)) |
| 476 | })) |
| 477 | .into() |
| 478 | } else { |
| 479 | Rc::new(model.filter(move |frame| { |
| 480 | filter_fn(frame, filter) |
| 481 | && (frame.id.to_lowercase().contains(&filter_text) |
| 482 | || frame.type_name.to_lowercase().contains(&filter_text) |
| 483 | || frame.file_name.to_lowercase().contains(&filter_text)) |
| 484 | })) |
| 485 | .into() |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | pub fn parent_layout_kind(element: &common::ElementRcNode) -> ui::LayoutKind { |
| 490 | element.parent().map(|p| p.layout_kind()).unwrap_or(default:ui::LayoutKind::None) |
| 491 | } |
| 492 | |
| 493 | fn filter_nodes_for_selection( |
| 494 | selection_candidate: &SelectionCandidate, |
| 495 | enter_component: bool, |
| 496 | ) -> Option<common::ElementRcNode> { |
| 497 | if !selection_candidate.is_in_root_component && !enter_component { |
| 498 | return None; |
| 499 | } |
| 500 | |
| 501 | selection_candidate.as_element_node().filter(|en: &ElementRcNode| { |
| 502 | en.with_element_node(|n: &Element| n.parent().map_or(true, |p| p.kind() != SyntaxKind::Component)) |
| 503 | }) |
| 504 | } |
| 505 | |
| 506 | pub fn select_element_behind_impl( |
| 507 | component_instance: &ComponentInstance, |
| 508 | selected_element_node: &common::ElementRcNode, |
| 509 | position: LogicalPoint, |
| 510 | enter_component: bool, |
| 511 | reverse: bool, |
| 512 | ) -> Option<common::ElementRcNode> { |
| 513 | let elements = collect_all_element_nodes_covering(position, component_instance); |
| 514 | let current_selection_position = |
| 515 | elements.iter().position(|sc| sc.is_selected_element_node(selected_element_node))?; |
| 516 | |
| 517 | let (start_position, iterations) = if reverse { |
| 518 | let start_position = current_selection_position.saturating_sub(1); |
| 519 | (start_position, current_selection_position) |
| 520 | } else { |
| 521 | let start_position = current_selection_position + 1; |
| 522 | (start_position, elements.len().saturating_sub(current_selection_position + 1)) |
| 523 | }; |
| 524 | |
| 525 | for i in 0..iterations { |
| 526 | let mapped_index = if reverse { |
| 527 | assert!(i <= start_position); |
| 528 | start_position - i |
| 529 | } else { |
| 530 | assert!(i + start_position < elements.len()); |
| 531 | start_position + i |
| 532 | }; |
| 533 | if let Some(en) = |
| 534 | filter_nodes_for_selection(elements.get(mapped_index).unwrap(), enter_component) |
| 535 | { |
| 536 | return Some(en); |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | None |
| 541 | } |
| 542 | |
| 543 | pub fn select_element_behind(x: f32, y: f32, enter_component: bool, reverse: bool) { |
| 544 | let Some(component_instance: ComponentInstance) = super::component_instance() else { |
| 545 | return; |
| 546 | }; |
| 547 | let position: Point2D = LogicalPoint::new(x, y); |
| 548 | let Some(selected_element_node: ElementRcNode) = |
| 549 | super::selected_element().and_then(|sel: ElementSelection| sel.as_element_node()) |
| 550 | else { |
| 551 | return; |
| 552 | }; |
| 553 | |
| 554 | let Some(en: ElementRcNode) = select_element_behind_impl( |
| 555 | &component_instance, |
| 556 | &selected_element_node, |
| 557 | position, |
| 558 | enter_component, |
| 559 | reverse, |
| 560 | ) else { |
| 561 | return; |
| 562 | }; |
| 563 | |
| 564 | select_element_node(&component_instance, &en, position:Some(position)); |
| 565 | } |
| 566 | |
| 567 | // Called from UI thread! |
| 568 | pub fn reselect_element() { |
| 569 | let Some(selected: ElementSelection) = super::selected_element() else { |
| 570 | super::set_selected_element(selection:None, &[], editor_notification:SelectionNotification::Never); |
| 571 | return; |
| 572 | }; |
| 573 | let Some(component_instance: ComponentInstance) = super::component_instance() else { |
| 574 | return; |
| 575 | }; |
| 576 | let positions: Vec> = component_instance.component_positions(&selected.path, selected.offset.into()); |
| 577 | |
| 578 | super::set_selected_element(selection:Some(selected), &positions, editor_notification:SelectionNotification::Never); |
| 579 | } |
| 580 | |
| 581 | #[cfg (test)] |
| 582 | mod tests { |
| 583 | use crate::common::test; |
| 584 | |
| 585 | use std::path::PathBuf; |
| 586 | |
| 587 | use i_slint_core::lengths::LogicalPoint; |
| 588 | use slint_interpreter::ComponentInstance; |
| 589 | |
| 590 | fn demo_app() -> ComponentInstance { |
| 591 | crate::preview::test::interpret_test( |
| 592 | "fluent" , |
| 593 | r#"import { Button } from "std-widgets.slint"; |
| 594 | |
| 595 | component SomeComponent { // 69 |
| 596 | @children |
| 597 | } |
| 598 | |
| 599 | component Main { // 109 |
| 600 | width: 200px; |
| 601 | height: 200px; |
| 602 | |
| 603 | HorizontalLayout { // 160 |
| 604 | Rectangle { // 194 |
| 605 | SomeComponent { // 225 |
| 606 | Button { // 264 |
| 607 | text: "Press me"; |
| 608 | } |
| 609 | } |
| 610 | } |
| 611 | } |
| 612 | } |
| 613 | |
| 614 | export component Entry inherits Main { /* @lsp:ignore-node */ } // 401 |
| 615 | "# , |
| 616 | ) |
| 617 | } |
| 618 | |
| 619 | #[test ] |
| 620 | fn test_find_covering_elements() { |
| 621 | let type_loader = demo_app(); |
| 622 | |
| 623 | let mut covers_center = super::collect_all_element_nodes_covering( |
| 624 | LogicalPoint::new(100.0, 100.0), |
| 625 | &type_loader, |
| 626 | ); |
| 627 | |
| 628 | // Remove the "button" implementation details. They must be at the start: |
| 629 | let button_path = PathBuf::from("builtin:/fluent/button.slint" ); |
| 630 | let first_non_button = covers_center |
| 631 | .iter() |
| 632 | .position(|sc| { |
| 633 | sc.as_element_node().map(|en| en.path_and_offset().0).as_ref() != Some(&button_path) |
| 634 | }) |
| 635 | .unwrap(); |
| 636 | covers_center.drain(0..first_non_button); |
| 637 | |
| 638 | let test_file = test::test_file_name("test_data.slint" ); |
| 639 | |
| 640 | let expected_offsets = [264_u32, 69, 225, 194, 160, 109]; |
| 641 | assert_eq!(covers_center.len(), expected_offsets.len()); |
| 642 | |
| 643 | for (candidate, expected_offset) in covers_center.iter().zip(&expected_offsets) { |
| 644 | let (path, offset) = candidate.as_element_node().unwrap().path_and_offset(); |
| 645 | assert_eq!(&path, &test_file); |
| 646 | assert_eq!(offset, (*expected_offset).into()); |
| 647 | } |
| 648 | |
| 649 | let covers_below = super::collect_all_element_nodes_covering( |
| 650 | LogicalPoint::new(100.0, 180.0), |
| 651 | &type_loader, |
| 652 | ); |
| 653 | |
| 654 | // All but the button itself as well as the SomeComponent (impl and use) |
| 655 | assert_eq!(covers_below.len(), covers_center.len() - 3); |
| 656 | |
| 657 | for (below, center) in covers_below.iter().zip(&covers_center[3..]) { |
| 658 | assert_eq!( |
| 659 | below.as_element_node().map(|en| en.path_and_offset()), |
| 660 | center.as_element_node().map(|en| en.path_and_offset()) |
| 661 | ); |
| 662 | } |
| 663 | } |
| 664 | |
| 665 | #[test ] |
| 666 | fn test_element_selection() { |
| 667 | let component_instance = demo_app(); |
| 668 | |
| 669 | let covers_center = super::collect_all_element_nodes_covering( |
| 670 | LogicalPoint::new(100.0, 100.0), |
| 671 | &component_instance, |
| 672 | ) |
| 673 | .iter() |
| 674 | .flat_map(|sc| sc.as_element_node()) |
| 675 | .map(|en| en.path_and_offset()) |
| 676 | .collect::<Vec<_>>(); |
| 677 | |
| 678 | eprintln!("Covers:" ); |
| 679 | for (i, (p, ts)) in covers_center.iter().enumerate() { |
| 680 | println!(" {i}: {p:?}:{ts:?}" ); |
| 681 | } |
| 682 | eprintln!("Done" ); |
| 683 | |
| 684 | // Select without crossing boundaries |
| 685 | // -------------------------------------------------------------------- |
| 686 | let select = super::select_element_at_impl( |
| 687 | &component_instance, |
| 688 | LogicalPoint::new(100.0, 100.0), |
| 689 | false, |
| 690 | ) |
| 691 | .unwrap(); |
| 692 | assert_eq!(&select.path_and_offset(), covers_center.first().unwrap()); |
| 693 | |
| 694 | // Try to move towards the viewer: |
| 695 | assert!(super::select_element_behind_impl( |
| 696 | &component_instance, |
| 697 | &select, |
| 698 | LogicalPoint::new(100.0, 100.0), |
| 699 | false, |
| 700 | true |
| 701 | ) |
| 702 | .is_none()); |
| 703 | |
| 704 | // Move deeper into the image: |
| 705 | let next = super::select_element_behind_impl( |
| 706 | &component_instance, |
| 707 | &select, |
| 708 | LogicalPoint::new(100.0, 100.0), |
| 709 | false, |
| 710 | false, |
| 711 | ) |
| 712 | .unwrap(); |
| 713 | assert_eq!(&next.path_and_offset(), covers_center.get(2).unwrap()); |
| 714 | let next = super::select_element_behind_impl( |
| 715 | &component_instance, |
| 716 | &next, |
| 717 | LogicalPoint::new(100.0, 100.0), |
| 718 | false, |
| 719 | false, |
| 720 | ) |
| 721 | .unwrap(); |
| 722 | assert_eq!(&next.path_and_offset(), covers_center.get(3).unwrap()); |
| 723 | let next = super::select_element_behind_impl( |
| 724 | &component_instance, |
| 725 | &next, |
| 726 | LogicalPoint::new(100.0, 100.0), |
| 727 | false, |
| 728 | false, |
| 729 | ) |
| 730 | .unwrap(); |
| 731 | assert_eq!(&next.path_and_offset(), covers_center.get(4).unwrap()); |
| 732 | |
| 733 | assert!(super::select_element_behind_impl( |
| 734 | &component_instance, |
| 735 | &next, |
| 736 | LogicalPoint::new(100.0, 100.0), |
| 737 | false, |
| 738 | false |
| 739 | ) |
| 740 | .is_none()); |
| 741 | |
| 742 | // Move towards the viewer: |
| 743 | let prev = super::select_element_behind_impl( |
| 744 | &component_instance, |
| 745 | &next, |
| 746 | LogicalPoint::new(100.0, 100.0), |
| 747 | false, |
| 748 | true, |
| 749 | ) |
| 750 | .unwrap(); |
| 751 | assert_eq!(&prev.path_and_offset(), covers_center.get(3).unwrap()); |
| 752 | let prev = super::select_element_behind_impl( |
| 753 | &component_instance, |
| 754 | &prev, |
| 755 | LogicalPoint::new(100.0, 100.0), |
| 756 | false, |
| 757 | true, |
| 758 | ) |
| 759 | .unwrap(); |
| 760 | assert_eq!(&prev.path_and_offset(), covers_center.get(2).unwrap()); |
| 761 | let prev = super::select_element_behind_impl( |
| 762 | &component_instance, |
| 763 | &prev, |
| 764 | LogicalPoint::new(100.0, 100.0), |
| 765 | false, |
| 766 | true, |
| 767 | ) |
| 768 | .unwrap(); |
| 769 | assert_eq!(&prev.path_and_offset(), covers_center.first().unwrap()); |
| 770 | |
| 771 | // Select with crossing component boundaries |
| 772 | // -------------------------------------------------------------------- |
| 773 | let select = super::select_element_at_impl( |
| 774 | &component_instance, |
| 775 | LogicalPoint::new(100.0, 100.0), |
| 776 | true, |
| 777 | ) |
| 778 | .unwrap(); |
| 779 | assert_eq!(&select.path_and_offset(), covers_center.first().unwrap()); |
| 780 | |
| 781 | // Move deeper into the image: |
| 782 | let next = super::select_element_behind_impl( |
| 783 | &component_instance, |
| 784 | &select, |
| 785 | LogicalPoint::new(100.0, 100.0), |
| 786 | true, |
| 787 | false, |
| 788 | ) |
| 789 | .unwrap(); |
| 790 | assert_eq!(&next.path_and_offset(), covers_center.get(2).unwrap()); |
| 791 | let next = super::select_element_behind_impl( |
| 792 | &component_instance, |
| 793 | &next, |
| 794 | LogicalPoint::new(100.0, 100.0), |
| 795 | true, |
| 796 | false, |
| 797 | ) |
| 798 | .unwrap(); |
| 799 | assert_eq!(&next.path_and_offset(), covers_center.get(3).unwrap()); |
| 800 | let next = super::select_element_behind_impl( |
| 801 | &component_instance, |
| 802 | &next, |
| 803 | LogicalPoint::new(100.0, 100.0), |
| 804 | true, |
| 805 | false, |
| 806 | ) |
| 807 | .unwrap(); |
| 808 | assert_eq!(&next.path_and_offset(), covers_center.get(4).unwrap()); |
| 809 | |
| 810 | assert!(super::select_element_behind_impl( |
| 811 | &component_instance, |
| 812 | &next, |
| 813 | LogicalPoint::new(100.0, 100.0), |
| 814 | true, |
| 815 | false |
| 816 | ) |
| 817 | .is_none()); |
| 818 | |
| 819 | // Move towards the viewer: |
| 820 | let prev = super::select_element_behind_impl( |
| 821 | &component_instance, |
| 822 | &next, |
| 823 | LogicalPoint::new(100.0, 100.0), |
| 824 | true, |
| 825 | true, |
| 826 | ) |
| 827 | .unwrap(); |
| 828 | assert_eq!(&prev.path_and_offset(), covers_center.get(3).unwrap()); |
| 829 | let prev = super::select_element_behind_impl( |
| 830 | &component_instance, |
| 831 | &prev, |
| 832 | LogicalPoint::new(100.0, 100.0), |
| 833 | true, |
| 834 | true, |
| 835 | ) |
| 836 | .unwrap(); |
| 837 | assert_eq!(&prev.path_and_offset(), covers_center.get(2).unwrap()); |
| 838 | let prev = super::select_element_behind_impl( |
| 839 | &component_instance, |
| 840 | &prev, |
| 841 | LogicalPoint::new(100.0, 100.0), |
| 842 | true, |
| 843 | true, |
| 844 | ) |
| 845 | .unwrap(); |
| 846 | assert_eq!(&prev.path_and_offset(), covers_center.first().unwrap()); |
| 847 | |
| 848 | assert!(super::select_element_behind_impl( |
| 849 | &component_instance, |
| 850 | &prev, |
| 851 | LogicalPoint::new(100.0, 100.0), |
| 852 | true, |
| 853 | true |
| 854 | ) |
| 855 | .is_none()); |
| 856 | } |
| 857 | } |
| 858 | |