mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-02 04:48:27 +00:00
Live-preview: Outline
This commit is contained in:
parent
e38f564f09
commit
489e0b8729
12 changed files with 599 additions and 31 deletions
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
355
tools/lsp/preview/outline.rs
Normal file
355
tools/lsp/preview/outline.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
"",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
149
tools/lsp/ui/views/outline-view.slint
Normal file
149
tools/lsp/ui/views/outline-view.slint
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue