lsp: Use a more "visual" element selection for Preview UI

Try a more visual selection approach over the rather technical
tree-based one from earlier.

In this commit, a click selects the element that was rendered at the
click position *last*. I use rendered loosely here: It even takes
elements into account that are invisible but still cover the clicked
location.

A double-click punshes through to the item rendered earlier (behind) the
currently selected item that also covers the clicked position.

A shift-double-click moves towards the later rendered elements again,
usually undoing the last double-click.
This commit is contained in:
Tobias Hunger 2024-01-21 22:12:03 +01:00 committed by Tobias Hunger
parent e93b307643
commit d50373951f
7 changed files with 142 additions and 152 deletions

View file

@ -390,7 +390,7 @@ pub fn reset_selections(ui: &ui::PreviewUi) {
pub fn set_selections( pub fn set_selections(
ui: Option<&ui::PreviewUi>, ui: Option<&ui::PreviewUi>,
element_position: Option<(&ElementRc, LogicalRect)>, element_position: Option<(&ElementRc, LogicalRect, usize)>,
positions: ComponentPositions, positions: ComponentPositions,
) { ) {
let Some(ui) = ui else { let Some(ui) = ui else {
@ -402,7 +402,7 @@ pub fn set_selections(
positions.geometries.len() + if element_position.is_some() { 1 } else { 0 }, positions.geometries.len() + if element_position.is_some() { 1 } else { 0 },
); );
if let Some((e, primary_position)) = element_position.as_ref() { if let Some((e, primary_position, _)) = element_position.as_ref() {
let border_color = if e.borrow().layout.is_some() { let border_color = if e.borrow().layout.is_some() {
i_slint_core::Color::from_argb_encoded(0xffff0000) i_slint_core::Color::from_argb_encoded(0xffff0000)
} else { } else {

View file

@ -63,74 +63,26 @@ pub fn unselect_element() {
} }
fn select_element( fn select_element(
x: f32,
y: f32,
component_instance: &ComponentInstance, component_instance: &ComponentInstance,
selected_element: Option<&ElementRc>, selected_element: &ElementRc,
) -> Option<ElementRc> { layer: usize,
let click_position = LogicalPoint::from_lengths(LogicalLength::new(x), LogicalLength::new(y)); ) {
eprintln!(" select_element({}, {layer})", selected_element.borrow().id);
let Some(c) = selected_element else { let Some(position) = component_instance.element_position(&selected_element) else {
unselect_element(); return;
return None;
}; };
let Some(position) = component_instance.element_position(&c) else { let secondary_positions = if let Some((path, offset)) = element_offset(selected_element) {
return None; component_instance.component_positions(path, offset)
} else {
ComponentPositions::default()
}; };
if position.contains(click_position) {
let secondary_positions = if let Some((path, offset)) = element_offset(&c) {
component_instance.component_positions(path, offset)
} else {
ComponentPositions::default()
};
super::set_selected_element(Some((&c, position)), secondary_positions); super::set_selected_element(Some((&selected_element, position, layer)), secondary_positions);
let document_position = lsp_element_position(&c); let document_position = lsp_element_position(&selected_element);
if !document_position.0.is_empty() { if !document_position.0.is_empty() {
super::ask_editor_to_show_document(document_position.0, document_position.1); super::ask_editor_to_show_document(document_position.0, document_position.1);
}
return Some(c.clone());
} }
None
}
// triggered from the UI, running in UI thread
pub fn select_element_at_impl(
x: f32,
y: f32,
component_instance: &ComponentInstance,
root_element: &ElementRc,
current_element: Option<&ElementRc>,
reverse: bool,
) -> Option<ElementRc> {
let re = root_element.borrow();
let mut fw_iter = re.children.iter();
let mut bw_iter = re.children.iter().rev();
let iterator: &mut dyn Iterator<Item = &ElementRc> =
if reverse { &mut bw_iter } else { &mut fw_iter };
let mut skip = current_element.is_some();
for c in &mut *iterator {
let c = self_or_embedded_component_root(c);
if skip {
if let Some(ce) = current_element {
if Rc::ptr_eq(ce, &c) {
skip = false;
}
}
continue;
}
if let Some(result) = select_element(x, y, component_instance, Some(&c)) {
return Some(result);
}
}
None
} }
fn element_offset(element: &ElementRc) -> Option<(PathBuf, u32)> { fn element_offset(element: &ElementRc) -> Option<(PathBuf, u32)> {
@ -176,23 +128,84 @@ fn root_element(component_instance: &ComponentInstance) -> ElementRc {
} }
} }
fn parent_element(root_element: &ElementRc, element: &ElementRc) -> Option<ElementRc> { fn visit_tree_element(
for c in &root_element.borrow().children { x: f32,
if Rc::ptr_eq(c, element) { y: f32,
return Some(root_element.clone()); component_instance: &ComponentInstance,
root_element: &ElementRc,
current_element: &ElementRc,
target_layer: usize,
current_layer: usize,
switch_files: bool,
previous: &(usize, Option<ElementRc>),
) -> ((usize, Option<ElementRc>), (usize, Option<ElementRc>)) {
let ce = self_or_embedded_component_root(current_element);
let mut current_layer = current_layer;
let mut previous = previous.clone();
for c in ce.borrow().children.iter().rev() {
let (p, (ncl, fe)) = visit_tree_element(
x,
y,
component_instance,
root_element,
c,
target_layer,
current_layer,
switch_files,
&previous,
);
current_layer = ncl;
previous = p;
if fe.is_some() {
return (previous, (current_layer, fe));
} }
} }
for c in &root_element.borrow().children { if element_covers_point(x, y, component_instance, &ce)
if let Some(p) = parent_element(c, element) { && !Rc::ptr_eq(current_element, root_element)
return Some(p); {
current_layer += 1;
let same_source = (|| {
let Some(re) = &root_element.borrow().node else {
return false;
};
let Some(ce) = &ce.borrow().node else {
return false;
};
Rc::ptr_eq(&re.source_file, &ce.source_file)
})();
let file_ok = switch_files || same_source;
if file_ok && current_layer < target_layer && current_layer > previous.0 {
eprintln!(
" visit: {x},{y} (target: {target_layer}/{current_layer}): {} => Found prev candidate in self!",
ce.borrow().id
);
previous = (current_layer, Some(ce.clone()))
}
if file_ok && current_layer > target_layer {
eprintln!(
" visit: {x},{y} (target: {target_layer}/{current_layer}): {} => Found next in self!",
ce.borrow().id
);
return (previous, (current_layer, Some(ce)));
}
if file_ok && current_layer < target_layer {
previous = (current_layer, Some(ce.clone()));
} }
} }
None // eprintln!(" visit: {x},{y} (target: {target_layer}/{current_layer}): {} => mot found", current_element.borrow().id);
(previous, (current_layer, None))
} }
// triggered from the UI, running in UI thread
pub fn select_element_at(x: f32, y: f32) { pub fn select_element_at(x: f32, y: f32) {
let Some(component_instance) = super::component_instance() else { let Some(component_instance) = super::component_instance() else {
return; return;
@ -200,75 +213,54 @@ pub fn select_element_at(x: f32, y: f32) {
let root_element = root_element(&component_instance); let root_element = root_element(&component_instance);
if let Some(selected_element) = super::selected_element() { if let Some((selected_element, _)) = super::selected_element() {
if element_covers_point(x, y, &component_instance, &selected_element) { if element_covers_point(x, y, &component_instance, &selected_element) {
// We clicked on the already selected element: Do nothing! // We clicked on the already selected element: Do nothing!
return; return;
} }
let mut parent = parent_element(&root_element, &selected_element);
while let Some(p) = &parent {
if select_element_at_impl(x, y, &component_instance, p, None, true).is_some() {
return;
}
parent = parent_element(&root_element, p);
}
} }
select_element_at_impl(x, y, &component_instance, &root_element, None, true); let (_, (layer, next)) = visit_tree_element(
x,
y,
&component_instance,
&root_element,
&root_element,
0,
0,
false,
&(0, None),
);
if let Some(n) = next {
select_element(&component_instance, &n, layer);
}
} }
// triggered from the UI, running in UI thread pub fn select_element_behind(x: f32, y: f32, switch_files: bool, reverse: bool) {
pub fn select_element_down(x: f32, y: f32, reverse: bool) {
let Some(component_instance) = super::component_instance() else { let Some(component_instance) = super::component_instance() else {
return; return;
}; };
// We have an actively selected element (via the earlier click-event :-): let root_element = root_element(&component_instance);
let Some(selected_element) = super::selected_element() else { let target_layer = super::selected_element().map(|(_, l)| l).unwrap_or_default();
return; eprintln!("select_element_behind: {x},{y} (switch: {switch_files}, reverse: {reverse}), target: {target_layer}");
};
if !reverse { let (previous, next) = visit_tree_element(
let _ = select_element_at_impl(x, y, &component_instance, &selected_element, None, true); x,
} else { y,
if element_covers_point(x, y, &component_instance, &selected_element) { &component_instance,
let _ = select_element( &root_element,
x, &root_element,
y, target_layer,
&component_instance, 0,
parent_element(&root_element(&component_instance), &selected_element).as_ref(), switch_files,
); &(0, None),
} );
} eprintln!("select_element_behind: {x},{y} (switch: {switch_files}, reverse: {reverse}) => Prev: {:?}, Next: {:?}", previous.1.as_ref().map(|e| e.borrow().id.clone()), next.1.as_ref().map(|e| e.borrow().id.clone()));
} let to_select = if reverse { previous } else { next };
eprintln!("select_element_behind: {x},{y} (switch: {switch_files}, reverse: {reverse}) => To select: {:?}", to_select.1.as_ref().map(|e| e.borrow().id.clone()));
// triggered from the UI, running in UI thread if let (layer, Some(ts)) = to_select {
pub fn select_element_front_to_back(x: f32, y: f32, reverse: bool) { eprintln!("select_element_behind: {x},{y} (switch: {switch_files}, reverse: {reverse}) => SETTING {}@{layer}", ts.borrow().id);
let Some(component_instance) = super::component_instance() else { select_element(&component_instance, &ts, layer);
return;
};
// We have an actively selected element (via the earlier click-event :-):
let Some(selected_element) = super::selected_element() else {
return;
};
if element_covers_point(x, y, &component_instance, &selected_element) {
let Some(parent_element) =
parent_element(&root_element(&component_instance), &selected_element)
else {
return;
};
// We clicked on the already selected element: Do nothing!
let _ = select_element_at_impl(
x,
y,
&component_instance,
&parent_element,
Some(&selected_element),
!reverse,
);
return;
} }
} }

View file

@ -211,33 +211,36 @@ struct PreviewState {
ui: Option<super::ui::PreviewUi>, ui: Option<super::ui::PreviewUi>,
handle: Rc<RefCell<Option<ComponentInstance>>>, handle: Rc<RefCell<Option<ComponentInstance>>>,
selected_element: Option<ElementWeak>, selected_element: Option<ElementWeak>,
selected_layer: usize,
} }
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();} thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
pub fn set_selected_element( pub fn set_selected_element(
element_position: Option<(&ElementRc, LogicalRect)>, element_position: Option<(&ElementRc, LogicalRect, usize)>,
positions: slint_interpreter::highlight::ComponentPositions, positions: slint_interpreter::highlight::ComponentPositions,
) { ) {
PREVIEW_STATE.with(move |preview_state| { PREVIEW_STATE.with(move |preview_state| {
let mut preview_state = preview_state.borrow_mut(); let mut preview_state = preview_state.borrow_mut();
if let Some((e, _)) = element_position.as_ref() { if let Some((e, _, l)) = element_position.as_ref() {
preview_state.selected_element = Some(Rc::downgrade(e)); preview_state.selected_element = Some(Rc::downgrade(e));
preview_state.selected_layer = *l;
} else { } else {
preview_state.selected_element = None; preview_state.selected_element = None;
preview_state.selected_layer = 0;
} }
super::set_selections(preview_state.ui.as_ref(), element_position, positions); super::set_selections(preview_state.ui.as_ref(), element_position, positions);
}) })
} }
pub fn selected_element() -> Option<ElementRc> { pub fn selected_element() -> Option<(ElementRc, usize)> {
PREVIEW_STATE.with(move |preview_state| { PREVIEW_STATE.with(move |preview_state| {
let preview_state = preview_state.borrow(); let preview_state = preview_state.borrow();
let Some(weak) = &preview_state.selected_element else { let Some(weak) = &preview_state.selected_element else {
return None; return None;
}; };
Weak::upgrade(&weak) Weak::upgrade(&weak).map(|e| (e, preview_state.selected_layer))
}) })
} }

View file

@ -44,8 +44,7 @@ pub fn create_ui(style: String) -> Result<PreviewUi, PlatformError> {
}); });
ui.on_unselect(super::element_selection::unselect_element); ui.on_unselect(super::element_selection::unselect_element);
ui.on_select_at(super::element_selection::select_element_at); ui.on_select_at(super::element_selection::select_element_at);
ui.on_select_front_to_back(super::element_selection::select_element_front_to_back); ui.on_select_behind(super::element_selection::select_element_behind);
ui.on_select_down(super::element_selection::select_element_down);
ui.on_can_drop(super::can_drop_component); ui.on_can_drop(super::can_drop_component);
ui.on_drop(super::drop_component); ui.on_drop(super::drop_component);

View file

@ -48,16 +48,17 @@ struct PreviewState {
lsp_notifier: Option<SignalLspFunction>, lsp_notifier: Option<SignalLspFunction>,
resource_url_mapper: Option<ResourceUrlMapperFunction>, resource_url_mapper: Option<ResourceUrlMapperFunction>,
selected_element: Option<ElementWeak>, selected_element: Option<ElementWeak>,
selected_layer: usize,
} }
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();} thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
pub fn selected_element() -> Option<ElementRc> { pub fn selected_element() -> Option<(ElementRc, usize)> {
PREVIEW_STATE.with(move |preview_state| { PREVIEW_STATE.with(move |preview_state| {
let preview_state = preview_state.borrow(); let preview_state = preview_state.borrow();
let Some(weak) = &preview_state.selected_element else { let Some(weak) = &preview_state.selected_element else {
return None; return None;
}; };
Weak::upgrade(&weak) Weak::upgrade(&weak).map(|e| (e, preview_state.selected_layer))
}) })
} }
@ -68,15 +69,17 @@ pub fn component_instance() -> Option<ComponentInstance> {
} }
pub fn set_selected_element( pub fn set_selected_element(
element_position: Option<(&ElementRc, LogicalRect)>, element_position: Option<(&ElementRc, LogicalRect, usize)>,
positions: ComponentPositions, positions: ComponentPositions,
) { ) {
PREVIEW_STATE.with(move |preview_state| { PREVIEW_STATE.with(move |preview_state| {
let mut preview_state = preview_state.borrow_mut(); let mut preview_state = preview_state.borrow_mut();
if let Some((e, _)) = element_position.as_ref() { if let Some((e, _, l)) = element_position.as_ref() {
preview_state.selected_element = Some(Rc::downgrade(e)); preview_state.selected_element = Some(Rc::downgrade(e));
preview_state.selected_layer = *l;
} else { } else {
preview_state.selected_element = None; preview_state.selected_element = None;
preview_state.selected_layer = 0;
} }
super::set_selections(preview_state.ui.as_ref(), element_position, positions); super::set_selections(preview_state.ui.as_ref(), element_position, positions);

View file

@ -36,8 +36,7 @@ export component DrawArea {
out property <length> preview-area-height: preview-visible ? i-preview-area-container.height : 0px; out property <length> preview-area-height: preview-visible ? i-preview-area-container.height : 0px;
callback select-at(/* x */ length, /* y */ length); callback select-at(/* x */ length, /* y */ length);
callback select-front-to-back(/* x */ length, /* y */ length, /* reverse? */ bool); callback select-behind(/* x */ length, /* y */ length, /* stay_in_file? */ bool, /* reverse */ bool);
callback select-down(/* x */ length, /* y */ length, /* reverse? */ bool);
callback show-document(/* url */ string, /* line */ int, /* column */ int); callback show-document(/* url */ string, /* line */ int, /* column */ int);
callback unselect(); callback unselect();
@ -121,13 +120,9 @@ export component DrawArea {
// This needs to fire AFTER clicked and double-clicked to work :-/ // This needs to fire AFTER clicked and double-clicked to work :-/
if (event.kind == PointerEventKind.up) { if (event.kind == PointerEventKind.up) {
if (self.selection-kind == SelectionKind.select_up_or_down) { if (self.selection-kind == SelectionKind.select_up_or_down) {
root.select_down(self.selection-x, self.selection-y, event.modifiers.shift); root.select_behind(self.selection-x, self.selection-y, event.modifiers.control, event.modifiers.shift);
} else if (self.selection-kind == SelectionKind.select-at) { } else if (self.selection-kind == SelectionKind.select-at) {
if (event.modifiers.control) { root.select-at(self.selection-x, self.selection-y);
root.select-front-to-back(self.selection-x, self.selection-y, event.modifiers.shift);
} else {
root.select-at(self.selection-x, self.selection-y);
}
} }
} }
self.selection-kind = SelectionKind.none; self.selection-kind = SelectionKind.none;

View file

@ -26,8 +26,7 @@ export component PreviewUi inherits Window {
callback can-drop(/* component_type */ string, /* x */ length, /* y */ length) -> bool; callback can-drop(/* component_type */ string, /* x */ length, /* y */ length) -> bool;
callback drop(/* component_type */ string, /* x */ length, /* y */ length); callback drop(/* component_type */ string, /* x */ length, /* y */ length);
callback select-at(/* x */ length, /* y */ length); callback select-at(/* x */ length, /* y */ length);
callback select-down(/* x */ length, /* y */ length, /* upwards? */ bool); callback select-behind(/* x */ length, /* y */ length, /* stay_in_file* */ bool, /* reverse */ bool);
callback select-front-to-back(/* x */ length, /* y */ length, /* backwards? */ bool);
callback show-document(/* url */ string, /* line */ int, /* column */ int); callback show-document(/* url */ string, /* line */ int, /* column */ int);
callback style-changed(); callback style-changed();
callback unselect(); callback unselect();
@ -110,8 +109,7 @@ export component PreviewUi inherits Window {
selections <=> root.selections; selections <=> root.selections;
select-at(x, y) => { root.select-at(x, y); } select-at(x, y) => { root.select-at(x, y); }
select-front-to-back(x, y, front) => { root.select-front-to-back(x, y, front); } select-behind(x, y, stay_in_file, reverse) => { root.select-behind(x, y, stay_in_file, reverse); }
select-down(x, y, up) => { root.select-down(x, y, up); }
show-document(url, line, column) => { root.show-document(url, line, column); } show-document(url, line, column) => { root.show-document(url, line, column); }
unselect() => { root.unselect(); } unselect() => { root.unselect(); }
} }