Live-preview: Outline

This commit is contained in:
Olivier Goffart 2025-06-14 12:55:52 +02:00
parent e38f564f09
commit 489e0b8729
12 changed files with 599 additions and 31 deletions

View file

@ -254,7 +254,7 @@ impl ElementRcNode {
/// Run with all the debug information on the node
pub fn with_element_debug<R>(
&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<R>(
&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<R>(&self, func: impl Fn(SyntaxNode) -> R) -> R {
pub fn with_decorated_node<R>(&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))
}

View file

@ -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<String>, 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);
}
});

View file

@ -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<InsertInformation> {
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<InsertInformation> {
@ -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)

View file

@ -0,0 +1,355 @@
// 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 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<Self::Data>;
/// return the children of the given parent.
/// None means the root.
fn children(&self, parent: Option<&Self::Id>) -> impl Iterator<Item = Self::Id>;
/// 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<T: Tree> {
cached_layout: RefCell<Vec<T::Id>>,
model_tracker: slint::ModelNotify,
source: T,
}
impl<T: Tree> TreeAdapterModel<T> {
pub fn new(source: T) -> Self {
let mut cached_layout: Vec<T::Id> = 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<T::Id>, 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<T: Tree> Model for TreeAdapterModel<T> {
type Data = T::Data;
fn row_count(&self) -> usize {
self.cached_layout.borrow().len()
}
fn row_data(&self, row: usize) -> Option<Self::Data> {
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<object_tree::Component>,
}
impl OutlineModel {
pub fn new(root_component: Rc<object_tree::Component>) -> 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<Self::Data> {
Some(id.1.clone())
}
fn children(&self, parent: Option<&Self::Id>) -> impl Iterator<Item = Self::Id> {
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<Rc<object_tree::Component>>) {
let api = ui.global::<ui::Api>();
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::<ui::Api>();
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<lsp_types::WorkspaceEdit> {
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::<usize>() {
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)
}

View file

@ -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<TextRange> {
}
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");
}

View file

@ -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<PropertyGroup>;
@ -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::<PropertyGroup>::new())).into(),

View file

@ -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(

View file

@ -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 <bool> 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 <bool> auto-clear-console: true;
}

View file

@ -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,
"",
);

View file

@ -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 <float> 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;
}
}
}
}
}

View file

@ -0,0 +1,149 @@
// 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
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 <bool> enabled <=> lv.enabled;
lv := ListView {
for item in outline-data: DragArea {
mime-type: "application/x-slint-component-move";
data: item.uri + ":" + item.offset;
property <bool> 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;
}
}
}
}
}
}
}
}

View file

@ -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);
}