mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 10:50:00 +00:00

Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
CI / python_test (windows-2022) (push) Blocked by required conditions
CI / files-changed (push) Waiting to run
CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.85) (push) Blocked by required conditions
CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
CI / node_test (macos-14) (push) Blocked by required conditions
CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
CI / node_test (windows-2022) (push) Blocked by required conditions
CI / python_test (macos-14) (push) Blocked by required conditions
CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (macos-13) (push) Blocked by required conditions
CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (macos-14, 1.85) (push) Blocked by required conditions
CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
CI / cpp_package_test (push) Blocked by required conditions
CI / vsce_build_test (push) Blocked by required conditions
CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
CI / mcu-embassy (push) Blocked by required conditions
CI / ffi_32bit_build (push) Blocked by required conditions
CI / docs (push) Blocked by required conditions
CI / wasm (push) Blocked by required conditions
CI / wasm_demo (push) Blocked by required conditions
CI / tree-sitter (push) Blocked by required conditions
CI / updater_test (0.3.0) (push) Blocked by required conditions
CI / fmt_test (push) Blocked by required conditions
CI / esp-idf-quick (push) Blocked by required conditions
CI / android (push) Blocked by required conditions
CI / miri (push) Blocked by required conditions
CI / test-figma-inspector (push) Blocked by required conditions
Rely on the fact that `component_instance.component_positions` read properties and do the property tracking for us. By accessing it from a callback we get free dirty notification.
865 lines
29 KiB
Rust
865 lines
29 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
use std::{path::PathBuf, rc::Rc};
|
|
|
|
use i_slint_compiler::{
|
|
object_tree::ElementRc,
|
|
parser::{SyntaxKind, TextSize},
|
|
};
|
|
use i_slint_core::lengths::{LogicalPoint, LogicalRect};
|
|
use slint_interpreter::{ComponentHandle, ComponentInstance};
|
|
|
|
use crate::common;
|
|
use crate::preview::{self, ext::ElementRcNodeExt, ui, SelectionNotification};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ElementSelection {
|
|
pub path: PathBuf,
|
|
pub offset: TextSize,
|
|
pub instance_index: usize,
|
|
}
|
|
|
|
impl ElementSelection {
|
|
pub fn as_element(&self) -> Option<ElementRc> {
|
|
let component_instance = super::component_instance()?;
|
|
|
|
let elements =
|
|
component_instance.element_node_at_source_code_position(&self.path, self.offset.into());
|
|
elements.get(self.instance_index).or_else(|| elements.first()).map(|(e, _)| e.clone())
|
|
}
|
|
|
|
pub fn as_element_node(&self) -> Option<common::ElementRcNode> {
|
|
let element = self.as_element()?;
|
|
|
|
let debug_index = {
|
|
let e = element.borrow();
|
|
e.debug.iter().position(|d| {
|
|
d.node.source_file.path() == self.path && d.node.text_range().start() == self.offset
|
|
})
|
|
};
|
|
|
|
debug_index.map(|i| common::ElementRcNode { element, debug_index: i })
|
|
}
|
|
}
|
|
|
|
// Look at an element and if it is a sub component, jump to its root_element()
|
|
fn self_or_embedded_component_root(element: &ElementRc) -> ElementRc {
|
|
let elem = element.borrow();
|
|
if elem.repeated.is_some() {
|
|
if let i_slint_compiler::langtype::ElementType::Component(base) = &elem.base_type {
|
|
return base.root_element.clone();
|
|
}
|
|
}
|
|
|
|
element.clone()
|
|
}
|
|
|
|
fn lsp_element_node_position(
|
|
element: &common::ElementRcNode,
|
|
) -> Option<(String, lsp_types::Range)> {
|
|
let location = element.with_element_node(|n| {
|
|
n.parent()
|
|
.filter(|p| p.kind() == i_slint_compiler::parser::SyntaxKind::SubElement)
|
|
.map_or_else(
|
|
|| Some(n.source_file.text_size_to_file_line_column(n.text_range().start())),
|
|
|p| Some(p.source_file.text_size_to_file_line_column(p.text_range().start())),
|
|
)
|
|
});
|
|
location.map(|(f, sl, sc, el, ec)| {
|
|
use lsp_types::{Position, Range};
|
|
let start = Position::new((sl as u32).saturating_sub(1), (sc as u32).saturating_sub(1));
|
|
let end = Position::new((el as u32).saturating_sub(1), (ec as u32).saturating_sub(1));
|
|
|
|
(f, Range::new(start, end))
|
|
})
|
|
}
|
|
|
|
fn element_covers_point(
|
|
position: LogicalPoint,
|
|
component_instance: &ComponentInstance,
|
|
selected_element: &ElementRc,
|
|
) -> Option<LogicalRect> {
|
|
slint_interpreter::highlight::element_positions(
|
|
&component_instance.clone_strong().into(),
|
|
selected_element,
|
|
slint_interpreter::highlight::ElementPositionFilter::ExcludeClipped,
|
|
)
|
|
.iter()
|
|
.find(|p| p.contains(position))
|
|
.copied()
|
|
}
|
|
|
|
pub fn unselect_element() {
|
|
super::set_selected_element(None, SelectionNotification::Never);
|
|
}
|
|
|
|
pub fn select_element_at_source_code_position(
|
|
path: PathBuf,
|
|
offset: TextSize,
|
|
position: Option<LogicalPoint>,
|
|
editor_notification: preview::SelectionNotification,
|
|
) {
|
|
let Some(component_instance) = super::component_instance() else {
|
|
return;
|
|
};
|
|
select_element_at_source_code_position_impl(
|
|
&component_instance,
|
|
path,
|
|
offset,
|
|
position,
|
|
editor_notification,
|
|
)
|
|
}
|
|
|
|
fn select_element_at_source_code_position_impl(
|
|
component_instance: &ComponentInstance,
|
|
path: PathBuf,
|
|
offset: TextSize,
|
|
position: Option<LogicalPoint>,
|
|
editor_notification: SelectionNotification,
|
|
) {
|
|
let positions = component_instance.component_positions(&path, offset.into());
|
|
|
|
let instance_index = position
|
|
.and_then(|p| positions.iter().enumerate().find_map(|(i, g)| g.contains(p).then_some(i)))
|
|
.unwrap_or_default();
|
|
|
|
super::set_selected_element(
|
|
Some(ElementSelection { path, offset, instance_index }),
|
|
editor_notification,
|
|
);
|
|
}
|
|
|
|
pub fn highlight_positions(
|
|
source_uri: slint::SharedString,
|
|
offset: i32,
|
|
) -> slint::ModelRc<ui::SelectionRectangle> {
|
|
let Some(component_instance) = super::component_instance() else {
|
|
return Default::default();
|
|
};
|
|
|
|
let Some(path) =
|
|
crate::Url::parse(source_uri.as_str()).ok().and_then(|u| crate::common::uri_to_file(&u))
|
|
else {
|
|
return Default::default();
|
|
};
|
|
let offset = TextSize::new(offset as u32);
|
|
let positions = component_instance.component_positions(&path, offset.into());
|
|
let model = slint::VecModel::from_iter(positions.iter().map(|g| ui::SelectionRectangle {
|
|
width: g.size.width,
|
|
height: g.size.height,
|
|
x: g.origin.x,
|
|
y: g.origin.y,
|
|
}));
|
|
slint::ModelRc::new(model)
|
|
}
|
|
|
|
fn select_element_node(
|
|
component_instance: &ComponentInstance,
|
|
selected_element: &common::ElementRcNode,
|
|
position: Option<LogicalPoint>,
|
|
) {
|
|
let (path, offset) = selected_element.path_and_offset();
|
|
|
|
select_element_at_source_code_position_impl(
|
|
component_instance,
|
|
path,
|
|
offset,
|
|
position,
|
|
SelectionNotification::Never, // We update directly;-)
|
|
);
|
|
|
|
if let Some(document_position) = lsp_element_node_position(selected_element) {
|
|
let to_lsp = preview::PREVIEW_STATE.with_borrow(|ps| ps.to_lsp.borrow().clone().unwrap());
|
|
to_lsp
|
|
.ask_editor_to_show_document(&document_position.0, document_position.1, false)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
// Return the real root element, skipping the WindowElement that might got added
|
|
pub fn root_element(component_instance: &ComponentInstance) -> ElementRc {
|
|
let root_element = component_instance.definition().root_component().root_element.clone();
|
|
if root_element.borrow().debug.is_empty() {
|
|
// The root element has no debug set if it is a window inserted by the compiler.
|
|
// That window will have one child -- the "real root", but it might
|
|
// have a few more compiler-generated nodes in front or behind the "real root"!
|
|
let child =
|
|
root_element.borrow().children.iter().find(|c| !c.borrow().debug.is_empty()).cloned();
|
|
child.unwrap_or(root_element)
|
|
} else {
|
|
root_element
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SelectionCandidate {
|
|
pub element: ElementRc,
|
|
pub debug_index: usize,
|
|
pub geometry: LogicalRect,
|
|
pub is_in_root_component: bool,
|
|
}
|
|
|
|
impl SelectionCandidate {
|
|
pub fn is_selected_element_node(&self, selection: &common::ElementRcNode) -> bool {
|
|
self.as_element_node().map(|en| en.path_and_offset()) == Some(selection.path_and_offset())
|
|
}
|
|
|
|
pub fn as_element_node(&self) -> Option<common::ElementRcNode> {
|
|
common::ElementRcNode::new(self.element.clone(), self.debug_index)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for SelectionCandidate {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "SelectionCandidate {{ {:?} }}@({:?})", self.as_element_node(), self.geometry)
|
|
}
|
|
}
|
|
|
|
// Traverse the element tree in reverse render order and collect information on
|
|
// all elements that "render" at the given x and y coordinates
|
|
fn collect_all_element_nodes_covering_impl(
|
|
position: LogicalPoint,
|
|
component_instance: &ComponentInstance,
|
|
current_element: &ElementRc,
|
|
result: &mut Vec<SelectionCandidate>,
|
|
) {
|
|
let ce = self_or_embedded_component_root(current_element);
|
|
|
|
for c in ce.borrow().children.iter().rev() {
|
|
collect_all_element_nodes_covering_impl(position, component_instance, c, result);
|
|
}
|
|
|
|
if let Some(geometry) = element_covers_point(position, component_instance, current_element) {
|
|
for (i, d) in ce.borrow().debug.iter().enumerate().rev() {
|
|
if !common::is_element_node_ignored(&d.node)
|
|
&& !d.node.source_file.path().starts_with("builtin:/")
|
|
{
|
|
// All nodes have the same geometry
|
|
result.push(SelectionCandidate {
|
|
element: ce.clone(),
|
|
debug_index: i,
|
|
is_in_root_component: false,
|
|
geometry,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn assign_is_in_root_component(candidates: &mut [SelectionCandidate]) {
|
|
let mut root_text_range: Option<i_slint_compiler::parser::TextRange> = None;
|
|
for sc in candidates.iter_mut().rev() {
|
|
let Some(en) = sc.as_element_node() else {
|
|
continue;
|
|
};
|
|
|
|
let node_text_range = en.with_element_node(|n| n.text_range());
|
|
if let Some(rtr) = root_text_range {
|
|
sc.is_in_root_component = rtr.contains_range(node_text_range);
|
|
} else {
|
|
root_text_range = Some(node_text_range);
|
|
sc.is_in_root_component = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn collect_all_element_nodes_covering(
|
|
position: LogicalPoint,
|
|
component_instance: &ComponentInstance,
|
|
) -> Vec<SelectionCandidate> {
|
|
let root_element = root_element(component_instance);
|
|
let mut elements = Vec::new();
|
|
collect_all_element_nodes_covering_impl(
|
|
position,
|
|
component_instance,
|
|
&root_element,
|
|
&mut elements,
|
|
);
|
|
|
|
assign_is_in_root_component(&mut elements);
|
|
|
|
elements
|
|
}
|
|
|
|
fn select_element_at_impl(
|
|
component_instance: &ComponentInstance,
|
|
position: LogicalPoint,
|
|
enter_component: bool,
|
|
) -> Option<common::ElementRcNode> {
|
|
for sc in &collect_all_element_nodes_covering(position, component_instance) {
|
|
if let Some(en) = filter_nodes_for_selection(sc, enter_component) {
|
|
return Some(en);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn select_element_at(x: f32, y: f32, enter_component: bool) {
|
|
let Some(component_instance) = super::component_instance() else {
|
|
return;
|
|
};
|
|
|
|
let position = LogicalPoint::new(x, y);
|
|
|
|
let Some(en) = select_element_at_impl(&component_instance, position, enter_component) else {
|
|
return;
|
|
};
|
|
|
|
select_element_node(&component_instance, &en, Some(position));
|
|
}
|
|
|
|
pub fn selection_stack_at(x: f32, y: f32) -> slint::ModelRc<ui::SelectionStackFrame> {
|
|
let Some(component_instance) = &super::component_instance() else {
|
|
return Default::default();
|
|
};
|
|
let root_element = root_element(component_instance);
|
|
let Some(root_geometry) = component_instance.element_positions(&root_element).first().cloned()
|
|
else {
|
|
return Default::default();
|
|
};
|
|
|
|
let position = LogicalPoint::new(x, y);
|
|
|
|
let (known_components, mut selected) = preview::PREVIEW_STATE.with(|preview_state| {
|
|
let preview_state = preview_state.borrow();
|
|
|
|
let known_components = preview_state.known_components.clone();
|
|
let selected =
|
|
preview_state.selected.as_ref().and_then(|s| s.as_element_node()).filter(|en| {
|
|
en.geometries(component_instance).iter().any(|gr| gr.contains(position))
|
|
});
|
|
|
|
(known_components, selected)
|
|
});
|
|
|
|
let mut longest_path_prefix = PathBuf::new();
|
|
|
|
let mut result = collect_all_element_nodes_covering(position, component_instance)
|
|
.iter()
|
|
.filter(|sn| filter_nodes_for_selection(sn, true).is_some())
|
|
.map(|sc| {
|
|
let (type_name, id, is_layout, is_selected, path, offset) = sc
|
|
.as_element_node()
|
|
.map(|en| {
|
|
let (path, offset) = en.path_and_offset();
|
|
let offset: u32 = offset.into();
|
|
|
|
let is_selected = if selected.is_none() {
|
|
select_element_node(component_instance, &en, Some(position));
|
|
selected = Some(en.clone());
|
|
true
|
|
} else {
|
|
selected.as_ref() == Some(&en)
|
|
};
|
|
|
|
let (type_name, id, is_layout) = en.with_element_debug(|di| {
|
|
let id = di
|
|
.node
|
|
.parent()
|
|
.and_then(|p| {
|
|
if p.kind() == SyntaxKind::SubElement {
|
|
p.child_token(SyntaxKind::Identifier)
|
|
.map(|t| t.text().to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let type_name = {
|
|
di.node
|
|
.parent()
|
|
.and_then(|p| {
|
|
if p.kind() == SyntaxKind::Component {
|
|
p.child_node(SyntaxKind::DeclaredIdentifier)
|
|
.map(|t| t.text().to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| {
|
|
di.node
|
|
.QualifiedName()
|
|
.map(|qn| qn.text().to_string().trim().to_string())
|
|
})
|
|
.unwrap_or_default()
|
|
.trim()
|
|
.to_string()
|
|
};
|
|
|
|
(type_name, id, di.layout.is_some())
|
|
});
|
|
|
|
(type_name, id, is_layout, is_selected, path, offset)
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if path.strip_prefix("/@").is_err() && path != PathBuf::new() {
|
|
if longest_path_prefix == PathBuf::new() {
|
|
longest_path_prefix = path.clone();
|
|
} else {
|
|
longest_path_prefix =
|
|
std::iter::zip(longest_path_prefix.components(), path.components())
|
|
.take_while(|(l, p)| l == p)
|
|
.map(|(l, _)| l)
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
let width = (sc.geometry.size.width / root_geometry.size.width) * 100.0;
|
|
let height = (sc.geometry.size.height / root_geometry.size.height) * 100.0;
|
|
let x = ((sc.geometry.origin.x + root_geometry.origin.x) / root_geometry.size.width)
|
|
* 100.0;
|
|
let y = ((sc.geometry.origin.y + root_geometry.origin.y) / root_geometry.size.height)
|
|
* 100.0;
|
|
|
|
let is_interactive = known_components
|
|
.iter()
|
|
.position(|kc| kc.name.as_str() == type_name.as_str())
|
|
.map(|index| known_components.get(index).unwrap().is_interactive)
|
|
.unwrap_or_default();
|
|
|
|
ui::SelectionStackFrame {
|
|
width,
|
|
height,
|
|
x,
|
|
y,
|
|
is_in_root_component: sc.is_in_root_component,
|
|
is_selected,
|
|
is_layout,
|
|
is_interactive,
|
|
type_name: type_name.into(),
|
|
file_name: path.to_string_lossy().to_string().into(),
|
|
element_path: path.to_string_lossy().to_string().into(),
|
|
element_offset: offset as i32,
|
|
id: id.into(),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
for frame in result.iter_mut() {
|
|
let file_name = PathBuf::from(frame.file_name.to_string());
|
|
let new_file_name = {
|
|
if let Some(library) = file_name.to_string_lossy().strip_prefix("/@") {
|
|
format!("@{library:?}")
|
|
} else if file_name == longest_path_prefix {
|
|
file_name.file_name().unwrap_or_default().to_string_lossy().to_string()
|
|
} else {
|
|
file_name
|
|
.strip_prefix(&longest_path_prefix)
|
|
.unwrap_or(&file_name)
|
|
.to_string_lossy()
|
|
.to_string()
|
|
}
|
|
};
|
|
frame.file_name = new_file_name.into();
|
|
}
|
|
|
|
Rc::new(slint::VecModel::from(result)).into()
|
|
}
|
|
|
|
pub fn filter_sort_selection_stack(
|
|
model: slint::ModelRc<ui::SelectionStackFrame>,
|
|
filter_text: slint::SharedString,
|
|
filter: ui::SelectionStackFilter,
|
|
) -> slint::ModelRc<ui::SelectionStackFrame> {
|
|
use slint::ModelExt;
|
|
use ui::{SelectionStackFilter, SelectionStackFrame};
|
|
|
|
fn filter_fn(frame: &SelectionStackFrame, filter: SelectionStackFilter) -> bool {
|
|
match filter {
|
|
SelectionStackFilter::Nothing => false,
|
|
SelectionStackFilter::Layouts => frame.is_layout,
|
|
SelectionStackFilter::Interactive => frame.is_interactive,
|
|
SelectionStackFilter::Others => !frame.is_interactive && !frame.is_layout,
|
|
SelectionStackFilter::LayoutsAndInteractive => frame.is_layout || frame.is_interactive,
|
|
SelectionStackFilter::LayoutsAndOthers => frame.is_layout || !frame.is_interactive,
|
|
SelectionStackFilter::InteractiveAndOthers => frame.is_interactive || !frame.is_layout,
|
|
SelectionStackFilter::Everything => true,
|
|
}
|
|
}
|
|
|
|
let filter_text = filter_text.to_string();
|
|
|
|
if filter_text.is_empty() && filter == SelectionStackFilter::Everything {
|
|
model
|
|
} else if filter_text.as_str().chars().any(|c| !c.is_lowercase()) {
|
|
Rc::new(model.filter(move |frame| {
|
|
filter_fn(frame, filter)
|
|
&& (frame.id.contains(&filter_text)
|
|
|| frame.type_name.contains(&filter_text)
|
|
|| frame.file_name.contains(&filter_text))
|
|
}))
|
|
.into()
|
|
} else {
|
|
Rc::new(model.filter(move |frame| {
|
|
filter_fn(frame, filter)
|
|
&& (frame.id.to_lowercase().contains(&filter_text)
|
|
|| frame.type_name.to_lowercase().contains(&filter_text)
|
|
|| frame.file_name.to_lowercase().contains(&filter_text))
|
|
}))
|
|
.into()
|
|
}
|
|
}
|
|
|
|
pub fn parent_layout_kind(element: &common::ElementRcNode) -> ui::LayoutKind {
|
|
element.parent().map(|p| p.layout_kind()).unwrap_or(ui::LayoutKind::None)
|
|
}
|
|
|
|
fn filter_nodes_for_selection(
|
|
selection_candidate: &SelectionCandidate,
|
|
enter_component: bool,
|
|
) -> Option<common::ElementRcNode> {
|
|
if !selection_candidate.is_in_root_component && !enter_component {
|
|
return None;
|
|
}
|
|
|
|
selection_candidate.as_element_node().filter(|en| {
|
|
en.with_element_node(|n| n.parent().is_none_or(|p| p.kind() != SyntaxKind::Component))
|
|
})
|
|
}
|
|
|
|
pub fn select_element_behind_impl(
|
|
component_instance: &ComponentInstance,
|
|
selected_element_node: &common::ElementRcNode,
|
|
position: LogicalPoint,
|
|
enter_component: bool,
|
|
reverse: bool,
|
|
) -> Option<common::ElementRcNode> {
|
|
let elements = collect_all_element_nodes_covering(position, component_instance);
|
|
let current_selection_position =
|
|
elements.iter().position(|sc| sc.is_selected_element_node(selected_element_node))?;
|
|
|
|
let (start_position, iterations) = if reverse {
|
|
let start_position = current_selection_position.saturating_sub(1);
|
|
(start_position, current_selection_position)
|
|
} else {
|
|
let start_position = current_selection_position + 1;
|
|
(start_position, elements.len().saturating_sub(current_selection_position + 1))
|
|
};
|
|
|
|
for i in 0..iterations {
|
|
let mapped_index = if reverse {
|
|
assert!(i <= start_position);
|
|
start_position - i
|
|
} else {
|
|
assert!(i + start_position < elements.len());
|
|
start_position + i
|
|
};
|
|
if let Some(en) =
|
|
filter_nodes_for_selection(elements.get(mapped_index).unwrap(), enter_component)
|
|
{
|
|
return Some(en);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn select_element_behind(x: f32, y: f32, enter_component: bool, reverse: bool) {
|
|
let Some(component_instance) = super::component_instance() else {
|
|
return;
|
|
};
|
|
let position = LogicalPoint::new(x, y);
|
|
let Some(selected_element_node) =
|
|
super::selected_element().and_then(|sel| sel.as_element_node())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let Some(en) = select_element_behind_impl(
|
|
&component_instance,
|
|
&selected_element_node,
|
|
position,
|
|
enter_component,
|
|
reverse,
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
select_element_node(&component_instance, &en, Some(position));
|
|
}
|
|
|
|
pub fn reselect_element() {
|
|
super::set_selected_element(super::selected_element(), SelectionNotification::Never);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::common::test;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use i_slint_core::lengths::LogicalPoint;
|
|
use slint_interpreter::ComponentInstance;
|
|
|
|
fn demo_app() -> ComponentInstance {
|
|
crate::preview::test::interpret_test(
|
|
"fluent",
|
|
r#"import { Button } from "std-widgets.slint";
|
|
|
|
component SomeComponent { // 69
|
|
@children
|
|
}
|
|
|
|
component Main { // 109
|
|
width: 200px;
|
|
height: 200px;
|
|
|
|
HorizontalLayout { // 160
|
|
Rectangle { // 194
|
|
SomeComponent { // 225
|
|
Button { // 264
|
|
text: "Press me";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component Entry inherits Main { /* @lsp:ignore-node */ } // 401
|
|
"#,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_covering_elements() {
|
|
let type_loader = demo_app();
|
|
|
|
let mut covers_center = super::collect_all_element_nodes_covering(
|
|
LogicalPoint::new(100.0, 100.0),
|
|
&type_loader,
|
|
);
|
|
|
|
// Remove the "button" implementation details. They must be at the start:
|
|
let button_path = PathBuf::from("builtin:/fluent/button.slint");
|
|
let first_non_button = covers_center
|
|
.iter()
|
|
.position(|sc| {
|
|
sc.as_element_node().map(|en| en.path_and_offset().0).as_ref() != Some(&button_path)
|
|
})
|
|
.unwrap();
|
|
covers_center.drain(0..first_non_button);
|
|
|
|
let test_file = test::test_file_name("test_data.slint");
|
|
|
|
let expected_offsets = [264_u32, 69, 225, 194, 160, 109];
|
|
assert_eq!(covers_center.len(), expected_offsets.len());
|
|
|
|
for (candidate, expected_offset) in covers_center.iter().zip(&expected_offsets) {
|
|
let (path, offset) = candidate.as_element_node().unwrap().path_and_offset();
|
|
assert_eq!(&path, &test_file);
|
|
assert_eq!(offset, (*expected_offset).into());
|
|
}
|
|
|
|
let covers_below = super::collect_all_element_nodes_covering(
|
|
LogicalPoint::new(100.0, 180.0),
|
|
&type_loader,
|
|
);
|
|
|
|
// All but the button itself as well as the SomeComponent (impl and use)
|
|
assert_eq!(covers_below.len(), covers_center.len() - 3);
|
|
|
|
for (below, center) in covers_below.iter().zip(&covers_center[3..]) {
|
|
assert_eq!(
|
|
below.as_element_node().map(|en| en.path_and_offset()),
|
|
center.as_element_node().map(|en| en.path_and_offset())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_element_selection() {
|
|
let component_instance = demo_app();
|
|
|
|
let covers_center = super::collect_all_element_nodes_covering(
|
|
LogicalPoint::new(100.0, 100.0),
|
|
&component_instance,
|
|
)
|
|
.iter()
|
|
.flat_map(|sc| sc.as_element_node())
|
|
.map(|en| en.path_and_offset())
|
|
.collect::<Vec<_>>();
|
|
|
|
eprintln!("Covers:");
|
|
for (i, (p, ts)) in covers_center.iter().enumerate() {
|
|
println!(" {i}: {p:?}:{ts:?}");
|
|
}
|
|
eprintln!("Done");
|
|
|
|
// Select without crossing boundaries
|
|
// --------------------------------------------------------------------
|
|
let select = super::select_element_at_impl(
|
|
&component_instance,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&select.path_and_offset(), covers_center.first().unwrap());
|
|
|
|
// Try to move towards the viewer:
|
|
assert!(super::select_element_behind_impl(
|
|
&component_instance,
|
|
&select,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
true
|
|
)
|
|
.is_none());
|
|
|
|
// Move deeper into the image:
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&select,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(2).unwrap());
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(3).unwrap());
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(4).unwrap());
|
|
|
|
assert!(super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
false
|
|
)
|
|
.is_none());
|
|
|
|
// Move towards the viewer:
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.get(3).unwrap());
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&prev,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.get(2).unwrap());
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&prev,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
false,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.first().unwrap());
|
|
|
|
// Select with crossing component boundaries
|
|
// --------------------------------------------------------------------
|
|
let select = super::select_element_at_impl(
|
|
&component_instance,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&select.path_and_offset(), covers_center.first().unwrap());
|
|
|
|
// Move deeper into the image:
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&select,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(2).unwrap());
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(3).unwrap());
|
|
let next = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
false,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&next.path_and_offset(), covers_center.get(4).unwrap());
|
|
|
|
assert!(super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
false
|
|
)
|
|
.is_none());
|
|
|
|
// Move towards the viewer:
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&next,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.get(3).unwrap());
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&prev,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.get(2).unwrap());
|
|
let prev = super::select_element_behind_impl(
|
|
&component_instance,
|
|
&prev,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(&prev.path_and_offset(), covers_center.first().unwrap());
|
|
|
|
assert!(super::select_element_behind_impl(
|
|
&component_instance,
|
|
&prev,
|
|
LogicalPoint::new(100.0, 100.0),
|
|
true,
|
|
true
|
|
)
|
|
.is_none());
|
|
}
|
|
}
|