From 489e0b8729ebef19c76fc31e7a72d97bf32a0f50 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Sat, 14 Jun 2025 12:55:52 +0200 Subject: [PATCH] Live-preview: Outline --- tools/lsp/common.rs | 6 +- tools/lsp/preview.rs | 8 + tools/lsp/preview/drop_location.rs | 16 +- tools/lsp/preview/outline.rs | 355 ++++++++++++++++++ tools/lsp/preview/properties.rs | 16 +- tools/lsp/preview/ui.rs | 5 +- tools/lsp/preview/ui/property_view.rs | 2 +- tools/lsp/ui/api.slint | 25 +- .../lsp/ui/components/property-widgets.slint | 10 +- tools/lsp/ui/main.slint | 36 +- tools/lsp/ui/views/outline-view.slint | 149 ++++++++ tools/lsp/ui/windowglobal.slint | 2 +- 12 files changed, 599 insertions(+), 31 deletions(-) create mode 100644 tools/lsp/preview/outline.rs create mode 100644 tools/lsp/ui/views/outline-view.slint diff --git a/tools/lsp/common.rs b/tools/lsp/common.rs index e66d459f27..bef581ccfc 100644 --- a/tools/lsp/common.rs +++ b/tools/lsp/common.rs @@ -254,7 +254,7 @@ impl ElementRcNode { /// Run with all the debug information on the node pub fn with_element_debug( &self, - func: impl Fn(&i_slint_compiler::object_tree::ElementDebugInfo) -> R, + func: impl FnOnce(&i_slint_compiler::object_tree::ElementDebugInfo) -> R, ) -> R { let elem = self.element.borrow(); let d = elem.debug.get(self.debug_index).unwrap(); @@ -264,14 +264,14 @@ impl ElementRcNode { /// Run with the `Element` node pub fn with_element_node( &self, - func: impl Fn(&i_slint_compiler::parser::syntax_nodes::Element) -> R, + func: impl FnOnce(&i_slint_compiler::parser::syntax_nodes::Element) -> R, ) -> R { let elem = self.element.borrow(); func(&elem.debug.get(self.debug_index).unwrap().node) } /// Run with the SyntaxNode incl. any id, condition, etc. - pub fn with_decorated_node(&self, func: impl Fn(SyntaxNode) -> R) -> R { + pub fn with_decorated_node(&self, func: impl FnOnce(SyntaxNode) -> R) -> R { let elem = self.element.borrow(); func(find_element_with_decoration(&elem.debug.get(self.debug_index).unwrap().node)) } diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index 666900415b..330afab3db 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -40,6 +40,7 @@ pub mod eval; mod ext; mod preview_data; use ext::ElementRcNodeExt; +mod outline; mod properties; pub mod ui; @@ -1064,6 +1065,13 @@ fn finish_parsing(preview_url: &Url, previewed_component: Option, succes ui::palette::set_palette(ui, palettes); ui::ui_set_uses_widgets(ui, uses_widgets); ui::ui_set_known_components(ui, &preview_state.known_components, index); + let component = document_cache.get_document(preview_url).and_then(|doc| { + match previewed_component.as_ref() { + Some(c_id) => doc.inner_components.iter().find(|c| c.id == c_id).cloned(), + None => doc.last_exported_component(), + } + }); + outline::reset_outline(ui, component); ui::ui_set_preview_data(ui, preview_data, previewed_component); } }); diff --git a/tools/lsp/preview/drop_location.rs b/tools/lsp/preview/drop_location.rs index f7350b3586..3359592135 100644 --- a/tools/lsp/preview/drop_location.rs +++ b/tools/lsp/preview/drop_location.rs @@ -314,7 +314,7 @@ pub struct DropMark { pub end: i_slint_core::lengths::LogicalPoint, } -fn insert_position_at_end( +pub fn insert_position_at_end( target_element_node: &common::ElementRcNode, ) -> Option { target_element_node.with_element_node(|node| { @@ -368,7 +368,7 @@ fn insert_position_at_end( }) } -fn insert_position_before_child( +pub fn insert_position_before_child( target_element_node: &common::ElementRcNode, child_index: usize, ) -> Option { @@ -1136,7 +1136,6 @@ pub fn create_move_element_workspace_edit( instance_index: usize, position: LogicalPoint, ) -> Option<(lsp_types::WorkspaceEdit, DropData)> { - let component_type = element.component_type(); let parent_of_element = element.parent(); let placeholder_text = if Some(&drop_info.target_element_node) == parent_of_element.as_ref() { @@ -1173,6 +1172,15 @@ pub fn create_move_element_workspace_edit( String::new() }; + create_swap_element_workspace_edit(drop_info, element, placeholder_text) +} + +pub fn create_swap_element_workspace_edit( + drop_info: &DropInformation, + element: &common::ElementRcNode, + placeholder_text: String, +) -> Option<(lsp_types::WorkspaceEdit, DropData)> { + let component_type = element.component_type(); let new_text = { let element_text_lines = extract_text_of_element(element, &["x", "y"]); @@ -1217,7 +1225,7 @@ pub fn create_move_element_workspace_edit( let mut edits = Vec::with_capacity(3); let remove_me = element.with_decorated_node(|node| { - node_removal_text_edit(&document_cache, &node, placeholder_text.clone()) + node_removal_text_edit(&document_cache, &node, placeholder_text) })?; if remove_me.url.to_file_path().as_ref().map(|p| p.as_path()) == Ok(source_file.path()) { selection_offset = text_edit::TextOffsetAdjustment::new(&remove_me.edit, &source_file) diff --git a/tools/lsp/preview/outline.rs b/tools/lsp/preview/outline.rs new file mode 100644 index 0000000000..90fc047646 --- /dev/null +++ b/tools/lsp/preview/outline.rs @@ -0,0 +1,355 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +use crate::common::uri_to_file; +use crate::preview::{self, ui}; +use core::cell::RefCell; +use i_slint_compiler::object_tree; +use i_slint_compiler::parser::{self, syntax_nodes, TextSize}; +use lsp_types::Url; +use slint::{ComponentHandle as _, Model, ModelRc, SharedString, ToSharedString as _}; +use std::rc::Rc; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum TreeNodeChange { + None, + Collapse, + Expand, +} + +trait Tree { + /// The slint::Model::Data that is being used in the UI + type Data: Clone + std::fmt::Debug; + /// An Id or index that can be used to identify the data + type Id: Clone + std::fmt::Debug; + + /// map id to data + fn data(&self, id: &Self::Id) -> Option; + /// return the children of the given parent. + /// None means the root. + fn children(&self, parent: Option<&Self::Id>) -> impl Iterator; + /// return the level in the tree of the given Id + fn level(&self, id: &Self::Id) -> usize; + /// Return if the node is expanded + fn is_expanded(&self, id: &Self::Id) -> bool; + + /// Update the data for a given id + /// Returns whether there was a change and we need to collapse or expand the node + /// + /// The id is mutable in case changing the data also changes the id + fn update_data(&self, id: &mut Self::Id, data: Self::Data) -> TreeNodeChange; +} + +struct TreeAdapterModel { + cached_layout: RefCell>, + model_tracker: slint::ModelNotify, + + source: T, +} + +impl TreeAdapterModel { + pub fn new(source: T) -> Self { + let mut cached_layout: Vec = source.children(None).collect(); + for child in (0..cached_layout.len()).rev() { + if source.is_expanded(&cached_layout[child]) { + Self::expand_recursive(&source, &mut cached_layout, child); + } + } + Self { + cached_layout: RefCell::new(cached_layout), + model_tracker: Default::default(), + source, + } + } + + fn expand(&self, row: usize) { + let mut cached_layout = self.cached_layout.borrow_mut(); + let count = Self::expand_recursive(&self.source, &mut cached_layout, row); + drop(cached_layout); + self.model_tracker.row_added(row + 1, count); + } + + /// Internal function for `expand` and return the amound of rows added + fn expand_recursive(source: &T, cached_layout: &mut Vec, row: usize) -> usize { + let mut count = 0; + let parent = cached_layout[row].clone(); + let index = row + 1; + cached_layout.splice(index..index, source.children(Some(&parent)).inspect(|_| count += 1)); + + for child in (index..index + count).rev() { + if source.is_expanded(&cached_layout[child]) { + count += Self::expand_recursive(source, cached_layout, child); + } + } + count + } + + fn collapse(&self, row: usize) { + let mut cached_layout = self.cached_layout.borrow_mut(); + let level = self.source.level(&cached_layout[row]); + let mut count = 0; + while row + 1 + count < cached_layout.len() + && self.source.level(&cached_layout[row + 1 + count]) > level + { + count += 1; + } + cached_layout.drain(row + 1..row + 1 + count); + self.model_tracker.row_removed(row + 1, count); + } +} + +impl Model for TreeAdapterModel { + type Data = T::Data; + + fn row_count(&self) -> usize { + self.cached_layout.borrow().len() + } + + fn row_data(&self, row: usize) -> Option { + self.cached_layout.borrow().get(row).and_then(|id| self.source.data(id)) + } + + fn model_tracker(&self) -> &dyn slint::ModelTracker { + &self.model_tracker + } + + fn set_row_data(&self, row: usize, data: Self::Data) { + let mut cached_layout = self.cached_layout.borrow_mut(); + let Some(old) = cached_layout.get_mut(row) else { return }; + let change = self.source.update_data(old, data); + drop(cached_layout); + self.model_tracker.row_changed(row); + match change { + TreeNodeChange::None => {} + TreeNodeChange::Collapse => self.collapse(row), + TreeNodeChange::Expand => self.expand(row), + } + } +} + +struct OutlineModel { + root_component: Rc, +} + +impl OutlineModel { + pub fn new(root_component: Rc) -> Self { + Self { root_component } + } +} + +impl Tree for OutlineModel { + type Data = ui::OutlineTreeNode; + type Id = (syntax_nodes::Element, ui::OutlineTreeNode); + + fn data(&self, id: &Self::Id) -> Option { + Some(id.1.clone()) + } + + fn children(&self, parent: Option<&Self::Id>) -> impl Iterator { + match parent { + None => { + let root = self.root_component.node.as_ref().map(|n| { + let elem = n.Element(); + let name = match elem.QualifiedName() { + None => n.DeclaredIdentifier().text().to_shared_string(), + Some(base) => slint::format!( + "{} inherits {} ", + n.DeclaredIdentifier().text(), + base.text() + ), + }; + let data = create_node(&elem, 0, name); + (elem, data) + }); + itertools::Either::Left(root.into_iter()) + } + Some(parent) => { + let indent_level = parent.1.indent_level + 1; + let mut iter = parent + .0 + .children() + .filter_map(move |n| { + let se = match n.kind() { + parser::SyntaxKind::SubElement => syntax_nodes::SubElement::from(n), + parser::SyntaxKind::RepeatedElement => { + syntax_nodes::RepeatedElement::from(n).SubElement() + } + parser::SyntaxKind::ConditionalElement => { + syntax_nodes::ConditionalElement::from(n).SubElement() + } + _ => return None, + }; + let elem = se.Element(); + if crate::common::is_element_node_ignored(&elem) { + return None; + } + let base = elem + .QualifiedName() + .map(|x| x.text().to_shared_string()) + .unwrap_or_default(); + let name = match se.child_text(parser::SyntaxKind::Identifier) { + None => base, + Some(id) => slint::format!("{id} := {base}"), + }; + let node = create_node(&elem, indent_level, name); + Some((elem, node)) + }) + .peekable(); + itertools::Either::Right(std::iter::from_fn(move || { + iter.next().map(|(elem, mut node)| { + node.is_last_child = iter.peek().is_none(); + (elem, node) + }) + })) + } + } + } + + fn level(&self, id: &Self::Id) -> usize { + id.1.indent_level as usize + } + + fn update_data(&self, id: &mut Self::Id, data: Self::Data) -> TreeNodeChange { + let r = if id.1.is_expended == data.is_expended { + TreeNodeChange::None + } else if data.is_expended { + TreeNodeChange::Expand + } else { + TreeNodeChange::Collapse + }; + id.1 = data; + r + } + + fn is_expanded(&self, id: &Self::Id) -> bool { + id.1.is_expended + } +} + +fn create_node( + element: &syntax_nodes::Element, + indent_level: i32, + name: SharedString, +) -> ui::OutlineTreeNode { + ui::OutlineTreeNode { + has_children: element + .SubElement() + .filter(|n| !crate::common::is_element_node_ignored(&n.Element())) + .next() + .is_some() + || element.RepeatedElement().next().is_some() + || element.ConditionalElement().next().is_some(), + is_expended: true, + indent_level, + name, + uri: crate::common::file_to_uri(element.source_file.path()).unwrap().to_shared_string(), + offset: usize::from(element.text_range().start()) as i32, + is_last_child: true, + } +} + +pub fn reset_outline(ui: &ui::PreviewUi, root_component: Option>) { + let api = ui.global::(); + match root_component { + Some(root) => api.set_outline(ModelRc::new(TreeAdapterModel::new(OutlineModel::new(root)))), + None => api.set_outline(Default::default()), + } +} + +pub fn setup(ui: &ui::PreviewUi) { + let api = ui.global::(); + api.on_outline_select_element(|uri, offset| { + super::element_selection::select_element_at_source_code_position( + uri_to_file(&Url::parse(uri.as_str()).unwrap()).unwrap(), + TextSize::new(offset as u32), + None, + super::SelectionNotification::Now, + ); + }); + api.on_outline_drop(|data, target_uri, target_offset, location| { + let Some(edit) = drop_edit(data, target_uri, target_offset, location) else { + return; + }; + preview::send_workspace_edit("Drop element".to_string(), edit, true); + }); +} + +fn drop_edit( + data: SharedString, + target_uri: SharedString, + target_offset: i32, + location: i32, +) -> Option { + let document_cache = super::document_cache()?; + let url = Url::parse(target_uri.as_str()).ok()?; + let target_elem = + document_cache.element_at_offset(&url, TextSize::new(target_offset as u32))?; + + let drop_info = if location == 0 { + preview::drop_location::DropInformation { + insert_info: preview::drop_location::insert_position_at_end(&target_elem)?, + target_element_node: target_elem, + drop_mark: None, + child_index: 0, + } + } else { + let parent = target_elem.parent()?; + let children = parent.children(); + let index = children.iter().position(|c| c == &target_elem)?; + if location < 0 { + preview::drop_location::DropInformation { + insert_info: preview::drop_location::insert_position_before_child(&parent, index)?, + target_element_node: parent, + drop_mark: None, + child_index: index, + } + } else if index == children.len() - 1 { + preview::drop_location::DropInformation { + insert_info: preview::drop_location::insert_position_at_end(&parent)?, + target_element_node: parent, + drop_mark: None, + child_index: index, + } + } else { + preview::drop_location::DropInformation { + insert_info: preview::drop_location::insert_position_before_child( + &parent, + index + 1, + )?, + target_element_node: parent, + drop_mark: None, + child_index: index + 1, + } + } + }; + + let workspace_edit = if let Some((item_uri, item_offset)) = data.rsplit_once(':') { + if *item_uri != *target_uri { + return None; + } + let moving_element = + document_cache.element_at_offset(&url, TextSize::new(item_offset.parse().ok()?))?; + if moving_element == drop_info.target_element_node { + return None; + } + preview::drop_location::create_swap_element_workspace_edit( + &drop_info, + &moving_element, + Default::default(), + )? + } else if let Ok(library_index) = data.parse::() { + let component = super::PREVIEW_STATE.with(|preview_state| { + let preview_state = preview_state.borrow(); + preview_state.known_components.get(library_index).cloned() + })?; + preview::drop_location::create_drop_element_workspace_edit( + &document_cache, + &component, + &drop_info, + )? + } else { + return None; + }; + + Some(workspace_edit.0) +} diff --git a/tools/lsp/preview/properties.rs b/tools/lsp/preview/properties.rs index 233cc50993..1e636c300d 100644 --- a/tools/lsp/preview/properties.rs +++ b/tools/lsp/preview/properties.rs @@ -81,7 +81,7 @@ pub struct PropertyInformation { pub struct ElementInformation { pub id: SmolStr, pub type_name: SmolStr, - pub range: TextRange, + pub offset: TextSize, } #[derive(Clone, Debug)] @@ -589,14 +589,14 @@ fn find_block_range(element: &common::ElementRcNode) -> Option { } fn get_element_information(element: &common::ElementRcNode) -> ElementInformation { - let range = element.with_decorated_node(|node| util::node_range_without_trailing_ws(&node)); + let offset = element.with_element_node(|n| n.text_range().start()); let e = element.element.borrow(); let type_name = if matches!(&e.base_type, ElementType::Builtin(b) if b.name == "Empty") { SmolStr::default() } else { e.base_type.to_smolstr() }; - ElementInformation { id: e.id.clone(), type_name, range } + ElementInformation { id: e.id.clone(), type_name, offset } } pub(crate) fn query_properties( @@ -978,14 +978,12 @@ pub mod tests { let result = get_element_information(&element); - let r = util::text_range_to_lsp_range( + let o = util::text_size_to_lsp_position( &element.with_element_node(|n| n.source_file.clone()), - result.range, + result.offset, ); - assert_eq!(r.start.line, 32); - assert_eq!(r.start.character, 12); - assert_eq!(r.end.line, 35); - assert_eq!(r.end.character, 13); + assert_eq!(o.line, 32); + assert_eq!(o.character, 12); assert_eq!(result.type_name.to_string(), "Text"); } diff --git a/tools/lsp/preview/ui.rs b/tools/lsp/preview/ui.rs index cb0cec0f6c..c9a4910473 100644 --- a/tools/lsp/preview/ui.rs +++ b/tools/lsp/preview/ui.rs @@ -145,6 +145,7 @@ pub fn create_ui( log_messages::setup(&ui); palette::setup(&ui); recent_colors::setup(&ui); + super::outline::setup(&ui); #[cfg(target_vendor = "apple")] api.set_control_key_name("command".into()); @@ -363,7 +364,7 @@ fn is_equal_element(c: &ElementInformation, n: &ElementInformation) -> bool { c.id == n.id && c.type_name == n.type_name && c.source_uri == n.source_uri - && c.range.start == n.range.start + && c.offset == n.offset } pub type PropertyGroupModel = ModelRc; @@ -1316,7 +1317,7 @@ pub fn ui_set_properties( type_name: "".into(), source_uri: "".into(), source_version: 0, - range: Range { start: 0, end: 0 }, + offset: 0, }, HashMap::new(), Rc::new(VecModel::from(Vec::::new())).into(), diff --git a/tools/lsp/preview/ui/property_view.rs b/tools/lsp/preview/ui/property_view.rs index e917a71446..e4d3be99c9 100644 --- a/tools/lsp/preview/ui/property_view.rs +++ b/tools/lsp/preview/ui/property_view.rs @@ -271,7 +271,7 @@ pub fn map_properties_to_ui( type_name: element.type_name.as_str().into(), source_uri, source_version, - range: ui::to_ui_range(element.range)?, + offset: u32::from(element.offset) as i32, }, declarations, Rc::new(VecModel::from( diff --git a/tools/lsp/ui/api.slint b/tools/lsp/ui/api.slint index 4619181612..9aa3da00b0 100644 --- a/tools/lsp/ui/api.slint +++ b/tools/lsp/ui/api.slint @@ -215,7 +215,8 @@ export struct ElementInformation { type-name: string, source-uri: string, source-version: int, - range: Range, + /// The offset within source-uri + offset: int, } export struct PaletteEntry { @@ -237,6 +238,18 @@ export struct LogMessage { level: LogMessageLevel, } +/// A node in the outline tree +export struct OutlineTreeNode { + has-children: bool, + is-expended: bool, + indent-level: int, + name: string, + uri: string, + offset: int, + is-last-child: bool, +} + + export global Api { // # Properties // ## General preview state: @@ -270,6 +283,7 @@ export global Api { // ## Log Output in-out property <[LogMessage]> log-output; + in-out property auto-clear-console: true; // ## Drawing Area // Borders around things @@ -475,6 +489,14 @@ export global Api { ]; + // ## Outline + in-out property <[OutlineTreeNode]> outline; + callback outline-select-element(uri: string, offset: int); + // Data is either a "file:offset" for an element to move, or just a component index. + // location = 0 means on top of the element, location = 1 means after the element, + // location = -1 means before + callback outline-drop(data: string, uri: string, offset: int, location: int); + // ## preview data in-out property <[PropertyContainer]> preview-data; @@ -575,5 +597,4 @@ export global Api { // Console / LogMessages pure callback filter-log-messages(messages: [LogMessage], pattern: string) -> [LogMessage]; callback clear-log-messages(); - in-out property auto-clear-console: true; } diff --git a/tools/lsp/ui/components/property-widgets.slint b/tools/lsp/ui/components/property-widgets.slint index 3c9eb5d181..feec9ae14c 100644 --- a/tools/lsp/ui/components/property-widgets.slint +++ b/tools/lsp/ui/components/property-widgets.slint @@ -249,7 +249,7 @@ export component PropertyInformationWidget inherits VerticalLayout { Api.set-code-binding( element-information.source-uri, element-information.source-version, - element-information.range.start, + element-information.offset, property-information.name, text, ); @@ -258,7 +258,7 @@ export component PropertyInformationWidget inherits VerticalLayout { return (Api.test-code-binding( root.element-information.source-uri, root.element-information.source-version, - root.element-information.range.start, + root.element-information.offset, root.property-information.name, text, )); @@ -267,7 +267,7 @@ export component PropertyInformationWidget inherits VerticalLayout { Api.set-code-binding( element-information.source-uri, element-information.source-version, - element-information.range.start, + element-information.offset, property-information.name, text); } @@ -275,7 +275,7 @@ export component PropertyInformationWidget inherits VerticalLayout { return (Api.test-code-binding( root.element-information.source-uri, root.element-information.source-version, - root.element-information.range.start, + root.element-information.offset, root.property-information.name, text)); } @@ -284,7 +284,7 @@ export component PropertyInformationWidget inherits VerticalLayout { Api.set-code-binding( element-information.source-uri, element-information.source-version, - element-information.range.start, + element-information.offset, property-information.name, "", ); diff --git a/tools/lsp/ui/main.slint b/tools/lsp/ui/main.slint index 0fa7747922..05b7bc2560 100644 --- a/tools/lsp/ui/main.slint +++ b/tools/lsp/ui/main.slint @@ -3,7 +3,7 @@ // cSpell: ignore Heade -import { Button, TabWidget } from "std-widgets.slint"; +import { Button, TabWidget, Palette } from "std-widgets.slint"; import { Api, ComponentItem, DiagnosticSummary } from "api.slint"; import { EditorSizeSettings, EditorSpaceSettings, Icons, PickerStyles } from "./components/styling.slint"; @@ -13,6 +13,7 @@ import { LibraryView } from "./views/library-view.slint"; import { DrawAreaMode, PreviewView } from "./views/preview-view.slint"; import { OutOfDateBox } from "./components/out-of-date-box.slint"; import { PropertyView } from "./views/property-view.slint"; +import { OutlineView } from "./views/outline-view.slint"; import { PreviewDataView } from "./views/preview-data-view.slint"; import { WindowGlobal, WindowManager } from "windowglobal.slint"; import { ColorPickerView } from "components/widgets/floating-brush-picker-widget.slint"; @@ -115,9 +116,34 @@ export component PreviewUi inherits Window { current-index: 0; Tab { title: @tr("Properties"); - if tw.current-index == 0: PropertyView { - opacity: preview.preview-is-current ? 1.0 : 0.3; - enabled: preview.preview-is-current; + if tw.current-index == 0: Rectangle { + property ratio: 50%; + w1 := PropertyView { + y: 0; + height: (parent.height - splitter.height) * ratio; + opacity: preview.preview-is-current ? 1.0 : 0.3; + enabled: preview.preview-is-current; + } + + splitter := TouchArea { + y: w1.height; + height: 3px; + moved => { + ratio = Math.clamp((self.y + self.mouse-y - self.pressed-y) / (parent.height - splitter.height), 0, 1); + } + mouse-cursor: ns-resize; + + Rectangle { + background: Palette.border; + } + } + + w2 := OutlineView { + y: splitter.y + splitter.height; + height: parent.height - self.y; + opacity: preview.preview-is-current ? 1.0 : 0.3; + enabled: preview.preview-is-current; + } } } @@ -128,6 +154,8 @@ export component PreviewUi inherits Window { enabled: preview.preview-is-current; } } + + } } } diff --git a/tools/lsp/ui/views/outline-view.slint b/tools/lsp/ui/views/outline-view.slint new file mode 100644 index 0000000000..08ac8e1be9 --- /dev/null +++ b/tools/lsp/ui/views/outline-view.slint @@ -0,0 +1,149 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +import { Palette, VerticalBox, ListView } from "std-widgets.slint"; +import { Api, OutlineTreeNode } from "../api.slint"; + +export component OutlineView inherits VerticalLayout { + property <[OutlineTreeNode]> outline-data <=> Api.outline; + in property enabled <=> lv.enabled; + + lv := ListView { + for item in outline-data: DragArea { + + mime-type: "application/x-slint-component-move"; + data: item.uri + ":" + item.offset; + + property selected: item.uri == Api.current-element.source-uri && item.offset == Api.current-element.offset; + + drop-as-child := DropArea { + enabled: (!item.has-children || !item.is-expended) && item.indent-level > 0; + + can-drop(event) => { + if event.mime-type != "application/x-slint-component" && event.mime-type != "application/x-slint-component-move" { + return false; + } + if !item.is-expended && item.has-children { + if !open-timer.running { + open-timer.running = true; + } + return false; + } + //return Api.outline-can-drop(event.data, item.uri, item.offset, 0); + true; + } + + dropped(event) => { + Api.outline-drop(event.data, item.uri, item.offset, 0); + } + } + + drop-before := DropArea { + enabled: item.indent-level > 0; + height: parent.height / 3; + + y: -self.height / 2; + x: indentation.width; + width: parent.width - self.x; + + can-drop(event) => { + if event.mime-type != "application/x-slint-component" && event.mime-type != "application/x-slint-component-move" { + return false; + } + //return Api.outline-can-drop(event.data, item.uri, item.offset, -1); + true; + } + + dropped(event) => { + Api.outline-drop(event.data, item.uri, item.offset, -1); + } + + Rectangle { + height: 1px; + background: Palette.foreground; + visible: parent.contains-drag; + } + } + + drop-after := DropArea { + y: parent.height - self.height / 2; + x: indentation.width; + height: parent.height / 3; + width: parent.width - self.x; + + enabled: item.indent-level > 0; + + can-drop(event) => { + if event.mime-type != "application/x-slint-component" && event.mime-type != "application/x-slint-component-move" { + return false; + } + //return Api.outline-can-drop(event.data, item.uri, item.offset, 1); + true; + } + + dropped(event) => { + Api.outline-drop(event.data, item.uri, item.offset, 1); + } + + Rectangle { + height: 1px; + background: Palette.foreground; + visible: parent.contains-drag; + } + } + + open-timer := Timer { + running: false; + interval: 0.5s; + triggered => { + item.is-expended = true; + open-timer.running = false; + } + } + + Rectangle { + background: drop-as-child.contains-drag ? Palette.alternate-background : + selected ? Palette.selection-background : transparent; + } + + HorizontalLayout { + indentation := Rectangle { + width: item.indent-level * 20px; + } + + t := Text { + text: !item.has-children ? "" : item.is-expended ? "⊟" : "⊞"; + color: selected ? Palette.selection-foreground : Palette.foreground; + horizontal-alignment: right; + vertical-alignment: center; + width: 20px; + TouchArea { + clicked => { + item.is-expended = !item.is-expended; + } + } + } + + Text { + text: item.name; + color: t.color; + vertical-alignment: center; + TouchArea { + clicked => { + Api.outline-select-element(item.uri, item.offset); + } + double-clicked => { + item.is-expended = !item.is-expended; + } + + pointer-event(event) => { + if event.kind == PointerEventKind.cancel { + open-timer.running = false; + } + } + } + } + } + } + } +} diff --git a/tools/lsp/ui/windowglobal.slint b/tools/lsp/ui/windowglobal.slint index 832349599b..ebdf42b7ba 100644 --- a/tools/lsp/ui/windowglobal.slint +++ b/tools/lsp/ui/windowglobal.slint @@ -367,7 +367,7 @@ export global WindowManager { Api.set-code-binding( current-element-information.source-uri, current-element-information.source-version, - current-element-information.range.start, + current-element-information.offset, current-property-information.name, text); }