Skip to content

Commit

Permalink
live preview: Implement resizing and moving of selected eleement
Browse files Browse the repository at this point in the history
Much polish needed, but it is a basis to build upon.
  • Loading branch information
hunger committed Feb 19, 2024
1 parent e1aefc6 commit 08372e5
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 42 deletions.
14 changes: 14 additions & 0 deletions tools/lsp/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ pub struct ComponentAddition {
pub component_text: String,
}

#[allow(unused)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct PropertyChange {
pub name: String,
pub value: String,
}

impl PropertyChange {
pub fn new(name: &str, value: String) -> Self {
PropertyChange { name: name.to_string(), value }
}
}

#[allow(unused)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub enum PreviewToLspMessage {
Expand All @@ -204,6 +217,7 @@ pub enum PreviewToLspMessage {
PreviewTypeChanged { is_external: bool },
RequestState { unused: bool }, // send all documents!
AddComponent { label: Option<String>, component: ComponentAddition },
UpdateElement { position: VersionedPosition, properties: Vec<PropertyChange> },
}

/// Information on the Element types available
Expand Down
39 changes: 39 additions & 0 deletions tools/lsp/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,45 @@ pub fn add_component(
.ok_or("Could not create workspace edit".into())
}

pub fn update_element(
ctx: &Context,
position: crate::common::VersionedPosition,
properties: Vec<crate::common::PropertyChange>,
) -> Result<lsp_types::WorkspaceEdit> {
let mut document_cache = ctx.document_cache.borrow_mut();
let file = lsp_types::Url::to_file_path(position.url())
.map_err(|_| "Failed to convert URL to file path".to_string())?;

if &document_cache.document_version(position.url()) != position.version() {
return Err("Document version mismatch.".into());
}

let doc = document_cache
.documents
.get_document(&file)
.ok_or_else(|| "Document not found".to_string())?;

let source_file = doc
.node
.as_ref()
.map(|n| n.source_file.clone())
.ok_or_else(|| "Document had no node".to_string())?;
let element_position = map_position(&source_file, position.offset().into());

let element = element_at_position(&mut document_cache, &position.url(), &element_position)
.ok_or_else(|| {
format!("No element found at the given start position {:?}", &element_position)
})?;

let (_, e) = crate::language::properties::set_bindings(
&mut document_cache,
position.url(),
&element,
&properties,
)?;
Ok(e.ok_or_else(|| "Failed to create workspace edit".to_string())?)
}

#[cfg(test)]
pub mod tests {
use super::*;
Expand Down
41 changes: 41 additions & 0 deletions tools/lsp/language/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,47 @@ pub(crate) fn set_binding(
}
}

pub(crate) fn set_bindings(
document_cache: &mut DocumentCache,
uri: &lsp_types::Url,
element: &ElementRc,
properties: &[crate::common::PropertyChange],
) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> {
let version = document_cache.document_version(uri);
let (responses, edits) = properties
.iter()
.map(|p| set_binding(document_cache, uri, element, &p.name, p.value.clone()))
.fold(
Ok((SetBindingResponse { diagnostics: Default::default() }, Vec::new())),
|prev_result: Result<(SetBindingResponse, Vec<lsp_types::TextEdit>)>, next_result| {
let (mut responses, mut edits) = prev_result?;
let (nr, ne) = next_result?;

responses.diagnostics.extend_from_slice(&nr.diagnostics);

match ne {
Some(lsp_types::WorkspaceEdit {
document_changes: Some(lsp_types::DocumentChanges::Edits(e)),
..
}) => {
edits.extend(e.get(0).unwrap().edits.iter().filter_map(|e| match e {
lsp_types::OneOf::Left(edit) => Some(edit.clone()),
_ => None,
}));
}
_ => { /* do nothing */ }
};

Ok((responses, edits))
},
)?;
if edits.is_empty() {
Ok((responses, None))
} else {
Ok((responses, Some(crate::common::create_workspace_edit(uri.clone(), version, edits))))
}
}

fn create_workspace_edit_for_remove_binding(
uri: &lsp_types::Url,
version: SourceFileVersion,
Expand Down
32 changes: 31 additions & 1 deletion tools/lsp/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,13 @@ async fn handle_preview_to_lsp_message(
crate::language::request_state(ctx);
}
M::AddComponent { label, component } => {
let edit = crate::language::add_component(ctx, component)?;
let edit = match crate::language::add_component(ctx, component) {
Ok(edit) => edit,
Err(e) => {
eprintln!("Error: {}", e);
return Ok(());
}
};
let response = ctx
.server_notifier
.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
Expand All @@ -454,6 +460,30 @@ async fn handle_preview_to_lsp_message(
.into());
}
}
M::UpdateElement { position, properties } => {
let edit = match crate::language::update_element(ctx, position, properties) {
Ok(e) => e,
Err(e) => {
eprintln!("Error: {e}");
return Ok(());
}
};
let response = ctx
.server_notifier
.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
lsp_types::ApplyWorkspaceEditParams {
label: Some("Element update".to_string()),
edit,
},
)?
.await?;
if !response.applied {
return Err(response
.failure_reason
.unwrap_or("Operation failed, no specific reason given".into())
.into());
}
}
}
Ok(())
}
62 changes: 61 additions & 1 deletion tools/lsp/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial

use crate::common::{
ComponentInformation, PreviewComponent, PreviewConfig, UrlVersion, VersionedUrl,
ComponentInformation, PreviewComponent, PreviewConfig, UrlVersion, VersionedPosition,
VersionedUrl,
};
use crate::lsp_ext::Health;
use crate::preview::element_selection::ElementSelection;
Expand Down Expand Up @@ -124,6 +125,64 @@ fn drop_component(
};
}

// triggered from the UI, running in UI thread
fn change_geometry_of_selected_element(x: f32, y: f32, width: f32, height: f32) {
let Some(selected) = PREVIEW_STATE.with(move |preview_state| {
let preview_state = preview_state.borrow();
preview_state.selected.clone()
}) else {
return;
};

let Some(selected_element) = selected_element() else {
return;
};
let Some(component_instance) = component_instance() else {
return;
};

let Some(geometry) = component_instance
.element_position(&selected_element)
.get(selected.instance_index)
.cloned()
else {
return;
};

let properties = {
let mut p = Vec::with_capacity(4);
if geometry.origin.x != x {
p.push(crate::common::PropertyChange::new("x", format!("{}px", x.round())));
}
if geometry.origin.y != y {
p.push(crate::common::PropertyChange::new("y", format!("{}px", y.round())));
}
if geometry.size.width != width {
p.push(crate::common::PropertyChange::new("width", format!("{}px", width.round())));
}
if geometry.size.height != height {
p.push(crate::common::PropertyChange::new("height", format!("{}px", height.round())));
}
p
};

if !properties.is_empty() {
let Ok(url) = Url::from_file_path(&selected.path) else {
return;
};

let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
let Some((version, _)) = cache.source_code.get(&url).cloned() else {
return;
};

send_message_to_lsp(crate::common::PreviewToLspMessage::UpdateElement {
position: VersionedPosition::new(VersionedUrl::new(url, version), selected.offset),
properties,
});
}
}

fn change_style() {
let cache = CONTENT_CACHE.get_or_init(Default::default).lock().unwrap();
let ui_is_visible = cache.ui_is_visible;
Expand Down Expand Up @@ -515,6 +574,7 @@ fn set_selections(
x: g.origin.x,
y: g.origin.y,
border_color: if i == main_index { border_color } else { secondary_border_color },
is_primary: i == main_index,
})
.collect::<Vec<_>>();
let model = Rc::new(slint::VecModel::from(values));
Expand Down
1 change: 1 addition & 0 deletions tools/lsp/preview/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub fn create_ui(style: String, experimental: bool) -> Result<PreviewUi, Platfor
ui.on_select_behind(super::element_selection::select_element_behind);
ui.on_can_drop(super::can_drop_component);
ui.on_drop(super::drop_component);
ui.on_selected_element_update_geometry(super::change_geometry_of_selected_element);

Ok(ui)
}
Expand Down
85 changes: 69 additions & 16 deletions tools/lsp/ui/draw-area.slint
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,56 @@ export struct Selection {
width: length,
height: length,
border-color: color,
is-primary: bool,
}

component SelectionFrame {
in property <Selection> selection;

x: root.selection.x;
y: root.selection.y;
width: root.selection.width;
height: root.selection.height;

callback update-geometry(/* x */ length, /* y */ length, /* width */ length, /* height */ length);

if !selection.is-primary: Rectangle {
x: 0;
y: 0;
border-color: root.selection.border-color;
border-width: 1px;
}

if selection.is-primary: Resizer {
is-moveable: true;
is-resizable: true;

x-position: root.x;
y-position: root.y;

color: root.selection.border-color;
x: 0;
y: 0;
width: root.width;
height: root.height;

update-geometry(x, y, w, h, done) => {
root.x = x;
root.y = y;
root.width = w;
root.height = h;

if done {
root.update-geometry(x, y, w, h);
}
}

inner := Rectangle {
border-color: root.selection.border-color;
border-width: 1px;
background: parent.resizing || parent.moving ? root.selection.border-color.with-alpha(0.5) : root.selection.border-color.with-alpha(0.0);
}
}
}

export component DrawArea {
Expand All @@ -38,6 +88,7 @@ export component DrawArea {
callback select-behind(/* x */ length, /* y */ length, /* enter_component? */ bool, /* reverse */ bool);
callback show-document(/* url */ string, /* line */ int, /* column */ int);
callback unselect();
callback selected-element-update-geometry(/* x */ length, /* y */ length, /* width */ length, /* height */ length);

preferred-height: max(i-preview-area-container.preferred-height, i-preview-area-container.min-height) + 2 * i-scroll-view.border;
preferred-width: max(i-preview-area-container.preferred-width, i-preview-area-container.min-width) + 2 * i-scroll-view.border;
Expand All @@ -51,8 +102,8 @@ export component DrawArea {
i-drawing-rect := Rectangle {
background: Palette.alternate-background;

width: max(i-scroll-view.visible-width, i-resizer.width + i-scroll-view.border);
height: max(i-scroll-view.visible-height, i-resizer.height + i-scroll-view.border);
width: max(i-scroll-view.visible-width, main-resizer.width + i-scroll-view.border);
height: max(i-scroll-view.visible-height, main-resizer.height + i-scroll-view.border);

unselect-area := TouchArea {
clicked => { root.unselect(); }
Expand All @@ -61,18 +112,22 @@ export component DrawArea {
}

i-content-border := Rectangle {
x: i-resizer.x + (i-resizer.width - self.width) / 2;
y: i-resizer.y + (i-resizer.height - self.height) / 2;
width: i-resizer.width + 2 * self.border-width;
height: i-resizer.height + 2 * self.border-width;
x: main-resizer.x + (main-resizer.width - self.width) / 2;
y: main-resizer.y + (main-resizer.height - self.height) / 2;
width: main-resizer.width + 2 * self.border-width;
height: main-resizer.height + 2 * self.border-width;
border-width: 1px;
border-color: Palette.border;
}

i-resizer := Resizer {
main-resizer := Resizer {
is-moveable: false;
is-resizable <=> i-preview-area-container.is-resizable;

resize(w, h) => {
x-position: parent.x;
y-position: parent.y;

update-geometry(_, _, w, h) => {
i-preview-area-container.width = clamp(w, i-preview-area-container.min-width, i-preview-area-container.max-width);
i-preview-area-container.height = clamp(h, i-preview-area-container.min-height, i-preview-area-container.max-height);
}
Expand All @@ -93,7 +148,6 @@ export component DrawArea {
}

i-preview-area-container := ComponentContainer {

property <bool> is-resizable: (self.min-width != self.max-width && self.min-height != self.max-height) && self.has-component;

component-factory <=> root.preview-area;
Expand All @@ -103,6 +157,7 @@ export component DrawArea {
// Instead, we use a init function to initialize
width: 0px;
height: 0px;

init => {
self.width = max(self.preferred-width, self.min-width);
self.height = max(self.preferred-height, self.min-height);
Expand Down Expand Up @@ -141,13 +196,11 @@ export component DrawArea {
}

i-selection-display-area := Rectangle {
for s in root.selections: Rectangle {
x: s.x;
y: s.y;
width: s.width;
height: s.height;
border-color: s.border-color;
border-width: 1px;
for s in root.selections: SelectionFrame {
selection: s;
update-geometry(x, y, w, h) => {
root.selected-element-update-geometry(x, y, w, h);
}
}
}
}
Expand Down
Loading

0 comments on commit 08372e5

Please sign in to comment.