Fix list editor panics during insertion (#6540)

# Important Notes
The mouse handling changes involve an unfortunate huge hack, where we enable mouse events on the mouse shape during box selection. That way we know for sure that no other shape will be able to receive mouse enter event. Then the list editor widget is modified to only actually respond to events when its background is hovered. We will definitely want a more proper way to handle mouse event contention, but it's definitely out of scope for current bugfixing.
This commit is contained in:
Paweł Grabarz 2023-05-17 20:53:51 +02:00 committed by GitHub
parent 34b7e9efb2
commit dcdba8d1ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1231 additions and 650 deletions

37
Cargo.lock generated
View File

@ -683,7 +683,7 @@ checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -797,6 +797,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.7.3" version = "0.7.3"
@ -1022,7 +1028,7 @@ version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"textwrap 0.11.0", "textwrap 0.11.0",
"unicode-width", "unicode-width",
] ]
@ -1034,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags 1.3.2",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"indexmap", "indexmap",
@ -2594,7 +2600,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"bit_field", "bit_field",
"bitflags", "bitflags 2.2.1",
"code-builder", "code-builder",
"console_error_panic_hook", "console_error_panic_hook",
"enso-callback", "enso-callback",
@ -3825,7 +3831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"headers-core", "headers-core",
"http", "http",
@ -4338,6 +4344,7 @@ dependencies = [
"ast", "ast",
"base64 0.13.1", "base64 0.13.1",
"bimap", "bimap",
"bitflags 2.2.1",
"engine-protocol", "engine-protocol",
"enso-config", "enso-config",
"enso-frp", "enso-frp",
@ -4544,7 +4551,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a989afac88279b0482f402d234b5fbd405bf1ad051308595b58de4e6de22346b" checksum = "a989afac88279b0482f402d234b5fbd405bf1ad051308595b58de4e6de22346b"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"serde", "serde",
"unicode-segmentation", "unicode-segmentation",
] ]
@ -4805,7 +4812,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb67c6dd0fa9b00619c41c5700b6f92d5f418be49b45ddb9970fbd4569df3c8" checksum = "bfb67c6dd0fa9b00619c41c5700b6f92d5f418be49b45ddb9970fbd4569df3c8"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -4900,7 +4907,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"memoffset", "memoffset",
@ -5103,7 +5110,7 @@ version = "0.10.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"foreign-types", "foreign-types",
"libc", "libc",
@ -5593,7 +5600,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"getopts", "getopts",
"memchr", "memchr",
"unicase", "unicase",
@ -5738,7 +5745,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -5897,7 +5904,7 @@ version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"errno", "errno",
"io-lifetimes", "io-lifetimes",
"libc", "libc",
@ -5972,7 +5979,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a617c811f5c9a7060fe511d35d13bf5b9f0463ce36d63ce666d05779df2b4eba" checksum = "a617c811f5c9a7060fe511d35d13bf5b9f0463ce36d63ce666d05779df2b4eba"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"bytemuck", "bytemuck",
"smallvec 1.10.0", "smallvec 1.10.0",
"ttf-parser", "ttf-parser",
@ -6065,7 +6072,7 @@ version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -6867,7 +6874,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",

View File

@ -136,3 +136,4 @@ syn = { version = "1.0", features = [
quote = { version = "1.0.23" } quote = { version = "1.0.23" }
semver = { version = "1.0.0", features = ["serde"] } semver = { version = "1.0.0", features = ["serde"] }
thiserror = "1.0.40" thiserror = "1.0.40"
bitflags = { version = "2.2.1" }

View File

@ -145,7 +145,7 @@ impl<T: Payload> ChildGenerator<T> {
let kind = kind.into(); let kind = kind.into();
let size = self.current_offset; let size = self.current_offset;
let children = self.children; let children = self.children;
Node { kind, size, children, ast_id, parenthesized: false, payload: default() } Node { kind, size, children, ast_id, ..default() }
} }
} }
@ -498,12 +498,11 @@ fn generate_node_for_opr_chain<T: Payload>(
Ok(( Ok((
Node { Node {
kind: if is_last { kind.clone() } else { node::Kind::chained().into() }, kind: if is_last { kind.clone() } else { node::Kind::chained().into() },
parenthesized: false, size: gen.current_offset,
size: gen.current_offset, children: gen.children,
children: gen.children, ast_id: elem.infix_id,
ast_id: elem.infix_id, ..default()
payload: default(),
}, },
elem.offset, elem.offset,
)) ))
@ -778,11 +777,12 @@ fn generate_expected_argument<T: Payload>(
argument_info: ArgumentInfo, argument_info: ArgumentInfo,
) -> Node<T> { ) -> Node<T> {
let mut gen = ChildGenerator::default(); let mut gen = ChildGenerator::default();
let extended_ast_id = node.ast_id.or(node.extended_ast_id);
gen.add_node(ast::Crumbs::new(), node); gen.add_node(ast::Crumbs::new(), node);
let arg_node = gen.generate_empty_node(InsertionPointType::ExpectedArgument { index, named }); let arg_node = gen.generate_empty_node(InsertionPointType::ExpectedArgument { index, named });
arg_node.node.set_argument_info(argument_info); arg_node.node.set_argument_info(argument_info);
let kind = node::Kind::chained().into(); let kind = node::Kind::chained().into();
Node { kind, size: gen.current_offset, children: gen.children, ..default() } Node { kind, size: gen.current_offset, children: gen.children, extended_ast_id, ..default() }
} }
/// Build a prefix application-like span tree structure where no prefix argument has been provided /// Build a prefix application-like span tree structure where no prefix argument has been provided
@ -873,8 +873,7 @@ fn tree_generate_node<T: Payload>(
} }
size = parent_offset; size = parent_offset;
} }
let payload = default(); Ok(Node { kind, parenthesized, size, children, ast_id, ..default() })
Ok(Node { kind, parenthesized, size, children, ast_id, payload })
} }
@ -940,6 +939,7 @@ mod test {
/// cleaner the expression IDs are removed before comparing trees. /// cleaner the expression IDs are removed before comparing trees.
fn clear_expression_ids<T>(node: &mut Node<T>) { fn clear_expression_ids<T>(node: &mut Node<T>) {
node.ast_id = None; node.ast_id = None;
node.extended_ast_id = None;
for child in &mut node.children { for child in &mut node.children {
clear_expression_ids(&mut child.node); clear_expression_ids(&mut child.node);
} }

View File

@ -187,6 +187,29 @@ impl<T> SpanTree<T> {
let root = self.root.map(f); let root = self.root.map(f);
SpanTree { root } SpanTree { root }
} }
/// Map over the nodes in given crumbs chain, from the root to the node identified by the
/// crumbs. Terminates on the first `Some(R)` result and returns it.
pub fn find_map_in_chain<'a, R, F>(
&self,
crumbs: impl IntoIterator<Item = &'a Crumb>,
mut f: F,
) -> Option<R>
where
F: FnMut(usize, &Node<T>) -> Option<R>,
{
let mut crumbs = crumbs.into_iter();
let mut current_node = &self.root;
let mut idx = 0;
loop {
let result = f(idx, current_node);
if result.is_some() {
return result;
}
idx += 1;
current_node = current_node.children.get(*crumbs.next()?)?;
}
}
} }
@ -305,7 +328,10 @@ impl<T> SpanTree<T> {
if let Some(ast_id) = node.ast_id { if let Some(ast_id) = node.ast_id {
write!(buffer, " ast_id={ast_id:?}").unwrap(); write!(buffer, " ast_id={ast_id:?}").unwrap();
} else if let Some(ext_id) = node.extended_ast_id {
write!(buffer, " ext_id={ext_id:?}").unwrap();
} }
buffer.push('\n'); buffer.push('\n');
let num_children = node.children.len(); let num_children = node.children.len();

View File

@ -36,12 +36,16 @@ pub trait Payload = Default + Clone;
#[derive(Clone, Debug, Default, Eq, PartialEq)] #[derive(Clone, Debug, Default, Eq, PartialEq)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct Node<T> { pub struct Node<T> {
pub kind: Kind, pub kind: Kind,
pub size: ByteDiff, pub size: ByteDiff,
pub children: Vec<Child<T>>, pub children: Vec<Child<T>>,
pub ast_id: Option<ast::Id>, pub ast_id: Option<ast::Id>,
pub parenthesized: bool, /// When this `Node` is a part of an AST extension (a virtual span that only exists in
pub payload: T, /// span-tree, but not in AST), this field will contain the AST ID of the expression it extends
/// (e.g. the AST of a function call with missing arguments, extended with expected arguments).
pub extended_ast_id: Option<ast::Id>,
pub parenthesized: bool,
pub payload: T,
} }
impl<T> Deref for Node<T> { impl<T> Deref for Node<T> {
@ -74,8 +78,9 @@ impl<T> Node<T> {
let size = self.size; let size = self.size;
let children = self.children.into_iter().map(|t| t.map(f)).collect_vec(); let children = self.children.into_iter().map(|t| t.map(f)).collect_vec();
let ast_id = self.ast_id; let ast_id = self.ast_id;
let extended_ast_id = self.extended_ast_id;
let payload = f(self.payload); let payload = f(self.payload);
Node { kind, parenthesized, size, children, ast_id, payload } Node { kind, parenthesized, size, children, ast_id, extended_ast_id, payload }
} }
} }

View File

@ -32,7 +32,7 @@ pub fn deserialize_widget_definitions(
e.context(format!("{msg} '{argument_name}'")) e.context(format!("{msg} '{argument_name}'"))
})?; })?;
let meta = widget.map(to_configuration); let meta = widget.map(to_configuration);
let argument_name = argument_name.to_owned(); let argument_name = argument_name.into_owned();
Ok(ArgumentWidgetConfig { argument_name, config: meta }) Ok(ArgumentWidgetConfig { argument_name, config: meta })
}, },
); );
@ -60,25 +60,25 @@ fn to_kind(inner: response::WidgetKindDefinition) -> widget::DynConfig {
response::WidgetKindDefinition::SingleChoice { label, values } => response::WidgetKindDefinition::SingleChoice { label, values } =>
widget::single_choice::Config { widget::single_choice::Config {
label: label.map(Into::into), label: label.map(Into::into),
entries: Rc::new(to_entries(&values)), entries: Rc::new(to_entries(values)),
} }
.into(), .into(),
response::WidgetKindDefinition::ListEditor { item_widget, item_default } => response::WidgetKindDefinition::ListEditor { item_widget, item_default } =>
widget::list_editor::Config { widget::list_editor::Config {
item_widget: Some(Rc::new(to_configuration(*item_widget))), item_widget: Some(Rc::new(to_configuration(*item_widget))),
item_default: item_default.into(), item_default: ImString::from(item_default).into(),
} }
.into(), .into(),
_ => widget::label::Config::default().into(), _ => widget::label::Config::default().into(),
} }
} }
fn to_entries(choices: &[response::Choice]) -> Vec<widget::Entry> { fn to_entries(choices: Vec<response::Choice>) -> Vec<widget::Entry> {
choices.iter().map(to_entry).collect() choices.into_iter().map(to_entry).collect()
} }
fn to_entry(choice: &response::Choice) -> widget::Entry { fn to_entry(choice: response::Choice) -> widget::Entry {
let value: ImString = (&choice.value).into(); let value: ImString = choice.value.into();
let label = choice.label.as_ref().map_or_else(|| value.clone(), |label| label.into()); let label = choice.label.map_or_else(|| value.clone(), |label| label.into());
widget::Entry { required_import: None, value, label } widget::Entry { required_import: None, value, label }
} }

View File

@ -14,7 +14,7 @@ use ide_view::graph_editor::component::node::input::widget;
/// A top level object received from the widget visualization, which contains widget definitions for /// A top level object received from the widget visualization, which contains widget definitions for
/// all arguments of a single Enso method. Configurations are paired with the name of function /// all arguments of a single Enso method. Configurations are paired with the name of function
/// argument they are associated with. /// argument they are associated with.
pub(super) type WidgetDefinitions<'a> = Vec<(&'a str, FallableWidgetDefinition<'a>)>; pub(super) type WidgetDefinitions<'a> = Vec<(Cow<'a, str>, FallableWidgetDefinition<'a>)>;
/// A wrapper type that allows deserialization of a widget definitions to partially fail: failure /// A wrapper type that allows deserialization of a widget definitions to partially fail: failure
/// message of individual widget definition deserialization will be preserved and deserialization /// message of individual widget definition deserialization will be preserved and deserialization
@ -56,6 +56,13 @@ pub(super) struct WidgetDefinition<'a> {
} }
/// Part of [`WidgetDefinition`] that is dependant on widget kind. /// Part of [`WidgetDefinition`] that is dependant on widget kind.
///
/// NOTE: Using `Cow<'a, str>` instead of `&'a str` is important here, because `serde_json` does not
/// support deserializing into borrowed str when the received value contains escape sequences. In
/// those cases (and ONLY then), the borrow serialization would fail. Using `Cow` allows us to
/// deserialize into borrowed strings when possible, but falls back to allocation in rare cases when
/// it is not.
/// See: https://github.com/serde-rs/json/issues/742
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
#[serde(tag = "constructor")] #[serde(tag = "constructor")]
pub(super) enum WidgetKindDefinition<'a> { pub(super) enum WidgetKindDefinition<'a> {
@ -65,7 +72,7 @@ pub(super) enum WidgetKindDefinition<'a> {
/// The text that is displayed when no value is chosen. By default, the parameter name is /// The text that is displayed when no value is chosen. By default, the parameter name is
/// used. /// used.
#[serde(borrow, default)] #[serde(borrow, default)]
label: Option<&'a str>, label: Option<Cow<'a, str>>,
/// A list of choices to display. /// A list of choices to display.
#[serde(borrow, default)] #[serde(borrow, default)]
values: Vec<Choice<'a>>, values: Vec<Choice<'a>>,
@ -80,7 +87,7 @@ pub(super) enum WidgetKindDefinition<'a> {
item_widget: Box<WidgetDefinition<'a>>, item_widget: Box<WidgetDefinition<'a>>,
/// The default value for new items inserted when the user adds a new element. /// The default value for new items inserted when the user adds a new element.
#[serde(borrow)] #[serde(borrow)]
item_default: &'a str, item_default: Cow<'a, str>,
}, },
/// A multi value widget. /// A multi value widget.
@ -131,9 +138,9 @@ pub enum Display {
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub(super) struct Choice<'a> { pub(super) struct Choice<'a> {
/// The value of the choice. Must be a valid Enso expression. /// The value of the choice. Must be a valid Enso expression.
pub value: &'a str, pub value: Cow<'a, str>,
/// Custom label to display in the dropdown. If not provided, IDE will create a label based on /// Custom label to display in the dropdown. If not provided, IDE will create a label based on
/// value. /// value.
#[serde(borrow)] #[serde(borrow)]
pub label: Option<&'a str>, pub label: Option<Cow<'a, str>>,
} }

View File

@ -37,6 +37,7 @@ sourcemap = "6.0"
span-tree = { path = "../../language/span-tree" } span-tree = { path = "../../language/span-tree" }
uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] } uuid = { version = "0.8", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true }
bitflags = { workspace = true }
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.4" version = "0.3.4"

View File

@ -287,6 +287,7 @@ pub mod joint {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
above = [compound::rectangle::shape];
pointer_events = false; pointer_events = false;
alignment = center; alignment = center;
(style: Style, color_rgba: Vector4<f32>) { (style: Style, color_rgba: Vector4<f32>) {
@ -320,13 +321,13 @@ fn corner_base_shape(
// FIXME [WD]: The 2 following impls are almost the same. Should be merged. This task should be // FIXME [WD]: The 2 following impls are almost the same. Should be merged. This task should be
// handled by Wojciech. // handled by Wojciech.
macro_rules! define_corner_start { macro_rules! define_corner_start {
() => { ($($args:tt)*) => {
/// Shape definition. /// Shape definition.
pub mod corner { pub mod corner {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
below = [joint]; $($args)*
alignment = center; alignment = center;
( style: Style ( style: Style
, radius : f32 , radius : f32
@ -421,12 +422,12 @@ macro_rules! define_corner_start {
macro_rules! define_corner_end { macro_rules! define_corner_end {
() => { ($($args:tt)*) => {
/// Shape definition. /// Shape definition.
pub mod corner { pub mod corner {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
below = [joint]; $($args)*
alignment = center; alignment = center;
( (
style: Style, style: Style,
@ -526,12 +527,12 @@ macro_rules! define_corner_end {
} }
macro_rules! define_line { macro_rules! define_line {
() => { ($($args:tt)*) => {
/// Shape definition. /// Shape definition.
pub mod line { pub mod line {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
below = [joint]; $($args)*
alignment = center; alignment = center;
( (
style: Style, style: Style,
@ -595,12 +596,12 @@ macro_rules! define_line {
}; };
} }
macro_rules! define_arrow { () => { macro_rules! define_arrow { ($($args:tt)*) => {
/// Shape definition. /// Shape definition.
pub mod arrow { pub mod arrow {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
above = [joint]; $($args)*
alignment = center; alignment = center;
( (
style: Style, style: Style,
@ -743,17 +744,31 @@ impl LayoutLine for back::line::View {
/// Shape definitions which will be rendered in the front layer (on top of nodes). /// Shape definitions which will be rendered in the front layer (on top of nodes).
pub mod front { pub mod front {
use super::*; use super::*;
define_corner_start!(); define_corner_start!(
define_line!(); above = [node::backdrop, compound::rectangle::shape];
define_arrow!(); below = [joint];
);
define_line!(
above = [node::backdrop, compound::rectangle::shape];
below = [joint];
);
define_arrow!(
above = [joint, node::backdrop, compound::rectangle::shape];
);
} }
/// Shape definitions which will be rendered in the bottom layer (below nodes). /// Shape definitions which will be rendered in the bottom layer (below nodes).
pub mod back { pub mod back {
use super::*; use super::*;
define_corner_end!(); define_corner_end!(
define_line!(); below = [node::backdrop];
define_arrow!(); );
define_line!(
below = [node::backdrop];
);
define_arrow!(
below = [node::backdrop];
);
} }

View File

@ -14,12 +14,12 @@ use crate::view;
use crate::CallWidgetsConfig; use crate::CallWidgetsConfig;
use crate::Type; use crate::Type;
use super::edge;
use engine_protocol::language_server::ExecutionEnvironment; use engine_protocol::language_server::ExecutionEnvironment;
use enso_frp as frp; use enso_frp as frp;
use enso_frp; use enso_frp;
use ensogl::animation::delayed::DelayedAnimation; use ensogl::animation::delayed::DelayedAnimation;
use ensogl::application::Application; use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color; use ensogl::data::color;
use ensogl::display; use ensogl::display;
use ensogl::display::scene::Layer; use ensogl::display::scene::Layer;
@ -104,33 +104,13 @@ pub type Comment = ImString;
// === Shape === // === Shape ===
// ============= // =============
/// Node background definition.
pub mod background {
use super::*;
ensogl::shape! {
pointer_events = false;
alignment = center;
(style:Style, bg_color:Vector4) {
let bg_color = Var::<color::Rgba>::from(bg_color);
let width = Var::<Pixels>::from("input_size.x");
let height = Var::<Pixels>::from("input_size.y");
let width = width - PADDING.px() * 2.0;
let height = height - PADDING.px() * 2.0;
let radius = RADIUS.px();
let shape = Rect((&width,&height)).corners_radius(radius);
let shape = shape.fill(bg_color);
shape.into()
}
}
}
/// Node backdrop. Contains shadow and selection. /// Node backdrop. Contains shadow and selection.
pub mod backdrop { pub mod backdrop {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
// Disabled to allow interaction with the output port. // Disabled to allow interaction with the output port.
below = [compound::rectangle::shape];
pointer_events = false; pointer_events = false;
alignment = center; alignment = center;
(style:Style, selection:f32) { (style:Style, selection:f32) {
@ -187,28 +167,6 @@ pub mod backdrop {
} }
} }
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub mod drag_area {
use super::*;
ensogl::shape! {
alignment = center;
(style:Style) {
let width : Var<Pixels> = "input_size.x".into();
let height : Var<Pixels> = "input_size.y".into();
let width = width - PADDING.px() * 2.0;
let height = height - PADDING.px() * 2.0;
let radius = 14.px();
let shape = Rect((&width,&height)).corners_radius(radius);
let shape = shape.fill(color::Rgba::new(0.0,0.0,0.0,0.000_001));
let out = shape;
out.into()
}
}
}
// ======================= // =======================
// === Error Indicator === // === Error Indicator ===
@ -336,6 +294,9 @@ ensogl::define_endpoints_2! {
/// Set read-only mode for input ports. /// Set read-only mode for input ports.
set_read_only (bool), set_read_only (bool),
/// Set the mode in which the cursor will indicate that editing of the node is possible.
set_edit_ready_mode (bool),
} }
Output { Output {
/// Press event. Emitted when user clicks on non-active part of the node, like its /// Press event. Emitted when user clicks on non-active part of the node, like its
@ -468,8 +429,7 @@ pub struct NodeModel {
pub app: Application, pub app: Application,
pub display_object: display::object::Instance, pub display_object: display::object::Instance,
pub backdrop: backdrop::View, pub backdrop: backdrop::View,
pub background: background::View, pub background: Rectangle,
pub drag_area: drag_area::View,
pub error_indicator: error_shape::View, pub error_indicator: error_shape::View,
pub profiling_label: ProfilingLabel, pub profiling_label: ProfilingLabel,
pub input: input::Area, pub input: input::Area,
@ -486,39 +446,18 @@ impl NodeModel {
/// Constructor. /// Constructor.
#[profile(Debug)] #[profile(Debug)]
pub fn new(app: &Application, registry: visualization::Registry) -> Self { pub fn new(app: &Application, registry: visualization::Registry) -> Self {
ensogl::shapes_order_dependencies! {
app.display.default_scene => {
//TODO[ao] The two lines below should not be needed - the ordering should be
// transitive. But removing them causes a visual glitches described in
// https://github.com/enso-org/ide/issues/1624
// The matter should be further investigated.
edge::back::corner -> backdrop;
edge::back::line -> backdrop;
edge::back::corner -> error_shape;
edge::back::line -> error_shape;
error_shape -> backdrop;
backdrop -> output::port::single_port;
backdrop -> output::port::multi_port;
output::port::single_port -> background;
output::port::multi_port -> background;
background -> drag_area;
drag_area -> edge::front::corner;
drag_area -> edge::front::line;
}
}
let scene = &app.display.default_scene; let scene = &app.display.default_scene;
let error_indicator = error_shape::View::new(); let error_indicator = error_shape::View::new();
let profiling_label = ProfilingLabel::new(app); let profiling_label = ProfilingLabel::new(app);
let backdrop = backdrop::View::new(); let backdrop = backdrop::View::new();
let background = background::View::new(); let background = Rectangle::new().build(|v| {
let drag_area = drag_area::View::new(); v.set_corner_radius(RADIUS);
});
let vcs_indicator = vcs::StatusIndicator::new(app); let vcs_indicator = vcs::StatusIndicator::new(app);
let display_object = display::object::Instance::new_named("Node"); let display_object = display::object::Instance::new_named("Node");
display_object.add_child(&profiling_label); display_object.add_child(&profiling_label);
display_object.add_child(&drag_area);
display_object.add_child(&backdrop); display_object.add_child(&backdrop);
display_object.add_child(&background); display_object.add_child(&background);
display_object.add_child(&vcs_indicator); display_object.add_child(&vcs_indicator);
@ -551,7 +490,6 @@ impl NodeModel {
display_object, display_object,
backdrop, backdrop,
background, background,
drag_area,
error_indicator, error_indicator,
profiling_label, profiling_label,
input, input,
@ -643,16 +581,18 @@ impl NodeModel {
let size = Vector2(width, height); let size = Vector2(width, height);
let padded_size = size + Vector2(PADDING, PADDING) * 2.0; let padded_size = size + Vector2(PADDING, PADDING) * 2.0;
self.backdrop.set_size(padded_size); self.backdrop.set_size(padded_size);
self.background.set_size(padded_size); self.background.set_size(size);
self.drag_area.set_size(padded_size);
self.error_indicator.set_size(padded_size); self.error_indicator.set_size(padded_size);
self.vcs_indicator.frp.set_size(padded_size); self.vcs_indicator.frp.set_size(padded_size);
let x_offset_to_node_center = x_offset_to_node_center(width); let x_offset_to_node_center = x_offset_to_node_center(width);
// Position shapes such that the center of their left edge is at the node origin:
// - Most shapes are still center-aligned, we have to move them horizontally.
self.backdrop.set_x(x_offset_to_node_center); self.backdrop.set_x(x_offset_to_node_center);
self.background.set_x(x_offset_to_node_center);
self.drag_area.set_x(x_offset_to_node_center);
self.error_indicator.set_x(x_offset_to_node_center); self.error_indicator.set_x(x_offset_to_node_center);
self.vcs_indicator.set_x(x_offset_to_node_center); self.vcs_indicator.set_x(x_offset_to_node_center);
// - Background is a bottom-left aligned Rectangle, thus we have to move it vertically.
self.background.set_y(-height / 2.0);
let action_bar_width = ACTION_BAR_WIDTH; let action_bar_width = ACTION_BAR_WIDTH;
self.action_bar self.action_bar
@ -730,17 +670,27 @@ impl Node {
// ths user hovers the drag area. The input port manager merges this information with // ths user hovers the drag area. The input port manager merges this information with
// port hover events and outputs the final hover event for any part inside of the node. // port hover events and outputs the final hover event for any part inside of the node.
let drag_area = &model.drag_area.events_deprecated; let background_enter = model.background.on_event::<mouse::Enter>();
drag_area_hover <- bool(&drag_area.mouse_out,&drag_area.mouse_over); let background_leave = model.background.on_event::<mouse::Leave>();
model.input.set_hover <+ drag_area_hover; background_hover <- bool(&background_leave, &background_enter);
let input_enter = model.input.on_event::<mouse::Over>();
let input_leave = model.input.on_event::<mouse::Out>();
input_hover <- bool(&input_leave, &input_enter);
node_hover <- background_hover || input_hover;
node_hover <- node_hover.debounce().on_change();
model.input.set_hover <+ node_hover;
model.output.set_hover <+ model.input.body_hover; model.output.set_hover <+ model.input.body_hover;
out.hover <+ model.output.body_hover; out.hover <+ model.output.body_hover;
// === Background Press === // === Background Press ===
out.background_press <+ model.drag_area.events_deprecated.mouse_down_primary; let background_press = model.background.on_event::<mouse::Down>();
out.background_press <+ model.input.on_background_press; let input_press = model.input.on_event::<mouse::Down>();
input_as_background_press <- input_press.gate(&input.set_edit_ready_mode);
any_background_press <- any(&background_press, &input_as_background_press);
any_primary_press <- any_background_press.filter(mouse::event::is_primary);
out.background_press <+ any_primary_press.constant(());
// === Selection === // === Selection ===
@ -819,6 +769,7 @@ impl Node {
model.input.set_view_mode <+ input.set_view_mode; model.input.set_view_mode <+ input.set_view_mode;
model.output.set_view_mode <+ input.set_view_mode; model.output.set_view_mode <+ input.set_view_mode;
model.input.set_edit_ready_mode <+ input.set_edit_ready_mode;
model.profiling_label.set_view_mode <+ input.set_view_mode; model.profiling_label.set_view_mode <+ input.set_view_mode;
model.vcs_indicator.set_visibility <+ input.set_view_mode.map(|&mode| { model.vcs_indicator.set_visibility <+ input.set_view_mode.map(|&mode| {
!matches!(mode,view::Mode::Profiling {..}) !matches!(mode,view::Mode::Profiling {..})
@ -981,12 +932,8 @@ impl Node {
// else { style.get_color(bg_color_path) } // else { style.get_color(bg_color_path) }
// })); // }));
// bg_color_anim.target <+ bg_color; // bg_color_anim.target <+ bg_color;
// eval bg_color_anim.value ((c)
// model.background.bg_color.set(color::Rgba::from(c).into())
// );
eval bg_color_anim.value ((c) eval bg_color_anim.value ([model] (c) { model.background.set_color(c.into()); });
model.background.bg_color.set(color::Rgba::from(c).into()));
// === Tooltip === // === Tooltip ===

View File

@ -365,7 +365,6 @@ ensogl::define_endpoints! {
on_port_press (Crumbs), on_port_press (Crumbs),
on_port_hover (Switch<Crumbs>), on_port_hover (Switch<Crumbs>),
on_port_code_update (Crumbs,ImString), on_port_code_update (Crumbs,ImString),
on_background_press (),
view_mode (view::Mode), view_mode (view::Mode),
/// A set of widgets attached to a method requests their definitions to be queried from an /// A set of widgets attached to a method requests their definitions to be queried from an
/// external source. The tuple contains the ID of the call expression the widget is attached /// external source. The tuple contains the ID of the call expression the widget is attached
@ -435,10 +434,11 @@ impl Area {
let ports_active = &frp.set_ports_active; let ports_active = &frp.set_ports_active;
edit_or_ready <- frp.set_edit_ready_mode || set_editing; edit_or_ready <- frp.set_edit_ready_mode || set_editing;
reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, (a, _)| *e && !a); reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, (a, _)| *e && !a);
port_vis <- all_with(&edit_or_ready, ports_active, |e, (a, _)| !e && *a); port_vis <- all_with(&set_editing, ports_active, |e, (a, _)| !e && *a);
frp.output.source.ports_visible <+ port_vis; frp.output.source.ports_visible <+ port_vis;
frp.output.source.editing <+ set_editing; frp.output.source.editing <+ set_editing;
model.widget_tree.set_ports_visible <+ frp.ports_visible; model.widget_tree.set_ports_visible <+ frp.ports_visible;
model.widget_tree.set_edit_ready_mode <+ frp.set_edit_ready_mode;
refresh_edges <- model.widget_tree.connected_port_updated.debounce(); refresh_edges <- model.widget_tree.connected_port_updated.debounce();
frp.output.source.input_edges_need_refresh <+ refresh_edges; frp.output.source.input_edges_need_refresh <+ refresh_edges;

View File

@ -23,20 +23,24 @@ use ensogl::display::shape;
// === Constants === // === Constants ===
// ================= // =================
/// The horizontal padding of ports. It affects how the port shape should extend the target text /// The default horizontal padding of ports. It affects how the port shape should extend the target
/// boundary on both sides. /// text boundary on both sides. Can be overriden per widget by using
/// [`super::ConfigContext::set_port_hover_padding`].
pub const PORT_PADDING_X: f32 = 4.0; pub const PORT_PADDING_X: f32 = 4.0;
/// The horizontal padding of port hover areas. It affects how the port hover should extend the /// The default horizontal padding of port hover areas. It affects how the port hover should extend
/// target text boundary on both sides. /// the target text boundary on both sides.
pub const HOVER_PADDING_X: f32 = 2.0; const HOVER_PADDING_X: f32 = 2.0;
/// The minimum size of the port visual area. /// The minimum size of the port visual area.
pub const BASE_PORT_HEIGHT: f32 = 18.0; pub const BASE_PORT_HEIGHT: f32 = 18.0;
/// The minimum size of the port hover area.
const BASE_PORT_HOVER_HEIGHT: f32 = 16.0;
/// The vertical hover padding of ports at low depth. It affects how the port hover should extend /// The vertical hover padding of ports at low depth. It affects how the port hover should extend
/// the target text boundary on both sides. /// the target text boundary on both sides.
pub const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HEIGHT) / 2.0; const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HOVER_HEIGHT) / 2.0;
@ -107,19 +111,16 @@ impl PortLayers {
#[derive(Debug)] #[derive(Debug)]
pub struct Port { pub struct Port {
/// Drop source must be kept at the top of the struct, so it will be dropped first. /// Drop source must be kept at the top of the struct, so it will be dropped first.
_on_cleanup: frp::DropSource, _on_cleanup: frp::DropSource,
crumbs: Rc<RefCell<span_tree::Crumbs>>, crumbs: Rc<RefCell<span_tree::Crumbs>>,
port_root: display::object::Instance, port_root: display::object::Instance,
widget_root: display::object::Instance, widget_root: display::object::Instance,
widget: DynWidget, widget: DynWidget,
port_shape: PortShape, port_shape: PortShape,
hover_shape: HoverShape, hover_shape: HoverShape,
/// Last set tree depth of the port. Allows skipping layout update when the depth has not /// Last set tree depth of the port. Allows skipping layout update when the depth has not
/// changed during reconfiguration. /// changed during reconfiguration.
current_depth: usize, current_depth: usize,
/// Whether or not the port was configured as primary. Allows skipping layout update when the
/// hierarchy level has not changed significantly during reconfiguration.
current_primary: bool,
} }
impl Port { impl Port {
@ -138,15 +139,9 @@ impl Port {
port_shape port_shape
.set_size_y(BASE_PORT_HEIGHT) .set_size_y(BASE_PORT_HEIGHT)
.allow_grow() .allow_grow()
.set_margin_left(-PORT_PADDING_X) .set_margin_vh(0.0, -PORT_PADDING_X)
.set_margin_right(-PORT_PADDING_X)
.set_alignment_left_center();
hover_shape
.set_size_y(BASE_PORT_HEIGHT)
.allow_grow()
.set_margin_left(-HOVER_PADDING_X)
.set_margin_right(-HOVER_PADDING_X)
.set_alignment_left_center(); .set_alignment_left_center();
hover_shape.set_size_y(BASE_PORT_HOVER_HEIGHT).allow_grow().set_alignment_left_center();
let layers = app.display.default_scene.extension::<PortLayers>(); let layers = app.display.default_scene.extension::<PortLayers>();
layers.add_to_partition(port_shape.display_object(), hover_shape.display_object(), 0); layers.add_to_partition(port_shape.display_object(), hover_shape.display_object(), 0);
@ -201,7 +196,6 @@ impl Port {
widget_root, widget_root,
port_root, port_root,
crumbs, crumbs,
current_primary: false,
current_depth: 0, current_depth: 0,
} }
} }
@ -211,10 +205,15 @@ impl Port {
/// ///
/// See [`crate::component::node::input::widget`] module for more information about widget /// See [`crate::component::node::input::widget`] module for more information about widget
/// lifecycle. /// lifecycle.
pub fn configure(&mut self, config: &DynConfig, ctx: ConfigContext) { pub fn configure(
&mut self,
config: &DynConfig,
ctx: ConfigContext,
pad_x_override: Option<f32>,
) {
self.crumbs.replace(ctx.span_node.crumbs.clone()); self.crumbs.replace(ctx.span_node.crumbs.clone());
self.set_connected(ctx.info.connection); self.set_connected(ctx.info.connection);
self.set_port_layout(&ctx); self.set_port_layout(&ctx, pad_x_override.unwrap_or(HOVER_PADDING_X));
self.widget.configure(config, ctx); self.widget.configure(config, ctx);
self.update_root(); self.update_root();
} }
@ -243,7 +242,7 @@ impl Port {
} }
} }
fn set_port_layout(&mut self, ctx: &ConfigContext) { fn set_port_layout(&mut self, ctx: &ConfigContext, margin_x: f32) {
let node_depth = ctx.span_node.crumbs.len(); let node_depth = ctx.span_node.crumbs.len();
if self.current_depth != node_depth { if self.current_depth != node_depth {
self.current_depth = node_depth; self.current_depth = node_depth;
@ -254,12 +253,15 @@ impl Port {
} }
let is_primary = ctx.info.nesting_level.is_primary(); let is_primary = ctx.info.nesting_level.is_primary();
if self.current_primary != is_primary { let margin_y = if is_primary { PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 };
self.current_primary = is_primary;
let margin = if is_primary { PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 }; let current_margin = self.hover_shape.margin();
self.hover_shape.set_size_y(BASE_PORT_HEIGHT + 2.0 * margin); let margin_needs_update = current_margin.x().start.as_pixels() != Some(-margin_x)
self.hover_shape.set_margin_top(-margin); || current_margin.y().start.as_pixels() != Some(-margin_y);
self.hover_shape.set_margin_bottom(-margin);
if margin_needs_update {
self.hover_shape.set_size_y(BASE_PORT_HOVER_HEIGHT + 2.0 * margin_y);
self.hover_shape.set_margin_vh(-margin_y, -margin_x);
} }
} }

View File

@ -48,7 +48,6 @@ use crate::component::node::input::area::NODE_HEIGHT;
use crate::component::node::input::area::TEXT_OFFSET; use crate::component::node::input::area::TEXT_OFFSET;
use crate::component::node::input::port::Port; use crate::component::node::input::port::Port;
use enso_config::ARGS;
use enso_frp as frp; use enso_frp as frp;
use enso_text as text; use enso_text as text;
use ensogl::application::Application; use ensogl::application::Application;
@ -58,6 +57,7 @@ use ensogl::display::shape::StyleWatch;
use ensogl::gui::cursor; use ensogl::gui::cursor;
use ensogl_component::drop_down::DropdownValue; use ensogl_component::drop_down::DropdownValue;
use span_tree::node::Ref as SpanRef; use span_tree::node::Ref as SpanRef;
use span_tree::TagValue;
use text::index::Byte; use text::index::Byte;
@ -85,6 +85,7 @@ pub const PRIMARY_PORT_MAX_NESTING_LEVEL: usize = 0;
ensogl::define_endpoints_2! { ensogl::define_endpoints_2! {
Input { Input {
set_ports_visible (bool), set_ports_visible (bool),
set_edit_ready_mode (bool),
set_read_only (bool), set_read_only (bool),
set_view_mode (crate::view::Mode), set_view_mode (crate::view::Mode),
set_profiling_status (crate::node::profiling::Status), set_profiling_status (crate::node::profiling::Status),
@ -179,6 +180,25 @@ macro_rules! define_widget_modules(
),* ),*
} }
bitflags::bitflags!{
/// A set of flags that determine the widget kind.
#[derive(Debug, Default, Clone, Copy)]
pub struct DynKindFlags: u32 {
$(
#[allow(missing_docs, non_upper_case_globals)]
const $name = 1 << ${index()};
)*
}
}
impl DynConfig {
fn flag(&self) -> DynKindFlags {
match self {
$(DynConfig::$name(_) => DynKindFlags::$name,)*
}
}
}
$( $(
impl const From<<$module::Widget as SpanWidget>::Config> for DynConfig { impl const From<<$module::Widget as SpanWidget>::Config> for DynConfig {
fn from(config: <$module::Widget as SpanWidget>::Config) -> Self { fn from(config: <$module::Widget as SpanWidget>::Config) -> Self {
@ -267,54 +287,84 @@ pub struct Configuration {
} }
impl Configuration { impl Configuration {
/// Derive widget configuration from Enso expression, node data in span tree and inferred value /// Derive widget configuration from Enso expression, node data in span tree and inferred node
/// type. When no configuration is provided with an override, this function will be used to /// info, like value type. When no configuration is provided with an override, this function
/// create a default configuration. /// will be used to create a default configuration.
///
/// Will never return any configuration kind specified in `disallow` parameter, except for
/// [`DynConfig::Label`] as an option of last resort.
fn from_node( fn from_node(
span_node: &SpanRef, span_node: &SpanRef,
usage_type: Option<crate::Type>, info: &NodeInfo,
expression: &str, expression: &str,
is_directly_connected: bool, disallow: DynKindFlags,
) -> Self { ) -> Self {
use span_tree::node::Kind; use span_tree::node::Kind;
let kind = &span_node.kind; let kind = &span_node.kind;
let has_children = !span_node.children.is_empty(); let has_children = !span_node.children.is_empty();
let allow = move |kind: DynKindFlags| !disallow.contains(kind);
use DynKindFlags as F;
const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector";
let is_list_editor_enabled = ARGS.groups.feature_preview.options.vector_editor.value;
let node_expr = &expression[span_node.span()]; let node_expr = &expression[span_node.span()];
let looks_like_vector = node_expr.starts_with('[') && node_expr.ends_with(']'); let looks_like_vector = node_expr.starts_with('[') && node_expr.ends_with(']');
let type_is_vector = |tp: &Option<String>| { let is_expected_arg = kind.is_expected_argument();
usage_type
.as_ref()
.map(|t| t.as_str())
.or(tp.as_deref())
.map_or(false, |tp| tp.contains(VECTOR_TYPE))
};
match kind { let usage_type = info.usage_type.as_ref().map(|t| t.as_str());
Kind::Argument(arg) if !arg.tag_values.is_empty() => let decl_type = kind.tp().map(|t| t.as_str());
Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values), let decl_or_usage = decl_type.or(usage_type);
Kind::Argument(_) if is_list_editor_enabled && looks_like_vector => Self::list_editor(),
Kind::Root if is_list_editor_enabled && looks_like_vector => Self::list_editor(), let first_decl_is_vector = || {
Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() => decl_type
if is_list_editor_enabled && (type_is_vector(&arg.tp) || looks_like_vector) { .map_or(false, |t| t.trim_start_matches('(').starts_with(list_editor::VECTOR_TYPE))
Self::list_editor() };
} else if !arg.tag_values.is_empty() { let type_may_be_vector = || {
Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values) decl_type.map_or(false, |t| t.contains(list_editor::VECTOR_TYPE))
} else { || usage_type.map_or(false, |t| t.contains(list_editor::VECTOR_TYPE))
Self::always(label::Config::default()) };
}, let allows_list = allow(F::ListEditor)
Kind::Operation if !has_children => && info.connection.is_none()
Self::maybe_with_port(label::Config::default(), is_directly_connected), && (looks_like_vector || (is_expected_arg && type_may_be_vector()));
Kind::Token if !has_children => Self::inert(label::Config::default()), let prefer_list = allows_list && first_decl_is_vector();
Kind::NamedArgument => Self::inert(hierarchy::Config), let tags = kind.tag_values().filter(|tags| !tags.is_empty());
Kind::InsertionPoint(_) => let first_tag = tags.and_then(|t| t.first());
Self::maybe_with_port(insertion_point::Config, is_directly_connected),
_ if has_children => Self::always(hierarchy::Config), let mut config =
_ => Self::always(label::Config::default()), match (kind, tags) {
} (Kind::Argument(_) | Kind::InsertionPoint(_), Some(tags))
if (allow(F::SingleChoice) || prefer_list) =>
Self::static_dropdown(kind.name().as_ref().map(Into::into), tags)
.into_list_item_if(prefer_list, decl_or_usage, first_tag),
(Kind::Root | Kind::Argument(_), _) if allows_list =>
Self::list_editor(None, decl_or_usage, first_tag),
(Kind::InsertionPoint(p), _)
if p.kind.is_expected_argument() && (allow(F::Label) || allows_list) =>
Self::always(label::Config::default()).into_list_item_if(
allows_list,
decl_or_usage,
first_tag,
),
_ if allow(F::Hierarchy) && has_children => Self::always(hierarchy::Config),
(Kind::Token | Kind::Operation, _) if allow(F::Label) =>
Self::inert(label::Config::default()),
(Kind::InsertionPoint(_), _) if allow(F::InsertionPoint) =>
Self::inert(insertion_point::Config),
_ => {
// Option of last resort, label is allowed in this case. Skip assert.
return Self::always(label::Config::default());
}
};
config.has_port = config.has_port || info.connection.is_some();
let allowed = allow(config.kind.flag());
assert!(allowed, "Created widget configuration of a kind that was disallowed.");
config
} }
const fn maybe_with_port<C>(kind: C, has_port: bool) -> Self const fn maybe_with_port<C>(kind: C, has_port: bool) -> Self
@ -334,16 +384,45 @@ impl Configuration {
/// Widget configuration for static dropdown, based on the tag values provided by suggestion /// Widget configuration for static dropdown, based on the tag values provided by suggestion
/// database. /// database.
fn static_dropdown( fn static_dropdown(label: Option<ImString>, tag_values: &[TagValue]) -> Configuration {
label: Option<ImString>,
tag_values: &[span_tree::TagValue],
) -> Configuration {
let entries = Rc::new(tag_values.iter().map(Entry::from).collect()); let entries = Rc::new(tag_values.iter().map(Entry::from).collect());
Self::always(single_choice::Config { label, entries }) Self::always(single_choice::Config { label, entries })
} }
fn list_editor() -> Configuration { fn into_list_item_if(
Self::always(list_editor::Config { item_widget: None, item_default: "_".into() }) self,
condition: bool,
typename: Option<&str>,
default_tag: Option<&TagValue>,
) -> Self {
if condition {
self.into_list_item(typename, default_tag)
} else {
self
}
}
fn into_list_item(self, typename: Option<&str>, default_tag: Option<&TagValue>) -> Self {
Self::list_editor(Some(Rc::new(self)), typename, default_tag)
}
/// An insertion point that always has a port.
pub const fn active_insertion_point() -> Self {
Self::always(insertion_point::Config)
}
fn list_editor(
item_widget: Option<Rc<Configuration>>,
typename: Option<&str>,
default_tag: Option<&TagValue>,
) -> Self {
let item_default = match default_tag {
Some(tag) => list_editor::DefaultValue::Tag(tag.clone()),
None => list_editor::DefaultValue::StaticExpression(
list_editor::infer_default_value_from_type(typename),
),
};
Self::always(list_editor::Config { item_widget, item_default })
} }
} }
@ -376,8 +455,8 @@ pub struct Entry {
pub label: ImString, pub label: ImString,
} }
impl From<&span_tree::TagValue> for Entry { impl From<&TagValue> for Entry {
fn from(tag_value: &span_tree::TagValue) -> Self { fn from(tag_value: &TagValue) -> Self {
let value: ImString = (&tag_value.expression).into(); let value: ImString = (&tag_value.expression).into();
let label: ImString = tag_value.label.as_ref().map_or_else(|| value.clone(), Into::into); let label: ImString = tag_value.label.as_ref().map_or_else(|| value.clone(), Into::into);
let required_import = tag_value.required_import.clone().map(Into::into); let required_import = tag_value.required_import.clone().map(Into::into);
@ -418,6 +497,7 @@ impl DropdownValue for Entry {
#[derive(Debug, Clone, CloneRef)] #[derive(Debug, Clone, CloneRef)]
pub struct WidgetsFrp { pub struct WidgetsFrp {
pub(super) set_ports_visible: frp::Sampler<bool>, pub(super) set_ports_visible: frp::Sampler<bool>,
pub(super) set_edit_ready_mode: frp::Sampler<bool>,
pub(super) set_read_only: frp::Sampler<bool>, pub(super) set_read_only: frp::Sampler<bool>,
pub(super) set_view_mode: frp::Sampler<crate::view::Mode>, pub(super) set_view_mode: frp::Sampler<crate::view::Mode>,
pub(super) set_profiling_status: frp::Sampler<crate::node::profiling::Status>, pub(super) set_profiling_status: frp::Sampler<crate::node::profiling::Status>,
@ -489,6 +569,7 @@ impl Tree {
eval transfer_ownership((request) model.transfer_ownership(*request)); eval transfer_ownership((request) model.transfer_ownership(*request));
set_ports_visible <- frp.set_ports_visible.sampler(); set_ports_visible <- frp.set_ports_visible.sampler();
set_edit_ready_mode <- frp.set_edit_ready_mode.sampler();
set_read_only <- frp.set_read_only.sampler(); set_read_only <- frp.set_read_only.sampler();
set_view_mode <- frp.set_view_mode.sampler(); set_view_mode <- frp.set_view_mode.sampler();
set_profiling_status <- frp.set_profiling_status.sampler(); set_profiling_status <- frp.set_profiling_status.sampler();
@ -505,6 +586,7 @@ impl Tree {
let connected_port_updated = frp.private.output.connected_port_updated.clone_ref(); let connected_port_updated = frp.private.output.connected_port_updated.clone_ref();
let widgets_frp = WidgetsFrp { let widgets_frp = WidgetsFrp {
set_ports_visible, set_ports_visible,
set_edit_ready_mode,
set_read_only, set_read_only,
set_view_mode, set_view_mode,
set_profiling_status, set_profiling_status,
@ -885,6 +967,7 @@ impl TreeModel {
parent_info: default(), parent_info: default(),
last_ast_depth: default(), last_ast_depth: default(),
extensions: default(), extensions: default(),
node_settings: default(),
}; };
let child = builder.child_widget(tree.root_ref(), default()); let child = builder.child_widget(tree.root_ref(), default());
@ -904,34 +987,7 @@ impl TreeModel {
/// is more stable across changes in the span tree than [`span_tree::Crumbs`]. The pointer is /// is more stable across changes in the span tree than [`span_tree::Crumbs`]. The pointer is
/// used to identify the widgets or ports in the widget tree. /// used to identify the widgets or ports in the widget tree.
pub fn get_node_widget_pointer(&self, span_node: &SpanRef) -> StableSpanIdentity { pub fn get_node_widget_pointer(&self, span_node: &SpanRef) -> StableSpanIdentity {
if let Some(id) = span_node.ast_id { StableSpanIdentity::from_node(span_node)
// This span represents an AST node, return a pointer directly to it.
StableSpanIdentity::new(Some(id), &[])
} else {
let root = span_node.span_tree.root_ref();
let root_ast_data = root.ast_id.map(|id| (id, 0));
// When the node does not represent an AST node, its widget will be identified by the
// closest parent AST node, if it exists. We have to find the closest parent node with
// AST ID, and then calculate the relative crumbs from it to the current node.
let (_, ast_parent_data) = span_node.crumbs.into_iter().enumerate().fold(
(root, root_ast_data),
|(node, last_seen), (index, crumb)| {
let ast_data = node.node.ast_id.map(|id| (id, index)).or(last_seen);
(node.child(*crumb).expect("Node ref must be valid"), ast_data)
},
);
match ast_parent_data {
// Parent AST node found, return a pointer relative to it.
Some((ast_id, ast_parent_index)) => {
let crumb_slice = &span_node.crumbs[ast_parent_index..];
StableSpanIdentity::new(Some(ast_id), crumb_slice)
}
// No parent AST node found. Return a pointer from root.
None => StableSpanIdentity::new(None, &span_node.crumbs),
}
}
} }
/// Perform an operation on a shared reference to a tree port under given pointer. When there is /// Perform an operation on a shared reference to a tree port under given pointer. When there is
@ -974,6 +1030,18 @@ pub(super) struct NodeInfo {
pub usage_type: Option<crate::Type>, pub usage_type: Option<crate::Type>,
} }
/// Settings that can be manipulated by the widget during its own configuration, and will impact
/// how the builder will treat the widget's children.
#[derive(Debug, Clone, Default)]
struct NodeSettings {
/// Whether the widget is manipulating the child margins itself. Will prevent the builder from
/// automatically adding margins calculated from span tree offsets.
manage_margins: bool,
/// Override the padding of port hover area, which is used during edge dragging to determine
/// which port is being hovered.
custom_port_hover_padding: Option<f32>,
}
/// A collection of common data used by all widgets and ports in the widget tree during /// A collection of common data used by all widgets and ports in the widget tree during
/// configuration. Provides the main widget's interface to the tree builder, allowing for creating /// configuration. Provides the main widget's interface to the tree builder, allowing for creating
/// child widgets. /// child widgets.
@ -1124,26 +1192,70 @@ impl NestingLevel {
/// rebuilding the tree. /// rebuilding the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct StableSpanIdentity { pub struct StableSpanIdentity {
/// AST ID of either the node itself, or the closest ancestor node which has one. Is [`None`] /// Identity base of either the node itself, or the closest ancestor node which has one.
/// when there is no such parent with assigned AST id. pub base: IdentityBase,
ast_id: Option<ast::Id>,
/// A hash of remaining data used to distinguish between tree nodes. We store a hash instead of /// A hash of remaining data used to distinguish between tree nodes. We store a hash instead of
/// the data directly, so the type can be trivially copied. The collision is extremely unlikely /// the data directly, so the type can be trivially copied. The collision is extremely unlikely
/// due to u64 being extremely large hash space, compared to the size of the used data. Many /// due to u64 being extremely large hash space, compared to the size of the used data. Many
/// nodes are also already fully distinguished by the AST ID alone. /// nodes are also already fully distinguished by the AST ID alone.
/// ///
/// Currently we are hashing a portion of span-tree crumbs, starting from the closest node with /// Currently we are hashing a portion of span-tree crumbs, starting from the closest node with
/// assigned AST id up to this node. The widgets should not rely on the exact kind of data /// assigned identity base up to this node. The widgets should not rely on the exact kind of
/// used, as it may be extended to include more information in the future. /// data used, as it may be extended to include more information in the future.
identity_hash: u64, pub identity_hash: u64,
}
/// Data that uniquely identifies some span nodes. Not all nodes have unique identity base. To
/// disambiguate nodes that don't have their own stable base, use [`StableSpanIdentity`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum IdentityBase {
/// There isn't any parent with assigned AST id.
#[default]
FromRoot,
/// AST ID of either the node itself, or the closest ancestor node which has one.
AstNode(ast::Id),
/// AST ID of the node that this span is an extension of. Only present if this span doesn't
/// have an unique AST ID assigned.
ExtNode(ast::Id),
} }
impl StableSpanIdentity { impl StableSpanIdentity {
fn new(ast_id: Option<ast::Id>, crumbs_since_ast: &[span_tree::Crumb]) -> Self { fn from_node(node: &span_tree::node::Ref) -> Self {
let (base, base_idx) = if let Some(ast_id) = node.ast_id {
(IdentityBase::AstNode(ast_id), node.crumbs.len())
} else if let Some(ext_id) = node.extended_ast_id {
let base_idx = node
.span_tree
.find_map_in_chain(&node.crumbs, |idx, node| {
(node.extended_ast_id == Some(ext_id)).then_some(idx)
})
.unwrap_or(node.crumbs.len());
(IdentityBase::ExtNode(ext_id), base_idx)
} else {
let mut found_base = (IdentityBase::FromRoot, node.crumbs.len());
let mut current_ext = None;
node.span_tree.find_map_in_chain(&node.crumbs, |idx, node| {
if let Some(ast) = node.ast_id {
found_base = (IdentityBase::AstNode(ast), idx);
} else if let Some(ext) = node.extended_ast_id {
if current_ext != Some(ext) {
found_base = (IdentityBase::ExtNode(ext), idx);
}
}
current_ext = node.extended_ast_id;
None::<()>
});
found_base
};
let identity_hash = Self::hash_crumbs_from_base(&node.crumbs[..], base_idx);
Self { base, identity_hash }
}
fn hash_crumbs_from_base(crumbs: &[usize], base_idx: usize) -> u64 {
let remaining_crumbs = &crumbs[base_idx..];
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
crumbs_since_ast.hash(&mut hasher); remaining_crumbs.hash(&mut hasher);
let identity_hash = hasher.finish(); hasher.finish()
Self { ast_id, identity_hash }
} }
/// Convert this pointer to a stable identity of a widget, making it unique among all widgets. /// Convert this pointer to a stable identity of a widget, making it unique among all widgets.
@ -1180,9 +1292,12 @@ impl WidgetIdentity {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct PointerUsage { struct PointerUsage {
/// Next sequence index that will be assigned to a widget created for the same span tree node. /// Next sequence index that will be assigned to a widget created for the same span tree node.
next_index: usize, next_index: usize,
/// The pointer index of a widget on this span tree that received a port, if any exist already. /// The pointer index of a widget on this span tree that received a port, if any exist already.
port_index: Option<usize>, port_index: Option<usize>,
/// The widget configuration kinds that were already used for this span tree node. Those will
/// be excluded from config possibilities of the next widget created for this node.
used_configs: DynKindFlags,
} }
impl PointerUsage { impl PointerUsage {
@ -1223,11 +1338,25 @@ struct TreeBuilder<'a> {
hierarchy: Vec<NodeHierarchy>, hierarchy: Vec<NodeHierarchy>,
pointer_usage: HashMap<StableSpanIdentity, PointerUsage>, pointer_usage: HashMap<StableSpanIdentity, PointerUsage>,
parent_info: Option<NodeInfo>, parent_info: Option<NodeInfo>,
node_settings: NodeSettings,
last_ast_depth: usize, last_ast_depth: usize,
extensions: Vec<Box<dyn Any>>, extensions: Vec<Box<dyn Any>>,
} }
impl<'a> TreeBuilder<'a> { impl<'a> TreeBuilder<'a> {
/// Signal to the builder that this widget manages child margins on its own. This will prevent
/// the builder from automatically adding margins to the widget's children based on the offset
/// from previous span.
pub fn manage_child_margins(&mut self) {
self.node_settings.manage_margins = true;
}
/// Override horizontal port hover area margin for ports of this children. The margin is used
/// during edge dragging to determine which port is being hovered.
pub fn override_port_hover_padding(&mut self, padding: Option<f32>) {
self.node_settings.custom_port_hover_padding = padding;
}
/// Create a new child widget, along with its whole subtree. The widget type will be /// Create a new child widget, along with its whole subtree. The widget type will be
/// automatically inferred, either based on the node kind, or on the configuration provided /// automatically inferred, either based on the node kind, or on the configuration provided
/// from the language server. If possible, an existing widget will be reused under the same /// from the language server. If possible, an existing widget will be reused under the same
@ -1272,29 +1401,19 @@ impl<'a> TreeBuilder<'a> {
// the current layer's state, so it can be restored later after visiting the child node. // the current layer's state, so it can be restored later after visiting the child node.
let parent_last_ast_depth = self.last_ast_depth; let parent_last_ast_depth = self.last_ast_depth;
let depth = span_node.crumbs.len(); let depth = span_node.crumbs.len();
let is_extended_ast = span_node.ast_id.is_none() && span_node.extended_ast_id.is_some();
// Figure out the widget tree pointer for the current node. That pointer determines the // Figure out the widget tree pointer for the current node. That pointer determines the
// widget identity, allowing it to maintain internal state. If the previous tree already // widget identity, allowing it to maintain internal state. If the previous tree already
// contained a widget for this pointer, we have to reuse it. // contained a widget for this pointer, we have to reuse it.
let main_ptr = match span_node.ast_id { let main_ptr = StableSpanIdentity::from_node(&span_node);
Some(ast_id) => {
self.last_ast_depth = depth;
StableSpanIdentity::new(Some(ast_id), &[])
}
None => {
let ast_id = self.parent_info.as_ref().and_then(|st| st.identity.main.ast_id);
let this_crumbs = &span_node.crumbs;
let crumbs_since_id = &this_crumbs[parent_last_ast_depth..];
StableSpanIdentity::new(ast_id, crumbs_since_id)
}
};
let ptr_usage = self.pointer_usage.entry(main_ptr).or_default(); let ptr_usage = self.pointer_usage.entry(main_ptr).or_default();
let widget_id = main_ptr.to_identity(ptr_usage); let widget_id = main_ptr.to_identity(ptr_usage);
let is_placeholder = span_node.is_expected_argument(); let is_placeholder = span_node.is_expected_argument();
let sibling_offset = span_node.sibling_offset.as_usize(); let sibling_offset = span_node.sibling_offset.as_usize();
let usage_type = main_ptr.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned(); let usage_type = span_node.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned();
// Prepare the widget node info and build context. // Prepare the widget node info and build context.
let connection_color = self.connected_map.get(&span_node.crumbs); let connection_color = self.connected_map.get(&span_node.crumbs);
@ -1302,35 +1421,6 @@ impl<'a> TreeBuilder<'a> {
let parent_connection = self.parent_info.as_ref().and_then(|info| info.connection); let parent_connection = self.parent_info.as_ref().and_then(|info| info.connection);
let subtree_connection = connection.or(parent_connection); let subtree_connection = connection.or(parent_connection);
// Get widget configuration. There are three potential sources for configuration, that are
// used in order, whichever is available first:
// 1. The `config_override` argument, which can be set by the parent widget if it wants to
// override the configuration for its child.
// 2. The override stored in the span tree node, located using `OverrideKey`. This can be
// set by an external source, e.g. based on language server.
// 3. The default configuration for the widget, which is determined based on the node kind,
// usage type and whether it has children.
let kind = &span_node.kind;
let config_override = || {
self.override_map.get(&OverrideKey {
call_id: kind.call_id()?,
argument_name: kind.argument_name()?.into(),
})
};
let inferred_config;
let configuration = match configuration.or_else(config_override) {
Some(config) => config,
None => {
let ty = usage_type.clone();
let expr = &self.node_expression;
let connected = connection.is_some();
inferred_config = Configuration::from_node(&span_node, ty, expr, connected);
&inferred_config
}
};
let widget_has_port = ptr_usage.request_port(&widget_id, configuration.has_port);
let insertion_index = self.hierarchy.len(); let insertion_index = self.hierarchy.len();
self.hierarchy.push(NodeHierarchy { self.hierarchy.push(NodeHierarchy {
identity: widget_id, identity: widget_id,
@ -1339,8 +1429,6 @@ impl<'a> TreeBuilder<'a> {
total_descendants: 0, total_descendants: 0,
}); });
let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node);
let disabled = self.node_disabled; let disabled = self.node_disabled;
let info = NodeInfo { let info = NodeInfo {
identity: widget_id, identity: widget_id,
@ -1352,10 +1440,56 @@ impl<'a> TreeBuilder<'a> {
usage_type, usage_type,
}; };
// Get widget configuration. There are three potential sources for configuration, that are
// used in order, whichever is available and allowed first:
// 1. The `config_override` argument, which can be set by the parent widget if it wants to
// override the configuration for its child.
// 2. The override stored in the span tree node, located using `OverrideKey`. This can be
// set by an external source, e.g. based on language server.
// 3. The default configuration for the widget, which is determined based on the node kind,
// usage type and whether it has children.
let kind = &span_node.kind;
let disallowed_configs = ptr_usage.used_configs;
let config_override = || {
self.override_map
.get(&OverrideKey {
call_id: kind.call_id()?,
argument_name: kind.argument_name()?.into(),
})
.filter(|cfg| !disallowed_configs.contains(cfg.kind.flag()))
};
let inferred_config;
let configuration = match configuration.or_else(config_override) {
Some(config) => config,
None => {
let expr = &self.node_expression;
inferred_config =
Configuration::from_node(&span_node, &info, expr, disallowed_configs);
&inferred_config
}
};
ptr_usage.used_configs |= configuration.kind.flag();
let widget_has_port =
ptr_usage.request_port(&widget_id, configuration.has_port && !is_extended_ast);
let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node);
let parent_info = std::mem::replace(&mut self.parent_info, Some(info.clone())); let parent_info = std::mem::replace(&mut self.parent_info, Some(info.clone()));
let port_pad = self.node_settings.custom_port_hover_padding;
let saved_node_settings = std::mem::take(&mut self.node_settings);
let parent_extensions_len = self.extensions.len(); let parent_extensions_len = self.extensions.len();
let ctx = ConfigContext { builder: &mut *self, span_node, info, parent_extensions_len }; let ctx = ConfigContext {
builder: &mut *self,
span_node,
info: info.clone(),
parent_extensions_len,
};
let app = ctx.app(); let app = ctx.app();
let frp = ctx.frp(); let frp = ctx.frp();
@ -1371,7 +1505,7 @@ impl<'a> TreeBuilder<'a> {
Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp), Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp),
None => Port::new(DynWidget::new(&configuration.kind, &ctx), app, frp), None => Port::new(DynWidget::new(&configuration.kind, &ctx), app, frp),
}; };
port.configure(&configuration.kind, ctx); port.configure(&configuration.kind, ctx, port_pad);
TreeNode::Port(port) TreeNode::Port(port)
} else { } else {
let mut widget = match old_node { let mut widget = match old_node {
@ -1392,23 +1526,28 @@ impl<'a> TreeBuilder<'a> {
self.parent_info = parent_info; self.parent_info = parent_info;
self.last_ast_depth = parent_last_ast_depth; self.last_ast_depth = parent_last_ast_depth;
self.extensions.truncate(parent_extensions_len); self.extensions.truncate(parent_extensions_len);
self.node_settings = saved_node_settings;
// Apply left margin to the widget, based on its offset relative to the previous sibling.
let child_root = child_node.display_object().clone(); let child_root = child_node.display_object().clone();
let offset = match () {
_ if !widget_id.is_first_widget_of_span() => 0,
_ if is_placeholder => 1,
_ => sibling_offset,
};
let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET; if !self.node_settings.manage_margins {
if child_root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) { // Apply left margin to the widget, based on its offset relative to the previous
child_root.set_margin_left(left_margin); // sibling.
let offset = match () {
_ if !widget_id.is_first_widget_of_span() => 0,
_ if is_placeholder => 1,
_ => sibling_offset,
};
let left_margin = offset as f32 * WIDGET_SPACING_PER_OFFSET;
if child_root.margin().x.start.as_pixels().map_or(true, |px| px != left_margin) {
child_root.set_margin_left(left_margin);
}
} }
let entry = TreeEntry { node: child_node, index: insertion_index }; let entry = TreeEntry { node: child_node, index: insertion_index };
self.new_nodes.insert(widget_id, entry); self.new_nodes.insert(widget_id, entry);
Child { id: widget_id, root_object: child_root } Child { info, root_object: child_root }
} }
} }
@ -1423,11 +1562,10 @@ impl<'a> TreeBuilder<'a> {
/// hierarchy. /// hierarchy.
#[derive(Debug, Clone, Deref)] #[derive(Debug, Clone, Deref)]
struct Child { struct Child {
/// The widget identity that is stable across rebuilds. The parent might use it to associate /// The node info used during building of the child widget. The parent might use it to
/// internal state with any particular child. When a new child is inserted between two existing /// associate internal state with any particular child. When a new child is inserted between
/// children, their identities will be maintained. /// two existing children, their identities will be maintained.
#[allow(dead_code)] pub info: NodeInfo,
pub id: WidgetIdentity,
/// The root object of the widget. In order to make the widget visible, it must be added to the /// The root object of the widget. In order to make the widget visible, it must be added to the
/// parent's view hierarchy. Every time a widget is [`configure`d], its root object may change. /// parent's view hierarchy. Every time a widget is [`configure`d], its root object may change.
/// The parent must not assume ownership over a root object of a removed child. The widget /// The parent must not assume ownership over a root object of a removed child. The widget

View File

@ -7,19 +7,37 @@ use crate::prelude::*;
use crate::component::node::input::area::TEXT_SIZE; use crate::component::node::input::area::TEXT_SIZE;
use crate::component::node::input::widget::Configuration; use crate::component::node::input::widget::Configuration;
use crate::component::node::input::widget::IdentityBase;
use crate::component::node::input::widget::TransferRequest; use crate::component::node::input::widget::TransferRequest;
use crate::component::node::input::widget::TreeNode; use crate::component::node::input::widget::TreeNode;
use crate::component::node::input::widget::WidgetIdentity; use crate::component::node::input::widget::WidgetIdentity;
use ensogl::control::io::mouse;
use ensogl::display; use ensogl::display;
use ensogl::display::object; use ensogl::display::object;
use ensogl::display::world::with_context; use ensogl::display::world::with_context;
use ensogl_component::list_editor::ListEditor; use ensogl_component::list_editor::ListEditor;
use span_tree::node::Kind; use span_tree::node::Kind;
use std::fmt::Write; use std::collections::hash_map::Entry;
// =================
// === Constants ===
// =================
/// The type name that enables the list editor widget.
pub const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector";
/// Extra space to the left and right of the list that will respond to mouse events. The list
/// insertion points will only be shown within the list bounding box extended by this margin.
const LIST_HOVER_MARGIN: f32 = 4.0;
const ITEMS_GAP: f32 = 10.0;
const INSERT_HOVER_MARGIN: f32 = 4.0;
const ITEM_HOVER_MARGIN: f32 = (ITEMS_GAP - INSERT_HOVER_MARGIN * 2.0) * 0.5;
const INSERTION_OFFSET: f32 = ITEMS_GAP * 0.5;
// =============== // ===============
// === Element === // === Element ===
// =============== // ===============
@ -35,32 +53,62 @@ struct Element {
alive: Option<()>, alive: Option<()>,
} }
#[derive(Debug)]
struct DragData { struct DragData {
element_id: WidgetIdentity, child_id: WidgetIdentity,
element: Element, element: Element,
expression: String, expression: String,
#[allow(dead_code)] #[allow(dead_code)]
owned_subtree: Vec<(WidgetIdentity, TreeNode)>, owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
} }
#[derive(Debug, Clone, CloneRef)] impl Debug for DragData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DragData")
.field("child_id", &self.child_id)
.field("element", &self.element)
.field("expression", &self.expression)
.field("owned_subtree.len", &self.owned_subtree.len())
.finish()
}
}
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
enum ElementIdentity {
UniqueBase(IdentityBase),
FullIdentity(WidgetIdentity),
}
#[derive(Clone, CloneRef)]
struct ListItem { struct ListItem {
element_id: Immutable<WidgetIdentity>, child_id: Immutable<WidgetIdentity>,
element_id: Immutable<ElementIdentity>,
display_object: object::Instance, display_object: object::Instance,
drag_data: Rc<RefCell<Option<DragData>>>, drag_data: Rc<RefCell<Option<DragData>>>,
} }
impl Debug for ListItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ListItem").field("element_id", &self.child_id).finish()
}
}
#[derive(Debug)]
struct ListItemCandidate {
compare_id: ElementIdentity,
assigned: bool,
item: ListItem,
}
impl PartialEq for ListItem { impl PartialEq for ListItem {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.element_id == other.element_id self.child_id == other.child_id
} }
} }
impl ListItem { impl ListItem {
fn take_drag_data(&self) -> Option<DragData> { fn take_drag_data(&self) -> Option<DragData> {
let mut borrow = self.drag_data.borrow_mut(); let mut borrow = self.drag_data.borrow_mut();
let can_take = matches!(&*borrow, Some(data) if data.element_id == *self.element_id); let can_take = matches!(&*borrow, Some(data) if data.child_id == *self.child_id);
can_take.and_option_from(|| borrow.take()) can_take.and_option_from(|| borrow.take())
} }
} }
@ -112,11 +160,18 @@ impl Widget {
let network = &self.network; let network = &self.network;
let model = &self.model; let model = &self.model;
let list = self.model.borrow().list.clone_ref(); let list = self.model.borrow().list.clone_ref();
let scene = scene();
let on_down = scene.on_event::<mouse::Down>();
let on_up = scene.on_event::<mouse::Up>();
frp::extend! { network frp::extend! { network
init <- source_();
// Adding elements. // Adding elements.
requested_new <- list.request_new_item.filter_map(|resp| resp.gui_interaction_payload()); requested_new <- list.request_new_item.filter_map(|resp| resp.gui_interaction_payload());
widgets_frp.value_changed <+ requested_new.filter_map(f!((idx) model.borrow_mut().on_new_item(*idx))); eval requested_new(
[model, widgets_frp] (idx) model.borrow_mut().on_new_item(*idx, &widgets_frp)
);
// Inserting dragged elements. // Inserting dragged elements.
inserted_by_user <- list.on_item_added.filter_map(|resp| resp.gui_interaction_payload()); inserted_by_user <- list.on_item_added.filter_map(|resp| resp.gui_interaction_payload());
@ -133,48 +188,88 @@ impl Widget {
})); }));
widgets_frp.transfer_ownership <+ remove_request._1(); widgets_frp.transfer_ownership <+ remove_request._1();
widgets_frp.value_changed <+ remove_request._0().map(|crumb| (crumb.clone(), None)); widgets_frp.value_changed <+ remove_request._0().map(|crumb| (crumb.clone(), None));
mouse_is_up <- bool(&on_down, &on_up);
ports_made_visible <- widgets_frp.set_ports_visible.on_true();
ports_made_invisible <- widgets_frp.set_ports_visible.on_false();
gated_invisible <- ports_made_invisible.buffered_gate(&mouse_is_up);
ports_were_active_during_interaction <- bool(&gated_invisible, &ports_made_visible);
// Enable list interactions only under specific conditions:
// - We are not dragging an edge, and it was not dragged during this mouse click.
// - The widgets are not set to read-only mode.
// - The user is not about to switch to edit mode.
// - The mouse is hovering the list interaction bounding box.
enable_interaction <- all_with4(
&ports_were_active_during_interaction,
&widgets_frp.set_edit_ready_mode,
&widgets_frp.set_read_only,
&init,
|ports, edit, read_only, _| !ports && !edit && !read_only
);
let list_enter = list.on_event::<mouse::Over>();
let list_leave = list.on_event::<mouse::Out>();
is_hovered <- bool(&list_leave, &list_enter).debounce().on_change();
enable_insertion <- enable_interaction && is_hovered;
list.enable_all_insertion_points <+ enable_insertion;
list.enable_last_insertion_point <+ enable_insertion;
list.enable_dragging <+ enable_interaction;
} }
init.emit(());
self self
} }
} }
#[derive(Debug)] #[derive(Debug)]
struct Model { struct Model {
self_id: WidgetIdentity, self_id: WidgetIdentity,
list: ListEditor<ListItem>, list: ListEditor<ListItem>,
elements: HashMap<WidgetIdentity, Element>, #[allow(dead_code)]
default_value: String, background: display::shape::Rectangle,
expression: String, elements: HashMap<ElementIdentity, Element>,
crumbs: span_tree::Crumbs, default_value: DefaultValue,
drag_data_rc: Rc<RefCell<Option<DragData>>>, expression: String,
received_drag: Option<DragData>, crumbs: span_tree::Crumbs,
insertion_indices: Vec<usize>, drag_data_rc: Rc<RefCell<Option<DragData>>>,
insertion_indices: Vec<usize>,
insert_with_brackets: bool,
} }
impl Model { impl Model {
fn new(ctx: &super::ConfigContext, display_object: &object::Instance) -> Self { fn new(ctx: &super::ConfigContext, display_object: &object::Instance) -> Self {
let list = ListEditor::new(&ctx.app().cursor); let list = ListEditor::new(&ctx.app().cursor);
list.gap(ITEMS_GAP);
list.set_size_hug_y(TEXT_SIZE).allow_grow_y(); list.set_size_hug_y(TEXT_SIZE).allow_grow_y();
display_object.use_auto_layout().set_children_alignment_left_center(); display_object.use_auto_layout().set_children_alignment_left_center();
let background = display::shape::Rectangle::new();
background.set_color(display::shape::INVISIBLE_HOVER_COLOR);
background.allow_grow().set_alignment_left_center();
background.set_margin_vh(0.0, -LIST_HOVER_MARGIN);
list.add_child(&background);
Self { Self {
self_id: ctx.info.identity, self_id: ctx.info.identity,
list, list,
background,
elements: default(), elements: default(),
default_value: default(), default_value: default(),
expression: default(), expression: default(),
crumbs: default(), crumbs: default(),
drag_data_rc: default(), drag_data_rc: default(),
received_drag: default(),
insertion_indices: default(), insertion_indices: default(),
insert_with_brackets: default(),
} }
} }
fn configure(&mut self, root: &object::Instance, cfg: &Config, mut ctx: super::ConfigContext) { fn configure(&mut self, root: &object::Instance, cfg: &Config, mut ctx: super::ConfigContext) {
self.expression.clear(); self.expression.clear();
self.default_value.clear(); self.default_value = cfg.item_default.clone();
self.expression.push_str(ctx.expression_at(ctx.span_node.span())); self.expression.push_str(ctx.expression_at(ctx.span_node.span()));
self.crumbs = ctx.span_node.crumbs.clone(); self.crumbs = ctx.span_node.crumbs.clone();
// Right now, nested list editors are broken. Prevent them from being created. Whenever // Right now, nested list editors are broken. Prevent them from being created. Whenever
// a nested list editor is requested, we instead use a hierarchical widget to display the // a nested list editor is requested, we instead use a hierarchical widget to display the
// child list items as ordinary expressions. // child list items as ordinary expressions.
@ -183,29 +278,21 @@ impl Model {
ctx.set_extension(Extension { already_in_list: true }); ctx.set_extension(Extension { already_in_list: true });
if already_in_list { if already_in_list {
let child = ctx.builder.child_widget_of_type( let child = ctx.builder.child_widget(ctx.span_node, ctx.info.nesting_level);
ctx.span_node,
ctx.info.nesting_level,
Some(&super::Configuration::always(super::hierarchy::Config)),
);
root.replace_children(&[&child.root_object]); root.replace_children(&[&child.root_object]);
} else if ctx.span_node.is_insertion_point() { } else if ctx.span_node.is_insertion_point() {
write!(self.default_value, "[{}]", cfg.item_default).unwrap(); self.insert_with_brackets = true;
self.configure_insertion_point(root, ctx) self.configure_insertion_point(root, ctx)
} else { } else {
self.default_value.push_str(&cfg.item_default);
self.configure_vector(root, cfg, ctx) self.configure_vector(root, cfg, ctx)
} }
} }
fn configure_insertion_point(&mut self, root: &object::Instance, ctx: super::ConfigContext) { fn configure_insertion_point(&mut self, root: &object::Instance, ctx: super::ConfigContext) {
self.elements.clear(); self.elements.clear();
let insertion_point = ctx.builder.child_widget_of_type( let insertion_point = ctx.builder.child_widget(ctx.span_node, ctx.info.nesting_level);
ctx.span_node, root.replace_children(&[&*insertion_point, self.list.display_object()]);
ctx.info.nesting_level, set_margins(self.list.display_object(), 0.0, 0.0);
Some(&super::Configuration::always(super::label::Config)),
);
root.replace_children(&[self.list.display_object(), &*insertion_point]);
} }
fn configure_vector( fn configure_vector(
@ -214,22 +301,26 @@ impl Model {
cfg: &Config, cfg: &Config,
ctx: super::ConfigContext, ctx: super::ConfigContext,
) { ) {
let no_nest = ctx.info.nesting_level; ctx.builder.manage_child_margins();
let nest = ctx.info.nesting_level.next(); let nest = ctx.info.nesting_level.next();
let child_config = cfg.item_widget.as_deref(); let child_config = cfg.item_widget.as_deref();
let mut build_child_widget = |i, nest, config, allow_margin: bool| { let mut build_child_widget = |i, config, hover_padding: f32| {
let mut node = ctx.span_node.clone().child(i).expect("invalid index"); let node = ctx.span_node.clone().child(i).expect("invalid index");
if !allow_margin { ctx.builder.override_port_hover_padding(Some(hover_padding));
node.sibling_offset = 0.into();
}
ctx.builder.child_widget_of_type(node, nest, config) ctx.builder.child_widget_of_type(node, nest, config)
}; };
let mut open_bracket = None; let mut open_bracket = None;
let mut close_bracket = None; let mut close_bracket = None;
let mut last_insert_crumb = None; let mut last_insert_crumb = None;
let mut list_items = SmallVec::<[ListItem; 16]>::new(); let mut list_items = SmallVec::<[ListItemCandidate; 16]>::new();
let mut new_items_range: Option<Range<usize>> = None;
let insert_config = Configuration::active_insertion_point();
let insert_config = Some(&insert_config);
let list_id_base = ctx.info.identity.base;
self.insertion_indices.clear(); self.insertion_indices.clear();
for (index, child) in ctx.span_node.node.children.iter().enumerate() { for (index, child) in ctx.span_node.node.children.iter().enumerate() {
@ -240,11 +331,11 @@ impl Model {
match node.kind { match node.kind {
Kind::Token if expr == "[" && open_bracket.is_none() => { Kind::Token if expr == "[" && open_bracket.is_none() => {
let child = build_child_widget(index, no_nest, None, true); let child = build_child_widget(index, None, 0.0);
open_bracket = Some(child.root_object); open_bracket = Some(child.root_object);
} }
Kind::Token if expr == "]" && close_bracket.is_none() => { Kind::Token if expr == "]" && close_bracket.is_none() => {
let child = build_child_widget(index, no_nest, None, false); let child = build_child_widget(index, None, 0.0);
close_bracket = Some(child.root_object); close_bracket = Some(child.root_object);
} }
Kind::InsertionPoint(_) => { Kind::InsertionPoint(_) => {
@ -252,50 +343,67 @@ impl Model {
} }
Kind::Argument(_) if last_insert_crumb.is_some() => { Kind::Argument(_) if last_insert_crumb.is_some() => {
let insert_index = last_insert_crumb.take().unwrap(); let insert_index = last_insert_crumb.take().unwrap();
let insert = build_child_widget(insert_index, no_nest, None, false); let insert =
let item = build_child_widget(index, nest, child_config, false); build_child_widget(insert_index, insert_config, INSERT_HOVER_MARGIN);
let element = self.elements.entry(item.id).or_insert_with(|| { let item = build_child_widget(index, child_config, ITEM_HOVER_MARGIN);
self.received_drag.take().map_or_else(Element::new, |d| d.element)
}); let id = match list_id_base == item.info.identity.base {
true => ElementIdentity::FullIdentity(item.info.identity),
false => ElementIdentity::UniqueBase(item.info.identity.base),
};
let entry = self.elements.entry(id);
let exists = matches!(entry, Entry::Occupied(_));
if !exists {
let i = list_items.len();
new_items_range =
Some(new_items_range.map_or(i..i + 1, |r| r.start..i + 1));
}
let element = entry.or_insert_with(Element::new);
set_margins(&insert, -INSERTION_OFFSET, INSERTION_OFFSET);
element.alive = Some(()); element.alive = Some(());
element.item_crumb = index; element.item_crumb = index;
element.expr_range = range; element.expr_range = range;
element.content.replace_children(&[&*insert, &*item]); element.content.replace_children(&[&*insert, &*item]);
self.insertion_indices.push(insert_index); self.insertion_indices.push(insert_index);
list_items.push(ListItem { let item = ListItem {
element_id: Immutable(item.id), child_id: Immutable(item.info.identity),
element_id: Immutable(id),
display_object: element.display_object.clone(), display_object: element.display_object.clone(),
drag_data: self.drag_data_rc.clone(), drag_data: self.drag_data_rc.clone(),
}); };
list_items.push(ListItemCandidate { compare_id: id, assigned: exists, item });
} }
_ => {} _ => {}
} }
} }
self.elements.retain(|_, child| child.alive.take().is_some()); let has_elements = !list_items.is_empty();
self.insertion_indices.extend(last_insert_crumb); let new_items_range = new_items_range.unwrap_or(0..0);
let mut non_assigned_items = list_items[new_items_range].iter_mut().filter(|c| !c.assigned);
let current_items = self.list.items(); self.elements.retain(|key, child| {
list_diff(&current_items, &list_items, |op| match op { let alive = child.alive.take().is_some();
DiffOp::Delete { at, old, present_later } => if !alive {
if present_later.is_some() if let Some(candidate) = non_assigned_items.next() {
|| list_items.iter().any(|i| i.display_object == old.display_object) candidate.compare_id = *key;
{ }
self.list.take_item(at);
} else {
self.list.remove(at);
},
DiffOp::Insert { at, new } => {
self.list.insert_item(at, new.clone_ref());
} }
DiffOp::Update { at, old, new } => alive
if old.display_object() != new.display_object() {
self.list.replace_item(at, new.clone_ref());
},
}); });
self.insertion_indices.extend(last_insert_crumb);
self.insert_with_brackets = open_bracket.is_none() && close_bracket.is_none();
let border_margins = has_elements.then_val_or_default(INSERTION_OFFSET);
let append_insert = last_insert_crumb.map(|index| {
let insert = build_child_widget(index, insert_config, INSERT_HOVER_MARGIN).root_object;
set_margins(&insert, border_margins, 0.0);
insert
});
let list_right_margin = append_insert.is_none().then_val_or_default(border_margins);
set_margins(self.list.display_object(), border_margins, list_right_margin);
let append_insert = last_insert_crumb
.map(|index| build_child_widget(index, no_nest, None, false).root_object);
let (open_bracket, close_bracket) = open_bracket.zip(close_bracket).unzip(); let (open_bracket, close_bracket) = open_bracket.zip(close_bracket).unzip();
let mut children = SmallVec::<[&object::Instance; 4]>::new(); let mut children = SmallVec::<[&object::Instance; 4]>::new();
children.extend(&open_bracket); children.extend(&open_bracket);
@ -303,6 +411,41 @@ impl Model {
children.extend(&append_insert); children.extend(&append_insert);
children.extend(&close_bracket); children.extend(&close_bracket);
root.replace_children(&children); root.replace_children(&children);
let mut need_reposition = false;
let current_items = self.list.items();
list_diff(
&current_items,
&list_items,
|old, new| *old.element_id == new.compare_id,
|op| match op {
DiffOp::Delete { at, old, present_later } =>
if present_later.is_some()
|| list_items.iter().any(|i| i.item.display_object == old.display_object)
{
self.list.take_item_no_reposition(at);
need_reposition = true;
} else {
self.list.trash_item_at(at);
},
DiffOp::Insert { at, new } => {
self.list.insert_item_no_reposition(at, new.item.clone_ref());
need_reposition = true;
}
DiffOp::Update { at, old, new } =>
if old.display_object() != new.item.display_object()
|| old.child_id != new.item.child_id
{
self.list.replace_item_no_reposition(at, new.item.clone_ref());
need_reposition = true;
},
},
);
if need_reposition {
self.list.reposition_items();
}
} }
fn on_item_removed(&mut self, item: ListItem) -> Option<(span_tree::Crumbs, TransferRequest)> { fn on_item_removed(&mut self, item: ListItem) -> Option<(span_tree::Crumbs, TransferRequest)> {
@ -310,7 +453,7 @@ impl Model {
let crumbs = self.crumbs.sub(element.item_crumb); let crumbs = self.crumbs.sub(element.item_crumb);
let request = TransferRequest { let request = TransferRequest {
new_owner: self.self_id, new_owner: self.self_id,
to_transfer: *item.element_id, to_transfer: *item.child_id,
whole_subtree: true, whole_subtree: true,
}; };
Some((crumbs, request)) Some((crumbs, request))
@ -321,11 +464,14 @@ impl Model {
req: TransferRequest, req: TransferRequest,
owned_subtree: Vec<(WidgetIdentity, TreeNode)>, owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
) { ) {
let element_id = req.to_transfer; let as_full = ElementIdentity::FullIdentity(req.to_transfer);
let as_unique = ElementIdentity::UniqueBase(req.to_transfer.base);
let element_id = if self.elements.contains_key(&as_unique) { as_unique } else { as_full };
let element = self.elements.remove(&element_id); let element = self.elements.remove(&element_id);
if let Some(element) = element { if let Some(element) = element {
let expression = self.expression[element.expr_range.clone()].to_owned(); let expression = self.expression[element.expr_range.clone()].to_owned();
let drag_data = DragData { element_id, element, expression, owned_subtree }; let drag_data =
DragData { child_id: req.to_transfer, element, expression, owned_subtree };
self.drag_data_rc.replace(Some(drag_data)); self.drag_data_rc.replace(Some(drag_data));
} else { } else {
error!("Grabbed item not found."); error!("Grabbed item not found.");
@ -337,23 +483,52 @@ impl Model {
item: ListItem, item: ListItem,
at: usize, at: usize,
) -> Option<(span_tree::Crumbs, Option<ImString>)> { ) -> Option<(span_tree::Crumbs, Option<ImString>)> {
self.received_drag = item.take_drag_data(); let element_id = *item.element_id;
let expression: ImString = mem::take(&mut self.received_drag.as_mut()?.expression).into(); let drag_data = item.take_drag_data()?;
let expression: ImString = drag_data.expression.into();
self.elements.insert(element_id, drag_data.element);
match &self.insertion_indices[..] { match &self.insertion_indices[..] {
&[] => Some((self.crumbs.clone(), Some(expression))), &[] => Some((self.crumbs.clone(), Some(expression))),
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))), ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
} }
} }
fn on_new_item(&mut self, at: usize) -> Option<(span_tree::Crumbs, Option<ImString>)> { fn on_new_item(&mut self, at: usize, frp: &super::WidgetsFrp) {
let expression: ImString = self.default_value.clone().into(); let (mut expression, import) = match &self.default_value {
match &self.insertion_indices[..] { DefaultValue::Tag(tag) =>
(tag.expression.clone().into(), tag.required_import.as_ref().map(ImString::from)),
DefaultValue::Expression(expr) => (expr.clone(), None),
DefaultValue::StaticExpression(expr) => (expr.into(), None),
};
if self.insert_with_brackets {
expression = format!("[{expression}]").into();
}
let insertion = match &self.insertion_indices[..] {
&[] => Some((self.crumbs.clone(), Some(expression))), &[] => Some((self.crumbs.clone(), Some(expression))),
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))), ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
};
if let Some(insertion) = insertion {
if let Some(import) = import {
frp.request_import.emit(import);
}
frp.value_changed.emit(insertion);
} }
} }
} }
fn set_margins(object: &display::object::Instance, left: f32, right: f32) {
let margin = object.margin().x();
let current_left = margin.start.as_pixels();
let current_right = margin.end.as_pixels();
if current_left != Some(left) || current_right != Some(right) {
object.set_margin_left(left);
object.set_margin_right(right);
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
/// VectorEditor widget configuration options. /// VectorEditor widget configuration options.
pub struct Config { pub struct Config {
@ -363,7 +538,31 @@ pub struct Config {
pub item_widget: Option<Rc<Configuration>>, pub item_widget: Option<Rc<Configuration>>,
/// Default expression to insert when adding new elements. /// Default expression to insert when adding new elements.
#[allow(dead_code)] #[allow(dead_code)]
pub item_default: ImString, pub item_default: DefaultValue,
}
/// The value to insert when adding a new element using an "plus" cursor.
#[derive(Debug, Clone, PartialEq)]
pub enum DefaultValue {
/// Use a tag value, both inserting its expression and requesting import if necessary.
Tag(span_tree::TagValue),
/// Use an arbitrary expression.
Expression(ImString),
/// Use a statically defined expression. Allows not allocating a new string for each new
/// widget on every reconfigure.
StaticExpression(&'static str),
}
impl Default for DefaultValue {
fn default() -> Self {
DefaultValue::StaticExpression("_")
}
}
impl From<ImString> for DefaultValue {
fn from(s: ImString) -> Self {
DefaultValue::Expression(s)
}
} }
impl super::SpanWidget for Widget { impl super::SpanWidget for Widget {
@ -374,7 +573,6 @@ impl super::SpanWidget for Widget {
} }
fn new(_: &Config, ctx: &super::ConfigContext) -> Self { fn new(_: &Config, ctx: &super::ConfigContext) -> Self {
console_log!("NEW");
let display_object = object::Instance::new_named("widget::ListEditor"); let display_object = object::Instance::new_named("widget::ListEditor");
let model = Model::new(ctx, &display_object); let model = Model::new(ctx, &display_object);
let network = frp::Network::new("widget::ListEditor"); let network = frp::Network::new("widget::ListEditor");
@ -382,7 +580,6 @@ impl super::SpanWidget for Widget {
} }
fn configure(&mut self, cfg: &Config, ctx: super::ConfigContext) { fn configure(&mut self, cfg: &Config, ctx: super::ConfigContext) {
console_log!("CONFIGURE");
let mut model = self.model.borrow_mut(); let mut model = self.model.borrow_mut();
model.configure(&self.display_object, cfg, ctx); model.configure(&self.display_object, cfg, ctx);
} }
@ -394,19 +591,18 @@ impl super::SpanWidget for Widget {
} }
#[derive(PartialEq)] #[derive(PartialEq)]
enum DiffOp<'old, 'new, T> { enum DiffOp<'old, 'new, O, N> {
Delete { at: usize, old: &'old T, present_later: Option<usize> }, Delete { at: usize, old: &'old O, present_later: Option<usize> },
Insert { at: usize, new: &'new T }, Insert { at: usize, new: &'new N },
Update { at: usize, old: &'old T, new: &'new T }, Update { at: usize, old: &'old O, new: &'new N },
} }
fn list_diff<'old, 'new, T>( fn list_diff<'old, 'new, O, N>(
old_elements: &'old [T], old_elements: &'old [O],
new_elements: &'new [T], new_elements: &'new [N],
mut f: impl FnMut(DiffOp<'old, 'new, T>), cmp: impl Fn(&O, &N) -> bool,
) where mut f: impl FnMut(DiffOp<'old, 'new, O, N>),
T: PartialEq, ) {
{
// Indices for next elements to process in both lists. // Indices for next elements to process in both lists.
let mut current_old = 0; let mut current_old = 0;
let mut current_new = 0; let mut current_new = 0;
@ -420,7 +616,7 @@ fn list_diff<'old, 'new, T>(
let new = &new_elements[current_new]; let new = &new_elements[current_new];
// Next pair of elements are equal, so we don't need to do anything. // Next pair of elements are equal, so we don't need to do anything.
if old == new { if cmp(old, new) {
f(DiffOp::Update { at, old, new }); f(DiffOp::Update { at, old, new });
current_old += 1; current_old += 1;
current_new += 1; current_new += 1;
@ -431,14 +627,14 @@ fn list_diff<'old, 'new, T>(
let remaining_old = &old_elements[current_old + 1..]; let remaining_old = &old_elements[current_old + 1..];
let remaining_new = &new_elements[current_new + 1..]; let remaining_new = &new_elements[current_new + 1..];
let old_still_in_new_list = remaining_new.contains(old); let old_still_in_new_list = remaining_new.iter().any(|new| cmp(old, new));
if !old_still_in_new_list { if !old_still_in_new_list {
f(DiffOp::Delete { at, old, present_later: None }); f(DiffOp::Delete { at, old, present_later: None });
current_old += 1; current_old += 1;
continue; continue;
} }
let index_in_remaining_old = remaining_old.iter().position(|x| x == new); let index_in_remaining_old = remaining_old.iter().position(|old| cmp(old, new));
match index_in_remaining_old { match index_in_remaining_old {
// Not present in old, thus it is an insertion. // Not present in old, thus it is an insertion.
None => { None => {
@ -451,7 +647,7 @@ fn list_diff<'old, 'new, T>(
Some(advance) => { Some(advance) => {
f(DiffOp::Delete { at, old, present_later: Some(current_old + advance + 1) }); f(DiffOp::Delete { at, old, present_later: Some(current_old + advance + 1) });
for k in 0..advance { for k in 0..advance {
let present_later = remaining_old[k + 1..].iter().position(|old| old == new); let present_later = remaining_old[k + 1..].iter().position(|old| cmp(old, new));
f(DiffOp::Delete { at, old: &remaining_old[k], present_later }); f(DiffOp::Delete { at, old: &remaining_old[k], present_later });
} }
current_old += advance + 1; current_old += advance + 1;
@ -485,3 +681,82 @@ fn list_diff<'old, 'new, T>(
struct Extension { struct Extension {
already_in_list: bool, already_in_list: bool,
} }
// ======================
// === Type inference ===
// ======================
/// Given an optional expected argument type, infer the default vector insertion value.
pub fn infer_default_value_from_type(typename: Option<&str>) -> &'static str {
let variant = typename.map_or(DefaultVariant::NotDefined, |typename| {
let possible_types = typename
.split(split_type_groups())
.map(remove_outer_parentheses)
.filter_map(|t| t.strip_prefix(VECTOR_TYPE))
.flat_map(|t| t.split(split_type_groups()).map(remove_outer_parentheses));
possible_types.fold(DefaultVariant::NotDefined, |acc, ty| acc.fold(ty))
});
variant.to_default_value()
}
fn remove_outer_parentheses(s: &str) -> &str {
let s = s.trim();
s.strip_prefix('(').and_then(|s| s.strip_suffix(')')).map(|s| s.trim()).unwrap_or(s)
}
fn split_type_groups() -> impl FnMut(char) -> bool {
let mut depth = 0i32;
move |c| match c {
'(' => {
depth += 1;
false
}
')' => {
depth -= 1;
false
}
'|' if depth == 0 => true,
_ => false,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum DefaultVariant {
NotDefined,
Numeric,
Boolean,
Text,
Any,
}
impl DefaultVariant {
fn from_single(ty: &str) -> Option<Self> {
match ty.strip_prefix("Standard.Base.Data.")? {
"Numbers.Integer" | "Numbers.Decimal" | "Numbers.Number" => Some(Self::Numeric),
"Boolean.Boolean" => Some(Self::Boolean),
"Text.Text" => Some(Self::Text),
"Any" => Some(Self::Any),
_ => None,
}
}
fn fold(self, ty: &str) -> Self {
match (self, Self::from_single(ty)) {
(a, None) => a,
(a, Some(b)) if a == b => a,
(Self::NotDefined, Some(b)) => b,
(Self::Text, _) | (_, Some(Self::Text)) => Self::Text,
_ => DefaultVariant::Any,
}
}
fn to_default_value(self) -> &'static str {
match self {
Self::Numeric => "0",
Self::Boolean => "False",
Self::Text => "''",
Self::Any | Self::NotDefined => "_",
}
}
}

View File

@ -40,6 +40,7 @@ const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0);
pub mod triangle { pub mod triangle {
use super::*; use super::*;
ensogl::shape! { ensogl::shape! {
above = [display::shape::compound::rectangle::shape];
alignment = left_bottom; alignment = left_bottom;
(style:Style, color:Vector4) { (style:Style, color:Vector4) {
let size = Var::canvas_size(); let size = Var::canvas_size();
@ -176,12 +177,7 @@ impl super::SpanWidget for Widget {
ctx.modify_extension::<super::label::Extension>(|ext| ext.bold = true); ctx.modify_extension::<super::label::Extension>(|ext| ext.bold = true);
} }
let config = match ctx.span_node.children.is_empty() { let child = ctx.builder.child_widget(ctx.span_node, ctx.info.nesting_level);
true => super::Configuration::always(super::label::Config),
false => super::Configuration::always(super::hierarchy::Config),
};
let child_level = ctx.info.nesting_level;
let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&config));
self.label_wrapper.replace_children(&[child.root_object]); self.label_wrapper.replace_children(&[child.root_object]);
} }
} }

View File

@ -173,6 +173,8 @@ pub mod single_port {
use ensogl::display::shape::*; use ensogl::display::shape::*;
ensogl::shape! { ensogl::shape! {
above = [node::backdrop];
below = [compound::rectangle::shape];
alignment = center; alignment = center;
(style:Style, size_multiplier:f32, opacity:f32, color_rgb:Vector3<f32>) { (style:Style, size_multiplier:f32, opacity:f32, color_rgb:Vector3<f32>) {
let overall_width = Var::<Pixels>::from("input_size.x"); let overall_width = Var::<Pixels>::from("input_size.x");
@ -300,6 +302,8 @@ pub mod multi_port {
} }
ensogl::shape! { ensogl::shape! {
above = [node::backdrop];
below = [compound::rectangle::shape];
alignment = center; alignment = center;
( style : Style ( style : Style
, size_multiplier : f32 , size_multiplier : f32

View File

@ -58,6 +58,7 @@ mod status_indicator_shape {
const INDICATOR_WIDTH_INNER: f32 = 10.0; const INDICATOR_WIDTH_INNER: f32 = 10.0;
ensogl::shape! { ensogl::shape! {
pointer_events = false;
alignment = center; alignment = center;
(style:Style,color_rgba:Vector4<f32>) { (style:Style,color_rgba:Vector4<f32>) {
let width = Var::<Pixels>::from("input_size.x"); let width = Var::<Pixels>::from("input_size.x");

View File

@ -8,6 +8,7 @@
#![feature(drain_filter)] #![feature(drain_filter)]
#![feature(entry_insert)] #![feature(entry_insert)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(macro_metavar_expr)]
#![feature(option_result_contains)] #![feature(option_result_contains)]
#![feature(specialization)] #![feature(specialization)]
#![feature(trait_alias)] #![feature(trait_alias)]
@ -3177,7 +3178,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
frp::extend! { network frp::extend! { network
_eval <- all_with(&out.node_hovered,&edit_mode,f!([model](tgt,e) _eval <- all_with(&out.node_hovered,&edit_mode,f!([model](tgt,e)
if let Some(tgt) = tgt { if let Some(tgt) = tgt {
model.with_node(tgt.value,|t| t.model().input.set_edit_ready_mode(*e && tgt.is_on())); model.with_node(tgt.value,|t| t.set_edit_ready_mode(*e && tgt.is_on()));
} }
)); ));
_eval <- all_with(&out.node_hovered,&out.some_edge_targets_unset,f!([model](tgt,ok) _eval <- all_with(&out.node_hovered,&out.some_edge_targets_unset,f!([model](tgt,ok)

View File

@ -115,11 +115,6 @@
"description": "Color theme.", "description": "Color theme.",
"primary": false "primary": false
}, },
"vectorEditor": {
"value": true,
"description": "Show Vector Editor widget on nodes.",
"primary": false
},
"newDashboard": { "newDashboard": {
"value": false, "value": false,
"description": "Determines whether the new dashboard with cloud integration is enabled." "description": "Determines whether the new dashboard with cloud integration is enabled."

View File

@ -1,6 +1,6 @@
# Options intended to be common for all developers. # Options intended to be common for all developers.
wasm-size-limit: 15.85 MiB wasm-size-limit: 15.87 MiB
required-versions: required-versions:
# NB. The Rust version is pinned in rust-toolchain.toml. # NB. The Rust version is pinned in rust-toolchain.toml.

View File

@ -73,13 +73,11 @@
#![allow(clippy::bool_to_int_with_if)] #![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)] #![allow(clippy::let_and_return)]
use ensogl_core::display::shape::compound::rectangle::*;
use ensogl_core::display::world::*; use ensogl_core::display::world::*;
use ensogl_core::prelude::*; use ensogl_core::prelude::*;
use ensogl_core::control::io::mouse; use ensogl_core::control::io::mouse;
use ensogl_core::data::bounding_box::BoundingBox; use ensogl_core::data::bounding_box::BoundingBox;
use ensogl_core::data::color;
use ensogl_core::display; use ensogl_core::display;
use ensogl_core::display::object::Event; use ensogl_core::display::object::Event;
use ensogl_core::display::object::ObjectOps; use ensogl_core::display::object::ObjectOps;
@ -311,6 +309,10 @@ ensogl_core::define_endpoints_2! { <T: ('static + Debug)>
/// See docs of this module to learn more. /// See docs of this module to learn more.
thrashing_offset_ratio(f32), thrashing_offset_ratio(f32),
/// Enable dragging of the list items. When disabled, the mouse events are always passed to
/// the items immediately.
enable_dragging(bool),
/// Enable insertion points (plus icons) when moving mouse next to any of the list items. /// Enable insertion points (plus icons) when moving mouse next to any of the list items.
enable_all_insertion_points(bool), enable_all_insertion_points(bool),
@ -347,13 +349,11 @@ pub struct ListEditor<T: 'static + Debug> {
#[derive(Debug)] #[derive(Debug)]
pub struct Model<T> { pub struct Model<T> {
cursor: Cursor, cursor: Cursor,
items: VecIndexedBy<ItemOrPlaceholder<T>, ItemOrPlaceholderIndex>, items: VecIndexedBy<ItemOrPlaceholder<T>, ItemOrPlaceholderIndex>,
root: display::object::Instance, root: display::object::Instance,
layout_with_icons: display::object::Instance, layout: display::object::Instance,
layout: display::object::Instance, gap: f32,
gap: f32,
add_elem_icon: Rectangle,
} }
impl<T> Model<T> { impl<T> Model<T> {
@ -363,19 +363,10 @@ impl<T> Model<T> {
let items = default(); let items = default();
let root = display::object::Instance::new_named("ListEditor"); let root = display::object::Instance::new_named("ListEditor");
let layout = display::object::Instance::new_named("layout"); let layout = display::object::Instance::new_named("layout");
let layout_with_icons = display::object::Instance::new_named("layout_with_icons");
let gap = default(); let gap = default();
layout_with_icons.use_auto_layout(); layout.use_auto_layout().allow_grow_y();
layout.use_auto_layout(); root.add_child(&layout);
layout_with_icons.add_child(&layout); Self { cursor, items, root, layout, gap }
root.add_child(&layout_with_icons);
let add_elem_icon = Rectangle().build(|t| {
t.set_corner_radius_max()
.set_size((14.0, 14.0))
.set_color(color::Rgba::new(0.0, 0.0, 0.0, 0.2));
});
layout_with_icons.add_child(&add_elem_icon);
Self { cursor, items, root, layout, layout_with_icons, gap, add_elem_icon }
} }
} }
@ -407,8 +398,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let network = self.frp.network(); let network = self.frp.network();
let model = &self.model; let model = &self.model;
let on_add_elem_icon_up = model.borrow().add_elem_icon.on_event::<mouse::Up>(); let on_down = model.borrow().root.on_event_capturing::<mouse::Down>();
let on_down = model.borrow().layout.on_event_capturing::<mouse::Down>();
let on_up_source = scene.on_event::<mouse::Up>(); let on_up_source = scene.on_event::<mouse::Up>();
let on_move = scene.on_event::<mouse::Move>(); let on_move = scene.on_event::<mouse::Move>();
@ -416,11 +406,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let on_resized = model.borrow().layout.on_resized.clone_ref(); let on_resized = model.borrow().layout.on_resized.clone_ref();
let drag_target = cursor::DragTarget::new(); let drag_target = cursor::DragTarget::new();
frp::extend! { network frp::extend! { network
on_down <- on_down.gate(&frp.enable_dragging);
frp.private.output.request_new_item <+ on_add_elem_icon_up.map(f_!([model] {
Response::gui(model.borrow().len())
}));
target <= on_down.map(|event| event.target()); target <= on_down.map(|event| event.target());
on_up <- on_up_source.identity(); on_up <- on_up_source.identity();
@ -486,12 +472,8 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
// Do not pass events to children, as we don't know whether we are about to drag // Do not pass events to children, as we don't know whether we are about to drag
// them yet. // them yet.
eval on_down_drag ([] (event) event.stop_propagation()); eval on_down_drag ([] (event) event.stop_propagation());
_eval <- no_drag.on_true().map3(&on_down, &target, |_, event, target| { resume_propagation <- no_drag.on_true().sync_gate(&on_down_drag);
target.emit_event(event.payload.clone()); _eval <- resume_propagation.map2(&on_down, |_, event| event.resume_propagation());
});
item_count_changed <- any_(&frp.on_item_added, &frp.on_item_removed);
eval_ item_count_changed (model.borrow().item_count_changed());
} }
self self
} }
@ -511,7 +493,8 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let model_borrowed = model.borrow(); let model_borrowed = model.borrow();
frp::extend! { network frp::extend! { network
gaps <- model_borrowed.layout.on_resized.map(f_!(model.gaps())); init <- source_();
gaps <- all(&model_borrowed.layout.on_resized, &init)._0().map(f_!(model.gaps()));
// We are debouncing the `is_dragging` stream to avoid double-borrow of list editor, as // We are debouncing the `is_dragging` stream to avoid double-borrow of list editor, as
// this event is fired immediately after list-editor instructs cursor to stop dragging. // this event is fired immediately after list-editor instructs cursor to stop dragging.
@ -521,21 +504,19 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
&frp.gap, &frp.gap,
&gaps, &gaps,
&pos_on_move, &pos_on_move,
&model.borrow().layout.on_resized, &model_borrowed.layout.on_resized,
&is_dragging, &is_dragging,
&frp.enable_all_insertion_points, &frp.enable_all_insertion_points,
&frp.enable_last_insertion_point, &frp.enable_last_insertion_point,
f!([model] (gap, gaps, pos, size, is_dragging, enable_all, enable_last) { f!([model] (gap, gaps, pos, size, is_dragging, enable_all, enable_last) {
let is_close_x = pos.x > -gap && pos.x < size.x + gap; let is_close_x = pos.x > -gap && pos.x < size.x + gap;
let is_close_y = pos.y > -gap && pos.y < size.y + gap; let is_close_y = pos.y > 0.0 && pos.y < size.y;
let is_close = is_close_x && is_close_y; let is_close = is_close_x && is_close_y;
let opt_gap = gaps.find(pos.x); let enabled = is_close && !is_dragging && (*enable_all || *enable_last);
opt_gap.and_then(|gap| { enabled
let last_gap = *gap == gaps.len() - 1; .and_option_from(|| gaps.find(pos.x))
let enabled = is_close && !is_dragging; .filter(|gap| *enable_all || (*enable_last && **gap == gaps.len() - 1))
let enabled = enabled && (*enable_all || (*enable_last && last_gap)); .and_then(|gap| model.item_or_placeholder_index_to_index(gap))
enabled.and_option_from(|| model.item_or_placeholder_index_to_index(gap))
})
}) })
).on_change(); ).on_change();
index <= opt_index; index <= opt_index;
@ -545,6 +526,9 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
insert_in_gap <- index.sample(&on_up_in_gap); insert_in_gap <- index.sample(&on_up_in_gap);
frp.private.output.request_new_item <+ insert_in_gap.map(|t| Response::gui(*t)); frp.private.output.request_new_item <+ insert_in_gap.map(|t| Response::gui(*t));
} }
init.emit(());
pointer_style pointer_style
} }
@ -603,25 +587,35 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
init_no_drag <- all_with(&pos_diff, &no_drag_threshold, |p, t| p.x.abs() > *t).on_true(); init_no_drag <- all_with(&pos_diff, &no_drag_threshold, |p, t| p.x.abs() > *t).on_true();
init_drag <- all_with(&pos_diff, init_drag_threshold, |p, t| p.y.abs() > *t).on_true(); init_drag <- all_with(&pos_diff, init_drag_threshold, |p, t| p.y.abs() > *t).on_true();
init_any <- any(init_no_drag, init_drag);
click_was_handled <- bool(&on_up_cleaning_phase, &init_any).on_change();
drag_disabled <- bool(&on_up, &init_no_drag).on_change(); drag_disabled <- bool(&on_up, &init_no_drag).on_change();
release_without_action <- on_up.gate_not(&click_was_handled);
no_action <- bool(&on_up_cleaning_phase, &release_without_action).on_change();
init_drag_not_disabled <- init_drag.gate_not(&drag_disabled); init_drag_not_disabled <- init_drag.gate_not(&drag_disabled);
is_dragging <- bool(&on_up_cleaning_phase, &init_drag_not_disabled).on_change(); is_dragging <- bool(&on_up_cleaning_phase, &init_drag_not_disabled).on_change();
drag_diff <- pos_diff.gate(&is_dragging); drag_diff <- pos_diff.gate(&is_dragging);
no_drag <- drag_disabled.gate_not(&is_dragging).on_change(); just_disabled <- drag_disabled.gate_not(&is_dragging).on_change();
no_drag <- any(...);
no_drag <+ no_action;
no_drag <+ just_disabled;
status <- bool(&on_up_cleaning_phase, &drag_diff).on_change(); status <- bool(&on_up_cleaning_phase, &drag_diff).on_change();
start <- status.on_true(); start <- status.on_true();
target_on_start <- target.sample(&start); target_on_start <- target.sample(&start);
let on_item_removed = &frp.private.output.on_item_removed; let on_item_removed = &frp.private.output.on_item_removed;
eval target_on_start([model, cursor, on_item_removed] (t) { eval target_on_start([model, cursor, on_item_removed, no_drag] (t) {
let item = model.borrow_mut().start_item_drag(t); let item = model.borrow_mut().start_item_drag(t);
if let Some((index, item)) = item { if let Some((index, item)) = item {
cursor.start_drag(item.clone_ref()); cursor.start_drag(item.clone_ref());
on_item_removed.emit(Response::gui((index, Rc::new(RefCell::new(Some(item)))))); on_item_removed.emit(Response::gui((index, Rc::new(RefCell::new(Some(item))))));
} else {
no_drag.emit(true);
no_drag.emit(false);
} }
}); });
} }
no_drag no_drag.into()
} }
/// Implementation of dropping items logic, including showing empty placeholders when the item /// Implementation of dropping items logic, including showing empty placeholders when the item
@ -669,6 +663,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
self.frp.primary_axis_no_drag_threshold(4.0); self.frp.primary_axis_no_drag_threshold(4.0);
self.frp.primary_axis_no_drag_threshold_decay_time(1000.0); self.frp.primary_axis_no_drag_threshold_decay_time(1000.0);
self.frp.thrashing_offset_ratio(1.0); self.frp.thrashing_offset_ratio(1.0);
self.frp.enable_dragging(true);
self.frp.enable_all_insertion_points(true); self.frp.enable_all_insertion_points(true);
self.frp.enable_last_insertion_point(true); self.frp.enable_last_insertion_point(true);
self self
@ -686,25 +681,34 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
self.model.borrow_mut().insert(index, item); self.model.borrow_mut().insert(index, item);
} }
pub fn replace_item(&self, index: Index, new_item: T) -> Option<T> { pub fn insert_item_no_reposition(&self, index: Index, item: T) {
self.model.borrow_mut().insert_no_reposition(index, item);
}
pub fn replace_item_no_reposition(&self, index: Index, new_item: T) -> Option<T> {
let mut model = self.model.borrow_mut(); let mut model = self.model.borrow_mut();
let index = model.index_to_item_or_placeholder_index(index)?; let index = model.index_to_item_or_placeholder_index(index)?;
let item = model.items.get_mut(index)?; let item = model.items.get_mut(index)?;
item.replace_element(new_item) item.replace_element(new_item)
} }
pub fn take_item(&self, index: Index) -> Option<T> { pub fn trash_item_at(&self, index: Index) -> Option<T> {
self.model.borrow_mut().trash_item_at(index)
}
pub fn take_item_no_reposition(&self, index: Index) -> Option<T> {
let mut model = self.model.borrow_mut(); let mut model = self.model.borrow_mut();
let index = model.index_to_item_or_placeholder_index(index)?; let index = model.index_to_item_or_placeholder_index(index)?;
match model.items.remove(index) { match model.items.remove(index) {
ItemOrPlaceholder::Item(item) => { ItemOrPlaceholder::Item(item) => Some(item.elem),
model.item_count_changed();
Some(item.elem)
}
ItemOrPlaceholder::Placeholder(_) => unreachable!(), ItemOrPlaceholder::Placeholder(_) => unreachable!(),
} }
} }
pub fn reposition_items(&self) {
self.model.borrow_mut().reposition_items();
}
pub fn item_at(&self, index: Index) -> Option<T> { pub fn item_at(&self, index: Index) -> Option<T> {
let model = self.model.borrow(); let model = self.model.borrow();
let index = model.index_to_item_or_placeholder_index(index)?; let index = model.index_to_item_or_placeholder_index(index)?;
@ -827,34 +831,41 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
} }
fn push(&mut self, item: T) -> Index { fn push(&mut self, item: T) -> Index {
let index = self.len(); let index = self.push_no_reposition(item);
let item = Item::new(item);
self.items.push(item.into());
self.reposition_items(); self.reposition_items();
index index
} }
fn push_no_reposition(&mut self, item: T) -> Index {
let index = self.len();
let item = Item::new(item);
self.items.push(item.into());
index
}
fn insert(&mut self, index: Index, item: T) -> Index { fn insert(&mut self, index: Index, item: T) -> Index {
let index = if let Some(index2) = self.index_to_item_or_placeholder_index(index) { self.insert_no_reposition(index, item);
self.reposition_items();
index
}
fn insert_no_reposition(&mut self, index: Index, item: T) -> Index {
if let Some(index2) = self.index_to_item_or_placeholder_index(index) {
let item = Item::new(item); let item = Item::new(item);
self.items.insert(index2, item.into()); self.items.insert(index2, item.into());
index index
} else { } else {
self.push(item) self.push_no_reposition(item)
}; }
self.reposition_items();
self.item_count_changed();
index
} }
/// Remove all items and add them again, in order of their current position. /// Remove all items and add them again, in order of their current position.
fn reposition_items(&mut self) { fn reposition_items(&mut self) {
self.retain_non_collapsed_items(); self.retain_non_collapsed_items();
for item in &self.items { let children: SmallVec<[_; 16]> =
if let Some(display_object) = item.display_object() { self.items.iter().filter_map(|i| i.display_object()).collect();
self.layout.add_child(&display_object); self.layout.replace_children(&children);
}
}
self.recompute_margins(); self.recompute_margins();
} }
@ -953,18 +964,16 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
/// Remove the selected item from the item list and mark it as an element being dragged. In the /// Remove the selected item from the item list and mark it as an element being dragged. In the
/// place where the element was a new placeholder will be created or existing placeholder will /// place where the element was a new placeholder will be created or existing placeholder will
/// be reused and scaled to cover the size of the dragged element. /// be reused and scaled to cover the size of the dragged element. Returns `None` if the target
/// is not an item.
/// ///
/// See docs of [`Self::start_item_drag_at`] for more information. /// See docs of [`Self::start_item_drag_at`] for more information.
fn start_item_drag(&mut self, target: &display::object::Instance) -> Option<(Index, T)> { fn start_item_drag(&mut self, target: &display::object::Instance) -> Option<(Index, T)> {
let objs = target.rev_parent_chain().reversed(); let objs = target.rev_parent_chain().reversed();
let target_index = objs.into_iter().find_map(|t| self.item_index_of(&t)); let target_index = objs.into_iter().find_map(|t| self.item_index_of(&t));
if let Some((index, index_or_placeholder_index)) = target_index { target_index.and_then(|(index, index_or_placeholder_index)| {
self.start_item_drag_at(index_or_placeholder_index).map(|item| (index, item)) self.start_item_drag_at(index_or_placeholder_index).map(|item| (index, item))
} else { })
warn!("Could not find the item to drag.");
None
}
} }
/// Remove the selected item from the item list and mark it as an element being dragged. In the /// Remove the selected item from the item list and mark it as an element being dragged. In the
@ -1077,8 +1086,9 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
warn!("An element was inserted without a placeholder. This should not happen."); warn!("An element was inserted without a placeholder. This should not happen.");
index index
}; };
let index = self.item_or_placeholder_index_to_index(actual_index);
self.reposition_items(); self.reposition_items();
self.item_or_placeholder_index_to_index(actual_index) index
} else { } else {
warn!("Called function to insert dragged element, but no element is being dragged."); warn!("Called function to insert dragged element, but no element is being dragged.");
None None
@ -1133,6 +1143,10 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
} }
fn gaps(&self) -> Gaps { fn gaps(&self) -> Gaps {
if self.items.is_empty() {
return Gaps { gaps: vec![f32::NEG_INFINITY..=f32::INFINITY] };
}
let mut gaps = Vec::new(); let mut gaps = Vec::new();
gaps.push(f32::NEG_INFINITY..=0.0); gaps.push(f32::NEG_INFINITY..=0.0);
let mut fist_gap = true; let mut fist_gap = true;
@ -1154,15 +1168,6 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
fn insert_index(&self, x: f32, center_points: &[f32]) -> ItemOrPlaceholderIndex { fn insert_index(&self, x: f32, center_points: &[f32]) -> ItemOrPlaceholderIndex {
center_points.iter().position(|t| x < *t).unwrap_or(self.items.len()).into() center_points.iter().position(|t| x < *t).unwrap_or(self.items.len()).into()
} }
/// If the item count drops to 0, display a button to add new items.
fn item_count_changed(&self) {
if self.len() == 0 {
self.layout_with_icons.add_child(&self.add_elem_icon);
} else {
self.add_elem_icon.unset_parent();
}
}
} }
impl<T: 'static + Debug> display::Object for ListEditor<T> { impl<T: 'static + Debug> display::Object for ListEditor<T> {

View File

@ -27,7 +27,7 @@ enso-types = { path = "../../types" }
enso-web = { path = "../../web" } enso-web = { path = "../../web" }
ensogl-text-embedded-fonts = { path = "../component/text/src/font/embedded" } ensogl-text-embedded-fonts = { path = "../component/text/src/font/embedded" }
bit_field = { version = "0.10.0" } bit_field = { version = "0.10.0" }
bitflags = { version = "1.3.2" } bitflags = { workspace = true }
console_error_panic_hook = { workspace = true } console_error_panic_hook = { workspace = true }
enum_dispatch = { version = "0.3.6" } enum_dispatch = { version = "0.3.6" }
failure = { workspace = true } failure = { workspace = true }

View File

@ -1,5 +1,5 @@
//! Events implementation. Events behave in a similar way to JavaScript Events. When an event is //! Events implementation. Events behave in a similar way to JavaScript Events. When an event is
//! emitted, it is propagated in three stages: capturing, target, and bubbling. Each stage is //! emitted, it is propagated in two stages: capturing and bubbling. Each stage is
//! configurable and some events propagation can be cancelled. To learn more about the mechanics, //! configurable and some events propagation can be cancelled. To learn more about the mechanics,
//! see: https://javascript.info/bubbling-and-capturing. //! see: https://javascript.info/bubbling-and-capturing.
@ -18,12 +18,39 @@ use crate::display::object::instance::WeakInstance;
/// is cancelled, or that the propagation cannot be cancelled. See docs of this module to learn /// is cancelled, or that the propagation cannot be cancelled. See docs of this module to learn
/// more. /// more.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State { pub enum State {
// Event is being propagated, and will continue to be propagated until all registered event
// handlers have been called. Can be cancelled with [`Event::stop_propagation`].
Running(Phase),
// Event is being propagated, and will continue to be propagated until all registered event
// handlers have been called. Cannot be cancelled.
RunningNonCancellable(Phase),
// Event has been cancelled, but the event propagation is still running. If the event were to
// be resumed, the propagation would continue on its own.
RunningCancelled(Phase),
// Event has been cancelled and [`InstanceDef::emit_event_impl`] function has returned. If the
// event were to be resumed, the propagation will have to be restarted.
StoppedCancelled(Phase),
// The event propagation reached the end, all event handlers have been called. Resuming the
// event will have no effect.
Finished,
}
impl Default for State {
fn default() -> Self {
State::Running(default())
}
}
/// Current phase of the event propagation. For cancelled events, it's the phase in which the event
/// was cancelled.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]
pub enum Phase {
#[default] #[default]
Running, Capturing,
RunningNonCancellable, Bubbling,
Cancelled,
} }
@ -49,11 +76,14 @@ pub struct SomeEvent {
impl SomeEvent { impl SomeEvent {
/// Constructor. /// Constructor.
pub fn new<T: 'static>(target: Option<WeakInstance>, payload: T) -> Self { pub fn new<T: 'static>(target: Option<WeakInstance>, payload: T) -> Self {
let event = Event::new(target, payload); Self::from_event(Event::new(target, payload))
}
fn from_event<T: 'static>(event: Event<T>) -> Self {
let state = event.state.clone_ref(); let state = event.state.clone_ref();
let current_target = event.current_target.clone_ref(); let current_target = event.current_target.clone_ref();
let captures = Rc::new(Cell::new(true)); let captures = event.captures.clone_ref();
let bubbles = Rc::new(Cell::new(true)); let bubbles = event.bubbles.clone_ref();
Self { data: frp::AnyData::new(event), state, current_target, captures, bubbles } Self { data: frp::AnyData::new(event), state, current_target, captures, bubbles }
} }
@ -64,7 +94,7 @@ impl SomeEvent {
/// Check whether the event was cancelled. /// Check whether the event was cancelled.
pub fn is_cancelled(&self) -> bool { pub fn is_cancelled(&self) -> bool {
self.state() == State::Cancelled matches!(self.state(), State::RunningCancelled(_) | State::StoppedCancelled(_))
} }
/// Enables or disables bubbling for this event. /// Enables or disables bubbling for this event.
@ -72,6 +102,41 @@ impl SomeEvent {
self.bubbles.set(value); self.bubbles.set(value);
} }
/// Determine the phase at which the event propagation should continue. This is internal
/// function and should not be used directly.
pub(crate) fn begin_propagation(&self) -> Option<(Phase, Option<Instance>)> {
match self.state.get() {
State::StoppedCancelled(phase) => {
let target = self.current_target.borrow().as_ref()?.upgrade()?;
self.state.set(State::Running(phase));
Some((phase, Some(target)))
}
_ => Some((Phase::Capturing, None)),
}
}
pub(crate) fn enter_phase(&self, phase: Phase) {
self.state.set(match self.state.get() {
State::Running(_) => State::Running(phase),
State::RunningNonCancellable(_) => State::RunningNonCancellable(phase),
State::RunningCancelled(_) => State::RunningCancelled(phase),
State::StoppedCancelled(_) => State::StoppedCancelled(phase),
State::Finished => State::Finished,
});
}
/// Mark the end of the event propagation. This is internal function and should not be used
/// directly.
pub(crate) fn finish_propagation(&self) {
self.state.set(match self.state.get() {
State::RunningCancelled(phase) => State::StoppedCancelled(phase),
_ => {
self.set_current_target(None);
State::Finished
}
});
}
/// Set the current target of the event. This is internal function and should not be used /// Set the current target of the event. This is internal function and should not be used
/// directly. /// directly.
pub(crate) fn set_current_target(&self, target: Option<&Instance>) { pub(crate) fn set_current_target(&self, target: Option<&Instance>) {
@ -125,6 +190,8 @@ pub struct EventData<T> {
target: Option<WeakInstance>, target: Option<WeakInstance>,
current_target: Rc<RefCell<Option<WeakInstance>>>, current_target: Rc<RefCell<Option<WeakInstance>>>,
state: Rc<Cell<State>>, state: Rc<Cell<State>>,
captures: Rc<Cell<bool>>,
bubbles: Rc<Cell<bool>>,
} }
impl<T: Debug> Debug for EventData<T> { impl<T: Debug> Debug for EventData<T> {
@ -136,11 +203,13 @@ impl<T: Debug> Debug for EventData<T> {
} }
} }
impl<T> Event<T> { impl<T: 'static> Event<T> {
fn new(target: Option<WeakInstance>, payload: T) -> Self { fn new(target: Option<WeakInstance>, payload: T) -> Self {
let state = default(); let state = default();
let current_target = Rc::new(RefCell::new(target.clone())); let current_target = Rc::new(RefCell::new(target.clone()));
let data = Rc::new(EventData { payload, target, current_target, state }); let captures = Rc::new(Cell::new(true));
let bubbles = Rc::new(Cell::new(true));
let data = Rc::new(EventData { payload, target, current_target, state, captures, bubbles });
Self { data } Self { data }
} }
@ -149,13 +218,35 @@ impl<T> Event<T> {
/// ///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation. /// See: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation.
pub fn stop_propagation(&self) { pub fn stop_propagation(&self) {
if self.state.get() == State::RunningNonCancellable { match self.state.get() {
warn!("Trying to cancel a non-cancellable event."); State::Running(phase) => self.state.set(State::RunningCancelled(phase)),
} else { State::RunningNonCancellable(_) => warn!("Trying to cancel a non-cancellable event."),
self.state.set(State::Cancelled); _ => {}
} }
} }
/// Emit event again with the same payload after it was cancelled. The event will start its
/// capturing phase handling again, starting at but not including this instance. If the passed
/// instance is no longer a part of the parent chain of the event's original target, the event
/// will start its capturing phase from scratch. If the original target no longer exists, the
/// event will be discarded.
pub fn resume_propagation(&self) {
match self.state.get() {
State::RunningCancelled(phase) => {
// When cancelled but not stopped yet, the propagation is still ongoing. We can
// reset the state back to running and let it continue.
self.state.set(State::Running(phase));
}
State::StoppedCancelled(_) =>
if let Some(target) = self.target() {
target.resume_event(SomeEvent::from_event(self.clone()));
},
_ => warn!("Trying to resume propagation of a non-cancelled event."),
}
}
/// A reference to the object onto which the event was dispatched. /// A reference to the object onto which the event was dispatched.
/// ///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Event/target. /// See: https://developer.mozilla.org/en-US/docs/Web/API/Event/target.

View File

@ -2144,7 +2144,6 @@ impl InstanceDef {
let this_weak = self.downgrade(); let this_weak = self.downgrade();
let mut children_borrow = self.children.borrow_mut(); let mut children_borrow = self.children.borrow_mut();
let num_children_before = children_borrow.len(); let num_children_before = children_borrow.len();
let mut pushed_out_children = false; let mut pushed_out_children = false;
let mut added_children = 0; let mut added_children = 0;
let mut next_free_index = new_children.len().max(*self.next_child_index.get()); let mut next_free_index = new_children.len().max(*self.next_child_index.get());
@ -2209,12 +2208,17 @@ impl InstanceDef {
if let Some(strong) = child_at_dest.upgrade() { if let Some(strong) = child_at_dest.upgrade() {
let mut bind = strong.parent_bind.data.borrow_mut(); let mut bind = strong.parent_bind.data.borrow_mut();
let bind = bind.as_mut().expect("Child should always have a parent bind."); let bind = bind.as_mut().expect("Child should always have a parent bind.");
bind.child_index = free_index; // Check if the removed child's position actually match its parent bind. It is
children_borrow.insert(free_index, child_at_dest); // possible that this child was already assigned to a different spot in previous
// In case we just put a child in its final spot, we have to mark as modified. // iteration. In that case, we don't want to update it again.
// If it ends up being deleted, the flag will be cleared anyway. if bind.child_index == new_child_index {
if bind.parent == this_weak { bind.child_index = free_index;
self.dirty.modified_children.set(free_index); children_borrow.insert(free_index, child_at_dest);
// In case we just put a child in its final spot, we have to mark as
// modified. If it ends up being deleted, the flag will be cleared anyway.
if bind.parent == this_weak {
self.dirty.modified_children.set(free_index);
}
} }
} }
} }
@ -2228,6 +2232,8 @@ impl InstanceDef {
let has_elements_to_remove = retained_children < num_children_before; let has_elements_to_remove = retained_children < num_children_before;
let need_cleanup = has_elements_to_remove || has_stale_indices; let need_cleanup = has_elements_to_remove || has_stale_indices;
self.next_child_index.set(ChildIndex(new_children.len()));
if need_cleanup { if need_cleanup {
let mut binds_to_drop = SmallVec::<[(ParentBind, WeakInstance); 8]>::new(); let mut binds_to_drop = SmallVec::<[(ParentBind, WeakInstance); 8]>::new();
@ -2252,7 +2258,6 @@ impl InstanceDef {
drop(children_borrow); drop(children_borrow);
self.next_child_index.set(ChildIndex(new_children.len()));
for (bind, weak) in binds_to_drop { for (bind, weak) in binds_to_drop {
bind.drop_with_removed_element(self, weak) bind.drop_with_removed_element(self, weak)
} }
@ -2455,13 +2460,11 @@ impl InstanceDef {
// This implementation is a bit complex because we do not want to clone network to the FRP // This implementation is a bit complex because we do not want to clone network to the FRP
// closure in order to avoid a memory leak. // closure in order to avoid a memory leak.
let network = &self.network; let network = &self.network;
let parent_bind = &self.parent_bind; let weak = self.downgrade();
let capturing_event_fan = &self.event.capturing_fan;
let bubbling_event_fan = &self.event.bubbling_fan;
frp::extend! { network frp::extend! { network
eval self.event.source ([parent_bind, capturing_event_fan, bubbling_event_fan] (event) { eval self.event.source ([] (event) {
let parent = parent_bind.parent(); weak.upgrade().map(|instance| instance.emit_event_impl(event));
Self::emit_event_impl(event, parent, &capturing_event_fan, &bubbling_event_fan); event.finish_propagation();
}); });
} }
self self
@ -2538,40 +2541,77 @@ impl InstanceDef {
self self
} }
fn emit_event_impl( /// The main event propagation implementation. Propagates the event through the display object
event: &event::SomeEvent, /// hierarchy in two phases: capturing and bubbling. See [`crate::display::object::event`]
parent: Option<Instance>, /// module to learn more about event phases.
capturing_event_fan: &frp::Fan, ///
bubbling_event_fan: &frp::Fan, /// The propagation itself is done in following steps:
) { /// - If event has been previously cancelled, figure out where to resume the propagation.
let rev_parent_chain = parent.map(|p| p.rev_parent_chain()).unwrap_or_default(); /// - Collect the parent chain of the event target.
if event.captures.get() { /// - Execute capturing phase - propagate event from the root to the target.
for object in &rev_parent_chain { /// - Execute bubbling phase - propagate event from the target to the root.
if !event.is_cancelled() { /// - If event has been cancelled, store the phase and target to resume the propagation.
event.set_current_target(Some(object)); fn emit_event_impl(&self, event: &event::SomeEvent) {
object.event.capturing_fan.emit(&event.data); let Some((resume_phase, mut resume_target)) = event.begin_propagation() else { return; };
} else {
break; let rev_parent_chain = self.rev_parent_chain();
} let only_target = &rev_parent_chain[rev_parent_chain.len().saturating_sub(1)..];
if resume_phase <= event::Phase::Capturing {
event.enter_phase(event::Phase::Capturing);
// If capturing phase is disabled, process only target.
let chain = if event.captures.get() { &rev_parent_chain } else { only_target };
// When resuming capturing phase, if the target is no longer in the parent chain, we
// start the capturing again from the root.
let resume_index = resume_target.take().map(|t| chain.iter().position(|o| o == &t));
let resume_position = match resume_index {
// Resumed and the last target is still in chain. Continue capturing past it.
// targets: a b c d | d c b a
// index: 0 1 2 3 | 3 2 1 0
// resume: >...|........
Some(Some(index)) => (index + 1).min(chain.len()),
// Either starting from scratch, or resumed but the last target is no longer in
// chain. In that case start the capturing from the root.
Some(None) | None => 0,
};
for object in &chain[resume_position..] {
let false = event.is_cancelled() else { return };
event.set_current_target(Some(object));
object.event.capturing_fan.emit(&event.data);
} }
} }
if !event.is_cancelled() {
capturing_event_fan.emit(&event.data); if resume_phase <= event::Phase::Bubbling {
} event.enter_phase(event::Phase::Bubbling);
if !event.is_cancelled() {
bubbling_event_fan.emit(&event.data); // If bubbling phase is disabled, process only target.
} let chain = if event.bubbles.get() { &rev_parent_chain } else { only_target };
if event.bubbles.get() {
for object in rev_parent_chain.iter().rev() { // When resuming bubbling phase, if the target is no longer in the parent chain, we
if !event.is_cancelled() { // end bubbling completely.
event.set_current_target(Some(object)); let resume_index = resume_target.take().map(|t| chain.iter().position(|o| o == &t));
object.event.bubbling_fan.emit(&event.data); let resume_position = match resume_index {
} else { // Resumed and the last target is still in chain. Continue bubbling past it.
break; // targets: a b c d | d c b a
} // index: 0 1 2 3 | 3 2 1 0
// resume: | >....
Some(Some(index)) => index.saturating_sub(1),
// Starting from scratch. Start bubbling from the last object in the chain.
None => chain.len(),
// Resumed in bubbling phase, but the last target is no longer in chain. Stop
// bubbling phase immediately.
Some(None) => 0,
};
for object in chain[..resume_position].iter().rev() {
let false = event.is_cancelled() else { return };
event.set_current_target(Some(object));
object.event.bubbling_fan.emit(&event.data);
} }
} }
event.set_current_target(None);
} }
fn new_event<T>(&self, payload: T) -> event::SomeEvent fn new_event<T>(&self, payload: T) -> event::SomeEvent
@ -2592,6 +2632,10 @@ impl InstanceDef {
self.event.source.emit(event); self.event.source.emit(event);
} }
pub(crate) fn resume_event(&self, event: event::SomeEvent) {
self.event.source.emit(event);
}
fn focused_descendant(&self) -> Option<Instance> { fn focused_descendant(&self) -> Option<Instance> {
self.event.focused_descendant.borrow().as_ref().and_then(|t| t.upgrade()) self.event.focused_descendant.borrow().as_ref().and_then(|t| t.upgrade())
} }
@ -3012,6 +3056,14 @@ pub trait LayoutOps: Object {
self.display_object().layout.margin.set(Vector2(margin, margin)); self.display_object().layout.margin.set(Vector2(margin, margin));
} }
/// Set vertical and horizontal margins of the object. Margin is the free space around the
/// object.
fn set_margin_vh(&self, vertical: impl Into<Unit>, horizontal: impl Into<Unit>) -> &Self {
let margin = Vector2(horizontal.into().into(), vertical.into().into());
self.display_object().layout.margin.set(margin);
self
}
/// Set margin of all sides of the object. Margin is the free space around the object. /// Set margin of all sides of the object. Margin is the free space around the object.
fn set_margin_trbl( fn set_margin_trbl(
&self, &self,
@ -3045,6 +3097,14 @@ pub trait LayoutOps: Object {
let vertical = SideSpacing::new(bottom.into(), top.into()); let vertical = SideSpacing::new(bottom.into(), top.into());
self.display_object().layout.padding.set(Vector2(horizontal, vertical)); self.display_object().layout.padding.set(Vector2(horizontal, vertical));
} }
/// Set vertical and horizontal padding of the object. Padding is the free space inside the
/// object.
fn set_padding_vh(&self, vertical: impl Into<Unit>, horizontal: impl Into<Unit>) -> &Self {
let padding = Vector2(horizontal.into().into(), vertical.into().into());
self.display_object().layout.padding.set(padding);
self
}
} }
@ -4632,6 +4692,7 @@ mod hierarchy_tests {
assert.new_node_parents([false, false, false, true, false]); assert.new_node_parents([false, false, false, true, false]);
} }
#[test]
fn replace_children_replace_all_test() { fn replace_children_replace_all_test() {
let (root, nodes, assert) = ReplaceChildrenTest::<5>::new(); let (root, nodes, assert) = ReplaceChildrenTest::<5>::new();
root.replace_children(&nodes); root.replace_children(&nodes);

View File

@ -219,19 +219,19 @@ impl Mouse {
(event: &mouse::Up) { (event: &mouse::Up) {
if display_mode.get().allow_mouse_events() { if display_mode.get().allow_mouse_events() {
let button = event.button(); let button = event.button();
frp_deprecated.up.emit(button);
let current_target = target.get(); let current_target = target.get();
if let Some(last_target) = last_pressed_elem.borrow_mut().remove(&button) { if let Some(last_target) = last_pressed_elem.borrow_mut().remove(&button) {
pointer_target_registry.with_mouse_target(last_target, |t, d| { pointer_target_registry.with_mouse_target(last_target, |t, d| {
t.emit_mouse_release(button);
d.emit_event(event.clone().unchecked_convert_to::<mouse::Release>()); d.emit_event(event.clone().unchecked_convert_to::<mouse::Release>());
t.emit_mouse_release(button);
}); });
} }
pointer_target_registry.with_mouse_target(current_target, |t, d| { pointer_target_registry.with_mouse_target(current_target, |t, d| {
t.emit_mouse_up(button);
d.emit_event(event.clone()); d.emit_event(event.clone());
t.emit_mouse_up(button);
}); });
frp_deprecated.up.emit(button);
} }
}), }),
); );

View File

@ -28,7 +28,7 @@ pub use shape::Shape;
pub mod shape { pub mod shape {
use super::*; use super::*;
crate::shape! { crate::shape! {
pointer_events_instanced = true, pointer_events_instanced = true;
( (
style: Style, style: Style,
color: Vector4, color: Vector4,
@ -92,12 +92,18 @@ pub mod shape {
/// such as circles, rings, or ring segments. The advantage of having a singular shape for these /// such as circles, rings, or ring segments. The advantage of having a singular shape for these
/// cases is that a single draw call can be used to render multiple GUI elements, which ultimately /// cases is that a single draw call can be used to render multiple GUI elements, which ultimately
/// enhances performance. /// enhances performance.
#[derive(Clone, CloneRef, Debug, Deref, Default)] #[derive(Clone, CloneRef, Deref, Default)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct Rectangle { pub struct Rectangle {
pub view: shape::View, pub view: shape::View,
} }
impl Debug for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rectangle").finish()
}
}
impl Rectangle { impl Rectangle {
fn modify_view(&self, f: impl FnOnce(&shape::View)) -> &Self { fn modify_view(&self, f: impl FnOnce(&shape::View)) -> &Self {
f(&self.view); f(&self.view);

View File

@ -637,6 +637,7 @@ macro_rules! shape {
$(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)? $(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)?
$(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)? $(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)?
$(pointer_events = $pointer_events:tt;)? $(pointer_events = $pointer_events:tt;)?
$(pointer_events_instanced = $inst_ptr:tt;)?
$(alignment = $alignment:tt;)? $(alignment = $alignment:tt;)?
($style:ident : Style $(,$gpu_param : ident : $gpu_param_type : ty)* $(,)?) {$($body:tt)*} ($style:ident : Style $(,$gpu_param : ident : $gpu_param_type : ty)* $(,)?) {$($body:tt)*}
) => { ) => {
@ -648,57 +649,11 @@ macro_rules! shape {
$(above = [$($always_above_1 $(::$always_above_2)*),*];)? $(above = [$($always_above_1 $(::$always_above_2)*),*];)?
$(below = [$($always_below_1 $(::$always_below_2)*),*];)? $(below = [$($always_below_1 $(::$always_below_2)*),*];)?
$(pointer_events = $pointer_events;)? $(pointer_events = $pointer_events;)?
[$style] ($($gpu_param : $gpu_param_type),*){$($body)*} $(pointer_events_instanced = $inst_ptr;)?
} [$style] (
}; $([$inst_ptr] disable_pointer_events : f32,)?
// Recognize `pointer_events_instanced = true`; in addition to passing it to `_shape!`, insert $([true] $gpu_param : $gpu_param_type,)*
// a suitable instance attribute into the list of GPU parameters. ){$($body)*}
(
$(type SystemData = $system_data:ident;)?
$(type ShapeData = $shape_data:ident;)?
$(flavor = $flavor:path;)?
$(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)?
$(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)?
$(pointer_events = $pointer_events:tt;)?
pointer_events_instanced = true,
$(alignment = $alignment:tt;)?
($style:ident : Style $(,$gpu_param : ident : $gpu_param_type : ty)* $(,)?) {$($body:tt)*}
) => {
$crate::_shape! {
$(SystemData($system_data))?
$(ShapeData($shape_data))?
$(flavor = [$flavor];)?
$(alignment = $alignment;)?
$(above = [$($always_above_1 $(::$always_above_2)*),*];)?
$(below = [$($always_below_1 $(::$always_below_2)*),*];)?
$(pointer_events = $pointer_events;)?
pointer_events_instanced = true;
[$style] (disable_pointer_events : f32$(,$gpu_param : $gpu_param_type)*){$($body)*}
}
};
// Recognize `pointer_events_instanced = false`. Only `true` and `false` are allowed, because
// if it were a computed value, we wouldn't know during macro expansion whether to create an
// instance parameter for it.
(
$(type SystemData = $system_data:ident;)?
$(type ShapeData = $shape_data:ident;)?
$(flavor = $flavor:path;)?
$(above = [$($always_above_1:tt $(::$always_above_2:tt)*),*];)?
$(below = [$($always_below_1:tt $(::$always_below_2:tt)*),*];)?
$(pointer_events = $pointer_events:tt;)?
pointer_events_instanced = false,
$(alignment = $alignment:tt;)?
($style:ident : Style $(,$gpu_param : ident : $gpu_param_type : ty)* $(,)?) {$($body:tt)*}
) => {
$crate::_shape! {
$(SystemData($system_data))?
$(ShapeData($shape_data))?
$(flavor = [$flavor];)?
$(alignment = $alignment;)?
$(above = [$($always_above_1 $(::$always_above_2)*),*];)?
$(below = [$($always_below_1 $(::$always_below_2)*),*];)?
$(pointer_events = $pointer_events;)?
[$style] ($($gpu_param : $gpu_param_type),*){$($body)*}
} }
}; };
} }
@ -907,7 +862,10 @@ macro_rules! _shape {
$(pointer_events = $pointer_events:tt;)? $(pointer_events = $pointer_events:tt;)?
$(pointer_events_instanced = $pointer_events_instanced:tt;)? $(pointer_events_instanced = $pointer_events_instanced:tt;)?
[$style:ident] [$style:ident]
($($gpu_param : ident : $gpu_param_type : ty),* $(,)?) ($(
$([true] $gpu_param:ident : $gpu_param_type:ty)?
$([false] $__gpu_param:ident : $__gpu_param_type:ty)?,
)*)
{$($body:tt)*} {$($body:tt)*}
) => { ) => {
@ -988,20 +946,23 @@ macro_rules! _shape {
gpu_params: &Self::GpuParams, gpu_params: &Self::GpuParams,
id: InstanceId id: InstanceId
) -> Shape { ) -> Shape {
$(let $gpu_param = ProxyParam::new(gpu_params.$gpu_param.at(id));)* let params = Self::InstanceParams {
let params = Self::InstanceParams { $($gpu_param),* }; $($(
$gpu_param: ProxyParam::new(gpu_params.$gpu_param.at(id)),
)?)*
};
Shape { params } Shape { params }
} }
fn new_gpu_params( fn new_gpu_params(
shape_system: &display::shape::ShapeSystemModel shape_system: &display::shape::ShapeSystemModel
) -> Self::GpuParams { ) -> Self::GpuParams {
$( $($(
let name = stringify!($gpu_param); let name = stringify!($gpu_param);
let val = gpu::data::default::gpu_default::<<$gpu_param_type as Parameter>::GpuType>(); let val = gpu::data::default::gpu_default::<<$gpu_param_type as Parameter>::GpuType>();
let $gpu_param = shape_system.add_input(name,val); let $gpu_param = shape_system.add_input(name,val);
)* )?)*
Self::GpuParams {$($gpu_param),*} Self::GpuParams {$($($gpu_param,)?)*}
} }
fn shape_def(__style_watch__: &display::shape::StyleWatch) fn shape_def(__style_watch__: &display::shape::StyleWatch)
@ -1015,11 +976,12 @@ macro_rules! _shape {
let $style = __style_watch__; let $style = __style_watch__;
// Silencing warnings about not used style. // Silencing warnings about not used style.
let _unused = &$style; let _unused = &$style;
$( $($(
let $gpu_param = <$gpu_param_type as Parameter>::create_var(concat!("input_",stringify!($gpu_param))); let name = concat!("input_",stringify!($gpu_param));
let $gpu_param = <$gpu_param_type as Parameter>::create_var(name);
// Silencing warnings about not used shader input variables. // Silencing warnings about not used shader input variables.
let _unused = &$gpu_param; let _unused = &$gpu_param;
)* )?)*
$($body)* $($body)*
} }
@ -1050,19 +1012,25 @@ macro_rules! _shape {
#[derive(Debug)] #[derive(Debug)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct InstanceParams { pub struct InstanceParams {
$(pub $gpu_param : ProxyParam<Attribute<<$gpu_param_type as Parameter>::GpuType>>),* $($(
pub $gpu_param : ProxyParam<Attribute<<$gpu_param_type as Parameter>::GpuType>>,
)?)*
} }
impl InstanceParamsTrait for InstanceParams { impl InstanceParamsTrait for InstanceParams {
fn swap(&self, other: &Self) { fn swap(&self, other: &Self) {
$(self.$gpu_param.swap(&other.$gpu_param);)* $($(
self.$gpu_param.swap(&other.$gpu_param);
)?)*
} }
} }
#[derive(Clone, CloneRef, Debug)] #[derive(Clone, CloneRef, Debug)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct GpuParams { pub struct GpuParams {
$(pub $gpu_param: gpu::data::Buffer<<$gpu_param_type as Parameter>::GpuType>),* $($(
pub $gpu_param: gpu::data::Buffer<<$gpu_param_type as Parameter>::GpuType>,
)?)*
} }

View File

@ -48,6 +48,7 @@ define_style! {
/// label selection. After setting the host to the label, cursor will not follow mouse anymore, /// label selection. After setting the host to the label, cursor will not follow mouse anymore,
/// it will inherit its position from the label instead. /// it will inherit its position from the label instead.
host: display::object::Instance, host: display::object::Instance,
pointer_events: bool,
size: Vector2<f32>, size: Vector2<f32>,
offset: Vector2<f32>, offset: Vector2<f32>,
color: color::Lcha, color: color::Lcha,
@ -132,6 +133,7 @@ impl Style {
let def_size = DEFAULT_SIZE(); let def_size = DEFAULT_SIZE();
self.offset = Some(StyleValue::new_no_animation(-size / 2.0)); self.offset = Some(StyleValue::new_no_animation(-size / 2.0));
self.size = Some(StyleValue::new_no_animation(size.abs() + def_size)); self.size = Some(StyleValue::new_no_animation(size.abs() + def_size));
self.pointer_events = Some(StyleValue::new_no_animation(true));
self self
} }
} }
@ -146,7 +148,7 @@ impl Style {
pub mod shape { pub mod shape {
use super::*; use super::*;
crate::shape! { crate::shape! {
pointer_events = false; pointer_events_instanced = true;
alignment = center; ( alignment = center; (
style: Style, style: Style,
press: f32, press: f32,
@ -363,6 +365,7 @@ impl Cursor {
})); }));
frp.set_style_override <+ should_trash.then_constant(Style::trash()); frp.set_style_override <+ should_trash.then_constant(Style::trash());
perform_trash <- on_up.gate(&should_trash); perform_trash <- on_up.gate(&should_trash);
frp.set_style_override <+ perform_trash.constant(None);
eval_ perform_trash (model.trash_dragged_item()); eval_ perform_trash (model.trash_dragged_item());
@ -461,6 +464,13 @@ impl Cursor {
Some(t) => plus.target.emit(t.value.unwrap_or(0.0)), Some(t) => plus.target.emit(t.value.unwrap_or(0.0)),
} }
let pointer_events = match &new_style.pointer_events {
None => false,
Some(t) => t.value.unwrap_or(false),
};
let disable_pointer_events = (!pointer_events) as i32 as f32;
model.for_each_view(|vw| vw.disable_pointer_events.set(disable_pointer_events));
*model.style.borrow_mut() = new_style.clone(); *model.style.borrow_mut() = new_style.clone();
}); });
@ -630,7 +640,6 @@ impl CursorModel {
self.stop_drag_internal(); self.stop_drag_internal();
Some(item) Some(item)
} else { } else {
warn!("Can't stop dragging an item because no item is being dragged.");
None None
} }
} }
@ -650,7 +659,6 @@ impl CursorModel {
} }
} }
} else { } else {
warn!("Can't stop dragging an item because no item is being dragged.");
None None
} }
} }

View File

@ -144,7 +144,7 @@ mod background {
ensogl_core::shape! { ensogl_core::shape! {
below = [texture, icon1, icon2]; below = [texture, icon1, icon2];
alignment = center; alignment = center;
(style: Style,) { (style: Style) {
Rect((296.0.px(), 326.0.px())).fill(color::Rgba::black()).into() Rect((296.0.px(), 326.0.px())).fill(color::Rgba::black()).into()
} }
} }

View File

@ -203,6 +203,21 @@ impl Network {
self.register(OwnedBufferedGate::new(label, event, behavior)) self.register(OwnedBufferedGate::new(label, event, behavior))
} }
/// Passes the incoming event of the first stream only if the second stream has emitted an event
/// since the last event of the first stream.
///
/// Event: 1---2---3-----4-------5---6---7---8--
/// Sync: --|--------|---|---|-----------------
/// Output: ----2---------4-------5--------------
pub fn sync_gate<T, T2>(&self, label: Label, event: &T, sync: &T2) -> Stream<Output<T>>
where
T: EventOutput,
T2: EventOutput, {
let can_emit = Rc::new(Cell::new(false));
self.map(label, sync, f_!(can_emit.set(true)));
self.filter(label, event, f_!(can_emit.take()))
}
/// Unwraps the value of incoming events and emits the unwrapped values. /// Unwraps the value of incoming events and emits the unwrapped values.
pub fn unwrap<T, S>(&self, label: Label, event: &T) -> Stream<S> pub fn unwrap<T, S>(&self, label: Label, event: &T) -> Stream<S>
where where

View File

@ -245,6 +245,12 @@ impl From<&&str> for ImString {
} }
} }
impl From<Cow<'_, str>> for ImString {
fn from(t: Cow<str>) -> Self {
t.into_owned().into()
}
}
impl From<ImString> for String { impl From<ImString> for String {
fn from(value: ImString) -> Self { fn from(value: ImString) -> Self {
match Rc::try_unwrap(value.content) { match Rc::try_unwrap(value.content) {