From e10e97c94414c1103d127c59e599b6783565260d Mon Sep 17 00:00:00 2001 From: Tobias Hunger Date: Wed, 24 Apr 2024 14:13:58 +0200 Subject: [PATCH] live-preview: Add placeholder when removing the last eleemnt in a layout Add a placeholder Rectangle into a layout whenever the last eleemnt is removed. This makes sure we can drop into the Layout again. Add infrastructure to find the parent element to ElementRcNode and move more code into the ElementRcNode. --- internal/core/software_renderer.rs | 8 +-- tools/lsp/common.rs | 52 ++++++++++++++ tools/lsp/preview.rs | 53 +++++++++----- tools/lsp/preview/drop_location.rs | 95 +++++++++++--------------- tools/lsp/preview/element_selection.rs | 36 +++++----- 5 files changed, 152 insertions(+), 92 deletions(-) diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index b1b9e7247..a3d6e1b68 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -1172,10 +1172,10 @@ fn prepare_scene( } dirty_region = (renderer.dirty_region.to_rect().cast() * factor) - .round_out() - .intersection(&euclid::rect(0., 0., i16::MAX as f32, i16::MAX as f32)) - .unwrap_or_default() - .cast(); + .round_out() + .intersection(&euclid::rect(0., 0., i16::MAX as f32, i16::MAX as f32)) + .unwrap_or_default() + .cast(); dirty_region = software_renderer.apply_dirty_region(dirty_region, size); diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index f4f50ef9f..05a21ea0a 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -5,6 +5,7 @@ use i_slint_compiler::diagnostics::{SourceFile, SourceFileVersion}; use i_slint_compiler::object_tree::ElementRc; +use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode}; use lsp_types::{TextEdit, Url, WorkspaceEdit}; use std::{collections::HashMap, path::PathBuf}; @@ -16,6 +17,17 @@ pub type UrlVersion = Option; #[cfg(target_arch = "wasm32")] use crate::wasm_prelude::*; +pub fn extract_element(node: SyntaxNode) -> Option { + match node.kind() { + SyntaxKind::Element => Some(node.into()), + SyntaxKind::SubElement => extract_element(node.child_node(SyntaxKind::Element)?), + SyntaxKind::ConditionalElement | SyntaxKind::RepeatedElement => { + extract_element(node.child_node(SyntaxKind::SubElement)?) + } + _ => None, + } +} + #[derive(Clone)] pub struct ElementRcNode { pub element: ElementRc, @@ -100,6 +112,46 @@ impl ElementRcNode { pub fn as_element(&self) -> &ElementRc { &self.element } + + pub fn parent(&self, root_element: ElementRc) -> Option { + let parent = self.with_element_node(|node| { + let mut ancestor = node.parent()?; + loop { + if ancestor.kind() == SyntaxKind::Element { + return Some(ancestor); + } + ancestor = ancestor.parent()?; + } + })?; + + let (parent_path, parent_offset) = + (parent.source_file.path().to_owned(), u32::from(parent.text_range().start())); + Self::find_in_or_below(root_element, &parent_path, parent_offset) + } + + pub fn children(&self) -> Vec { + self.with_element_node(|node| { + let mut children = Vec::new(); + for c in node.children() { + if let Some(element) = extract_element(c.clone()) { + let e_path = element.source_file.path().to_path_buf(); + let e_offset = u32::from(element.text_range().start()); + + let Some(child_node) = ElementRcNode::find_in_or_below( + self.as_element().clone(), + &e_path, + e_offset, + ) else { + continue; + }; + + children.push(child_node); + } + } + + children + }) + } } pub fn create_workspace_edit( diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index 9ca076bd2..e3b12c948 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -6,7 +6,7 @@ use crate::lsp_ext::Health; use crate::preview::element_selection::ElementSelection; use crate::util; use i_slint_compiler::object_tree::ElementRc; -use i_slint_compiler::parser::{syntax_nodes::Element, SyntaxKind}; +use i_slint_compiler::parser::{syntax_nodes, SyntaxKind}; use i_slint_core::component_factory::FactoryContext; use i_slint_core::lengths::{LogicalLength, LogicalPoint}; use i_slint_core::model::VecModel; @@ -168,6 +168,22 @@ fn drop_component(component_type: slint::SharedString, x: f32, y: f32) { }; } +fn placeholder_node_text(selected: &common::ElementRcNode) -> String { + let Some(component_instance) = component_instance() else { + return Default::default(); + }; + let root_element = element_selection::root_element(&component_instance); + let Some(parent) = selected.parent(root_element) else { + return Default::default(); + }; + + if parent.layout_kind() != ui::LayoutKind::None && parent.children().len() == 1 { + return format!("Rectangle {{ /* {} */ }}", NODE_IGNORE_COMMENT); + } + + Default::default() +} + // triggered from the UI, running in UI thread fn delete_selected_element() { let Some(selected) = selected_element() else { @@ -183,24 +199,26 @@ fn delete_selected_element() { return; }; - let Some(range) = selected.as_element_node().and_then(|en| { - en.with_element_node(|n| { - if let Some(parent) = &n.parent() { - if parent.kind() == SyntaxKind::SubElement { - return util::map_node(parent); - } + let Some(selected_node) = selected.as_element_node() else { + return; + }; + + let Some(range) = selected_node.with_element_node(|n| { + if let Some(parent) = &n.parent() { + if parent.kind() == SyntaxKind::SubElement { + return util::map_node(parent); } - util::map_node(n) - }) + } + util::map_node(n) }) else { return; }; - let edit = common::create_workspace_edit( - url, - version, - vec![lsp_types::TextEdit { range, new_text: "".into() }], - ); + // Insert a placeholder node into layouts if those end up empty: + let new_text = placeholder_node_text(&selected_node); + + let edit = + common::create_workspace_edit(url, version, vec![lsp_types::TextEdit { range, new_text }]); send_message_to_lsp(crate::common::PreviewToLspMessage::SendWorkspaceEdit { label: Some("Delete element".to_string()), @@ -501,7 +519,8 @@ async fn reload_preview_impl( let (_, from_cache) = get_url_from_cache(&component.url).unwrap_or_default(); if let Some(component_name) = &component.component { format!( - "{from_cache}\nexport component _SLINT_LivePreview inherits {component_name} {{ /* {NODE_IGNORE_COMMENT} */ }}\n", + "{from_cache}\nexport component _SLINT_LivePreview inherits {component_name} {{ /* {} */ }}\n", + NODE_IGNORE_COMMENT, ) } else { from_cache @@ -831,11 +850,11 @@ pub fn lsp_to_preview_message( /// Use this in nodes you want the language server and preview to /// ignore a node for code analysis purposes. -const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; +pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; /// Check whether a node is marked to be ignored in the LSP/live preview /// using a comment containing `@lsp:ignore-node` -fn is_element_node_ignored(node: &Element) -> bool { +pub fn is_element_node_ignored(node: &syntax_nodes::Element) -> bool { node.children_with_tokens().any(|nt| { nt.as_token() .map(|t| t.kind() == SyntaxKind::Comment && t.text().contains(NODE_IGNORE_COMMENT)) diff --git a/tools/lsp/preview/drop_location.rs b/tools/lsp/preview/drop_location.rs index eab36f992..9cfa7d32c 100644 --- a/tools/lsp/preview/drop_location.rs +++ b/tools/lsp/preview/drop_location.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial use i_slint_compiler::diagnostics::SourceFile; -use i_slint_compiler::parser::{syntax_nodes, SyntaxKind, SyntaxNode}; +use i_slint_compiler::parser::SyntaxKind; use i_slint_core::lengths::{LogicalPoint, LogicalRect, LogicalSize}; use slint_interpreter::ComponentInstance; @@ -163,11 +163,13 @@ fn calculate_drop_information_for_layout( LogicalSize::new(new_midpoint - last_midpoint, geometry.size.height), ); if hit_rect.contains(position) { - let start_pos = if pos == 0 { - geometry.origin.x - } else { - last_endpoint + (c.origin.x - last_endpoint) / 2.0 - }; + let start = (c.origin.x - last_endpoint) / 2.0; + let start_pos = last_endpoint + + if start.floor() < geometry.origin.x { + geometry.origin.x + } else { + start + }; let end_pos = start_pos + 1.0; return ( @@ -224,11 +226,13 @@ fn calculate_drop_information_for_layout( LogicalSize::new(geometry.size.width, new_midpoint - last_midpoint), ); if hit_rect.contains(position) { - let start_pos = if pos == 0 { - geometry.origin.y - } else { - last_endpoint + (c.origin.y - last_endpoint) / 2.0 - }; + let start = (c.origin.y - last_endpoint) / 2.0; + let start_pos = last_endpoint + + if start.floor() < geometry.origin.y { + geometry.origin.y + } else { + start + }; let end_pos = start_pos + 1.0; return ( @@ -427,17 +431,6 @@ fn drop_target_element_nodes( result } -fn extract_element(node: SyntaxNode) -> Option { - match node.kind() { - SyntaxKind::Element => Some(node.into()), - SyntaxKind::SubElement => extract_element(node.child_node(SyntaxKind::Element)?), - SyntaxKind::ConditionalElement | SyntaxKind::RepeatedElement => { - extract_element(node.child_node(SyntaxKind::SubElement)?) - } - _ => None, - } -} - fn find_element_to_drop_into( component_instance: &ComponentInstance, position: LogicalPoint, @@ -484,40 +477,18 @@ fn find_drop_location( let layout_kind = drop_target_node.layout_kind(); if layout_kind != ui::LayoutKind::None { let geometry = drop_target_node.geometry_at(component_instance, position)?; - let children_information: Vec<_> = drop_target_node.with_element_node(|node| { - let mut children_info = Vec::new(); - for c in node.children() { - if let Some(element) = extract_element(c.clone()) { - if preview::is_element_node_ignored(&element) { - continue; - } - - let e_path = element.source_file.path().to_path_buf(); - let e_offset = u32::from(element.text_range().start()); - - let Some(child_node) = common::ElementRcNode::find_in_or_below( - drop_target_node.as_element().clone(), - &e_path, - e_offset, - ) else { - continue; - }; - let Some(c_geometry) = child_node.geometry_in(component_instance, &geometry) - else { - continue; - }; - children_info.push(c_geometry); - } - } - - children_info - }); + let children_geometries: Vec<_> = drop_target_node + .children() + .iter() + .filter(|c| !c.with_element_node(preview::is_element_node_ignored)) + .filter_map(|c| c.geometry_in(component_instance, &geometry)) + .collect(); let (drop_mark, child_index) = calculate_drop_information_for_layout( &geometry, position, &layout_kind, - &children_information, + &children_geometries, ); let insert_info = { @@ -566,14 +537,22 @@ fn drop_ignored_elements_from_node( node.with_element_node(|node| { node.children() .filter_map(|c| { - let e = extract_element(c.clone())?; + let e = common::extract_element(c.clone())?; if preview::is_element_node_ignored(&e) { let first_et = e.first_token()?; let before_et = first_et.prev_token()?; let start_pos = if before_et.kind() == SyntaxKind::Whitespace && before_et.text().contains('\n') { - e.text_range().start() // Leave WS in place, so that the next token after us can go into our place + before_et.text_range().end() + - rowan::TextSize::from( + before_et + .text() + .split('\n') + .last() + .map(|s| s.len()) + .unwrap_or_default() as u32, + ) } else if before_et.kind() == SyntaxKind::Whitespace { before_et.text_range().start() // Cut away all WS! } else { @@ -585,7 +564,15 @@ fn drop_ignored_elements_from_node( let end_pos = if after_et.kind() == SyntaxKind::Whitespace && after_et.text().contains('\n') { - after_et.text_range().end() // Eat all the WS so that the next non-WS is at our start + after_et.text_range().start() + + rowan::TextSize::from( + after_et + .text() + .split('\n') + .next() + .map(|s| s.len() + 1) + .unwrap_or_default() as u32, + ) } else { last_et.text_range().end() // Use existing WS or not WS as appropriate }; diff --git a/tools/lsp/preview/element_selection.rs b/tools/lsp/preview/element_selection.rs index 203711ae6..c5313c5ff 100644 --- a/tools/lsp/preview/element_selection.rs +++ b/tools/lsp/preview/element_selection.rs @@ -7,7 +7,7 @@ use i_slint_compiler::object_tree::{Component, ElementRc}; use i_slint_core::lengths::LogicalPoint; use slint_interpreter::ComponentInstance; -use crate::common::ElementRcNode; +use crate::{common, preview}; #[derive(Clone, Debug)] pub struct ElementSelection { @@ -25,7 +25,7 @@ impl ElementSelection { elements.get(self.instance_index).or_else(|| elements.first()).map(|(e, _)| e.clone()) } - pub fn as_element_node(&self) -> Option { + pub fn as_element_node(&self) -> Option { let element = self.as_element()?; let debug_index = { @@ -36,7 +36,7 @@ impl ElementSelection { }) }; - debug_index.map(|i| ElementRcNode { element, debug_index: i }) + debug_index.map(|i| common::ElementRcNode { element, debug_index: i }) } } @@ -52,7 +52,9 @@ fn self_or_embedded_component_root(element: &ElementRc) -> ElementRc { element.clone() } -fn lsp_element_node_position(element: &ElementRcNode) -> Option<(String, lsp_types::Range)> { +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) @@ -122,7 +124,7 @@ fn select_element_at_source_code_position_impl( fn select_element_node( component_instance: &ComponentInstance, - selected_element: &ElementRcNode, + selected_element: &common::ElementRcNode, position: Option, ) { let (path, offset) = selected_element.path_and_offset(); @@ -158,12 +160,12 @@ pub struct SelectionCandidate { } impl SelectionCandidate { - pub fn is_selected_element_node(&self, selection: &ElementRcNode) -> bool { + 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 { - ElementRcNode::new(self.element.clone(), self.debug_index) + pub fn as_element_node(&self) -> Option { + common::ElementRcNode::new(self.element.clone(), self.debug_index) } } @@ -241,14 +243,14 @@ pub fn collect_all_element_nodes_covering( pub fn is_root_element_node( component_instance: &ComponentInstance, - element_node: &ElementRcNode, + element_node: &common::ElementRcNode, ) -> bool { let root_element = root_element(component_instance); let Some((root_path, root_offset)) = root_element .borrow() .debug .iter() - .find(|(n, _)| !super::is_element_node_ignored(n)) + .find(|(n, _)| !preview::is_element_node_ignored(n)) .map(|(n, _)| (n.source_file.path().to_owned(), u32::from(n.text_range().start()))) else { return false; @@ -260,7 +262,7 @@ pub fn is_root_element_node( pub fn is_same_file_as_root_node( component_instance: &ComponentInstance, - element_node: &ElementRcNode, + element_node: &common::ElementRcNode, ) -> bool { let root_element = root_element(component_instance); let Some(root_path) = @@ -277,7 +279,7 @@ fn select_element_at_impl( component_instance: &ComponentInstance, position: LogicalPoint, enter_component: bool, -) -> Option { +) -> Option { for sc in &collect_all_element_nodes_covering(position, component_instance) { if let Some(en) = filter_nodes_for_selection(component_instance, sc, enter_component) { return Some(en); @@ -309,7 +311,7 @@ pub fn select_element_at(x: f32, y: f32, enter_component: bool) { select_element_node(&component_instance, &en, Some(position)); } -pub fn is_element_node_in_layout(element: &ElementRcNode) -> bool { +pub fn is_element_node_in_layout(element: &common::ElementRcNode) -> bool { if element.debug_index > 0 { // If we are not the first node, then we might have been inlined right // after a layout managing us @@ -336,10 +338,10 @@ fn filter_nodes_for_selection( component_instance: &ComponentInstance, selection_candidate: &SelectionCandidate, enter_component: bool, -) -> Option { +) -> Option { let en = selection_candidate.as_element_node()?; - if en.with_element_node(super::is_element_node_ignored) { + if en.with_element_node(preview::is_element_node_ignored) { return None; } @@ -356,11 +358,11 @@ fn filter_nodes_for_selection( pub fn select_element_behind_impl( component_instance: &ComponentInstance, - selected_element_node: &ElementRcNode, + selected_element_node: &common::ElementRcNode, position: LogicalPoint, enter_component: bool, reverse: bool, -) -> Option { +) -> Option { 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))?;