New node design (#7311)

Fixes #6552
Fixes #6910
Fixes #6872

Implementation of new node design. Includes many changes related to stylesheet update handling and per-style FRP construction, as well as refactoring of scene layers used by graph editor. Some additional components were migrated to use `Rectangle` shape and new mouse handling events. Fixed text rendering, where random thin lines appeared at the borders of glyph sprites. Refined edge layout to match new node sizes and not leave any visible gaps between line segments.

The node colors are currently randomly selected from predefined list. Later this will be improved to use group information from the suggestion database, once that is fully migrated to use the documentation tags, thus removing the dependency on the execution context.


https://github.com/enso-org/enso/assets/919491/aa687e53-a2fa-4e95-a15f-132c05e6337a


<img width="653" alt="image" src="https://github.com/enso-org/enso/assets/919491/30f3e897-62fc-40ea-b57b-124ac923bafd">
This commit is contained in:
Paweł Grabarz 2023-07-27 15:00:47 +02:00 committed by GitHub
parent e4357f8890
commit bb39eeb12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 5462 additions and 4341 deletions

View File

@ -208,6 +208,10 @@
- [The libraries' authors may put entities to groups by adding GROUP tag in the
docstring]. It was requested as more convenient way than specifying full names
in package.yaml.
- [Graph editor node was redesigned][7311]. Nodes have a color and icon matching
the selected entry in the component browser. Clear separating lines between
method arguments were added. The node selection was made easier with
additional thick interactive selection border.
[5910]: https://github.com/enso-org/enso/pull/5910
[6279]: https://github.com/enso-org/enso/pull/6279
@ -230,6 +234,7 @@
[7164]: https://github.com/enso-org/enso/pull/7164
[7372]: https://github.com/enso-org/enso/pull/7372
[7337]: https://github.com/enso-org/enso/pull/7337
[7311]: https://github.com/enso-org/enso/pull/7311
#### EnsoGL (rendering engine)

11
Cargo.lock generated
View File

@ -2661,6 +2661,7 @@ dependencies = [
"Inflector",
"bit_field",
"bitflags 2.2.1",
"bytemuck",
"code-builder",
"console_error_panic_hook",
"enso-callback",
@ -2675,6 +2676,7 @@ dependencies = [
"enso-types",
"enso-web",
"ensogl-derive",
"ensogl-derive-theme",
"ensogl-text-embedded-fonts",
"enum_dispatch",
"failure",
@ -2710,6 +2712,7 @@ dependencies = [
name = "ensogl-derive-theme"
version = "0.1.0"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 1.0.107",
@ -4285,7 +4288,6 @@ dependencies = [
"enso-shapely",
"ensogl",
"ensogl-component",
"ensogl-derive-theme",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
"ensogl-text",
@ -4335,7 +4337,6 @@ dependencies = [
"approx 0.5.1",
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
@ -4356,7 +4357,6 @@ version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-text",
@ -4368,7 +4368,6 @@ version = "0.1.0"
dependencies = [
"enso-frp",
"ensogl-core",
"ensogl-derive-theme",
"ensogl-grid-view",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
@ -4384,6 +4383,7 @@ name = "ide-view-component-list-panel-icons"
version = "0.1.0"
dependencies = [
"ensogl-core",
"ensogl-hardcoded-theme",
"failure",
]
@ -4399,7 +4399,6 @@ dependencies = [
"enso-suggestion-database",
"ensogl",
"ensogl-component",
"ensogl-derive-theme",
"ensogl-hardcoded-theme",
"horrorshow",
"ide-ci",
@ -4418,7 +4417,6 @@ dependencies = [
"enso-frp",
"enso-prelude",
"ensogl",
"ensogl-derive-theme",
"ensogl-drop-down-menu",
"ensogl-gui-component",
"ensogl-hardcoded-theme",
@ -4448,6 +4446,7 @@ dependencies = [
"ensogl-hardcoded-theme",
"ensogl-text-msdf",
"failure",
"ide-view-component-list-panel-icons",
"ide-view-execution-environment-selector",
"indexmap",
"js-sys",

View File

@ -687,7 +687,7 @@ pub trait TraversableAst: Sized {
position += child_offset;
ast = child;
}
Ok(text::Range::new(position, position + ast.len()))
Ok(text::Range::new(position, position + ast.repr_len()))
}
}

View File

@ -321,7 +321,7 @@ impl Ast {
if searched_token == token {
found_child = true
} else if !found_child {
position += token.len()
position += token.repr_len()
}
});
if found_child {
@ -335,7 +335,7 @@ impl Ast {
pub fn span_of_child_at(&self, crumb: &Crumb) -> FallibleResult<enso_text::Range<Byte>> {
let child = self.get(crumb)?;
let offset = self.child_offset(child)?;
Ok(enso_text::Range::new(offset, offset + child.len()))
Ok(enso_text::Range::new(offset, offset + child.repr_len()))
}
}
@ -808,14 +808,14 @@ pub trait HasRepr {
/// Get the representation length in bytes.
///
/// May be implemented in a quicker way than building string. Must meet the constraint
/// `x.len() == x.repr().len()` for any `x: impl HasRepr`.
fn len(&self) -> Bytes {
/// `x.repr_len() == x.repr().len()` for any `x: impl HasRepr`.
fn repr_len(&self) -> Bytes {
self.repr().len().bytes()
}
/// Check if the representation is empty.
fn is_empty(&self) -> bool {
self.len() <= 0.bytes()
self.repr_len() <= 0.bytes()
}
/// Get the representation length in chars.
@ -883,7 +883,7 @@ impl<T: HasTokens> HasRepr for T {
consumer.repr
}
fn len(&self) -> Bytes {
fn repr_len(&self) -> Bytes {
let mut consumer = LengthBuilder::default();
self.feed_to(&mut consumer);
consumer.length
@ -934,8 +934,8 @@ where T: HasRepr
self.deref().repr()
}
fn len(&self) -> Bytes {
self.deref().len()
fn repr_len(&self) -> Bytes {
self.deref().repr_len()
}
fn char_count(&self) -> usize {
@ -1005,8 +1005,8 @@ where T: HasRepr
self.deref().repr()
}
fn len(&self) -> Bytes {
self.deref().len()
fn repr_len(&self) -> Bytes {
self.deref().repr_len()
}
fn char_count(&self) -> usize {
@ -1325,7 +1325,7 @@ mod tests {
#[test]
fn ast_length() {
let ast = Ast::prefix(Ast::var(""), Ast::var("YY"));
assert_eq!(ast.len(), 6.bytes());
assert_eq!(ast.repr_len(), 6.bytes());
assert_eq!(ast.char_count(), 5);
}
@ -1424,7 +1424,7 @@ mod tests {
fn utf8_lengths() {
let var = Ast::var("");
assert_eq!(var.char_count(), 1);
assert_eq!(var.len(), 3.bytes());
assert_eq!(var.repr_len(), 3.bytes());
let idmap = var.id_map();
assert_eq!(idmap.vec[0].0, enso_text::Range::new(0.byte(), 3.byte()));
@ -1432,6 +1432,6 @@ mod tests {
let builder_with_char = Token::Chr('壱');
assert_eq!(builder_with_char.char_count(), 1);
assert_eq!(builder_with_char.len(), 3.bytes());
assert_eq!(builder_with_char.repr_len(), 3.bytes());
}
}

View File

@ -25,7 +25,7 @@ pub fn main() {
let parens_cr1 = ast::crumbs::MatchCrumb::Segs { val: val.clone(), index: 0 };
let parens_cr = ast::crumbs::MatchCrumb::Segs { val, index: 0 };
let _input_span_tree = builder::TreeBuilder::new(36)
.add_child(0, 14, node::Kind::chained(), PrefixCrumb::Func)
.add_child(0, 14, node::Kind::ChainedPrefix, PrefixCrumb::Func)
.add_child(0, 9, node::Kind::Operation, PrefixCrumb::Func)
.set_ast_id(Uuid::new_v4())
.done()
@ -56,7 +56,7 @@ pub fn main() {
let input_span_tree2 = Node::<()>::new()
.new_child(|t| {
t.new_ast_id()
.kind(node::Kind::chained())
.kind(node::Kind::ChainedPrefix)
.crumbs(PrefixCrumb::Func)
.new_child(|t| {
t.size(9.bytes())

View File

@ -485,14 +485,14 @@ mod test {
, Case{expr:"+" , span:0..0 , action:Set , expected:"foo +" }
, Case{expr:"+" , span:1..1 , action:Set , expected:"+ foo" }
, Case{expr:"a + b" , span:0..0 , action:Set , expected:"foo + a + b" }
, Case{expr:"a + b" , span:4..4 , action:Set , expected:"a + foo + b" }
, Case{expr:"a + b" , span:3..3 , action:Set , expected:"a + foo + b" }
, Case{expr:"a + b" , span:5..5 , action:Set , expected:"a + b + foo" }
, Case{expr:"+ b" , span:3..3 , action:Set , expected:"+ b + foo" }
, Case{expr:"a + b + c" , span:0..0 , action:Set , expected:"foo + a + b + c"}
, Case{expr:"a + b + c" , span:8..8 , action:Set , expected:"a + b + foo + c"}
, Case{expr:"a + b + c" , span:7..7 , action:Set , expected:"a + b + foo + c"}
, Case{expr:", b" , span:3..3 , action:Set , expected:", b , foo" }
, Case{expr:"f a b" , span:2..2 , action:Set , expected:"f foo a b" }
, Case{expr:"f a b" , span:4..4 , action:Set , expected:"f a foo b" }
, Case{expr:"f a b" , span:1..1 , action:Set , expected:"f foo a b" }
, Case{expr:"f a b" , span:3..3 , action:Set , expected:"f a foo b" }
, Case{expr:"f a b" , span:5..5 , action:Set , expected:"f a b foo" }
, Case{expr:"if a then b", span:3..4 , action:Set , expected: "if foo then b" }
, Case{expr:"if a then b", span:10..11, action:Set , expected: "if a then foo" }
@ -546,20 +546,20 @@ mod test {
Case { expr: "a + b", span: 0..0, expected: &[Set] },
Case { expr: "a + b", span: 0..1, expected: &[Set] },
Case { expr: "a + b", span: 2..3, expected: &[] },
Case { expr: "a + b", span: 4..4, expected: &[Set] },
Case { expr: "a + b", span: 3..3, expected: &[Set] },
Case { expr: "a + b", span: 4..5, expected: &[Set] },
Case { expr: "a + b", span: 5..5, expected: &[Set] },
Case { expr: "a + b + c", span: 0..0, expected: &[Set] },
Case { expr: "a + b + c", span: 0..1, expected: &[Set, Erase] },
Case { expr: "a + b + c", span: 4..4, expected: &[Set] },
Case { expr: "a + b + c", span: 3..3, expected: &[Set] },
Case { expr: "a + b + c", span: 4..5, expected: &[Set, Erase] },
Case { expr: "a + b + c", span: 8..8, expected: &[Set] },
Case { expr: "a + b + c", span: 7..7, expected: &[Set] },
Case { expr: "a + b + c", span: 8..9, expected: &[Set, Erase] },
Case { expr: "a + b + c", span: 9..9, expected: &[Set] },
Case { expr: "f a b", span: 0..1, expected: &[Set] },
Case { expr: "f a b", span: 2..2, expected: &[Set] },
Case { expr: "f a b", span: 1..1, expected: &[Set] },
Case { expr: "f a b", span: 2..3, expected: &[Set, Erase] },
Case { expr: "f a b", span: 4..4, expected: &[Set] },
Case { expr: "f a b", span: 3..3, expected: &[Set] },
Case { expr: "f a b", span: 4..5, expected: &[Set, Erase] },
Case { expr: "f a b", span: 5..5, expected: &[Set] },
Case { expr: "f a", span: 2..3, expected: &[Set] },

View File

@ -348,7 +348,7 @@ fn generate_node_for_ast(
tree_generate_node(tree, kind, context, ast.id),
ast::Shape::Block(block) => block_generate_node(block, kind, context, ast.id),
_ => {
let size = (ast.len().value as i32).byte_diff();
let size = (ast.repr_len().value as i32).byte_diff();
let ast_id = ast.id;
if let Some(info) = ast_id.and_then(|id| context.call_info(id)) {
let node = Node::new()
@ -451,7 +451,7 @@ fn generate_node_for_opr_chain(
let left_crumbs = if has_left { vec![elem.crumb_to_previous()] } else { vec![] };
let mut gen = ChildGenerator::default();
if is_first && has_left {
if is_first && has_left && !app_base.uses_method_notation {
gen.generate_empty_node(InsertionPointType::BeforeArgument(0));
}
let node = gen.add_node(left_crumbs, node);
@ -501,12 +501,16 @@ fn generate_node_for_opr_chain(
let arg_crumbs = elem.crumb_to_operand(has_left);
let arg_ast = Located::new(arg_crumbs, &operand.arg);
gen.spacing(operand.offset);
if has_left {
if has_left && !app_base.uses_method_notation {
gen.generate_empty_node(InsertionPointType::BeforeArgument(i + 1));
}
gen.spacing(operand.offset);
let argument_kind = node::Kind::argument().with_removable(removable);
let argument_kind: node::Kind = if app_base.uses_method_notation {
node::Kind::Access
} else {
node::Kind::argument().with_removable(removable).into()
};
let argument = gen.generate_ast_node(arg_ast, argument_kind, context)?;
if let Some((index, info)) = infix_right_argument_info {
@ -515,7 +519,7 @@ fn generate_node_for_opr_chain(
}
}
if is_last {
if is_last && !app_base.uses_method_notation {
gen.generate_empty_node(InsertionPointType::Append);
}
@ -523,7 +527,7 @@ fn generate_node_for_opr_chain(
gen.reverse_children();
}
let kind = if is_last { kind.clone() } else { node::Kind::chained().into() };
let kind = if is_last { kind.clone() } else { node::Kind::chained_infix() };
Ok((gen.into_node(kind, elem.infix_id), elem.offset))
})?;
Ok(node)
@ -591,7 +595,6 @@ fn generate_node_for_prefix_chain(
ArgumentPosition::ChainArgument { arg, named, info } => {
let mut gen = ChildGenerator::default();
gen.add_node(vec![PrefixCrumb::Func.into()], node);
gen.spacing(arg.sast.off);
let arg_name = named.as_ref().map(|named| named.name);
@ -605,6 +608,7 @@ fn generate_node_for_prefix_chain(
let mut arg_kind = node::Kind::from(
node::Kind::argument()
.in_prefix_chain()
.with_removable(removable)
.with_name(arg_name.map(ToString::to_string)),
);
@ -622,6 +626,7 @@ fn generate_node_for_prefix_chain(
));
}
inserted_arguments += 1;
gen.spacing(arg.sast.off);
// For named arguments, we need to generate the named argument span-tree
// structure. The actual argument node is nested inside the named argument.
@ -640,7 +645,7 @@ fn generate_node_for_prefix_chain(
gen.generate_empty_node(InsertionPointType::Append);
}
Ok(gen.into_node(node::Kind::chained(), arg.prefix_id))
Ok(gen.into_node(node::Kind::ChainedPrefix, arg.prefix_id))
}
ArgumentPosition::Placeholder { info, named } =>
Ok(generate_expected_argument(node, named, i, info)),
@ -796,7 +801,7 @@ fn generate_expected_argument(
let arg_node = gen.generate_empty_node(InsertionPointType::ExpectedArgument { index, named });
arg_node.node.set_argument_info(argument_info);
arg_node.node.set_port_id(port_id);
gen.into_node(node::Kind::chained(), None).with_extended_ast_id(extended_ast_id)
gen.into_node(node::Kind::chained_infix(), None).with_extended_ast_id(extended_ast_id)
}
/// Build a prefix application-like span tree structure where no prefix argument has been provided
@ -1037,17 +1042,17 @@ mod test {
.add_empty_child(0, BeforeArgument(0))
.add_leaf(0, 1, node::Kind::argument(), InfixCrumb::LeftOperand)
.add_leaf(2, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(4, BeforeArgument(1))
.add_empty_child(3, BeforeArgument(1))
.add_child(4, 7, node::Kind::argument(), InfixCrumb::RightOperand)
.add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func)
.add_empty_child(4, BeforeArgument(0))
.add_leaf(4, 3, node::Kind::argument(), PrefixCrumb::Arg)
.add_empty_child(3, BeforeArgument(0))
.add_leaf(4, 3, node::Kind::prefix_argument(), PrefixCrumb::Arg)
.add_empty_child(7, Append)
.done()
.add_empty_child(11, Append)
.done()
.add_leaf(12, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(14, BeforeArgument(1))
.add_empty_child(13, BeforeArgument(1))
.add_leaf(14, 1, node::Kind::argument(), InfixCrumb::RightOperand)
.add_empty_child(15, Append)
.build();
@ -1064,33 +1069,33 @@ mod test {
clear_parameter_infos(&mut tree.root);
let expected = TreeBuilder::new(26)
.add_child(0, 22, node::Kind::chained(), InfixCrumb::LeftOperand)
.add_child(0, 5, node::Kind::chained(), InfixCrumb::LeftOperand)
.add_child(0, 22, node::Kind::chained_infix(), InfixCrumb::LeftOperand)
.add_child(0, 5, node::Kind::chained_infix(), InfixCrumb::LeftOperand)
.add_empty_child(0, BeforeArgument(0))
.add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand)
.add_leaf(2, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(4, BeforeArgument(1))
.add_empty_child(3, BeforeArgument(1))
.add_leaf(4, 1, node::Kind::argument().removable(), InfixCrumb::RightOperand)
.done()
.add_leaf(6, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(8, BeforeArgument(2))
.add_empty_child(7, BeforeArgument(2))
.add_child(8, 14, node::Kind::argument().removable(), InfixCrumb::RightOperand)
.add_child(0, 11, node::Kind::chained(), PrefixCrumb::Func)
.add_child(0, 7, node::Kind::chained(), PrefixCrumb::Func)
.add_child(0, 11, node::Kind::ChainedPrefix, PrefixCrumb::Func)
.add_child(0, 7, node::Kind::ChainedPrefix, PrefixCrumb::Func)
.add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func)
.add_empty_child(4, BeforeArgument(0))
.add_leaf(4, 3, node::Kind::argument().removable(), PrefixCrumb::Arg)
.add_empty_child(3, BeforeArgument(0))
.add_leaf(4, 3, node::Kind::prefix_argument().removable(), PrefixCrumb::Arg)
.done()
.add_empty_child(8, BeforeArgument(1))
.add_leaf(8, 3, node::Kind::argument().removable(), PrefixCrumb::Arg)
.add_empty_child(7, BeforeArgument(1))
.add_leaf(8, 3, node::Kind::prefix_argument().removable(), PrefixCrumb::Arg)
.done()
.add_empty_child(12, BeforeArgument(2))
.add_leaf(12, 2, node::Kind::argument().removable(), PrefixCrumb::Arg)
.add_empty_child(11, BeforeArgument(2))
.add_leaf(12, 2, node::Kind::prefix_argument().removable(), PrefixCrumb::Arg)
.add_empty_child(14, Append)
.done()
.done()
.add_leaf(23, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(25, BeforeArgument(3))
.add_empty_child(24, BeforeArgument(3))
.add_leaf(25, 1, node::Kind::argument().removable(), InfixCrumb::RightOperand)
.add_empty_child(26, Append)
.build();
@ -1110,7 +1115,7 @@ mod test {
.add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand)
.add_leaf(1, 2, node::Kind::Operation, InfixCrumb::Operator)
.add_child(3, 3, node::Kind::chained(), InfixCrumb::RightOperand)
.add_child(3, 3, node::Kind::chained_infix(), InfixCrumb::RightOperand)
.add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), InfixCrumb::LeftOperand)
.add_leaf(1, 2, node::Kind::Operation, InfixCrumb::Operator)
@ -1132,7 +1137,7 @@ mod test {
let expected = TreeBuilder::new(5)
.add_empty_child(0, Append)
.add_leaf(0, 2, node::Kind::Operation, SectionRightCrumb::Opr)
.add_child(2, 2, node::Kind::chained(), SectionRightCrumb::Arg)
.add_child(2, 2, node::Kind::chained_infix(), SectionRightCrumb::Arg)
.add_empty_child(0, Append)
.add_leaf(0, 1, node::Kind::argument().removable(), SectionLeftCrumb::Arg)
.add_leaf(1, 1, node::Kind::Operation, SectionLeftCrumb::Opr)
@ -1152,8 +1157,8 @@ mod test {
let expected = TreeBuilder::new(13)
.add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func)
.add_empty_child(4, BeforeArgument(0))
.add_leaf(4, 9, node::Kind::argument(), PrefixCrumb::Arg)
.add_empty_child(3, BeforeArgument(0))
.add_leaf(4, 9, node::Kind::prefix_argument(), PrefixCrumb::Arg)
.add_empty_child(13, Append)
.build();
@ -1192,7 +1197,7 @@ mod test {
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
[_func, arg0] => assert_eq!(arg0.argument_info(), Some(this_param(Some(call_id)))),
[_func, arg0] => assert_eq!(arg0.argument_info(), Some(&this_param(Some(call_id)))),
sth_else => panic!("There should be 2 leaves, found: {}", sth_else.len()),
}
let expected = TreeBuilder::new(3)
@ -1213,12 +1218,12 @@ mod test {
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
[_func, arg0] => assert_eq!(arg0.argument_info(), Some(this_param(Some(call_id)))),
[_func, arg0] => assert_eq!(arg0.argument_info(), Some(&this_param(Some(call_id)))),
sth_else => panic!("There should be 2 leaves, found: {}", sth_else.len()),
}
let expected = TreeBuilder::new(8)
.add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func)
.add_leaf(4, 4, node::Kind::argument().removable().indexed(0), PrefixCrumb::Arg)
.add_leaf(4, 4, node::Kind::prefix_argument().removable().indexed(0), PrefixCrumb::Arg)
.build();
clear_expression_ids(&mut tree.root);
clear_parameter_infos(&mut tree.root);
@ -1238,17 +1243,17 @@ mod test {
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
[_func, arg0, arg1, arg2] => {
assert_eq!(arg0.argument_info(), Some(this_param(call_id)));
assert_eq!(arg1.argument_info(), Some(param1(call_id)));
assert_eq!(arg2.argument_info(), Some(param2(call_id)));
assert_eq!(arg0.argument_info(), Some(&this_param(call_id)));
assert_eq!(arg1.argument_info(), Some(&param1(call_id)));
assert_eq!(arg2.argument_info(), Some(&param2(call_id)));
}
sth_else => panic!("There should be 4 leaves, found: {}", sth_else.len()),
}
let expected = TreeBuilder::new(8)
.add_child(0, 8, node::Kind::chained(), Crumbs::default())
.add_child(0, 8, node::Kind::chained(), Crumbs::default())
.add_child(0, 8, node::Kind::chained_infix(), Crumbs::default())
.add_child(0, 8, node::Kind::ChainedPrefix, Crumbs::default())
.add_leaf(0, 3, node::Kind::Operation, PrefixCrumb::Func)
.add_leaf(4, 4, node::Kind::argument().removable().indexed(0), PrefixCrumb::Arg)
.add_leaf(4, 4, node::Kind::prefix_argument().removable().indexed(0), PrefixCrumb::Arg)
.done()
.add_empty_child(8, InsertionPoint::expected_argument(1))
.done()
@ -1270,21 +1275,18 @@ mod test {
let ctx = MockContext::new_single(ast.id.unwrap(), invocation_info);
let mut tree: SpanTree = SpanTree::new(&ast, &ctx).unwrap();
match tree.root_ref().leaf_iter().collect_vec().as_slice() {
[_, _this, _, _, _func, _, arg1, arg2] => {
assert_eq!(arg1.argument_info(), Some(param1(call_id)));
assert_eq!(arg2.argument_info(), Some(param2(call_id)));
[_this, _operator, _access, arg1, arg2] => {
assert_eq!(arg1.argument_info(), Some(&param1(call_id)));
assert_eq!(arg2.argument_info(), Some(&param2(call_id)));
}
sth_else => panic!("There should be 8 leaves, found: {}", sth_else.len()),
sth_else => panic!("There should be 5 leaves, found: {}", sth_else.len()),
}
let expected = TreeBuilder::new(8)
.add_child(0, 8, node::Kind::chained(), Crumbs::default())
.add_child(0, 8, node::Kind::chained_infix(), Crumbs::default())
.add_child(0, 8, node::Kind::Operation, Crumbs::default())
.add_empty_child(0, BeforeArgument(0))
.add_leaf(0, 4, node::Kind::argument(), InfixCrumb::LeftOperand)
.add_leaf(4, 1, node::Kind::Operation, InfixCrumb::Operator)
.add_empty_child(5, BeforeArgument(1))
.add_leaf(5, 3, node::Kind::argument(), InfixCrumb::RightOperand)
.add_empty_child(8, Append)
.add_leaf(5, 3, node::Kind::Access, InfixCrumb::RightOperand)
.done()
.add_empty_child(8, InsertionPoint::expected_argument(0))
.done()

View File

@ -128,7 +128,7 @@ mod tests {
// gg-children: ()() ()() ()
let tree: SpanTree = TreeBuilder::new(14)
.add_child(0, 10, Kind::chained(), vec![LeftOperand])
.add_child(0, 10, Kind::chained_infix(), vec![LeftOperand])
.add_leaf(0, 3, Kind::this(), vec![LeftOperand])
.add_leaf(4, 1, Kind::Operation, vec![Operator])
.add_child(6, 3, Kind::argument(), vec![RightOperand])
@ -137,10 +137,10 @@ mod tests {
.done()
.done()
.add_leaf(11, 1, Kind::Operation, vec![Operator])
.add_child(13, 1, Kind::chained(), vec![RightOperand])
.add_child(13, 1, Kind::chained_infix(), vec![RightOperand])
.add_leaf(0, 3, Kind::this(), vec![LeftOperand])
.add_leaf(4, 1, Kind::Operation, vec![Operator])
.add_child(6, 5, Kind::chained(), vec![RightOperand])
.add_child(6, 5, Kind::chained_infix(), vec![RightOperand])
.add_leaf(0, 1, Kind::this(), vec![LeftOperand])
.add_leaf(2, 1, Kind::Operation, vec![Operator])
.add_leaf(4, 1, Kind::argument(), vec![RightOperand])

View File

@ -333,10 +333,15 @@ impl SpanTree {
write!(buffer, "({:?})", inner.kind).unwrap();
}
if let Some(name) = node.kind.name() {
if let Some(name) = node.kind.argument_name() {
write!(buffer, " name={name:?}").unwrap();
}
if let Some(icon) = node.application.as_ref().and_then(|a| a.icon_name.as_ref()) {
write!(buffer, " icon={icon:?}").unwrap();
}
if let Some(call_id) = node.kind.call_id() {
write!(buffer, " call_id={call_id:?}").unwrap();
}

View File

@ -103,9 +103,6 @@ impl Node {
pub fn is_expected_argument(&self) -> bool {
self.kind.is_expected_argument()
}
pub fn is_function_parameter(&self) -> bool {
self.kind.is_function_parameter()
}
}
@ -150,12 +147,12 @@ impl Node {
#[allow(missing_docs)]
impl Node {
pub fn name(&self) -> Option<&str> {
self.kind.name()
self.kind.argument_name()
}
pub fn tp(&self) -> Option<&String> {
self.kind.tp()
}
pub fn argument_info(&self) -> Option<ArgumentInfo> {
pub fn argument_info(&self) -> Option<&ArgumentInfo> {
self.kind.argument_info()
}
pub fn set_argument_info(&mut self, info: ArgumentInfo) {
@ -927,11 +924,11 @@ mod test {
// An example with single call and expected arguments.
// See also `generate::test::generating_span_tree_for_unfinished_call`
let tree: SpanTree = TreeBuilder::new(8)
.add_child(0, 8, node::Kind::chained_this(), ast::crumbs::Crumbs::default())
.add_child(0, 8, node::Kind::this(), ast::crumbs::Crumbs::default())
.add_child(0, 8, node::Kind::Operation, ast::crumbs::Crumbs::default())
.add_leaf(0, 4, node::Kind::this(), LeftOperand)
.add_leaf(4, 1, node::Kind::Operation, Operator)
.add_leaf(5, 3, node::Kind::argument(), RightOperand)
.add_leaf(5, 3, node::Kind::prefix_argument(), RightOperand)
.done()
.add_empty_child(8, InsertionPoint::expected_argument(0))
.done()

View File

@ -17,10 +17,14 @@ use crate::TagValue;
pub enum Kind {
/// A root of the expression tree.
Root,
/// A node chained with parent node. See crate's docs for more info about chaining.
Chained(Chained),
/// A node chained with parent node, part of prefix method application.
ChainedPrefix,
/// A node chained with parent node, part of infix operator application.
ChainedInfix(ArgumentInfo),
/// A node representing operation (operator or function) of parent Infix, Section or Prefix.
Operation,
/// A part of the access chain that is not the primary target (not the leftmost node).
Access,
/// A node being a normal (not target) parameter of parent Infix, Section or Prefix.
Argument(Argument),
/// A node representing a named argument. Always contains two tokens and an `Argument` node.
@ -41,21 +45,21 @@ pub enum Kind {
#[allow(missing_docs)]
impl Kind {
pub fn chained() -> Chained {
default()
}
pub fn chained_this() -> Chained {
Chained::this()
}
pub fn this() -> Argument {
Self::argument().with_name(Some(Argument::THIS.into()))
Self::prefix_argument().with_name(Some(Argument::THIS.into()))
}
pub fn argument() -> Argument {
default()
}
pub fn prefix_argument() -> Argument {
Argument::default().in_prefix_chain()
}
pub fn insertion_point() -> InsertionPoint {
default()
}
pub fn chained_infix() -> Self {
Self::ChainedInfix(default())
}
}
@ -67,7 +71,7 @@ impl Kind {
matches!(self, Self::Root { .. })
}
pub fn is_chained(&self) -> bool {
matches!(self, Self::Chained { .. })
matches!(self, Self::ChainedPrefix | Self::ChainedInfix(..))
}
pub fn is_operation(&self) -> bool {
matches!(self, Self::Operation { .. })
@ -75,8 +79,8 @@ impl Kind {
pub fn is_this(&self) -> bool {
matches!(
self,
Self::Argument(Argument { name, .. }) | Self::Chained(Chained { name, .. })
if name.as_deref() == Some(Argument::THIS)
Self::Argument(arg)
if arg.info.name.as_deref() == Some(Argument::THIS)
)
}
pub fn is_argument(&self) -> bool {
@ -103,10 +107,9 @@ impl Kind {
matches!(self, Self::InsertionPoint(t) if t.kind.is_expected_argument())
}
/// Match any kind that can be a function parameter. This includes `This`, `Argument` and
/// expected argument.
pub fn is_function_parameter(&self) -> bool {
self.is_this() || self.is_argument() || self.is_expected_argument()
/// Match the argument in a prefix method application.
pub fn is_prefix_argument(&self) -> bool {
matches!(self, Self::Argument(a) if a.in_prefix_chain)
}
/// If this kind is an expected argument, return its argument index.
@ -125,24 +128,6 @@ impl Kind {
// === API ===
impl Kind {
/// Name getter.
pub fn name(&self) -> Option<&str> {
match self {
Self::Argument(t) => t.name.as_deref(),
Self::InsertionPoint(t) => t.name.as_deref(),
_ => None,
}
}
/// Type getter.
pub fn tp(&self) -> Option<&String> {
match self {
Self::Argument(t) => t.tp.as_ref(),
Self::InsertionPoint(t) => t.tp.as_ref(),
_ => None,
}
}
/// Removable flag getter.
pub fn removable(&self) -> bool {
match self {
@ -153,30 +138,30 @@ impl Kind {
/// `ArgumentInfo` getter. Returns `None` if the node could not be attached with the
/// information.
pub fn argument_info(&self) -> Option<ArgumentInfo> {
Some(match self {
Self::Argument(t) =>
ArgumentInfo::new(t.name.clone(), t.tp.clone(), t.call_id, t.tag_values.clone()),
Self::InsertionPoint(t) =>
ArgumentInfo::new(t.name.clone(), t.tp.clone(), t.call_id, t.tag_values.clone()),
_ => return None,
})
pub fn argument_info(&self) -> Option<&ArgumentInfo> {
match self {
Self::Argument(t) => Some(&t.info),
Self::InsertionPoint(t) => Some(&t.info),
Self::ChainedInfix(info) => Some(info),
_ => None,
}
}
/// Get a reference to the name of an argument represented by this node, if available. Returns
/// `None` if the node could not be attached with the argument information.
pub fn argument_name(&self) -> Option<&str> {
match self {
Self::Chained(t) => t.name.as_deref(),
Self::Argument(t) => t.name.as_deref(),
Self::InsertionPoint(t) => t.name.as_deref(),
_ => None,
}
self.argument_info().and_then(|info| info.name.as_deref())
}
/// Type getter.
pub fn tp(&self) -> Option<&String> {
self.argument_info().and_then(|info| info.tp.as_ref())
}
/// Get the definition index of an argument represented by this node, if available. Returns
/// `None` if the node could not be attached with the argument information.
pub fn definition_index(&self) -> Option<usize> {
#[allow(clippy::single_match)]
match self {
Self::Argument(t) => t.definition_index,
_ => None,
@ -186,78 +171,38 @@ impl Kind {
/// Get a reference to tag values of an argument represented by this node, if available. Returns
/// `None` if the node could not be attached with the argument information.
pub fn tag_values(&self) -> Option<&[TagValue]> {
match self {
Self::Argument(t) => Some(&t.tag_values),
Self::InsertionPoint(t) => Some(&t.tag_values),
_ => None,
}
self.argument_info().map(|info| &info.tag_values[..])
}
/// Get the function call AST ID associated with this argument.
pub fn call_id(&self) -> Option<ast::Id> {
self.argument_info().and_then(|info| info.call_id)
}
/// `ArgumentInfo` setter.
pub fn set_argument_info(&mut self, argument_info: ArgumentInfo) {
match self {
Self::Chained(t) => t.call_id,
Self::Argument(t) => t.call_id,
Self::InsertionPoint(t) => t.call_id,
_ => None,
Self::Argument(t) => t.info = argument_info,
Self::InsertionPoint(t) => t.info = argument_info,
Self::ChainedInfix(info) => *info = argument_info,
_ => (),
}
}
/// `ArgumentInfo` setter. Returns bool indicating whether the operation was possible
/// or was skipped.
pub fn set_argument_info(&mut self, argument_info: ArgumentInfo) -> bool {
match self {
Self::Chained(t) => {
t.name = argument_info.name;
t.call_id = argument_info.call_id;
true
}
Self::Argument(t) => {
t.name = argument_info.name;
t.tp = argument_info.tp;
t.call_id = argument_info.call_id;
t.tag_values = argument_info.tag_values;
true
}
Self::InsertionPoint(t) => {
t.name = argument_info.name;
t.tp = argument_info.tp;
t.call_id = argument_info.call_id;
t.tag_values = argument_info.tag_values;
true
}
_ => false,
/// Argument definition index setter.
pub fn set_definition_index(&mut self, index: usize) {
if let Self::Argument(t) = self {
t.definition_index = Some(index);
}
}
/// Argument definition index setter. Returns bool indicating whether the operation was
/// possible.
pub fn set_definition_index(&mut self, index: usize) -> bool {
/// Call ID setter..
pub fn set_call_id(&mut self, call_id: Option<ast::Id>) {
match self {
Self::Argument(t) => {
t.definition_index = Some(index);
true
}
_ => false,
}
}
/// Call ID setter. Returns bool indicating whether the operation was possible.
pub fn set_call_id(&mut self, call_id: Option<ast::Id>) -> bool {
match self {
Self::Chained(t) => {
t.call_id = call_id;
true
}
Self::Argument(t) => {
t.call_id = call_id;
true
}
Self::InsertionPoint(t) => {
t.call_id = call_id;
true
}
_ => false,
Self::Argument(t) => t.info.call_id = call_id,
Self::InsertionPoint(t) => t.info.call_id = call_id,
Self::ChainedInfix(info) => info.call_id = call_id,
_ => (),
}
}
@ -265,8 +210,10 @@ impl Kind {
pub fn variant_name(&self) -> &str {
match self {
Self::Root => "Root",
Self::Chained(_) => "Chained",
Self::ChainedPrefix => "ChainedPrefix",
Self::ChainedInfix(_) => "ChainedInfix",
Self::Operation => "Operation",
Self::Access => "Access",
Self::Argument(_) => "Argument",
Self::NamedArgument => "NamedArgument",
Self::Token => "Token",
@ -286,46 +233,6 @@ impl Default for Kind {
}
// ===============
// === Chained ===
// ===============
/// A node chained with parent node, potentially being a first argument of a nested infix call
/// expression.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[allow(missing_docs)]
pub struct Chained {
pub name: Option<String>,
/// The AST ID of function application that this Chained is a target of. If this is a part of
/// an infix operator chain (e.g. `1 + 2 + 3`) and this chained represents `1 + 2`
/// subexpression, it is effectively a target (`self`) argument of the second `+` operator.
/// In that case the `call_id` will point at its parent `1 + 2 + 3` expression.
pub call_id: Option<ast::Id>,
}
// === Setters ===
impl Chained {
/// Create chained in `self` position of parent expression.
pub fn this() -> Self {
Self { name: Some(Argument::THIS.into()), ..default() }
}
/// Set Chained `call_id` field. See [`Chained::call_id`] for more information.
pub fn with_call_id(mut self, call_id: Option<ast::Id>) -> Self {
self.call_id = call_id;
self
}
}
impl From<Chained> for Kind {
fn from(t: Chained) -> Self {
Self::Chained(t)
}
}
// ================
// === Argument ===
@ -337,12 +244,10 @@ impl From<Chained> for Kind {
#[allow(missing_docs)]
pub struct Argument {
pub removable: bool,
pub in_prefix_chain: bool,
/// The index of the argument in the function definition.
pub definition_index: Option<usize>,
pub name: Option<String>,
pub tp: Option<String>,
pub call_id: Option<ast::Id>,
pub tag_values: Vec<TagValue>,
pub info: ArgumentInfo,
}
@ -353,27 +258,31 @@ impl Argument {
pub const THIS: &'static str = "self";
pub fn typed(mut self, tp: String) -> Self {
self.tp = Some(tp);
self.info.tp = Some(tp);
self
}
pub fn removable(mut self) -> Self {
self.removable = true;
self
}
pub fn in_prefix_chain(mut self) -> Self {
self.in_prefix_chain = true;
self
}
pub fn with_removable(mut self, rm: bool) -> Self {
self.removable = rm;
self
}
pub fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self.info.name = name;
self
}
pub fn with_tp(mut self, tp: Option<String>) -> Self {
self.tp = tp;
self.info.tp = tp;
self
}
pub fn with_call_id(mut self, call_id: Option<ast::Id>) -> Self {
self.call_id = call_id;
self.info.call_id = call_id;
self
}
pub fn indexed(mut self, index: usize) -> Self {
@ -401,11 +310,8 @@ impl From<Argument> for Kind {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[allow(missing_docs)]
pub struct InsertionPoint {
pub kind: InsertionPointType,
pub name: Option<String>,
pub tp: Option<String>,
pub call_id: Option<ast::Id>,
pub tag_values: Vec<TagValue>,
pub kind: InsertionPointType,
pub info: ArgumentInfo,
}
// === Constructors ===
@ -436,7 +342,7 @@ impl InsertionPoint {
}
pub fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self.info.name = name;
self
}
}
@ -460,7 +366,6 @@ impl From<InsertionPoint> for Kind {
pub enum InsertionPointType {
BeforeArgument(usize),
Append,
// FIXME: When this insert type can be assigned to node without name?
/// AST should be inserted as an argument at given index into the chain.
/// Note that this is just argument index in the application, it may be not the same as the
/// index of the function parameter, as `this` argument might be passed using the `this.func`

View File

@ -6,10 +6,10 @@ use crate::prelude::*;
use crate::model::execution_context::VisualizationUpdateData;
use super::response;
use enso_suggestion_database::entry::argument_tag_values;
use enso_suggestion_database::SuggestionDatabase;
use ide_view::graph_editor::component::node::input::widget;
use ide_view::graph_editor::component::node::input::widget::single_choice::ChoiceArgConfig;
use ide_view::graph_editor::ArgumentWidgetConfig;
@ -87,7 +87,7 @@ fn to_kind(
item_default: ImString::from(item_default).into(),
}
.into(),
_ => widget::label::Config::default().into(),
_ => widget::label::Config.into(),
}
}
@ -95,7 +95,7 @@ fn to_choices_and_arguments(
choices: Vec<response::Choice>,
db: &SuggestionDatabase,
parser: &parser::Parser,
) -> (Vec<widget::Choice>, Vec<ChoiceArgConfig>) {
) -> (Vec<widget::Choice>, Vec<widget::single_choice::ChoiceArgConfig>) {
let mut args = Vec::new();
let expressions = choices.iter().map(|c| c.value.as_ref());
@ -116,7 +116,7 @@ fn to_widget_choice(
db: &SuggestionDatabase,
parser: &parser::Parser,
tag: span_tree::TagValue,
arguments: &mut Vec<ChoiceArgConfig>,
arguments: &mut Vec<widget::single_choice::ChoiceArgConfig>,
) -> widget::Choice {
let value: ImString = tag.expression.into();
let label = choice.label.map_or_else(|| value.clone(), |label| label.into());
@ -127,7 +127,11 @@ fn to_widget_choice(
Ok(None) => {}
Ok(Some(config)) => {
let configuration = to_configuration(config, db, parser);
let arg = ChoiceArgConfig { choice_index, name: arg.name.into(), configuration };
let arg = widget::single_choice::ChoiceArgConfig {
choice_index,
name: arg.name.into(),
configuration,
};
arguments.push(arg);
}
Err(err) => {

View File

@ -7,11 +7,11 @@ use crate::prelude::*;
use crate::controller::graph::RequiredImport;
use crate::controller::searcher::Filter;
use crate::model::execution_context::GroupQualifiedName;
use crate::model::suggestion_database;
use enso_doc_parser::DocSection;
use enso_doc_parser::Tag;
use enso_suggestion_database::entry;
use enso_suggestion_database::Entry;
use ensogl::data::color;
use ordered_float::OrderedFloat;
use std::cmp;
@ -126,13 +126,13 @@ impl Eq for MatchInfo {}
#[derive(Clone, Debug, PartialEq)]
pub enum Suggestion {
/// A component from the [`suggestion_database`]. When this component is picked in the
/// Component Browser, the code returned by [`suggestion_database::Entry::code_to_insert`] will
/// Component Browser, the code returned by [`Entry::code_to_insert`] will
/// be inserted into the program.
FromDatabase {
/// The ID of the component in the [`suggestion_database`].
id: suggestion_database::entry::Id,
id: entry::Id,
/// The component's entry in the [`suggestion_database`].
entry: Rc<suggestion_database::Entry>,
entry: Rc<Entry>,
},
/// A virtual component containing a hardcoded snippet of code. When this component is picked
/// in the Component Browser, the [`Snippet::code`] will be inserted into the program.
@ -174,6 +174,8 @@ pub struct Component {
/// A group id, being an index of `group` field of [`List`] structure.
pub group_id: Option<usize>,
pub match_info: MatchInfo,
/// The component string representation that will be used during matching.
pub label: ImString,
}
impl Component {
@ -181,21 +183,29 @@ impl Component {
///
/// The matching info will be filled for an empty pattern.
pub fn new_from_database_entry(
id: suggestion_database::entry::Id,
entry: Rc<suggestion_database::Entry>,
id: entry::Id,
entry: Rc<Entry>,
group_id: Option<usize>,
) -> Self {
let label = match entry.kind {
entry::Kind::Module
if entry.defined_in.is_main_module() || entry.defined_in.is_top_element() =>
format!("{}", entry.defined_in).into(),
_ => match entry.self_type.as_ref() {
Some(self_type) => format!("{}.{}", self_type.alias_name(), entry.name).into(),
None => entry.name.to_im_string(),
},
};
let data = Suggestion::FromDatabase { id, entry };
Self { suggestion: data, group_id, match_info: default() }
Self { suggestion: data, label, group_id, match_info: default() }
}
/// The label which should be displayed in the Component Browser.
pub fn label(&self) -> String {
/// The formatted label and alias which should be displayed in the Component Browser.
pub fn label_with_matched_alias(&self) -> ImString {
match &self.match_info {
MatchInfo::Matches { kind: MatchKind::Alias(alias), .. } => {
format!("{alias} ({self})")
}
_ => self.to_string(),
MatchInfo::Matches { kind: MatchKind::Alias(alias), .. } =>
format!("{alias} ({label})", label = self.label).into(),
_ => self.label.clone(),
}
}
@ -204,9 +214,9 @@ impl Component {
self.suggestion.name()
}
/// The [ID](suggestion_database::entry::Id) of the component in the [`suggestion_database`], or
/// The [ID](entry::Id) of the component in the [`suggestion_database`], or
/// `None` if not applicable.
pub fn id(&self) -> Option<suggestion_database::entry::Id> {
pub fn id(&self) -> Option<entry::Id> {
match &self.suggestion {
Suggestion::FromDatabase { id, .. } => Some(*id),
Suggestion::Virtual { .. } => None,
@ -223,7 +233,7 @@ impl Component {
/// Currently, only modules can be entered, and then the Browser should display content and
/// submodules of the entered module.
pub fn can_be_entered(&self) -> bool {
use suggestion_database::entry::Kind as EntryKind;
use entry::Kind as EntryKind;
matches!(&self.suggestion, Suggestion::FromDatabase { entry, .. } if entry.kind == EntryKind::Module)
}
@ -232,8 +242,8 @@ impl Component {
/// It should be called each time the filtering pattern changes.
pub fn update_matching_info(&mut self, filter: Filter) {
// Match the input pattern to the component label.
let label = self.to_string();
let label_matches = fuzzly::matches(&label, filter.pattern.as_str());
let label = self.label.as_str();
let label_matches = fuzzly::matches(label, filter.pattern.as_str());
let label_subsequence = label_matches.and_option_from(|| {
let metric = fuzzly::metric::default();
fuzzly::find_best_subsequence(label, filter.pattern.as_str(), metric)
@ -320,24 +330,6 @@ impl Component {
}
}
impl Display for Component {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use suggestion_database::entry::Kind;
match &self.suggestion {
Suggestion::FromDatabase { entry, .. } => match entry.kind {
Kind::Module
if entry.defined_in.is_main_module() || entry.defined_in.is_top_element() =>
write!(f, "{}", entry.defined_in),
_ => match entry.self_type.as_ref() {
Some(self_type) => write!(f, "{}.{}", self_type.alias_name(), entry.name),
None => write!(f, "{}", entry.name),
},
},
Suggestion::Virtual { snippet } => write!(f, "{}", snippet.name),
}
}
}
// ============
@ -422,7 +414,11 @@ pub(crate) mod tests {
use enso_suggestion_database::mock_suggestion_database;
pub fn check_displayed_components(list: &List, expected: Vec<&str>) {
let components = list.displayed().iter().map(|component| component.label()).collect_vec();
let components = list
.displayed()
.iter()
.map(|component| component.label_with_matched_alias())
.collect_vec();
assert_eq!(components, expected);
}

View File

@ -302,6 +302,7 @@ impl<'a> Builder<'a> {
});
for snippet in snippets {
let component = Component {
label: snippet.name.clone(),
suggestion: component::Suggestion::Virtual { snippet },
group_id: Some(group_index),
match_info: Default::default(),
@ -395,8 +396,12 @@ mod tests {
fn check_filterable_components(list: &component::List, mut expected: Vec<&str>) {
expected.sort();
let components =
list.components.iter().map(|component| component.label()).sorted().collect_vec();
let components = list
.components
.iter()
.map(|component| component.label_with_matched_alias())
.sorted()
.collect_vec();
assert_eq!(components, expected);
}

View File

@ -135,7 +135,7 @@ impl ast::TokenConsumer for AstAtPositionFinder {
.push(AstWithRange { ast: ast.clone_ref(), range: (start..end).into() });
}
}
other => self.current_offset += other.len(),
other => self.current_offset += other.repr_len(),
}
}
}

View File

@ -74,7 +74,7 @@ macro_rules! kind_to_icon {
fn component_to_entry_model(component: &component::Component) -> component_grid::EntryModel {
let can_be_entered = component.can_be_entered();
let match_info = &component.match_info;
let caption = component.label();
let caption = component.label_with_matched_alias();
let highlighted = bytes_of_matched_letters(match_info, &caption);
let icon = match &component.suggestion {
component::Suggestion::FromDatabase { entry, .. } => {
@ -86,7 +86,7 @@ fn component_to_entry_model(component: &component::Component) -> component_grid:
component::Suggestion::Virtual { snippet } => snippet.icon,
};
component_grid::EntryModel {
caption: caption.into(),
caption,
highlighted: Rc::new(highlighted),
icon,
can_be_entered,

View File

@ -17,7 +17,6 @@ enso-shapely = { path = "../../../lib/rust/shapely" }
engine-protocol = { path = "../controller/engine-protocol" }
ensogl = { path = "../../../lib/rust/ensogl" }
ensogl-component = { path = "../../../lib/rust/ensogl/component" }
ensogl-derive-theme = { path = "../../../lib/rust/ensogl/app/theme/derive" }
ensogl-gui-component = { path = "../../../lib/rust/ensogl/component/gui" }
ensogl-text = { path = "../../../lib/rust/ensogl/component/text" }
ensogl-text-msdf = { path = "../../../lib/rust/ensogl/component/text/src/font/msdf" }

View File

@ -13,7 +13,6 @@ ensogl-core = { path = "../../../../../lib/rust/ensogl/core" }
ensogl-gui-component = { path = "../../../../../lib/rust/ensogl/component/gui/" }
ensogl-grid-view = { path = "../../../../../lib/rust/ensogl/component/grid-view/" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-derive-theme = { path = "../../../../../lib/rust/ensogl/app/theme/derive" }
ensogl-scroll-area = { path = "../../../../../lib/rust/ensogl/component/scroll-area" }
ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" }
ensogl-shadow = { path = "../../../../../lib/rust/ensogl/component/shadow" }

View File

@ -7,7 +7,6 @@ edition = "2021"
[dependencies]
enso-frp = { path = "../../../../../../lib/rust/frp" }
ensogl-core = { path = "../../../../../../lib/rust/ensogl/core" }
ensogl-derive-theme = { path = "../../../../../../lib/rust/ensogl/app/theme/derive" }
ensogl-hardcoded-theme = { path = "../../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-text = { path = "../../../../../../lib/rust/ensogl/component/text" }
ensogl-grid-view = { path = "../../../../../../lib/rust/ensogl/component/grid-view" }

View File

@ -9,8 +9,8 @@ use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::scene::Layer;
use ensogl_core::display::style::FromTheme;
use ensogl_core::Animation;
use ensogl_derive_theme::FromTheme;
use ensogl_grid_view::entry::Contour;
use ensogl_grid_view::entry::EntryFrp;
use ensogl_grid_view::Col;
@ -162,7 +162,7 @@ impl EntryData {
let display_object = display::object::Instance::new();
let text = app.new_view::<ensogl_text::Text>();
if let Some(layer) = text_layer {
text.add_to_scene_layer(layer);
layer.add(&text);
}
let ellipsis = ellipsis::View::new();
let separator = separator::View::new();

View File

@ -166,7 +166,6 @@ impl Model {
let entries: Entries = default();
let show_ellipsis = Rc::new(Cell::new(false));
frp::new_network! { network
init <- source_();
requested_entry <- grid.model_for_entry_needed.map2(&grid.grid_size,
f!([entries, show_ellipsis]((row, col), grid_size) {
let (_, cols) = grid_size;
@ -178,11 +177,9 @@ impl Model {
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = entry::Style::from_theme(&network, &style_frp);
frp::extend! { network
params <- style.update.map(|s| entry::Params { style: s.clone(), greyed_out_start: None });
params <- style.map(|s| entry::Params { style: s.clone(), greyed_out_start: None });
grid.set_entries_params <+ params;
}
init.emit(());
style.init.emit(());
Self { display_object, grid, entries, network, mask, show_ellipsis }
}

View File

@ -7,7 +7,6 @@ edition = "2021"
[dependencies]
enso-frp = { path = "../../../../../../lib/rust/frp" }
ensogl-core = { path = "../../../../../../lib/rust/ensogl/core" }
ensogl-derive-theme = { path = "../../../../../../lib/rust/ensogl/app/theme/derive" }
ensogl-hardcoded-theme = { path = "../../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-text = { path = "../../../../../../lib/rust/ensogl/component/text" }
ensogl-grid-view = { path = "../../../../../../lib/rust/ensogl/component/grid-view" }

View File

@ -162,7 +162,7 @@ impl Data {
icon.set_size((icon::SIZE, icon::SIZE));
label.set_long_text_truncation_mode(true);
if let Some(layer) = text_layer {
label.add_to_scene_layer(layer);
layer.add(&label);
}
Self { display_object, label, background, icon, style }
}

View File

@ -5,8 +5,10 @@ use crate::prelude::*;
use enso_frp as frp;
use ensogl_core::data::color;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_core::display::shape::ThemeAccess;
use ensogl_core::display::style;
use ensogl_core::display::style::data::DataMatch;
use ensogl_derive_theme::FromTheme;
use ensogl_core::display::style::FromTheme;
use ensogl_hardcoded_theme::application::component_browser::component_list_panel as panel_theme;
use ensogl_hardcoded_theme::application::component_browser::component_list_panel::grid as grid_theme;
use entry_theme::highlight::selection as selection_theme;
@ -43,40 +45,22 @@ impl Color {
Self::Arbitrary(color) => *color,
}
}
}
impl ThemeAccess for Color {
/// A custom accessor for retrieving the color from the stylesheet using the [`FromTheme`]
/// macro. In the stylesheet, the color can be defined as either a `color::Rgba` or a `float`
/// value for the mixing coefficient. This accessor produces the corresponding variants of
/// [`Color`] or returns a default value ([`Color::MainColorWithAlpha(0.0)`]) if there is no
/// such property in the stylesheet.
fn accessor<P: Into<ensogl_core::display::style::Path>>(
network: &frp::Network,
style: &StyleWatchFrp,
path: P,
) -> frp::Sampler<Self> {
let path = path.into();
let value = style.get(path.clone());
frp::extend! { network
init <- source_();
color <- value.all_with(&init, move |data, _| {
data.color().map(|color| {
let color = color::Lcha::from(color);
Color::Arbitrary(color)
}).unwrap_or_else(|| {
let alpha_multiplier = match data.number() {
Some(number) => number,
None => {
error!("Neither color nor alpha defined for {path}.");
0.0
}
};
Color::ComponentGroup { alpha_multiplier }
})
});
sampler <- color.sampler();
}
init.emit(());
sampler
fn from_style_data(path_str: &str, data: &Option<style::Data>) -> Self {
data.color()
.map(|color| Color::Arbitrary(color::Lcha::from(color)))
.or_else(move || data.number().map(|a| Color::ComponentGroup { alpha_multiplier: a }))
.unwrap_or_else(|| {
warn!("Neither color nor alpha defined for {path_str}.");
Color::ComponentGroup { alpha_multiplier: 0.0 }
})
}
}
@ -95,15 +79,12 @@ impl Color {
#[derive(Clone, Copy, Debug, Default, PartialEq, FromTheme)]
pub struct Colors {
#[theme_path = "entry_theme::text::color"]
#[accessor = "Color::accessor"]
pub text: Color,
#[theme_path = "entry_theme::background::intensity"]
pub background_intensity: f32,
#[theme_path = "entry_theme::highlight::hover::color"]
#[accessor = "Color::accessor"]
pub hover_highlight: Color,
#[theme_path = "entry_theme::icon::color"]
#[accessor = "Color::accessor"]
pub icon: Color,
}
@ -114,12 +95,10 @@ pub struct Colors {
#[derive(Clone, Copy, Debug, Default, PartialEq, FromTheme)]
pub struct SelectionColors {
#[theme_path = "selection_theme::text::color"]
#[accessor = "Color::accessor"]
pub text: Color,
#[theme_path = "selection_theme::background::intensity"]
pub background_intensity: f32,
#[theme_path = "selection_theme::icon::color"]
#[accessor = "Color::accessor"]
pub icon: Color,
}

View File

@ -42,7 +42,7 @@ use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::scene::Layer;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl_core::display::style::FromTheme;
use ensogl_grid_view as grid_view;
use ensogl_grid_view::Col;
use ensogl_grid_view::Row;
@ -147,6 +147,20 @@ pub type Grid = grid_view::scrollable::SelectableGridView<entry::View>;
// === GroupColors ===
/// The grid's style structure.
#[allow(missing_docs)]
#[derive(Clone, Copy, Default, Debug, PartialEq, FromTheme)]
#[base_path = "theme::group_colors"]
struct GroupColorsTheme {
group_0: color::Lcha,
group_1: color::Lcha,
group_2: color::Lcha,
group_3: color::Lcha,
group_4: color::Lcha,
group_5: color::Lcha,
local_scope_group: color::Lcha,
}
/// Default groups colors.
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub struct GroupColors {
@ -156,6 +170,23 @@ pub struct GroupColors {
outside_group: color::Lcha,
}
impl From<GroupColorsTheme> for GroupColors {
fn from(theme: GroupColorsTheme) -> Self {
Self {
variants: [
theme.group_0,
theme.group_1,
theme.group_2,
theme.group_3,
theme.group_4,
theme.group_5,
],
outside_group: theme.local_scope_group,
}
}
}
// === Style ===
@ -164,11 +195,13 @@ pub struct GroupColors {
#[derive(Clone, Copy, Default, Debug, PartialEq, FromTheme)]
#[base_path = "theme"]
pub struct Style {
pub width: f32,
pub height: f32,
pub padding: f32,
pub column_gap: f32,
pub entry_height: f32,
pub width: f32,
pub height: f32,
pub padding: f32,
pub column_gap: f32,
pub entry_height: f32,
#[theme_path = "panel_theme::corners_radius"]
pub corners_radius: f32,
}
impl Style {
@ -442,7 +475,6 @@ impl component::Frp<Model> for Frp {
let grid_scroll_frp = grid.scroll_frp();
let grid_extra_scroll_frp = grid.extra_scroll_frp();
let grid_selection_frp = grid.selection_highlight_frp();
let corners_radius = style_frp.get_number(panel_theme::corners_radius);
let style = Style::from_theme(network, style_frp);
let entry_style = entry::Style::from_theme(network, style_frp);
let colors = entry::style::Colors::from_theme(network, style_frp);
@ -470,36 +502,24 @@ impl component::Frp<Model> for Frp {
// === Groups colors ===
let group0 = style_frp.get_color(theme::group_colors::group_0);
let group1 = style_frp.get_color(theme::group_colors::group_1);
let group2 = style_frp.get_color(theme::group_colors::group_2);
let group3 = style_frp.get_color(theme::group_colors::group_3);
let group4 = style_frp.get_color(theme::group_colors::group_4);
let group5 = style_frp.get_color(theme::group_colors::group_5);
groups <- all7(&style.init, &group0, &group1, &group2, &group3, &group4, &group5);
let local_scope_group = style_frp.get_color(theme::group_colors::local_scope_group);
group_colors <- all_with(&groups, &local_scope_group, |&((), g0, g1, g2, g3, g4, g5), ls| {
GroupColors {
variants: [g0, g1, g2, g3, g4, g5].map(color::Lcha::from),
outside_group: ls.into(),
}
});
let group_colors_theme = GroupColorsTheme::from_theme(network, style_frp);
group_colors <- group_colors_theme.map(|t| GroupColors::from(*t));
// === Style and Entries Params ===
style_and_content_size <- all(&style.update, &grid.content_size);
style_and_content_size <- all(&style, &grid.content_size);
entries_style <-
all4(&style.update, &entry_style.update, &colors.update, &group_colors);
all4(&style, &entry_style, &colors, &group_colors);
entries_params <- entries_style.map(f!((s) model.entries_params(s)));
selection_entries_style <- all(entries_params, selection_colors.update);
selection_entries_style <- all(entries_params, selection_colors);
selection_entries_params <-
selection_entries_style.map(f!((input) model.selection_entries_params(input)));
grid_scroll_frp.resize <+ style_and_content_size.map(Model::grid_size);
grid_position <- style_and_content_size.map(Model::grid_position);
eval grid_position ((pos) model.grid.set_xy(*pos));
grid_scroll_frp.set_corner_radius_bottom_right <+ all(&corners_radius, &style.init)._0();
grid.set_entries_size <+ style.update.map(|s| s.entry_size());
grid_scroll_frp.set_corner_radius_bottom_right <+ style.map(|s| s.corners_radius);
grid.set_entries_size <+ style.map(|s| s.entry_size());
grid.set_entries_params <+ entries_params;
grid_selection_frp.set_entries_params <+ selection_entries_params;
@ -526,7 +546,7 @@ impl component::Frp<Model> for Frp {
// === Scrolling ===
grid_extra_scroll_frp.set_preferred_margins_around_entry <+ style.update.map(
grid_extra_scroll_frp.set_preferred_margins_around_entry <+ style.map(
f!((style) model.navigation_scroll_margins(style))
);
@ -544,10 +564,6 @@ impl component::Frp<Model> for Frp {
}
grid.resize_grid(0, COLUMN_COUNT);
style.init.emit(());
entry_style.init.emit(());
colors.init.emit(());
selection_colors.init.emit(());
}
fn default_shortcuts() -> Vec<Shortcut> {

View File

@ -6,4 +6,5 @@ edition = "2021"
[dependencies]
ensogl-core = { path = "../../../../../../lib/rust/ensogl/core" }
ensogl-hardcoded-theme = { path = "../../../../../../lib/rust/ensogl/app/theme/hardcoded" }
failure = { workspace = true }

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ mod prelude {
}
pub mod common_part;
pub mod component_icons;
mod define_macro;

View File

@ -56,7 +56,7 @@ use ensogl_core::define_endpoints_2;
use ensogl_core::display;
use ensogl_core::display::object::ObjectOps;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl_core::display::style::FromTheme;
use ensogl_grid_view as grid_view;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::application::component_browser::component_list_panel as theme;
@ -346,7 +346,7 @@ impl component::Frp<Model> for Frp {
let panel_style = Style::from_theme(network, style);
let grid_style = grid::Style::from_theme(network, style);
let navigator_style = navigator::Style::from_theme(network, style);
style <- all_with3(&panel_style.update, &grid_style.update, &navigator_style.update, |&panel, &grid, &navigator| AllStyles {panel, grid, navigator});
style <- all_with3(&panel_style, &grid_style, &navigator_style, |&panel, &grid, &navigator| AllStyles {panel, grid, navigator});
eval style ((style) model.update_style(style));
output.size <+ style.map(|style| style.size());
@ -366,9 +366,6 @@ impl component::Frp<Model> for Frp {
eval_ input.hide (model.blur());
}
panel_style.init.emit(());
grid_style.init.emit(());
navigator_style.init.emit(());
}
}

View File

@ -5,12 +5,8 @@
use ensogl_core::prelude::*;
use enso_frp as frp;
use ensogl_core::data::color;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl_hardcoded_theme::application::component_browser::component_list_panel as list_panel_theme;
use list_panel_theme::navigator as theme;
use ensogl_hardcoded_theme::application::component_browser::component_list_panel::navigator as theme;

View File

@ -15,7 +15,6 @@ enso-profiler = { path = "../../../../lib/rust/profiler" }
ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-component = { path = "../../../../lib/rust/ensogl/component" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-derive-theme = { path = "../../../../lib/rust/ensogl/app/theme/derive" }
ide-view-graph-editor = { path = "../graph-editor" }
wasm-bindgen = { workspace = true }
serde_json = { workspace = true }

View File

@ -36,7 +36,6 @@ use ensogl::display::DomSymbol;
use ensogl::system::web;
use ensogl::Animation;
use ensogl_component::shadow;
use ensogl_derive_theme::FromTheme;
use ensogl_hardcoded_theme::application::component_browser::documentation as theme;
use graph_editor::component::visualization;
use ide_view_graph_editor as graph_editor;
@ -301,8 +300,6 @@ impl View {
let style_frp = StyleWatchFrp::new(&scene.style_sheet);
let style = Style::from_theme(network, &style_frp);
frp::extend! { network
init <- source_();
// === Displaying documentation ===
docs <- any(...);
@ -318,25 +315,25 @@ impl View {
// === Hovered item preview caption ===
spring_muliplier <- style.update.map(|s| s.caption_animation_spring_multiplier);
spring_muliplier <- style.map(|s| s.caption_animation_spring_multiplier);
caption_anim.set_spring <+ spring_muliplier.map(|m| Spring::default() * m);
show_caption <- frp.show_hovered_item_preview_caption.on_true();
hide_caption <- frp.show_hovered_item_preview_caption.on_false();
caption_anim.target <+ show_caption.constant(1.0);
caption_anim.target <+ hide_caption.constant(0.0);
_eval <- all_with(&caption_anim.value, &style.update, f!((value, style) {
_eval <- all_with(&caption_anim.value, &style, f!((value, style) {
model.set_caption_height(value * style.caption_height, style)
}));
// === Size ===
size <- style.update.map(|s| Vector2(s.width, s.height));
size <- style.map(|s| Vector2(s.width, s.height));
eval size((size) model.set_size(*size));
// === Style ===
eval style.update((style) model.update_style(*style));
eval style((style) model.update_style(*style));
// === Activation ===
@ -368,8 +365,6 @@ impl View {
frp.source.is_hovered <+ model.overlay.events_deprecated.mouse_over.constant(true);
frp.source.is_hovered <+ model.overlay.events_deprecated.mouse_out.constant(false);
}
init.emit(());
style.init.emit(());
self
}
}

View File

@ -31,6 +31,7 @@ use ensogl::frp;
use ensogl::system::web;
use ide_view_graph_editor::component::edge::Edge;
use ide_view_graph_editor::component::node;
use ide_view_graph_editor::GraphLayers;
@ -63,6 +64,8 @@ pub fn main() {
let greenish = color::Lcha(0.5, 0.5, 0.5, 1.0);
let lowest_layer = &scene.layers.viz;
let layers = GraphLayers::new(&scene.layers);
let source = Rectangle::new();
let source_center = SOURCE_CENTER;
let source_size = Vector2(ARBITRARY_NODE_WIDTH, node::HEIGHT);
@ -87,7 +90,7 @@ pub fn main() {
world.add_child(&target);
let target_moved = make_draggable(scene, &target);
let edge = Edge::new(&app);
let edge = Edge::new(&app, &layers);
edge.set_disabled(false);
edge.set_color(greenish);
edge.source_size(source_size);

View File

@ -11,7 +11,6 @@ crate-type = ["cdylib", "rlib"]
enso-frp = { path = "../../../../lib/rust/frp" }
enso-prelude = { path = "../../../../lib/rust/prelude" }
ensogl = { path = "../../../../lib/rust/ensogl" }
ensogl-derive-theme = { path = "../../../../lib/rust/ensogl/app/theme/derive" }
ensogl-drop-down-menu = { path = "../../../../lib/rust/ensogl/component/drop-down-menu" }
ensogl-gui-component = { path = "../../../../lib/rust/ensogl/component/gui" }
ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcoded" }

View File

@ -31,7 +31,7 @@ use ensogl::data::color::Rgba;
use ensogl::data::text;
use ensogl::display;
use ensogl::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl::display::style::FromTheme;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::graph_editor::execution_environment_selector as theme;
@ -219,13 +219,12 @@ impl component::Frp<Model> for Frp {
let output = &frp.output;
let style = Style::from_theme(network, style_watch);
let style_update = style.update;
frp::extend! { network
// == Layout ==
eval style_update((style) {
eval style((style) {
model.update_dropdown_style(style);
model.update_background_style(style);
model.update_play_button_style(style);
@ -255,11 +254,10 @@ impl component::Frp<Model> for Frp {
// == Outputs ==
output.play_press <+ play_button.pressed;
output.size <+ style_update.map(|style| {
Vector2::new(style.overall_width(),style.height)
output.size <+ style.map(|style| {
Vector2::new(style.overall_width(), style.height)
}).on_change();
}
style.init.emit(());
}
}

View File

@ -6,7 +6,7 @@ use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl::display::style::FromTheme;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::graph_editor::execution_environment_selector::play_button as theme;
@ -157,7 +157,7 @@ impl component::Frp<Model> for Frp {
let style = Style::from_theme(network, style_watch);
frp::extend! { network
eval style.update ((style) model.update_style(style));
eval style ((style) model.update_style(style));
eval_ input.reset (model.set_playing(false));
@ -166,7 +166,6 @@ impl component::Frp<Model> for Frp {
eval_ output.pressed (model.set_playing(true));
}
style.init.emit(());
}
}

View File

@ -6,7 +6,7 @@ use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl::display::shape::StyleWatchFrp;
use ensogl_derive_theme::FromTheme;
use ensogl::display::style::FromTheme;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::graph_editor::execution_mode_selector::play_button as theme;
@ -157,7 +157,7 @@ impl component::Frp<Model> for Frp {
let style = Style::from_theme(network, style_watch);
frp::extend! { network
eval style.update ((style) model.update_style(style));
eval style ((style) model.update_style(style));
eval_ input.reset (model.set_playing(false));
@ -166,7 +166,6 @@ impl component::Frp<Model> for Frp {
eval_ output.pressed (model.set_playing(true));
}
style.init.emit(());
}
}

View File

@ -27,6 +27,7 @@ ensogl-hardcoded-theme = { path = "../../../../lib/rust/ensogl/app/theme/hardcod
ensogl-text-msdf = { path = "../../../../lib/rust/ensogl/component/text/src/font/msdf" }
failure = { workspace = true }
ide-view-execution-environment-selector = { path = "../execution-environment-selector" }
ide-view-component-list-panel-icons = { path = "../component-browser/component-list-panel/icons" }
indexmap = "1.9.2"
js-sys = { workspace = true }
nalgebra = { workspace = true }

View File

@ -90,19 +90,19 @@ pub struct GridWindow {
#[derive(Debug, display::Object)]
#[allow(missing_docs)]
pub struct Model<T> {
app: Application,
size: Rc<Cell<Vector2>>,
display_object: display::object::Instance,
app: Application,
size: Rc<Cell<Vector2>>,
display_object: display::object::Instance,
// Note that we are using a simple `GridView` and our own scrollbar, instead of the
// `scrollable::GridView` to avoid adding `ScrollAreas` to the scene, as the clipping they
// provide though the `mask::View` is not free in terms of performance (they add a draw call
// cost) and we don't need it here because we need to clip DOM elements anyway.
text_grid: GridView<grid_view_entry::Entry>,
dom_entry_root: web::HtmlDivElement,
clipping_div: DomSymbol,
scroll_bar_horizontal: Scrollbar,
scroll_bar_vertical: Scrollbar,
text_provider: Rc<RefCell<Option<T>>>,
text_grid: GridView<grid_view_entry::Entry>,
dom_entry_root: web::HtmlDivElement,
clipping_div: DomSymbol,
h_scrollbar: Scrollbar,
v_scrollbar: Scrollbar,
text_provider: Rc<RefCell<Option<T>>>,
}
impl<T: 'static> Model<T> {
@ -118,8 +118,8 @@ impl<T: 'static> Model<T> {
let text_grid: GridView<grid_view_entry::Entry> = GridView::new(&app);
display_object.add_child(&text_grid);
let scroll_bar_horizontal = Scrollbar::new(&app);
let scroll_bar_vertical = Scrollbar::new(&app);
let h_scrollbar = Scrollbar::new(&app);
let v_scrollbar = Scrollbar::new(&app);
Model {
app,
@ -127,8 +127,8 @@ impl<T: 'static> Model<T> {
clipping_div,
size,
dom_entry_root,
scroll_bar_horizontal,
scroll_bar_vertical,
h_scrollbar,
v_scrollbar,
display_object,
text_provider,
}
@ -159,19 +159,18 @@ impl<T: 'static> Model<T> {
}
fn init_scrollbars(&self) {
self.display_object.add_child(&self.scroll_bar_horizontal);
self.display_object.add_child(&self.scroll_bar_vertical);
self.scroll_bar_vertical.set_rotation_z(-90.0_f32.to_radians());
self.display_object.add_child(&self.h_scrollbar);
self.display_object.add_child(&self.v_scrollbar);
self.v_scrollbar.set_rotation_z(-90.0_f32.to_radians());
}
fn set_size(&self, size: Vector2) {
let scrollbar_width = scrollbar::WIDTH - scrollbar::PADDING;
let h_y = -size.y / 2.0 + scrollbar_width / 2.0;
self.scroll_bar_horizontal.set_y(h_y);
self.scroll_bar_horizontal.set_length(size.x);
let v_x = size.x / 2.0 - scrollbar_width / 2.0;
self.scroll_bar_vertical.set_x(v_x);
self.scroll_bar_vertical.set_length(size.y);
self.v_scrollbar.set_xy((size.x / 2.0 - scrollbar::WIDTH, size.y / 2.0));
self.h_scrollbar.set_xy((-size.x / 2.0, -size.y / 2.0));
self.v_scrollbar.set_length(size.y);
self.h_scrollbar.set_length(size.x);
self.v_scrollbar.set_thumb_size(size.y - 2.0 * PADDING_TEXT);
self.h_scrollbar.set_thumb_size(size.x - 2.0 * PADDING_TEXT);
let text_padding = Vector2::new(PADDING_TEXT, PADDING_TEXT);
self.clipping_div.set_dom_size(size - 2.0 * text_padding);
self.size.set(size);
@ -312,8 +311,8 @@ impl<T: TextProvider + 'static> TextGrid<T> {
) {
let network = &self.network;
let text_grid = &self.text_grid;
let scrollbar_h = &self.model.scroll_bar_horizontal;
let scrollbar_v = &self.model.scroll_bar_vertical;
let h_scrollbar = &self.model.h_scrollbar;
let v_scrollbar = &self.model.v_scrollbar;
let dom_entry_root = &self.model.dom_entry_root;
let on_data_update = on_data_update.clone_ref();
let frp = &self.frp;
@ -321,7 +320,7 @@ impl<T: TextProvider + 'static> TextGrid<T> {
frp::extend! { network
scroll_position <- all(&scrollbar_h.thumb_position, &scrollbar_v.thumb_position);
scroll_position <- all(&h_scrollbar.thumb_position, &v_scrollbar.thumb_position);
longest_line_with_init <- all(&init, &text_provider.longest_line)._1();
lines_with_init <- all(&init, &text_provider.line_count)._1();
@ -346,31 +345,28 @@ impl<T: TextProvider + 'static> TextGrid<T> {
text_grid_content_size_y <- text_grid.content_size.map(|size| size.y).on_change();
text_grid_content_size_y_previous <- text_grid_content_size_y.previous();
scrollbar_h.set_max <+ text_grid_content_size_x;
scrollbar_v.set_max <+ text_grid_content_size_y;
scrollbar_h.set_thumb_size <+ frp.set_size.map(|size| size.x - 2.0 * PADDING_TEXT);
scrollbar_v.set_thumb_size <+ frp.set_size.map(|size| size.y - 2.0 * PADDING_TEXT);
h_scrollbar.set_max <+ text_grid_content_size_x;
v_scrollbar.set_max <+ text_grid_content_size_y;
horizontal_scrollbar_change_args <- all(
text_grid_content_size_x,
text_grid_content_size_x_previous,
scrollbar_h.thumb_position
h_scrollbar.thumb_position
);
on_content_size_x_change <- horizontal_scrollbar_change_args
.sample(&text_grid_content_size_x);
scrollbar_h.jump_to <+ on_content_size_x_change.map(
h_scrollbar.jump_to <+ on_content_size_x_change.map(
|(content_size_x, content_size_x_previous, thumb_position)| {
thumb_position * content_size_x_previous / content_size_x
});
vertical_scrollbar_change_args <- all(text_grid_content_size_y,
text_grid_content_size_y_previous,
scrollbar_v.thumb_position
v_scrollbar.thumb_position
);
on_content_size_y_change <- vertical_scrollbar_change_args
.sample(&text_grid_content_size_y);
scrollbar_v.jump_to <+ on_content_size_y_change.map(
v_scrollbar.jump_to <+ on_content_size_y_change.map(
|(content_size_y, content_size_y_previous, thumb_position)| {
thumb_position * content_size_y_previous / content_size_y
});
@ -459,7 +455,7 @@ impl FontLoadedNotifier {
*callback.borrow_mut() = None;
}));
callback.set(_closure);
callback.replace(Some(_closure));
let _promise = match web::document.fonts().ready() {
Ok(promise) => callback.borrow().as_ref().map(|closure| promise.then(closure)),

View File

@ -15,6 +15,7 @@ use crate::component::visualization::instance::PreprocessorConfiguration;
use super::GridPosition;
use super::GridSize;
use super::GridWindow;
use enso_prelude::serde_reexports::Deserialize;

View File

@ -4,6 +4,8 @@ use crate::prelude::*;
use ensogl::display::shape::*;
use ensogl::display::traits::*;
use crate::GraphLayers;
use enso_frp as frp;
use enso_frp;
use ensogl::application::Application;
@ -38,6 +40,8 @@ define_endpoints_2! {
Input {
/// The width and height of the source node in pixels.
source_size(Vector2),
/// The width and height of the target port in pixels.
target_size(Vector2),
/// The location of the center of the target node's input port.
target_position(Vector2),
/// Whether the target end of the edge is attached to a node (If `false`, it is being
@ -78,9 +82,9 @@ pub struct Edge {
impl Edge {
/// Constructor.
#[profile(Detail)]
pub fn new(app: &Application) -> Self {
pub fn new(app: &Application, layers: &GraphLayers) -> Self {
let frp = Frp::new();
let model = Rc::new(EdgeModel::new(&app.display.default_scene));
let model = Rc::new(EdgeModel::new(&app.display.default_scene, layers));
let network = &frp.network;
let display_object = &model.display_object;
let output = &frp.private.output;
@ -96,6 +100,7 @@ impl Edge {
eval frp.source_attached ((t) model.inputs.set_source_attached(*t));
eval frp.target_attached ((t) model.inputs.set_target_attached(*t));
eval frp.source_size ((t) model.inputs.set_source_size(*t));
eval frp.target_size ((t) model.inputs.set_target_size(*t));
eval frp.set_disabled ((t) model.inputs.set_disabled(*t));
// Mouse events.
@ -159,8 +164,10 @@ impl Edge {
struct EdgeModel {
/// The parent display object of all the edge's parts.
display_object: display::object::Instance,
/// The [`Scene`], needed for coordinate conversions and special layer assignments.
/// The [`Scene`], needed for coordinate conversions.
scene: Scene,
/// The [`GraphLayers`], used for special layer assignments.
layers: GraphLayers,
/// The raw inputs the state is computed from.
inputs: Inputs,
/// The state, as of the last redraw.
@ -172,11 +179,15 @@ struct EdgeModel {
impl EdgeModel {
/// Constructor.
#[profile(Debug)]
pub fn new(scene: &Scene) -> Self {
let display_object = display::object::Instance::new_named("Edge");
let scene = scene.clone_ref();
scene.layers.main_edges_level.add(&display_object);
Self { display_object, scene, inputs: default(), state: default(), shapes: default() }
pub fn new(scene: &Scene, layers: &GraphLayers) -> Self {
Self {
display_object: display::object::Instance::new_named("Edge"),
scene: scene.clone_ref(),
layers: layers.clone_ref(),
inputs: default(),
state: default(),
shapes: default(),
}
}
/// Redraws the connection.
@ -184,7 +195,7 @@ impl EdgeModel {
pub fn redraw(&self) {
let state = self.calculate_state();
self.apply_state(&state);
self.state.set(state);
self.state.replace(Some(state));
}
fn calculate_state(&self) -> State {
@ -194,7 +205,15 @@ impl EdgeModel {
let target_offset = self.target_offset();
let target_attached = self.inputs.target_attached.get();
let source_attached = self.inputs.source_attached.get();
let layout = layout::layout(self.source_half_width(), target_offset, target_attached);
let source_size = self.inputs.source_size.get();
let target_size = self.inputs.target_size.get();
let layout = layout::layout(
target_offset,
source_size,
target_size,
source_attached,
target_attached,
);
let is_attached = target_attached && source_attached;
let focus_split = is_attached
.then(|| {
@ -205,14 +224,14 @@ impl EdgeModel {
// `mouse::Out` event.
self.inputs.hover_position.get().and_then(|position| {
let position = self.scene_pos_to_parent_pos(position);
let source_height = self.inputs.source_size.get().y();
let source_height = source_size.y();
layout::find_position(position, &layout, source_height, render::HOVER_WIDTH)
})
})
.flatten();
let styles = StyleWatch::new(&self.scene.style_sheet);
let normal_color = if self.inputs.disabled.get() {
styles.get_color(theme::code::syntax::disabled)
styles.get_color(theme::graph_editor::edge::disabled_color)
} else {
self.inputs.color.get()
};
@ -243,7 +262,7 @@ impl EdgeModel {
))
.or(any4(layout, colors, focus_split, is_attached).changed(
|(
Layout { corners, arrow, .. },
Layout { corners, arrow, source_size, .. },
Colors { source_color, target_color, .. },
FocusSplit { focus_split, .. },
IsAttached { is_attached, .. },
@ -262,6 +281,7 @@ impl EdgeModel {
focus_split: *focus_split,
is_attached: *is_attached,
});
self.shapes.redraw_cutout(self, *is_attached, *source_size);
},
))
.or(any(layout, colors).changed(
@ -287,10 +307,6 @@ impl EdgeModel {
// === Low-level operations ===
impl EdgeModel {
fn source_half_width(&self) -> f32 {
self.inputs.source_size.get().x() / 2.0
}
fn screen_pos_to_scene_pos(&self, screen_pos: Vector2) -> SceneCoords {
let screen_pos_3d = Vector3(screen_pos.x(), screen_pos.y(), 0.0);
SceneCoords(self.scene.screen_to_scene_coordinates(screen_pos_3d).xy())
@ -320,6 +336,10 @@ impl ShapeParent for EdgeModel {
fn scene(&self) -> &Scene {
&self.scene
}
fn layers(&self) -> &GraphLayers {
&self.layers
}
}

View File

@ -16,6 +16,8 @@ pub(super) struct Inputs {
/// The width and height of the node that originates the edge. The edge may begin anywhere
/// around the bottom half of the node.
pub source_size: Cell<Vector2>,
/// The width and height of the port that the edge is attached to.
pub target_size: Cell<Vector2>,
/// The coordinates of the node input the edge connects to. The edge enters the node from
/// above.
pub target_position: Cell<ParentCoords>,
@ -44,6 +46,10 @@ impl Inputs {
self.source_size.set(size);
}
pub(super) fn set_target_size(&self, size: Vector2) {
self.target_size.set(size);
}
pub(super) fn set_disabled(&self, disabled: bool) {
self.disabled.set(disabled);
}

View File

@ -88,6 +88,8 @@ mod shared {
pub(super) const NODE_CORNER_RADIUS: f32 = crate::component::node::CORNER_RADIUS;
/// The preferred arc radius.
pub(super) const RADIUS_BASE: f32 = 20.0;
/// The maximum size in pixels of overdraw between edge segments. Prevents visible gaps.
pub(super) const SEGMENT_OVERLAP: f32 = 0.5;
}
use shared::*;
@ -122,13 +124,18 @@ mod three_corner {
// ==============
/// Determine the positions and shapes of all the components of the edge.
pub(super) fn layout(source_half_width: f32, target: Vector2, target_attached: bool) -> Layout {
let (junction_points, max_radius, attachment_length) =
junction_points(source_half_width, target, target_attached);
pub(super) fn layout(
target: Vector2,
source_size: Vector2,
target_size: Vector2,
source_attached: bool,
target_attached: bool,
) -> Layout {
let (junction_points, max_radius, target_attachment) =
junction_points(target, source_size, target_size, source_attached, target_attached);
let corners = corners(&junction_points, max_radius).collect_vec();
let arrow = arrow(target, &junction_points);
let target_attachment = attachment_length.map(|length| TargetAttachment { target, length });
Layout { corners, arrow, target_attachment }
Layout { corners, arrow, target_attachment, source_size }
}
@ -141,16 +148,26 @@ pub(super) fn layout(source_half_width: f32, target: Vector2, target_attached: b
/// given offset. Return the points, the maximum radius that should be used to draw the corners
/// connecting them, and the length of the target attachment bit.
fn junction_points(
source_half_width: f32,
target: Vector2,
source_size: Vector2,
target_size: Vector2,
source_attached: bool,
target_attached: bool,
) -> (Vec<Vector2>, f32, Option<f32>) {
) -> (Vec<Vector2>, f32, Option<TargetAttachment>) {
let source_half_width = source_size.x() / 2.0;
let source_half_height = source_size.y() / 2.0;
// The maximum x-distance from the source (our local coordinate origin) for the point where the
// edge will begin.
let source_max_x_offset = (source_half_width - NODE_CORNER_RADIUS).max(0.0);
// The maximum y-length of the target-attachment segment. If the layout allows, the
// target-attachment segment will fully exit the node before the first corner begins.
let target_max_attachment_height = target_attached.then_some(NODE_HEIGHT / 2.0);
let target_max_attachment_height =
target_attached.then_some((NODE_HEIGHT - target_size.y) / 2.0);
let attachment = target_max_attachment_height.map(|length| TargetAttachment {
target: target + Vector2(0.0, NODE_HEIGHT / 2.0),
length,
});
let target_well_below_source =
target.y() + target_max_attachment_height.unwrap_or_default() <= -MIN_APPROACH_HEIGHT;
let target_below_source = target.y() < -NODE_HEIGHT / 2.0;
@ -184,8 +201,10 @@ fn junction_points(
let circle_offset = arc_origin_x - source_arc_origin;
let intersection = circle_intersection(circle_offset, NODE_CORNER_RADIUS, radius);
-(radius - intersection).abs()
} else if source_attached {
SOURCE_NODE_OVERLAP - source_half_height
} else {
SOURCE_NODE_OVERLAP - NODE_HEIGHT / 2.0
source_half_height
};
let source = Vector2(source_x, source_y);
// The target attachment will extend as far toward the edge of the node as it can without
@ -193,11 +212,11 @@ fn junction_points(
let attachment_height = target_max_attachment_height.map(|dy| min(dy, target.y().abs()));
let attachment_y = target.y() + attachment_height.unwrap_or_default();
let target_attachment = Vector2(target.x(), attachment_y);
(vec![source, target_attachment], max_radius, attachment_height)
(vec![source, target_attachment], max_radius, attachment)
} else {
use three_corner::*;
// The edge originates from either side of the node.
let source_x = source_half_width.copysign(target.x());
let source_x = source_max_x_offset.copysign(target.x());
let distance_x = (target.x() - source_x).abs();
let (j0_x, j1_x, height_adjustment);
if horizontal_room_for_3_corners {
@ -234,9 +253,8 @@ fn junction_points(
let j0 = Vector2(j0_x, top / 2.0);
let j1 = Vector2(j1_x, top);
// The corners meet the target attachment at the top of the node.
let attachment_height = target_max_attachment_height.unwrap_or_default();
let target_attachment = target + Vector2(0.0, attachment_height);
(vec![source, j0, j1, target_attachment], RADIUS_MAX, Some(attachment_height))
let attachment_target = attachment.map_or(target, |a| a.target);
(vec![source, j0, j1, attachment_target], RADIUS_MAX, attachment)
}
}
@ -350,26 +368,71 @@ impl Corner {
Vector2(x_clip, y_clip)
}
/// Calculate vertical and horizontal line overlap to avoid visible gaps between segments.
#[inline]
fn overlap_padding(self, line_width: f32) -> Vector2 {
let Corner { horizontal, vertical, .. } = self;
let offset = (horizontal - vertical).abs();
Vector2(
SEGMENT_OVERLAP.min(offset.x() - line_width * 0.5).max(0.0),
SEGMENT_OVERLAP.min(offset.y() - line_width * 0.5).max(0.0),
)
}
/// Calculate origin offset caused by overlap padding.
#[inline]
fn overlap_offset(self, line_width: f32) -> Vector2 {
let Corner { horizontal, vertical, .. } = self;
let offset = horizontal - vertical;
let pad = self.overlap_padding(line_width);
// Position the overlap according to clip direction. For straight lines, the overlap is
// centered on the line.
let x = match () {
_ if offset.x() < 0.0 => -pad.x(),
_ if offset.y() == 0.0 => -0.5 * pad.x(),
_ => 0.0,
};
let y = match () {
_ if offset.y() > 0.0 => -pad.y(),
_ if offset.x() == 0.0 => -0.5 * pad.y(),
_ => 0.0,
};
Vector2(x, y)
}
#[inline]
pub fn origin(self, line_width: f32) -> Vector2 {
let Corner { horizontal, vertical, .. } = self;
let x = horizontal.x().min(vertical.x() - line_width / 2.0);
let y = vertical.y().min(horizontal.y() - line_width / 2.0);
let offset = horizontal - vertical;
let pad_offset = self.overlap_offset(line_width);
let half_line_width_w = offset.y().abs().min(line_width / 2.0);
let half_line_width_h = offset.x().abs().min(line_width / 2.0);
let x = pad_offset.x() + (horizontal.x()).min(vertical.x() - half_line_width_w);
let y = pad_offset.y() + (vertical.y()).min(horizontal.y() - half_line_width_h);
Vector2(x, y)
}
#[inline]
pub fn size(self, line_width: f32) -> Vector2 {
let Corner { horizontal, vertical, .. } = self;
let offset = horizontal - vertical;
let width = (offset.x().abs() + line_width / 2.0).max(line_width);
let height = (offset.y().abs() + line_width / 2.0).max(line_width);
let offset = (horizontal - vertical).abs();
let pad = self.overlap_padding(line_width);
let half_line_width_w = offset.y().min(line_width / 2.0);
let half_line_width_h = offset.x().min(line_width / 2.0);
let width = pad.x() + (offset.x() + half_line_width_w).max(half_line_width_w * 2.0);
let height = pad.y() + (offset.y() + half_line_width_h).max(half_line_width_h * 2.0);
Vector2(width, height)
}
#[inline]
pub fn max_radius(self) -> f32 {
self.max_radius
pub fn radius(self, line_width: f32) -> f32 {
let Corner { horizontal, vertical, .. } = self;
let offset = (horizontal - vertical).abs();
let smaller_offset = offset.x().min(offset.y());
let piecewise_limit = (smaller_offset * 2.0)
.min(line_width)
.max(smaller_offset + smaller_offset.min(line_width / 2.0));
(self.max_radius + line_width / 2.0).min(piecewise_limit)
}
fn bounding_box(self, line_width: f32) -> BoundingBox {

View File

@ -6,11 +6,14 @@
use crate::prelude::*;
use ensogl::display::shape::*;
use crate::GraphLayers;
use super::layout::Corner;
use super::layout::EdgeSplit;
use super::layout::Oriented;
use super::layout::SplitArc;
use super::layout::TargetAttachment;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::Scene;
@ -34,11 +37,11 @@ mod arrow {
}
mod attachment {
/// Extra length to add to the top of the target-attachment bit, to ensure that it
/// Extra length to add to the top and bottom of the target-attachment bit, to ensure that it
/// appears to pass through the top of the node. Without this adjustment, inexact
/// floating-point math and anti-aliasing would cause a 1-pixel gap artifact right where
/// the attachment should meet the corner at the edge of the node.
pub(super) const TOP_ADJUSTMENT: f32 = 0.5;
pub(super) const LENGTH_ADJUSTMENT: f32 = 0.1;
}
@ -63,6 +66,9 @@ pub(super) struct Shapes {
target_attachment: RefCell<Option<Rectangle>>,
/// Arrow drawn on long backward edges to indicate data flow direction.
dataflow_arrow: RefCell<Option<Rectangle>>,
/// An rectangle representing the source node shape when the edge is in detached state. Used
/// to mask out the edge fragment that would otherwise be drawn over the source node.
source_cutout: RefCell<Option<Rectangle>>,
}
impl Shapes {
@ -85,8 +91,8 @@ impl Shapes {
let shape = shape.unwrap_or_else(|| parent.new_dataflow_arrow());
shape.set_xy(arrow_center - arrow::SIZE / 2.0);
shape.set_color(color);
Self::set_layer(parent, is_attached, &shape);
self.dataflow_arrow.set(shape);
Self::set_layer(parent, &shape, is_attached, false);
self.dataflow_arrow.replace(Some(shape));
}
}
@ -104,7 +110,7 @@ impl Shapes {
*self.hover_sections.borrow_mut() = corners
.iter()
.zip(hover_factory)
.map(|(corner, shape)| draw_corner(shape, **corner, HOVER_WIDTH))
.map(|(corner, shape)| draw_corner(shape, **corner, INVISIBLE_HOVER_COLOR, HOVER_WIDTH))
.collect();
}
@ -125,31 +131,44 @@ impl Shapes {
source_color,
target_color,
);
for shape in &new_sections {
Self::set_layer(parent, is_attached, shape);
}
let arc_shapes = self.split_arc.take();
if let Some(split_corner) = split_corner {
let source_side = rectangle_geometry(*split_corner.source_end, LINE_WIDTH);
let target_side = rectangle_geometry(*split_corner.target_end, LINE_WIDTH);
let split_arc = split_corner.split_arc;
if let Some(split_arc) = split_arc {
if let Some(split_arc) = split_corner.split_arc {
let arc_shapes = arc_shapes.unwrap_or_else(|| [parent.new_arc(), parent.new_arc()]);
let arc_shapes = draw_split_arc(arc_shapes, split_arc);
arc_shapes[0].color.set(source_color.into());
arc_shapes[1].color.set(target_color.into());
self.split_arc.set(arc_shapes);
self.split_arc.replace(Some(arc_shapes));
}
let (source_shape, target_shape) =
(section_factory.next().unwrap(), section_factory.next().unwrap());
source_shape.set_border_color(source_color);
target_shape.set_border_color(target_color);
new_sections.push(draw_geometry(source_shape, source_side));
new_sections.push(draw_geometry(target_shape, target_side));
new_sections.extend([
draw_corner(source_shape, *split_corner.source_end, source_color, LINE_WIDTH),
draw_corner(target_shape, *split_corner.target_end, target_color, LINE_WIDTH),
]);
}
for (i, shape) in new_sections.iter().enumerate() {
Self::set_layer(parent, shape, is_attached, i == 0);
}
*self.sections.borrow_mut() = new_sections;
}
pub(crate) fn redraw_cutout(
&self,
parent: &impl ShapeParent,
is_attached: bool,
source_size: Vector2,
) {
let cutout = self.source_cutout.take();
if !is_attached {
let cutout = cutout.unwrap_or_else(|| parent.new_cutout());
cutout.set_xy(-source_size / 2.0);
cutout.set_size(source_size);
self.source_cutout.replace(Some(cutout));
}
}
/// Redraw the sections that aren't split by the focus position.
pub(super) fn redraw_complete_sections(
&self,
@ -174,11 +193,7 @@ impl Shapes {
}
})
.zip(section_factory)
.map(|((color, corner), shape)| {
let shape = draw_corner(shape, **corner, LINE_WIDTH);
shape.set_border_color(color);
shape
})
.map(|((color, corner), shape)| draw_corner(shape, **corner, color, LINE_WIDTH))
.collect()
}
@ -193,20 +208,30 @@ impl Shapes {
if let Some(TargetAttachment { target, length }) = target_attachment
&& length > f32::EPSILON {
let shape = shape.unwrap_or_else(|| parent.new_target_attachment());
shape.set_size_y(length + attachment::TOP_ADJUSTMENT);
shape.set_xy(target + Vector2(-LINE_WIDTH / 2.0, attachment::TOP_ADJUSTMENT));
shape.set_size_y(length + attachment::LENGTH_ADJUSTMENT * 2.0);
let offset = Vector2(-LINE_WIDTH / 2.0, - length - attachment::LENGTH_ADJUSTMENT);
shape.set_xy(target + offset);
shape.set_color(color);
self.target_attachment.set(shape);
self.target_attachment.replace(Some(shape));
}
}
/// Add the given shape to the appropriate layer depending on whether it is attached.
fn set_layer(parent: &impl ShapeParent, is_attached: bool, shape: &Rectangle) {
(match is_attached {
true => &parent.scene().layers.main_edges_level,
false => &parent.scene().layers.main_above_inactive_nodes_level,
})
.add(shape)
fn set_layer(
parent: &impl ShapeParent,
shape: &Rectangle,
below_nodes: bool,
near_source: bool,
) {
let layers = parent.layers();
let layer = if below_nodes {
&layers.edge_below_nodes
} else if near_source {
&layers.masked_edge_above_nodes
} else {
&layers.edge_above_nodes
};
layer.add(shape);
}
}
@ -283,12 +308,12 @@ mod arc {
pub(super) trait ShapeParent: display::Object {
fn scene(&self) -> &Scene;
fn layers(&self) -> &GraphLayers;
/// Create a shape object to render one of the [`Corner`]s making up the edge.
fn new_section(&self) -> Rectangle {
let new = Rectangle::new();
new.set_corner_radius_max();
new.set_border_and_inset(LINE_WIDTH);
new.set_inner_border(LINE_WIDTH, 0.0);
new.set_color(color::Rgba::transparent());
new.set_pointer_events(false);
self.display_object().add_child(&new);
@ -299,10 +324,10 @@ pub(super) trait ShapeParent: display::Object {
/// [`Corner`]s making up the edge.
fn new_hover_section(&self) -> Rectangle {
let new = Rectangle::new();
new.set_corner_radius_max();
new.set_border_and_inset(HOVER_WIDTH);
new.set_inner_border(HOVER_WIDTH, 0.0);
new.set_color(color::Rgba::transparent());
self.display_object().add_child(&new);
self.layers().edge_below_nodes.add(&new);
new
}
@ -312,7 +337,7 @@ pub(super) trait ShapeParent: display::Object {
let arc = arc::View::new();
arc.stroke_width.set(LINE_WIDTH);
self.display_object().add_child(&arc);
self.scene().layers.below_main.add(&arc);
self.layers().edge_below_nodes.add(&arc);
arc
}
@ -324,7 +349,7 @@ pub(super) trait ShapeParent: display::Object {
new.set_border_color(color::Rgba::transparent());
new.set_pointer_events(false);
self.display_object().add_child(&new);
self.scene().layers.main_above_all_nodes_level.add(&new);
self.layers().edge_above_nodes.add(&new);
new
}
@ -336,6 +361,20 @@ pub(super) trait ShapeParent: display::Object {
self.display_object().add_child(&new);
new.into()
}
/// Create a shape object to render the cutout mask for the edge nearby the source node.
fn new_cutout(&self) -> Rectangle {
let cutout = Rectangle::new();
self.display_object().add_child(&cutout);
// FIXME (temporary assumption): Currently we assume that the node background is a rectangle
// with always rounded corners. Ideally we would somehow use actual source node's background
// shape for this.
cutout.set_corner_radius(crate::component::node::CORNER_RADIUS);
self.layers().edge_above_nodes_cutout.add(&cutout);
// Pointer events must be enabled, so that the hover area is masked out as well.
cutout.set_pointer_events(true);
cutout
}
}
@ -349,43 +388,21 @@ pub(super) trait ShapeParent: display::Object {
/// Note that the shape's `inset` and `border` should be the same value as the provided
/// [`line_width`]. They are not set here as an optimization: When shapes are reused, the value does
/// not need to be set again, reducing needed GPU uploads.
pub(super) fn draw_corner(shape: Rectangle, corner: Corner, line_width: f32) -> Rectangle {
draw_geometry(shape, rectangle_geometry(corner, line_width))
}
fn draw_geometry(shape: Rectangle, geometry: RectangleGeometry) -> Rectangle {
shape.set_clip(geometry.clip);
shape.set_size(geometry.size);
shape.set_xy(geometry.xy);
shape.set_corner_radius(geometry.radius);
pub(super) fn draw_corner(
shape: Rectangle,
corner: Corner,
color: color::Rgba,
line_width: f32,
) -> Rectangle {
shape.set_xy(corner.origin(line_width));
shape.set_size(corner.size(line_width));
shape.set_clip(corner.clip());
shape.set_corner_radius(corner.radius(line_width));
shape.set_border_color(color);
shape
}
// === Rectangle Geometry ===
#[derive(Debug, Copy, Clone, Default)]
struct RectangleGeometry {
pub clip: Vector2,
pub size: Vector2,
pub xy: Vector2,
pub radius: f32,
}
/// Return [`Rectangle`] geometry parameters to draw this corner shape.
fn rectangle_geometry(corner: Corner, line_width: f32) -> RectangleGeometry {
// Convert from a layout radius (in the center of the line) to a [`Rectangle`] radius (on the
// inside edge of the border).
let radius = max(corner.max_radius() - line_width / 2.0, 0.0);
RectangleGeometry {
clip: corner.clip(),
size: corner.size(line_width),
xy: corner.origin(line_width),
radius,
}
}
// ==============================
// === Rendering Partial Arcs ===

View File

@ -4,6 +4,7 @@ use super::layout::Corner;
use super::layout::EdgeSplit;
use super::layout::Oriented;
use super::layout::TargetAttachment;
use ensogl::data::color;
@ -34,6 +35,8 @@ pub(super) struct Layout {
pub arrow: Option<Vector2>,
/// The target-attachment end.
pub target_attachment: Option<TargetAttachment>,
/// The size of the source node.
pub source_size: Vector2,
}
/// An edge's color scheme.

View File

@ -10,6 +10,7 @@ use crate::selection::BoundingBox;
use crate::tooltip;
use crate::view;
use crate::CallWidgetsConfig;
use crate::GraphLayers;
use crate::Type;
use engine_protocol::language_server::ExecutionEnvironment;
@ -20,12 +21,11 @@ use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::shape::compound::rectangle;
use ensogl::display::style::FromTheme;
use ensogl::gui;
use ensogl::Animation;
use ensogl_component::text;
use ensogl_hardcoded_theme as theme;
use ensogl_hardcoded_theme;
// ==============
@ -54,23 +54,22 @@ pub use expression::Expression;
// === Constants ===
// =================
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const ACTION_BAR_WIDTH: f32 = 180.0;
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const ACTION_BAR_HEIGHT: f32 = 15.0;
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const CORNER_RADIUS: f32 = 14.0;
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const HEIGHT: f32 = 28.0;
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const PADDING: f32 = 40.0;
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub const RADIUS: f32 = 14.0;
/// Base height of a single-line node.
pub const HEIGHT: f32 = 32.0;
/// The additional reserved space around base background shape to allow for drawing node backdrop
/// shapes, such as selection or status.
pub const BACKDROP_INSET: f32 = 25.0;
/// Size of error indicator border around node.
pub const ERROR_BORDER_WIDTH: f32 = 3.0;
/// Distance between node and error indicator border.
pub const ERROR_BORDER_DISTANCE: f32 = 3.0;
/// Radius of rounded corners of base node background shape. Node also contains other shapes, such
/// as selection, that will also received rounded corners by growing the background shape.
pub const CORNER_RADIUS: f32 = HEIGHT / 2.0;
/// Space between the documentation comment and the node.
pub const COMMENT_MARGIN: f32 = 10.0;
const INFINITE: f32 = 99999.0;
const ERROR_VISUALIZATION_SIZE: Vector2 = visualization::container::DEFAULT_SIZE;
const VISUALIZATION_OFFSET_Y: f32 = -20.0;
@ -85,105 +84,122 @@ const UNRESOLVED_SYMBOL_TYPE: &str = "Builtins.Main.Unresolved_Symbol";
// ===============
// === Comment ===
// ===============
/// String with documentation comment text for this node.
///
/// This is just a plain string, as this is what text area expects and node just redirects this
/// value,
pub type Comment = ImString;
// ==============
// === Shapes ===
// ==============
/// A node's background area and selection.
#[derive(Debug, Clone, CloneRef, display::Object)]
#[derive(Debug, Clone, display::Object)]
pub struct Background {
_network: frp::Network,
#[display_object]
shape: Rectangle,
inset: Immutable<f32>,
selection_color: Immutable<color::Rgba>,
shape: Rectangle,
selection_shape: Rectangle,
selection_animation: Animation<f32>,
color_animation: color::Animation,
node_is_hovered: frp::Any<bool>,
size_and_center: frp::Source<(Vector2, Vector2)>,
}
#[derive(Debug, Clone, Default, FromTheme)]
struct BackgroundStyle {
#[theme_path = "theme::graph_editor::node::selection::opacity"]
selection_opacity: f32,
#[theme_path = "theme::graph_editor::node::selection::hover_opacity"]
selection_hover_opacity: f32,
#[theme_path = "theme::graph_editor::node::selection::size"]
selection_size: f32,
}
impl Background {
fn new(style: &StyleWatchFrp) -> Self {
let selection_color =
style.get_color(ensogl_hardcoded_theme::graph_editor::node::selection).value();
let selection_size =
style.get_number(ensogl_hardcoded_theme::graph_editor::node::selection::size).value();
let selection_offset =
style.get_number(ensogl_hardcoded_theme::graph_editor::node::selection::offset).value();
let inset = selection_size + selection_offset;
let network = frp::Network::new("Background");
let style = BackgroundStyle::from_theme(&network, style);
let shape = Rectangle();
shape.set_corner_radius(RADIUS);
shape.set_frame_border(selection_size);
shape.set_border_color(color::Rgba::transparent());
shape.set_inset(inset);
Self { shape, inset: Immutable(inset), selection_color: Immutable(selection_color) }
let selection_shape = Rectangle();
let color_animation = color::Animation::new(&network);
let selection_animation = Animation::new(&network);
let hover_animation = Animation::new(&network);
shape.add_child(&selection_shape);
let selection_enter = selection_shape.on_event::<mouse::Enter>();
let selection_leave = selection_shape.on_event::<mouse::Leave>();
frp::extend! { network
selection_is_hovered <- bool(&selection_leave, &selection_enter);
node_is_hovered <- any(...);
is_hovered <- selection_is_hovered || node_is_hovered;
hover_animation.target <+ is_hovered.switch_constant(INVISIBLE_HOVER_COLOR.alpha, 1.0);
selection_colors <- color_animation.value.all_with3(
&hover_animation.value, &style,
|color, hover, style| (
color.multiply_alpha(style.selection_opacity),
color.multiply_alpha(style.selection_hover_opacity * hover),
)
);
selection_border <- selection_animation.value.all_with(&style,
|selection, style| style.selection_size * (1.0 - selection)
);
size_and_center <- source();
eval size_and_center([shape] (size_and_center) {
let (size, center) = *size_and_center;
shape.set_xy(center - size / 2.0);
shape.set_size(size);
shape.set_corner_radius(CORNER_RADIUS);
});
size_and_selection <- size_and_center.all_with(&style,
|(size, _), style| (*size, style.selection_size)
).on_change();
eval size_and_selection([selection_shape] (size_and_selection) {
let (size, total_selection_size) = *size_and_selection;
let total_selection_vec = Vector2::from_element(total_selection_size);
// selection shape is positioned relative to the background shape.
selection_shape.set_xy(-total_selection_vec);
selection_shape.set_size(size + total_selection_vec * 2.0);
selection_shape.set_corner_radius(CORNER_RADIUS + total_selection_size);
});
eval color_animation.value((color) shape.set_color(color.into()););
eval selection_border((border) selection_shape.set_border_and_inset(*border););
eval selection_colors([selection_shape] ((selected_color, unselected_color)) {
selection_shape.set_color(selected_color.into());
selection_shape.set_border_color(unselected_color.into());
});
}
selection_animation.target.emit(0.0);
hover_animation.precision.emit(0.01);
hover_animation.target.emit(INVISIBLE_HOVER_COLOR.alpha);
Self {
_network: network,
shape,
selection_shape,
selection_animation,
node_is_hovered,
color_animation,
size_and_center,
}
}
fn set_selected(&self, degree: f32) {
let selected = self.selection_color;
let blended = color::Rgba(selected.red, selected.green, selected.blue, degree);
self.shape.set_border_color(blended);
fn set_selected(&self, selected: bool) {
self.selection_animation.target.emit(if selected { 1.0 } else { 0.0 });
}
fn set_color(&self, color: color::Lcha) {
self.color_animation.target.emit(color);
}
fn set_size_and_center_xy(&self, size: Vector2<f32>, center: Vector2<f32>) {
let size_with_inset = size + 2.0 * Vector2(*self.inset, *self.inset);
let origin = center - size_with_inset / 2.0;
self.shape.set_size(size_with_inset);
self.shape.set_xy(origin);
self.size_and_center.emit((size, center));
}
}
// =======================
// === Error Indicator ===
// =======================
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub mod error_shape {
use super::*;
ensogl::shape! {
below = [rectangle];
alignment = center;
(style:Style,color_rgba:Vector4<f32>) {
use ensogl_hardcoded_theme::graph_editor::node as node_theme;
let width = Var::<Pixels>::from("input_size.x");
let height = Var::<Pixels>::from("input_size.y");
let zoom = Var::<f32>::from("1.0/zoom()");
let width = width - PADDING.px() * 2.0;
let height = height - PADDING.px() * 2.0;
let radius = RADIUS.px();
let error_width = style.get_number(node_theme::error::width).px();
let repeat_x = style.get_number(node_theme::error::repeat_x).px();
let repeat_y = style.get_number(node_theme::error::repeat_y).px();
let stripe_width = style.get_number(node_theme::error::stripe_width);
let stripe_angle = style.get_number(node_theme::error::stripe_angle);
let repeat = Var::<Vector2<Pixels>>::from((repeat_x,repeat_y));
let stripe_width = Var::<Pixels>::from(zoom * stripe_width);
let stripe_red = Rect((&stripe_width,INFINITE.px()));
let stripe_angle_rad = stripe_angle.radians();
let pattern = stripe_red.repeat(repeat).rotate(stripe_angle_rad);
let mask = Rect((&width,&height)).corners_radius(radius);
let mask = mask.grow(error_width);
let pattern = mask.intersection(pattern).fill(color_rgba);
pattern.into()
}
}
}
// ============
// === Node ===
// ============
@ -205,7 +221,7 @@ ensogl::define_endpoints_2! {
/// Set whether the output context is explicitly enabled: `Some(true/false)` for
/// enabled/disabled; `None` for no context switch expression.
set_context_switch (Option<bool>),
set_comment (Comment),
set_comment (ImString),
set_error (Option<Error>),
/// Set the expression USAGE type. This is not the definition type, which can be set with
/// `set_expression` instead. In case the usage type is set to None, ports still may be
@ -247,13 +263,12 @@ ensogl::define_endpoints_2! {
/// execution context. It is the responsibility of the parent component to apply the changes
/// and update the node with new expression tree using `set_expression`.
on_expression_modified (span_tree::Crumbs, ImString),
comment (Comment),
comment (ImString),
context_switch (bool),
skip (bool),
freeze (bool),
hover (bool),
error (Option<Error>),
expression_label_visible (bool),
/// The [`display::object::Model::position`] of the Node. Emitted when the Display Object
/// hierarchy is updated (see: [`ensogl_core::display::object::Instance::update`]).
position (Vector2),
@ -271,6 +286,9 @@ ensogl::define_endpoints_2! {
/// call's target expression (`self` or first argument).
requested_widgets (ast::Id, ast::Id),
request_import (ImString),
base_color (color::Lcha),
port_color (color::Lcha),
}
}
@ -353,43 +371,38 @@ impl Deref for Node {
}
/// Internal data of `Node`
#[derive(Clone, CloneRef, Debug, display::Object)]
#[derive(Clone, Debug, display::Object)]
#[allow(missing_docs)]
pub struct NodeModel {
// Required for switching the node to a different layer
pub app: Application,
pub layers: GraphLayers,
pub display_object: display::object::Instance,
pub background: Background,
pub error_indicator: error_shape::View,
pub error_indicator: Rectangle,
pub profiling_label: ProfilingLabel,
pub input: input::Area,
pub output: output::Area,
pub visualization: visualization::Container,
pub error_visualization: error::Container,
pub action_bar_wrapper: display::object::Instance,
pub action_bar: action_bar::ActionBar,
pub vcs_indicator: vcs::StatusIndicator,
pub style: StyleWatchFrp,
pub comment: text::Text,
pub interaction_state: Rc<Cell<InteractionState>>,
pub interaction_state: Cell<InteractionState>,
}
impl NodeModel {
/// Constructor.
#[profile(Debug)]
pub fn new(app: &Application, registry: visualization::Registry) -> Self {
use display::shape::compound::rectangle;
ensogl::shapes_order_dependencies! {
app.display.default_scene => {
error_shape -> output::port::single_port;
error_shape -> output::port::multi_port;
output::port::single_port -> rectangle;
output::port::multi_port -> rectangle;
}
}
pub fn new(app: &Application, layers: &GraphLayers, registry: visualization::Registry) -> Self {
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let error_indicator = error_shape::View::new();
let error_indicator = Rectangle();
error_indicator
.set_corner_radius_max()
.set_pointer_events(false)
.set_color(color::Rgba::transparent())
.set_border_and_inset(ERROR_BORDER_WIDTH);
let profiling_label = ProfilingLabel::new(app);
let background = Background::new(&style);
let vcs_indicator = vcs::StatusIndicator::new(app);
@ -399,7 +412,7 @@ impl NodeModel {
display_object.add_child(&background);
display_object.add_child(&vcs_indicator);
let input = input::Area::new(app);
let input = input::Area::new(app, layers);
let visualization = visualization::Container::new(app, registry);
display_object.add_child(&visualization);
@ -409,7 +422,11 @@ impl NodeModel {
error_visualization.frp.set_size.emit(ERROR_VISUALIZATION_SIZE);
let action_bar = action_bar::ActionBar::new(app);
display_object.add_child(&action_bar);
let action_bar_wrapper = display::object::Instance::new_named("action_bar_wrapper");
action_bar_wrapper.set_size((0.0, 0.0)).set_xy((0.0, 0.0));
action_bar_wrapper.add_child(&action_bar);
action_bar.set_alignment_right_center();
display_object.add_child(&action_bar_wrapper);
let output = output::Area::new(app);
display_object.add_child(&output);
@ -419,9 +436,8 @@ impl NodeModel {
let interaction_state = default();
let app = app.clone_ref();
Self {
app,
layers: layers.clone(),
display_object,
background,
error_indicator,
@ -430,6 +446,7 @@ impl NodeModel {
output,
visualization,
error_visualization,
action_bar_wrapper,
action_bar,
vcs_indicator,
style,
@ -452,51 +469,20 @@ impl NodeModel {
self.set_layers_for_state(new_state);
}
/// Set whether the node is being interacted with by moving an edge with the mouse.
pub fn set_editing_edge(&self, editing: bool) {
let new_state = self.interaction_state.update(|state| state.editing_edge(editing));
self.set_layers_for_state(new_state);
}
fn set_layers_for_state(&self, new_state: InteractionState) {
let scene = &self.app.display.default_scene;
let main_layer;
let background_layer;
let text_layer;
let action_bar_layer;
match new_state {
InteractionState::EditingExpression => {
// Move all sub-components to `edited_node` layer.
//
// `action_bar` is moved to the `edited_node` layer as well, though normally it
// lives on a separate `above_nodes` layer, unlike every other node component.
main_layer = scene.layers.edited_node.default_partition();
background_layer = main_layer.clone();
text_layer = &scene.layers.edited_node_text;
action_bar_layer = &scene.layers.edited_node;
}
InteractionState::EditingEdge => {
main_layer = scene.layers.main_nodes_level.clone();
background_layer = scene.layers.main_active_nodes_level.clone();
text_layer = &scene.layers.label;
action_bar_layer = &scene.layers.above_nodes;
}
InteractionState::Normal => {
main_layer = scene.layers.main_nodes_level.clone();
background_layer = main_layer.clone();
text_layer = &scene.layers.label;
action_bar_layer = &scene.layers.above_nodes;
}
}
main_layer.add(&self.display_object);
background_layer.add(&self.background);
action_bar_layer.add(&self.action_bar);
// For the text layer, [`Layer::add`] can't be used because text rendering currently uses a
// separate layer management API.
self.output.set_label_layer(text_layer);
self.input.set_label_layer(text_layer);
self.profiling_label.set_label_layer(text_layer);
self.comment.add_to_scene_layer(text_layer);
let (below, main) = match new_state {
InteractionState::Normal => (&self.layers.main_backdrop, &self.layers.main_nodes),
InteractionState::EditingExpression =>
(&self.layers.edited_backdrop, &self.layers.edited_nodes),
};
main.body.add(&self.display_object);
below.backdrop.add(&self.background.selection_shape);
below.backdrop.add(&self.error_indicator);
main.action_bar.add(&self.action_bar_wrapper);
main.below_body.add(&self.output);
main.output_hover.add(self.output.hover_root());
self.action_bar.set_layers(main);
}
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
@ -526,20 +512,18 @@ impl NodeModel {
fn set_width(&self, width: f32) -> Vector2 {
let height = self.height();
let size = Vector2(width, height);
let padded_size = size + Vector2(PADDING, PADDING) * 2.0;
self.error_indicator.set_size(padded_size);
let padded_size = size + Vector2(BACKDROP_INSET, BACKDROP_INSET) * 2.0;
let error_padding = ERROR_BORDER_WIDTH + ERROR_BORDER_DISTANCE;
let error_size = size + Vector2(error_padding, error_padding) * 2.0;
self.output.frp.set_size(size);
self.error_indicator.set_size(error_size);
self.vcs_indicator.frp.set_size(padded_size);
let x_offset_to_node_center = x_offset_to_node_center(width);
let background_origin = Vector2(x_offset_to_node_center, 0.0);
self.background.set_size_and_center_xy(size, background_origin);
self.error_indicator.set_x(x_offset_to_node_center);
self.error_indicator.set_xy((-error_padding, -height / 2.0 - error_padding));
self.vcs_indicator.set_x(x_offset_to_node_center);
let action_bar_width = ACTION_BAR_WIDTH;
self.action_bar
.set_x(x_offset_to_node_center + width / 2.0 + CORNER_RADIUS + action_bar_width / 2.0);
self.action_bar.frp.set_size(Vector2::new(action_bar_width, ACTION_BAR_HEIGHT));
self.visualization.set_xy(VISUALIZATION_OFFSET);
// Error visualization has origin in the center, while regular visualization has it at the
// top left corner.
@ -570,29 +554,34 @@ impl NodeModel {
#[profile(Debug)]
fn set_error_color(&self, color: &color::Lcha) {
self.error_indicator.color_rgba.set(color::Rgba::from(color).into());
if color.alpha < f32::EPSILON {
self.error_indicator.unset_parent();
} else {
self.error_indicator.set_border_color(color.into());
self.display_object.add_child(&self.error_indicator);
}
}
fn set_selected(&self, degree: f32) {
self.background.set_selected(degree);
#[profile(Debug)]
fn update_colors(&self, color: color::Lcha, port_color: color::Lcha) {
self.background.set_color(color);
self.output.set_port_color(port_color);
}
fn set_selected(&self, selected: bool) {
self.background.set_selected(selected);
}
}
impl Node {
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
#[profile(Debug)]
pub fn new(app: &Application, registry: visualization::Registry) -> Self {
pub fn new(app: &Application, layers: &GraphLayers, registry: visualization::Registry) -> Self {
let frp = Frp::new();
let network = frp.network();
let out = &frp.private.output;
let input = &frp.private.input;
let model = Rc::new(NodeModel::new(app, registry));
let selection = Animation::<f32>::new(network);
let model = Rc::new(NodeModel::new(app, layers, registry));
let display_object = &model.display_object;
// TODO[ao] The comment color should be animated, but this is currently slow. Will be fixed
@ -605,12 +594,14 @@ impl Node {
frp::extend! { network
init <- source::<()>();
// Hook up the display object position updates to the node's FRP. Required to calculate
// the bounding box.
out.position <+ display_object.on_transformed.map(f_!(display_object.position().xy()));
}
frp::extend! { network
// === Hover ===
// The hover discovery of a node is an interesting process. First, we discover whether
// ths user hovers the background. The input port manager merges this information with
// port hover events and outputs the final hover event for any part inside of the node.
@ -626,30 +617,29 @@ impl Node {
model.input.set_hover <+ node_hover;
model.output.set_hover <+ model.input.body_hover;
out.hover <+ model.output.body_hover;
model.background.node_is_hovered <+ out.hover;
}
frp::extend! { network
// === Background Press ===
let background_press = model.background.on_event::<mouse::Down>();
let input_press = model.input.on_event::<mouse::Down>();
input_as_background_press <- input_press.gate(&input.set_edit_ready_mode);
// When editing, clicks focus the `Text` component and set the cursor position.
background_as_input_press <- background_press.gate(&model.input.editing);
model.input.mouse_down <+_ background_as_input_press;
background_press <- background_press.gate_not(&model.input.editing);
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;
}
frp::extend! { network
// === Selection ===
deselect_target <- input.deselect.constant(0.0);
select_target <- input.select.constant(1.0);
selection.target <+ any(&deselect_target, &select_target);
eval selection.value ((t) model.set_selected(*t));
selected <- bool(&input.deselect, &input.select);
eval selected ((selected) model.set_selected(*selected));
}
frp::extend! { network
// === Expression ===
let unresolved_symbol_type = Some(Type(ImString::new(UNRESOLVED_SYMBOL_TYPE)));
@ -668,9 +658,12 @@ impl Node {
model.input.update_widgets <+ input.update_widgets;
model.output.set_expression_visibility <+ input.set_output_expression_visibility;
}
frp::extend! { network
// === Comment ===
let comment_base_color = style_frp.get_color(theme::graph_editor::node::text);
comment_color <- all_with(
&comment_base_color, &model.output.expression_label_visibility,
@ -690,29 +683,31 @@ impl Node {
model.comment.set_y(*height / 2.0));
model.comment.set_content <+ input.set_comment;
out.comment <+ model.comment.content.map(|text| text.to_im_string());
}
frp::extend! { network
// === Size ===
input_width <- all(&model.input.frp.width, &init)._0();
new_size <- input_width.map(f!((w) model.set_width(*w)));
model.output.frp.set_size <+ new_size;
}
frp::extend! { network
// === Action Bar ===
out.context_switch <+ action_bar.action_context_switch;
out.skip <+ action_bar.action_skip;
out.freeze <+ action_bar.action_freeze;
show_action_bar <- out.hover && input.show_quick_action_bar_on_hover;
show_action_bar <- node_hover && input.show_quick_action_bar_on_hover;
eval show_action_bar ((t) action_bar.set_visibility(t));
eval input.show_quick_action_bar_on_hover((value) action_bar.show_on_hover(value));
action_bar.set_action_freeze_state <+ input.set_freeze_macro;
action_bar.set_action_skip_state <+ input.set_skip_macro;
action_bar.set_action_context_switch_state <+ input.set_context_switch;
action_bar.set_execution_environment <+ input.set_execution_environment;
}
frp::extend! { network
// === View Mode ===
model.input.set_view_mode <+ input.set_view_mode;
@ -721,8 +716,9 @@ impl Node {
model.vcs_indicator.set_visibility <+ input.set_view_mode.map(|&mode| {
!matches!(mode,view::Mode::Profiling {..})
});
}
frp::extend! { network
// === Read-only mode ===
action_bar.set_read_only <+ input.set_read_only;
@ -758,7 +754,8 @@ impl Node {
viz_enabled <- enabled && no_error_set;
visualization.set_view_state <+ viz_enabled.on_true().constant(visualization::ViewState::Enabled);
visualization.set_view_state <+ viz_enabled.on_false().constant(visualization::ViewState::Disabled);
}
frp::extend! { network
// Integration between visualization and action bar.
visualization.set_visualization <+ input.set_visualization;
is_enabled <- visualization.view_state.map(|state|{
@ -772,7 +769,8 @@ impl Node {
action_bar.set_action_visibility_state <+ button_set_to_true_with_error.constant(false);
visualization.set_view_state <+ action_bar.user_action_visibility.on_false().constant(visualization::ViewState::Disabled);
}
frp::extend! { network
// Show preview visualization after some delay, depending on whether we show an error
// or are in quick preview mode. Also, omit the preview if we don't have an
// expression.
@ -789,7 +787,8 @@ impl Node {
});
hover_onset_delay.set_delay <+ preview_show_delay;
hide_tooltip <- preview_show_delay.map(|&delay| delay <= f32::EPSILON);
}
frp::extend! { network
output_hover <- model.output.on_port_hover.map(|s| s.is_on());
visualization_hover <- bool(&model.visualization.on_event::<mouse::Out>(), &model.visualization.on_event::<mouse::Over>());
hovered_for_preview <- output_hover || visualization_hover;
@ -812,7 +811,8 @@ impl Node {
vis_preview_visible <- vis_preview_visible.on_change();
visualization.set_view_state <+ vis_preview_visible.on_true().constant(visualization::ViewState::Preview);
visualization.set_view_state <+ vis_preview_visible.on_false().constant(visualization::ViewState::Disabled);
}
frp::extend! { network
update_error <- all(input.set_error,preview_visible);
eval update_error([model]((error,visible)){
if *visible {
@ -830,9 +830,9 @@ impl Node {
}
// === Profiling Indicator ===
frp::extend! { network
// === Profiling Indicator ===
model.profiling_label.set_min_global_duration
<+ input.set_profiling_min_global_duration;
model.profiling_label.set_max_global_duration
@ -841,46 +841,7 @@ impl Node {
model.input.set_profiling_status <+ input.set_profiling_status;
}
let bg_color_anim = color::Animation::new(network);
frp::extend! { network
// === Color Handling ===
let bgg = style_frp.get_color(ensogl_hardcoded_theme::graph_editor::node::background);
let profiling_theme = profiling::Theme::from_styles(style_frp,network);
profiling_color <- all_with5
(&input.set_profiling_status,&input.set_profiling_min_global_duration,
&input.set_profiling_max_global_duration,&profiling_theme,&bgg,
|&status,&min,&max,&theme,&bgg| {
if status.is_finished() {
status.display_color(min,max,theme).with_alpha(1.0)
} else {
color::Lcha::from(bgg)
}
});
bg_color_anim.target <+ all_with3(&bgg,&input.set_view_mode,&profiling_color,
|bgg,&mode,&profiling_color| {
match mode {
view::Mode::Normal => color::Lcha::from(*bgg),
view::Mode::Profiling => profiling_color,
}
});
// FIXME [WD]: Uncomment when implementing disabled icon.
// bg_color <- frp.set_disabled.map(f!([model,style](disabled) {
// model.input.frp.set_disabled(*disabled);
// let bg_color_path = ensogl_hardcoded_theme::graph_editor::node::background;
// if *disabled { style.get_color_dim(bg_color_path) }
// else { style.get_color(bg_color_path) }
// }));
// bg_color_anim.target <+ bg_color;
eval bg_color_anim.value ((c) model.background.shape.set_color(c.into()););
// === Tooltip ===
// Hide tooltip if we show the preview vis.
@ -888,14 +849,15 @@ impl Node {
// Propagate output tooltip. Only if it is not hidden, or to disable it.
block_tooltip <- hide_tooltip && has_tooltip;
app.frp.set_tooltip <+ model.output.frp.tooltip.gate_not(&block_tooltip);
}
frp::extend! { network
// === Type Labels ===`
// === Type Labels ===
model.output.set_type_label_visibility
<+ visualization.visible.not().and(&no_error_set);
model.output.set_type_label_visibility <+ no_error_set.and_not(&visualization.visible);
}
frp::extend! { network
// === Bounding Box ===
let visualization_size = &model.visualization.frp.size;
@ -905,13 +867,42 @@ impl Node {
inner_bbox_input <- all2(&out.position,&new_size);
out.inner_bounding_box <+ inner_bbox_input.map(|(a,b)| bounding_box(*a,*b,None));
}
frp::extend! { network
// === VCS Handling ===
model.vcs_indicator.frp.set_status <+ input.set_vcs_status;
}
frp::extend! { network
// === Colors ===
let port_color_tint = style_frp.get_color_lcha(theme::graph_editor::node::port_color_tint);
let editing_color = style_frp.get_color_lcha(theme::graph_editor::node::background);
base_color_source <- source();
out.base_color <+ base_color_source;
out.port_color <+ out.base_color.all_with(&port_color_tint, |c, tint| tint.over(*c));
background_color <- model.input.frp.editing.switch(&frp.base_color, &editing_color);
node_colors <- all(background_color, frp.port_color);
eval node_colors(((base, port)) model.update_colors(*base, *port));
model.input.set_node_colors <+ node_colors;
}
// TODO: handle color change. Will likely require moving the node background and backdrop
// into a widget, which is also necessary to later support "split" nodes, where '.' chains
// are displayed as separate shapes.
let colors = [
color::Lcha(0.4911, 0.3390, 0.72658, 1.0),
color::Lcha(0.4468, 0.3788, 0.96805, 1.0),
color::Lcha(0.4437, 0.1239, 0.70062, 1.0),
];
let mut hasher = crate::DefaultHasher::new();
Rc::as_ptr(&model).hash(&mut hasher);
base_color_source.emit(colors[hasher.finish() as usize % colors.len()]);
// Init defaults.
init.emit(());
model.error_visualization.set_layer(visualization::Layer::Front);
@ -925,7 +916,7 @@ impl Node {
#[profile(Debug)]
fn error_color(error: &Option<Error>, style: &StyleWatch) -> color::Lcha {
use ensogl_hardcoded_theme::graph_editor::node::error as error_theme;
use theme::graph_editor::node::error as error_theme;
if let Some(error) = error {
let path = match *error.kind {
@ -981,8 +972,6 @@ fn bounding_box(
pub enum InteractionState {
/// The node is being edited (with the cursor / Component Browser).
EditingExpression,
/// An edge of the node is being interacted with.
EditingEdge,
/// The node is not being interacted with.
#[default]
Normal,
@ -995,14 +984,6 @@ impl InteractionState {
false => Self::Normal,
}
}
fn editing_edge(self, active: bool) -> Self {
match (self, active) {
(Self::EditingExpression, _) => self,
(_, true) => Self::EditingEdge,
(_, false) => Self::Normal,
}
}
}
@ -1017,33 +998,22 @@ pub mod test_utils {
/// Addional [`NodeModel`] API for tests.
pub trait NodeModelExt {
/// Return the `SinglePortView` of the first output port of the node.
///
/// Returns `None`:
/// 1. If there are no output ports.
/// 2. If the port does not have a `PortShapeView`. Some port models does not initialize
/// the `PortShapeView`, see [`output::port::Model::init_shape`].
/// 3. If the output port is [`MultiPortView`].
fn output_port_shape(&self) -> Option<output::port::SinglePortView>;
/// Return the `Shape` used for mouse handling of the first output port of the node. Returns
/// `None` if there are no output ports.
fn output_port_hover_shape(&self) -> Option<Rectangle>;
/// Return the `Shape` of the first input port of the node. Returns `None` if there are no
/// input ports.
fn input_port_hover_shape(&self) -> Option<input::port::HoverShape>;
/// Return the `Shape` used for mouse handling of the first input port of the node. Returns
/// `None` if there are no input ports.
fn input_port_hover_shape(&self) -> Option<Rectangle>;
}
impl NodeModelExt for NodeModel {
fn output_port_shape(&self) -> Option<output::port::SinglePortView> {
let ports = self.output.model.ports();
let port = ports.first()?;
let shape = port.shape.as_ref()?;
use output::port::PortShapeView::Single;
match shape {
Single(shape) => Some(shape.clone_ref()),
_ => None,
}
fn output_port_hover_shape(&self) -> Option<Rectangle> {
let shapes = self.output.model.port_hover_shapes();
shapes.into_iter().next()
}
fn input_port_hover_shape(&self) -> Option<input::port::HoverShape> {
fn input_port_hover_shape(&self) -> Option<Rectangle> {
let shapes = self.input.model.port_hover_shapes();
shapes.into_iter().next()
}

View File

@ -3,11 +3,13 @@
use crate::prelude::*;
use ensogl::display::shape::*;
use crate::layers::MainNodeLayers;
use engine_protocol::language_server::ExecutionEnvironment;
use enso_config::ARGS;
use enso_frp as frp;
use ensogl::application::tooltip;
use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl_component::toggle_button;
use ensogl_component::toggle_button::ColorableShape;
@ -27,11 +29,15 @@ pub mod icon;
// === Constants ===
// ==================
const BUTTON_PADDING: f32 = 0.5;
const BUTTON_OFFSET: f32 = 0.5;
const BUTTON_SIZE: f32 = 15.0;
const BUTTON_GAP: f32 = BUTTON_SIZE * 0.5;
/// Grow the hover area in x direction by this amount. Used to close the gap between action
/// icons and node.
const HOVER_EXTENSION_X: f32 = 15.0;
const HOVER_EXTENSION: Vector2 = Vector2(15.0, 11.0);
/// The size of additional hover area that is drawn below the node background. Necessary to prevent
/// easily losing the hover state when moving the mouse towards the action bar.
const HOVER_BRIDGE_SIZE: Vector2 = Vector2(10.0, 26.0);
const HOVER_HIDE_DELAY_MS: i32 = 20;
const VISIBILITY_TOOLTIP_LABEL: &str = "Show preview";
const DISABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Don't write to files and databases";
const ENABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL: &str = "Allow writing to files and databases";
@ -39,18 +45,6 @@ const FREEZE_TOOLTIP_LABEL: &str = "Freeze";
const SKIP_TOOLTIP_LABEL: &str = "Skip";
// ===============
// === Shapes ===
// ===============
/// Invisible rectangular area that can be hovered.
fn hover_area() -> Rectangle {
let area = Rectangle();
area.set_color(INVISIBLE_HOVER_COLOR);
area
}
// ===========
// === Frp ===
@ -58,7 +52,6 @@ fn hover_area() -> Rectangle {
ensogl::define_endpoints! {
Input {
set_size (Vector2),
set_visibility (bool),
/// Set whether the `visibility` icon should be toggled on or off.
set_action_visibility_state (bool),
@ -74,8 +67,8 @@ ensogl::define_endpoints! {
}
Output {
mouse_over (),
mouse_out (),
mouse_enter (),
mouse_leave (),
action_visibility (bool),
/// The last visibility selection by the user. Ignores changes to the
/// visibility chooser icon made through the input API.
@ -103,37 +96,57 @@ struct Icons {
impl Icons {
fn new(app: &Application) -> Self {
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Icons");
display_object
.use_auto_layout()
.reverse_columns()
.set_gap((BUTTON_GAP, 0.0))
.set_padding_xy(HOVER_EXTENSION)
.set_children_alignment_left_center();
let visibility = labeled_button(app, VISIBILITY_TOOLTIP_LABEL);
let context_switch = ContextSwitchButton::enable(app);
let freeze = labeled_button(app, FREEZE_TOOLTIP_LABEL);
let skip = labeled_button(app, SKIP_TOOLTIP_LABEL);
display_object.add_child(&visibility);
display_object.add_child(&context_switch);
if ARGS.groups.feature_preview.options.skip_and_freeze.value {
display_object.add_child(&freeze);
display_object.add_child(&skip);
}
// The visibility icon looks smaller than the other ones, so we make it bigger. This is a
// purely aesthetic adjustment.
visibility.set_size((BUTTON_SIZE * 1.2, BUTTON_SIZE * 1.2));
visibility.set_margin_all(-BUTTON_SIZE * 0.2);
Self { display_object, visibility, context_switch, freeze, skip }
}
fn set_visibility(&self, visible: bool) {
self.visibility.frp.set_visibility(visible);
self.visibility.set_visibility(visible);
self.context_switch.set_visibility(visible);
self.freeze.frp.set_visibility(visible);
self.skip.frp.set_visibility(visible);
self.freeze.set_visibility(visible);
self.skip.set_visibility(visible);
let pointer_events_val = if visible { 0.0 } else { 1.0 };
self.visibility.view().disable_pointer_events.set(pointer_events_val);
self.freeze.view().disable_pointer_events.set(pointer_events_val);
self.skip.view().disable_pointer_events.set(pointer_events_val);
}
fn set_read_only(&self, read_only: bool) {
self.context_switch.set_read_only(read_only);
self.freeze.frp.set_read_only(read_only);
self.skip.frp.set_read_only(read_only);
self.freeze.set_read_only(read_only);
self.skip.set_read_only(read_only);
}
}
fn labeled_button<Icon: ColorableShape>(app: &Application, label: &str) -> ToggleButton<Icon> {
let tooltip_style = tooltip::Style::set_label(label.to_owned());
ToggleButton::new(app, tooltip_style)
let button = ToggleButton::new(app, tooltip_style);
button.set_size((BUTTON_SIZE, BUTTON_SIZE));
button
}
@ -155,11 +168,11 @@ struct ContextSwitchButton {
impl ContextSwitchButton {
fn enable(app: &Application) -> Self {
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("ContextSwitchButton");
display_object.use_auto_layout().set_children_alignment_left_center();
let disable_button = labeled_button(app, DISABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL);
let enable_button = labeled_button(app, ENABLE_OUTPUT_CONTEXT_TOOLTIP_LABEL);
disable_button.set_size((100.pc(), 100.pc()));
enable_button.set_size((100.pc(), 100.pc()));
display_object.add_child(&enable_button);
let globally_enabled = Rc::new(Cell::new(false));
Self { globally_enabled, disable_button, enable_button, display_object }
@ -179,12 +192,10 @@ impl ContextSwitchButton {
fn set_execution_environment(&self, environment: &ExecutionEnvironment) {
if environment.output_context_enabled() != self.globally_enabled.get() {
if environment.output_context_enabled() {
self.remove_child(&self.enable_button);
self.add_child(&self.disable_button);
self.replace_children(&[&self.disable_button]);
self.globally_enabled.set(true);
} else {
self.remove_child(&self.disable_button);
self.add_child(&self.enable_button);
self.replace_children(&[&self.enable_button]);
self.globally_enabled.set(false);
}
}
@ -193,6 +204,9 @@ impl ContextSwitchButton {
fn set_visibility(&self, visible: bool) {
self.disable_button.set_visibility(visible);
self.enable_button.set_visibility(visible);
let pointer_events_val = if visible { 0.0 } else { 1.0 };
self.disable_button.view().disable_pointer_events.set(pointer_events_val);
self.enable_button.view().disable_pointer_events.set(pointer_events_val);
}
fn set_read_only(&self, read_only: bool) {
@ -214,104 +228,57 @@ impl ContextSwitchButton {
#[derive(Clone, CloneRef, Debug, display::Object)]
struct Model {
display_object: display::object::Instance,
hover_area: Rectangle,
icons: Icons,
size: Rc<Cell<Vector2>>,
shapes: compound::events::MouseEvents,
styles: StyleWatch,
display_object: display::object::Instance,
hover_area: Rectangle,
/// Additional hover area that is drawn below the node background. Serves as an always active
/// hover area, but does not block the interactions of widgets on nearby nodes. It also
/// provides additional "bridge" between the node and the action bar, so that the hover
/// state is not lost when moving the mouse. It is drawn below the node background to not
/// cover it, as it can also have its own mouse interactions.
hover_area_below_nodes: Rectangle,
icons: Icons,
styles: StyleWatch,
}
impl Model {
fn new(app: &Application) -> Self {
let scene = &app.display.default_scene;
let display_object = display::object::Instance::new();
let hover_area = hover_area();
let display_object = display::object::Instance::new_named("ActionBar");
let hover_area = Rectangle::new();
let hover_area_below_nodes = Rectangle::new();
hover_area.set_color(INVISIBLE_HOVER_COLOR);
hover_area.allow_grow().set_alignment_left_center();
hover_area_below_nodes.set_color(INVISIBLE_HOVER_COLOR);
hover_area_below_nodes
.allow_grow_x()
.set_size_y(HOVER_BRIDGE_SIZE.y)
.set_alignment_right_center()
.set_margin_right(-HOVER_BRIDGE_SIZE.x);
let icons = Icons::new(app);
let shapes = compound::events::MouseEvents::default();
let size = default();
display_object.add_child(&hover_area);
display_object.add_child(&hover_area_below_nodes);
display_object.add_child(&icons);
let styles = StyleWatch::new(&scene.style_sheet);
shapes.add_sub_shape(&hover_area);
shapes.add_sub_shape(&icons.visibility.view());
shapes.add_sub_shape(&icons.context_switch.disable_button.view());
shapes.add_sub_shape(&icons.context_switch.enable_button.view());
shapes.add_sub_shape(&icons.freeze.view());
shapes.add_sub_shape(&icons.skip.view());
use display::shape::compound::rectangle;
ensogl::shapes_order_dependencies! {
scene => {
rectangle -> icon::visibility;
rectangle -> icon::disable_output_context;
rectangle -> icon::enable_output_context;
rectangle -> icon::freeze;
rectangle -> icon::skip;
compound::rectangle::shape -> icon::visibility;
compound::rectangle::shape -> icon::disable_output_context;
compound::rectangle::shape -> icon::enable_output_context;
compound::rectangle::shape -> icon::freeze;
compound::rectangle::shape -> icon::skip;
}
}
Self { display_object, hover_area, icons, size, shapes, styles }.init()
Self { display_object, hover_area, hover_area_below_nodes, icons, styles }
}
fn init(self) -> Self {
self.add_child(&self.hover_area);
self.add_child(&self.icons);
self
}
fn place_button_in_slot(&self, button: &dyn display::Object, index: usize) {
let icon_size = self.icon_size();
let index = index as f32;
let padding = BUTTON_PADDING;
let offset = BUTTON_OFFSET;
button.set_x(((1.0 + padding) * index + offset) * icon_size.x);
button.set_size(icon_size);
}
fn icon_size(&self) -> Vector2 {
Vector2::new(self.size.get().y, self.size.get().y)
}
fn layout_hover_area_to_cover_buttons(&self, button_count: usize) {
let button_count = button_count as f32;
let size = self.size.get();
let padding = BUTTON_PADDING;
let offset = BUTTON_OFFSET;
let hover_padding = 1.0;
let button_width = self.icon_size().x;
let hover_width =
button_width * (button_count + hover_padding + offset + padding) + HOVER_EXTENSION_X;
let hover_height = button_width * 2.0;
let hover_area_size = Vector2::new(hover_width, hover_height);
self.hover_area.set_size(hover_area_size);
let center_offset = -size.x / 2.0;
let padding_offset = -0.5 * hover_padding * button_width - HOVER_EXTENSION_X / 2.0;
let hover_origin = Vector2(center_offset + padding_offset, -hover_height / 2.0);
self.hover_area.set_xy(hover_origin);
}
fn set_size(&self, size: Vector2) {
self.size.set(size);
self.icons.set_x(-size.x / 2.0);
self.place_button_in_slot(&self.icons.visibility, 0);
self.place_button_in_slot(&self.icons.context_switch, 1);
if ARGS.groups.feature_preview.options.skip_and_freeze.value {
self.place_button_in_slot(&self.icons.skip, 2);
self.place_button_in_slot(&self.icons.freeze, 3);
}
let buttons_count = if ARGS.groups.feature_preview.options.skip_and_freeze.value {
// Toggle visualization, skip and freeze buttons.
4
} else {
// Toggle visualization button only.
2
};
self.layout_hover_area_to_cover_buttons(buttons_count);
// The appears smaller than the other ones, so this is an aesthetic adjustment.
self.icons.visibility.set_scale_xy(Vector2::new(1.2, 1.2));
fn set_visibility(&self, visible: bool) {
self.icons.set_visibility(visible);
self.hover_area.set_pointer_events(visible);
}
}
@ -360,8 +327,6 @@ impl ActionBar {
// === Input Processing ===
eval frp.set_size ((size) model.set_size(*size));
eval frp.set_visibility ((t) model.icons.set_visibility(*t));
eval frp.set_action_visibility_state ((state) model.icons.visibility.set_state(state));
eval frp.set_action_skip_state ((state) model.icons.skip.set_state(state));
eval frp.set_action_freeze_state ((state) model.icons.freeze.set_state(state));
@ -376,10 +341,19 @@ impl ActionBar {
// === Mouse Interactions ===
visibility_init <- source::<bool>();
visibility_mouse <- bool(&model.shapes.mouse_out,&model.shapes.mouse_over);
visibility <- any(&visibility_init,&visibility_mouse);
visibility <- visibility && frp.show_on_hover;
eval visibility ((t) model.icons.set_visibility(*t));
let mouse_enter = model.display_object.on_event::<mouse::Enter>();
let mouse_leave = model.display_object.on_event::<mouse::Leave>();
visibility_mouse <- bool(&mouse_leave,&mouse_enter);
let hide_delay = frp::io::timer::Timeout::new(network);
hide_delay.restart <+ mouse_leave.constant(HOVER_HIDE_DELAY_MS);
visibility_delay <- bool(&hide_delay.on_expired, &mouse_leave);
visibility <- frp.set_visibility || visibility_mouse;
visibility <- visibility || visibility_delay;
visibility <- visibility && frp.show_on_hover;
eval visibility ((t) model.set_visibility(*t));
// === Icon Actions ===
@ -438,6 +412,11 @@ impl ActionBar {
self
}
/// Configure this action bar to use specific node layers.
pub fn set_layers(&self, main: &MainNodeLayers) {
main.below_body_hover.add(&self.model.hover_area_below_nodes);
}
}

View File

@ -16,7 +16,8 @@ pub mod visibility {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
@ -53,7 +54,8 @@ pub mod visibility2 {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");
@ -97,6 +99,7 @@ pub mod expand {
use super::*;
ensogl::shape! {
above = [compound::rectangle];
alignment = center;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color: Var<color::Rgba> = color_rgba.into();
@ -131,7 +134,8 @@ pub mod freeze {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");
@ -146,7 +150,7 @@ pub mod freeze {
let lock_top = make_ring(&lock_top_radius,&lock_top_radius - &lock_top_width);
let lock_top = lock_top.intersection(BottomHalfPlane().rotate(right_angle*2.0));
let lock_top = lock_top.translate_y(lock_body_radius - &unit * 3.0);
let vertical_bar = Rect((&lock_top_width,&unit * 4.0));
let vertical_bar = Rect((&lock_top_width,&unit * 4.01));
let left_bar = vertical_bar.translate_x(&lock_top_radius-&lock_top_width/2.0);
let right_bar = vertical_bar.translate_x(-&lock_top_radius+&lock_top_width/2.0);
let icon = lock_body + lock_top + left_bar + right_bar;
@ -169,7 +173,8 @@ pub mod skip {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");
@ -206,7 +211,8 @@ pub mod disable_output_context {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");
@ -237,7 +243,8 @@ pub mod enable_output_context {
use super::*;
ensogl::shape! {
alignment = center;
above = [compound::rectangle];
pointer_events_instanced = true;
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");

View File

@ -41,7 +41,7 @@ pub fn initialize_edited_node_animator(
let network = &frp.network();
let out = &frp.output;
let searcher_cam = scene.layers.node_searcher.camera();
let edited_node_cam = scene.layers.edited_node.camera();
let edited_node_cam = model.layers.edited_nodes.camera();
let main_cam = scene.layers.main.camera();
let growth_animation = Animation::new(network);

View File

@ -11,6 +11,7 @@ use crate::node::input::widget::OverrideKey;
use crate::node::profiling;
use crate::view;
use crate::CallWidgetsConfig;
use crate::GraphLayers;
use crate::Type;
use enso_frp as frp;
@ -18,9 +19,7 @@ use enso_frp;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::world::with_context;
use ensogl::gui::cursor;
use ensogl::Animation;
use ensogl_component::text;
use ensogl_component::text::buffer::selection::Selection;
use ensogl_component::text::FromInContextSnapped;
@ -39,7 +38,7 @@ pub const TEXT_OFFSET: f32 = 10.0;
pub const NODE_HEIGHT: f32 = 18.0;
/// Text size used for input area text.
pub const TEXT_SIZE: f32 = 12.0;
pub const TEXT_SIZE: f32 = 11.5;
@ -163,14 +162,13 @@ impl From<node::Expression> for Expression {
/// Internal model of the port area.
#[derive(Debug, display::Object)]
pub struct Model {
display_object: display::object::Instance,
edit_mode_label: text::Text,
label_layer: RefCell<display::scene::layer::WeakLayer>,
edit_mode_label_displayed: Cell<bool>,
expression: RefCell<Expression>,
styles: StyleWatch,
styles_frp: StyleWatchFrp,
widget_tree: widget::Tree,
layers: GraphLayers,
display_object: display::object::Instance,
edit_mode_label: text::Text,
expression: RefCell<Expression>,
styles: StyleWatch,
styles_frp: StyleWatchFrp,
widget_tree: widget::Tree,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
@ -182,23 +180,20 @@ struct WidgetBind {
impl Model {
/// Constructor.
#[profile(Debug)]
pub fn new(app: &Application) -> Self {
pub fn new(app: &Application, layers: &GraphLayers) -> Self {
let display_object = display::object::Instance::new_named("input");
let edit_mode_label = app.new_view::<text::Text>();
let label_layer = RefCell::new(app.display.default_scene.layers.label.downgrade());
let edit_mode_label_displayed = default();
let expression = default();
let styles = StyleWatch::new(&app.display.default_scene.style_sheet);
let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
display_object.add_child(&edit_mode_label);
let widget_tree = widget::Tree::new(app);
with_context(|ctx| ctx.layers.widget.add(&widget_tree));
let layers = layers.clone_ref();
Self {
layers,
display_object,
edit_mode_label,
edit_mode_label_displayed,
label_layer,
expression,
styles,
styles_frp,
@ -214,44 +209,27 @@ impl Model {
let expression = self.expression.borrow();
self.edit_mode_label.set_content(expression.code.clone());
self.display_object.remove_child(&self.widget_tree);
self.show_edit_mode_label();
self.display_object.add_child(&self.edit_mode_label);
// A workaround to fix the cursor position calculation when clicking into the node:
// Since the object is not updated immediately after `add_child`, we need to force
// update the node layout for label to be able to calculate cursor position properly.
self.display_object.update(&scene());
self.edit_mode_label.set_cursor_at_mouse_position();
// [`ensogl_text`] has not been ported to the new focus API yet.
self.edit_mode_label.deprecated_focus();
} else {
self.hide_edit_mode_label();
self.display_object.remove_child(&self.edit_mode_label);
self.display_object.add_child(&self.widget_tree);
self.edit_mode_label.set_content("");
}
}
/// A workaround to fix the cursor position calculation when clicking into the node.
///
/// Using standard [`ObjectOps::add_child`] and [`ObjectOps::remove_child`] for
/// [`edit_mode_label`] does not allow setting the cursor position in the same frame after
/// showing the label, as the position of the display object is not updated yet. To fix this,
/// we hide the label by using the `DETACHED` layer so that its position is always kept up to
/// date. To show the label, one can use [`Self::show_edit_mode_label`]. (which will move it
/// back to the correct scene layer)
fn hide_edit_mode_label(&self) {
self.edit_mode_label_displayed.set(false);
scene().layers.DETACHED.add(&self.edit_mode_label);
}
/// Show the edit mode label by placing it in the correct layer.
/// See [`Self::hide_edit_mode_label`].
fn show_edit_mode_label(&self) {
if let Some(layer) = self.label_layer.borrow().upgrade() {
self.edit_mode_label_displayed.set(true);
self.edit_mode_label.add_to_scene_layer(&layer);
} else {
error!("Cannot show edit mode label, the layer is missing.");
}
self.edit_mode_label.deprecated_set_focus(edit_mode_active);
}
#[profile(Debug)]
fn init(self, app: &Application) -> Self {
let text_color = self.styles.get_color(theme::graph_editor::node::text);
let text_cursor_color: color::Lch = text_color.into();
self.edit_mode_label.set_single_line_mode(true);
app.commands.set_command_enabled(
@ -260,6 +238,7 @@ impl Model {
false,
);
self.edit_mode_label.set_property_default(text_color);
self.edit_mode_label.set_selection_color(text_cursor_color);
self.edit_mode_label.set_property_default(text::Size(TEXT_SIZE));
self.edit_mode_label.remove_all_cursors();
@ -268,21 +247,9 @@ impl Model {
self.widget_tree.set_xy(widgets_origin);
self.edit_mode_label.set_xy(label_origin);
self.set_edit_mode(false);
self.hide_edit_mode_label();
self
}
fn set_label_layer(&self, layer: &display::scene::Layer) {
*self.label_layer.borrow_mut() = layer.downgrade();
// Currently, we never sets label layer when it's already displayed, but - as
// `set_label_layer` is a public method of this component - we're taking extra measures.
// See [`Self::show_edit_mode_label`] and [`Self::hide_edit_mode_label`].
if self.edit_mode_label_displayed.get() {
self.show_edit_mode_label();
}
}
fn set_connections(&self, map: &HashMap<PortId, color::Lcha>) {
self.widget_tree.set_connections(map);
}
@ -325,7 +292,12 @@ impl Model {
/// If the widget tree was marked as dirty since its last update, rebuild it.
fn rebuild_widget_tree_if_dirty(&self) {
let expr = self.expression.borrow();
self.widget_tree.rebuild_tree_if_dirty(&expr.span_tree, &expr.code, &self.styles);
self.widget_tree.rebuild_tree_if_dirty(
&expr.span_tree,
&expr.code,
&self.layers,
&self.styles_frp,
);
}
/// Scan node expressions for all known method calls, for which the language server can provide
@ -354,7 +326,8 @@ impl Model {
self.widget_tree.rebuild_tree(
&new_expression.span_tree,
&new_expression.code,
&self.styles,
&self.layers,
&self.styles_frp,
);
*self.expression.borrow_mut() = new_expression;
@ -362,7 +335,7 @@ impl Model {
/// Get hover shapes for all input ports of a node. Mainly used in tests to manually dispatch
/// mouse events.
pub fn port_hover_shapes(&self) -> Vec<super::port::HoverShape> {
pub fn port_hover_shapes(&self) -> Vec<Rectangle> {
self.widget_tree.port_hover_shapes()
}
}
@ -419,9 +392,8 @@ ensogl::define_endpoints! {
/// colored if the definition type was present.
set_expression_usage_type (ast::Id,Option<Type>),
/// Signal a mouse click in the input area. The click position will be determined by the
/// current pointer position, and the cursor position will be updated in the text area.
mouse_down (),
/// Set the primary (background) and secondary (port) node colors.
set_node_colors ((color::Lcha, color::Lcha)),
}
Output {
@ -472,11 +444,10 @@ pub struct Area {
impl Area {
/// Constructor.
#[profile(Debug)]
pub fn new(app: &Application) -> Self {
let model = Rc::new(Model::new(app));
pub fn new(app: &Application, layers: &GraphLayers) -> Self {
let model = Rc::new(Model::new(app, layers));
let frp = Frp::new();
let network = &frp.network;
let selection_color = Animation::new(network);
frp::extend! { network
init <- source::<()>();
@ -582,22 +553,11 @@ impl Area {
// === View Mode ===
frp.output.source.view_mode <+ frp.set_view_mode;
in_profiling_mode <- frp.view_mode.map(|m| m.is_profiling());
finished <- frp.set_profiling_status.map(|s| s.is_finished());
profiled <- in_profiling_mode && finished;
model.widget_tree.set_read_only <+ frp.set_read_only;
model.widget_tree.set_view_mode <+ frp.set_view_mode;
model.widget_tree.set_profiling_status <+ frp.set_profiling_status;
use theme::code::syntax;
let std_selection_color = model.styles_frp.get_color(syntax::selection);
let profiled_selection_color = model.styles_frp.get_color(syntax::profiling::selection);
selection_color_rgba <- profiled.switch(&std_selection_color,&profiled_selection_color);
selection_color.target <+ selection_color_rgba.map(|c| color::Lcha::from(c));
model.edit_mode_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c));
model.widget_tree.node_base_color <+ frp.set_node_colors._0();
model.widget_tree.node_port_color <+ frp.set_node_colors._1();
}
init.emit(());
@ -623,6 +583,11 @@ impl Area {
})
}
/// An the computed layout size of a specific port.
pub fn port_size(&self, port: PortId) -> Vector2<f32> {
self.model.port_hover_pointer_style(&Switch::On(port)).map_or_default(|s| s.get_size())
}
/// A type of the specified port.
pub fn port_type(&self, port: PortId) -> Option<Type> {
self.model.port_type(port)
@ -632,9 +597,4 @@ impl Area {
pub fn port_crumbs(&self, port: PortId) -> Option<Crumbs> {
self.model.expression.borrow().ports_map.get(&port).cloned()
}
/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.model.set_label_layer(layer);
}
}

View File

@ -6,15 +6,15 @@ use crate::component::node::input::widget::ConfigContext;
use crate::component::node::input::widget::DynConfig;
use crate::component::node::input::widget::DynWidget;
use crate::component::node::input::widget::EdgeData;
use crate::component::node::input::widget::WidgetsFrp;
use enso_frp as frp;
use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::layer::LayerSymbolPartition;
use ensogl::display::shape;
use ensogl::display::shape::compound::rectangle;
use ensogl::display::shape::Rectangle;
use ensogl::display::world::with_context;
use span_tree::PortId;
@ -33,7 +33,7 @@ pub const PORT_PADDING_X: f32 = 4.0;
const HOVER_PADDING_X: f32 = 2.0;
/// The minimum size of the port visual area.
pub const BASE_PORT_HEIGHT: f32 = 18.0;
pub const BASE_PORT_HEIGHT: f32 = 24.0;
/// The minimum size of the port hover area.
const BASE_PORT_HOVER_HEIGHT: f32 = 16.0;
@ -44,64 +44,6 @@ const PRIMARY_PORT_HOVER_PADDING_Y: f32 = (crate::node::HEIGHT - BASE_PORT_HOVER
// ===========================
// === Shapes / PortLayers ===
// ===========================
type PortShape = shape::compound::rectangle::Rectangle;
type PortShapeView = shape::compound::rectangle::shape::Shape;
/// Shape used for handling mouse events in the port, such as hovering or dropping an edge.
pub type HoverShape = shape::compound::rectangle::Rectangle;
type HoverShapeView = shape::compound::rectangle::shape::Shape;
/// An scene extension that maintains layer partitions for port shapes. It is shared by all ports in
/// the scene. The port selection and hover shapes are partitioned by span tree depth, so that the
/// ports deeper in the tree will always be displayed on top. For hover layers, that gives them
/// priority to receive mouse events.
#[derive(Clone, CloneRef)]
struct PortLayers {
port_layer: display::scene::Layer,
hover_layer: display::scene::Layer,
partitions: Rc<
RefCell<Vec<(LayerSymbolPartition<PortShapeView>, LayerSymbolPartition<HoverShapeView>)>>,
>,
}
impl display::scene::Extension for PortLayers {
fn init(scene: &display::Scene) -> Self {
let port_layer = scene.layers.port.clone_ref();
let hover_layer = scene.layers.port_hover.clone_ref();
Self { port_layer, hover_layer, partitions: default() }
}
}
impl PortLayers {
/// Add a display object to the partition at given depth, effectively setting its display order.
/// If the partition does not exist yet, it will be created.
fn add_to_partition(
&self,
port: &display::object::Instance,
hover: &display::object::Instance,
depth: usize,
) {
let mut partitions = self.partitions.borrow_mut();
if partitions.len() <= depth {
partitions.resize_with(depth + 1, || {
(
self.port_layer.create_symbol_partition("input port"),
self.hover_layer.create_symbol_partition("input port hover"),
)
})
}
let (port_partition, hover_partition) = &partitions[depth];
port_partition.add(port);
hover_partition.add(hover);
}
}
// ============
// === Port ===
// ============
@ -111,47 +53,43 @@ impl PortLayers {
#[derive(Debug, display::Object)]
pub struct Port {
/// Drop source must be kept at the top of the struct, so it will be dropped first.
_on_cleanup: frp::DropSource,
port_id: frp::Source<PortId>,
_on_cleanup: frp::DropSource,
port_id: frp::Source<PortId>,
#[display_object]
port_root: display::object::Instance,
widget_root: display::object::Instance,
widget: DynWidget,
port_shape: PortShape,
hover_shape: HoverShape,
/// Last set tree depth of the port. Allows skipping layout update when the depth has not
/// changed during reconfiguration.
current_depth: usize,
port_root: display::object::Instance,
widget_root: display::object::Instance,
widget: DynWidget,
port_shape: Rectangle,
hover_shape: Rectangle,
}
impl Port {
/// Create a new port for given widget. The widget will be placed as a child of the port's root
/// display object, and its layout size will be used to determine the port's size.
pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self {
pub fn new(widget: DynWidget, ctx: &ConfigContext) -> Self {
let port_root = display::object::Instance::new_named("Port");
let widget_root = widget.display_object().clone_ref();
let port_shape = PortShape::new();
let hover_shape = HoverShape::new();
port_shape.set_corner_radius_max().set_pointer_events(false);
let port_shape = Rectangle();
let hover_shape = Rectangle();
port_shape.set_pointer_events(false).set_corner_radius_max();
hover_shape.set_pointer_events(true).set_color(shape::INVISIBLE_HOVER_COLOR);
ctx.layers.hover.add(&hover_shape);
port_root.add_child(&widget_root);
widget_root.set_margin_left(0.0);
port_shape
.set_size_y(BASE_PORT_HEIGHT)
.allow_grow()
.set_margin_vh(0.0, -PORT_PADDING_X)
.set_margin_xy((-PORT_PADDING_X, 0.0))
.set_alignment_left_center();
port_root.add_child(&port_shape);
hover_shape.set_size_y(BASE_PORT_HOVER_HEIGHT).allow_grow().set_alignment_left_center();
let layers = app.display.default_scene.extension::<PortLayers>();
layers.add_to_partition(port_shape.display_object(), hover_shape.display_object(), 0);
let mouse_enter = hover_shape.on_event::<mouse::Enter>();
let mouse_leave = hover_shape.on_event::<mouse::Leave>();
let mouse_down = hover_shape.on_event::<mouse::Down>();
let frp = ctx.frp();
if frp.set_ports_visible.value() {
port_root.add_child(&hover_shape);
}
@ -194,7 +132,6 @@ impl Port {
widget_root,
port_root,
port_id,
current_depth: 0,
}
}
@ -208,7 +145,9 @@ impl Port {
config: &DynConfig,
ctx: ConfigContext,
pad_x_override: Option<f32>,
port_hover_layer: &LayerSymbolPartition<rectangle::Shape>,
) {
port_hover_layer.add(&self.hover_shape);
match ctx.span_node.port_id {
Some(id) => self.port_id.emit(id),
None => error!("Port widget created on node with no port ID assigned."),
@ -224,12 +163,16 @@ impl Port {
/// port's visible shape from the display hierarchy.
fn set_connected(&self, status: Option<EdgeData>) {
match status {
Some(data) => {
self.port_root.add_child(&self.port_shape);
self.port_shape.color.set(color::Rgba::from(data.color).into())
}
None => {
self.port_root.remove_child(&self.port_shape);
// Put the port shape of a disconnected node into the detached layer, instead of
// removing it from the display hierarchy. This is required for the port shape
// position and size to be calculated for disconnected ports, as that information
// is used when hovering over the port with a detached edge.
with_context(|ctx| ctx.layers.DETACHED.add(&self.port_shape));
}
Some(data) => {
self.port_shape.set_color(data.color.into());
with_context(|ctx| ctx.layers.DETACHED.remove(&self.port_shape));
}
};
}
@ -245,15 +188,6 @@ impl Port {
}
fn set_port_layout(&mut self, ctx: &ConfigContext, margin_x: f32) {
let node_depth = ctx.span_node.crumbs.len();
if self.current_depth != node_depth {
self.current_depth = node_depth;
let layers = ctx.app().display.default_scene.extension::<PortLayers>();
let port_shape = self.port_shape.display_object();
let hover_shape = self.hover_shape.display_object();
layers.add_to_partition(port_shape, hover_shape, node_depth);
}
let is_primary = ctx.info.nesting_level.is_primary();
let margin_y = if is_primary { PRIMARY_PORT_HOVER_PADDING_Y } else { 0.0 };
@ -263,7 +197,7 @@ impl Port {
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);
self.hover_shape.set_margin_xy((-margin_x, -margin_y));
}
}
@ -286,7 +220,12 @@ impl Port {
}
/// Get the port's hover shape. Used for testing to simulate mouse events.
pub fn hover_shape(&self) -> &HoverShape {
pub fn hover_shape(&self) -> &Rectangle {
&self.hover_shape
}
/// Get the port's visual shape.
pub fn visual_shape(&self) -> &Rectangle {
&self.port_shape
}
}

View File

@ -47,22 +47,30 @@ use crate::prelude::*;
use crate::component::node::input::area::NODE_HEIGHT;
use crate::component::node::input::area::TEXT_OFFSET;
use crate::component::node::input::port::Port;
use crate::display::shape::Rectangle;
use crate::layers::CommonLayers;
use crate::GraphLayers;
use enso_frp as frp;
use enso_text as text;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::shape::StyleWatch;
use ensogl::display::scene;
use ensogl::display::shape::StyleWatchFrp;
use ensogl::display::style::FromTheme;
use ensogl::display::Scene;
use ensogl::gui::cursor;
use ensogl_component::drop_down::DropdownValue;
use span_tree::node::Ref as SpanRef;
use span_tree::PortId;
use span_tree::TagValue;
use std::any::TypeId;
use text::index::Byte;
/// A prelude module imported in all widget modules.
pub(super) mod prelude {
pub use super::Choice;
pub use super::ConfigContext;
@ -76,6 +84,15 @@ pub(super) mod prelude {
pub use super::TreeNode;
pub use super::WidgetIdentity;
pub use super::WidgetsFrp;
pub use ensogl::control::io::mouse;
pub use ensogl::data::color;
pub use ensogl::display;
pub use ensogl::display::object;
pub use ensogl::display::shape::Rectangle;
pub use ensogl::display::shape::StyleWatchFrp;
pub use ensogl::display::style::FromTheme;
pub use ensogl_hardcoded_theme as theme;
pub use span_tree::node::Ref as SpanRef;
}
@ -105,6 +122,8 @@ ensogl::define_endpoints_2! {
set_view_mode (crate::view::Mode),
set_profiling_status (crate::node::profiling::Status),
set_disabled (bool),
node_base_color (color::Lcha),
node_port_color (color::Lcha),
}
Output {
value_changed (span_tree::Crumbs, Option<ImString>),
@ -191,6 +210,29 @@ pub enum Score {
Perfect,
}
impl Score {
/// A score from hard boolean condition. Either the widget matches perfectly, or it is a
/// complete mismatch. Useful for more "technical" widgets that are mostly implementation
/// details of the IDE.
pub fn only_if(condition: bool) -> Self {
if condition {
Score::Perfect
} else {
Score::Mismatch
}
}
/// Conditionally declare that the widget can be used if the condition is true, but only if it
/// is explicitly requested using an override.
pub fn allow_override_if(condition: bool) -> Self {
if condition {
Score::OnlyOverride
} else {
Score::Mismatch
}
}
}
/// Generate implementation for [`DynWidget`] enum and its associated [`Config`] enum. Those enums
/// are used to represent any possible widget kind and its configuration.
macro_rules! define_widget_modules(
@ -248,14 +290,7 @@ macro_rules! define_widget_modules(
/// Create default configuration of the widget kind contained within this flag.
fn default_config(&self, ctx: &ConfigContext) -> Configuration {
match self {
$(&DynKindFlags::$name => {
let config = $module::Widget::default_config(ctx);
Configuration {
display: config.display,
has_port: config.has_port,
kind: config.kind.into(),
}
},)*
$(&DynKindFlags::$name => $module::Widget::default_config(ctx).into_dyn(),)*
_ => panic!("No widget kind specified.")
}
}
@ -285,6 +320,12 @@ macro_rules! define_widget_modules(
}
}
fn name(&self) -> &'static str {
match self {
$(DynWidget::$name(_) => stringify!($name),)*
}
}
pub(super) fn configure(&mut self, config: &DynConfig, ctx: ConfigContext) {
match (self, config) {
$((DynWidget::$name(model), DynConfig::$name(config)) => {
@ -324,6 +365,8 @@ macro_rules! define_widget_modules(
);
define_widget_modules! {
/// A widget for top-level Enso method calls. Displays an icon.
Method method,
/// A widget for selecting a single value from a list of available options.
SingleChoice single_choice,
/// A widget for managing a list of values - adding, removing or reordering them.
@ -332,8 +375,17 @@ define_widget_modules! {
InsertionPoint insertion_point,
/// Default span tree traversal widget.
Hierarchy hierarchy,
/// Dedicated widget for '_' symbol in argument position.
Blank blank,
/// Default widget that only displays text.
Label label,
/// Separating line between top-level argument widgets. Can only be assigned through override,
/// which is currently done in hierarchy widget.
Separator separator,
/// Displays the argument name next to its value widget. Can only be assigned through override,
/// which is currently done in separator widget.
ArgumentName argument_name,
}
// =====================
@ -379,10 +431,7 @@ impl Configuration {
}
let matched_kind = best_match.map_or(DynKindFlags::Label, |(kind, _)| kind);
let mut config = matched_kind.default_config(ctx);
config.has_port = config.has_port || ctx.info.connection.is_some();
config
matched_kind.default_config(ctx)
}
/// An insertion point that always has a port.
@ -404,6 +453,16 @@ impl<KindConfig> Configuration<KindConfig> {
fn inert(kind: KindConfig) -> Self {
Self::maybe_with_port(kind, false)
}
/// Convert this configuration into a dynamic version.
pub fn into_dyn(self) -> Configuration
where KindConfig: Into<DynConfig> {
Configuration {
display: self.display,
has_port: self.has_port,
kind: self.kind.into(),
}
}
}
/// Widget display mode. Determines when the widget should be expanded.
@ -476,9 +535,14 @@ impl DropdownValue for Choice {
/// Widget FRP endpoints that can be used by widget views, and go straight to the root.
#[derive(Debug, Clone, CloneRef)]
pub struct WidgetsFrp {
pub(super) node_base_color: frp::Sampler<color::Lcha>,
pub(super) node_port_color: frp::Sampler<color::Lcha>,
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>,
/// Whether the widget should allow user interaction, either mouse hover or click. A
/// combination of `set_read_only`, `set_edit_ready_mode` and `set_ports_visible` signals.
pub(super) allow_interaction: frp::Sampler<bool>,
pub(super) set_view_mode: frp::Sampler<crate::view::Mode>,
pub(super) set_profiling_status: frp::Sampler<crate::node::profiling::Status>,
pub(super) hovered_port_children: frp::Sampler<HashSet<WidgetIdentity>>,
@ -549,12 +613,18 @@ impl Tree {
set_read_only <- frp.set_read_only.sampler();
set_view_mode <- frp.set_view_mode.sampler();
set_profiling_status <- frp.set_profiling_status.sampler();
node_base_color <- frp.node_base_color.sampler();
node_port_color <- frp.node_port_color.sampler();
on_port_hover <- any(...);
on_port_press <- any(...);
frp.private.output.on_port_hover <+ on_port_hover;
frp.private.output.on_port_press <+ on_port_press;
allow_interaction <- all_with3(
&set_edit_ready_mode, &set_read_only, &set_ports_visible,
|edit, read_only, ports_visible| !(*edit || *read_only || *ports_visible)
).sampler();
port_hover_chain_dirty <- all(&on_port_hover, &frp.on_rebuild_finished)._0().debounce();
hovered_port_children <- port_hover_chain_dirty.map(
f!([model] (port) port.into_on().map_or_default(|id| model.port_child_widgets(id)))
@ -567,9 +637,12 @@ impl Tree {
let pointer_style = frp.private.output.pointer_style.clone_ref();
let connected_port_updated = frp.private.output.connected_port_updated.clone_ref();
let widgets_frp = WidgetsFrp {
node_base_color,
node_port_color,
set_ports_visible,
set_edit_ready_mode,
set_read_only,
allow_interaction,
set_view_mode,
set_profiling_status,
transfer_ownership,
@ -621,10 +694,11 @@ impl Tree {
&self,
tree: &span_tree::SpanTree,
node_expression: &str,
styles: &StyleWatch,
layers: &GraphLayers,
styles: &StyleWatchFrp,
) {
if self.model.tree_dirty.get() {
self.rebuild_tree(tree, node_expression, styles);
self.rebuild_tree(tree, node_expression, layers, styles);
}
}
@ -636,10 +710,18 @@ impl Tree {
&self,
tree: &span_tree::SpanTree,
node_expression: &str,
styles: &StyleWatch,
layers: &GraphLayers,
styles: &StyleWatchFrp,
) {
self.model.rebuild_tree(self.widgets_frp.clone_ref(), tree, node_expression, styles);
self.model.rebuild_tree(
self.widgets_frp.clone_ref(),
tree,
node_expression,
layers,
styles,
);
self.frp.private.output.on_rebuild_finished.emit(());
trace!("Widget tree:\n{:?}", self.pretty_printer());
}
/// Get the root display object of the widget port for given span tree node. Not all nodes must
@ -649,7 +731,7 @@ impl Tree {
}
/// Get hover shapes for all ports in the tree. Used in tests to manually dispatch mouse events.
pub fn port_hover_shapes(&self) -> Vec<super::port::HoverShape> {
pub fn port_hover_shapes(&self) -> Vec<Rectangle> {
let nodes = self.model.nodes_map.borrow();
self.model
.hierarchy
@ -665,6 +747,59 @@ impl Tree {
self.frp.private.output.marked_dirty_sync.emit(());
}
}
/// Wrap the widget into a pretty-printing adapter that implements `Debug` and prints the widget
/// hierarchy. See [`Tree::debug_print`] method for more details.
///
/// Note that this printer emits multi-line output. In order for those lines to be properly
/// aligned, it should be always printed on a new line.
pub fn pretty_printer(&self) -> WidgetTreePrettyPrint<'_> {
WidgetTreePrettyPrint { tree: self }
}
/// Get pretty-printed representation of this widget tree for debugging purposes.
fn debug_print(&self) -> String {
let mut result = String::new();
let hierarchy = self.model.hierarchy.borrow();
let Some(root) = hierarchy.first() else { return "<EMPTY>".to_string() };
self.print_subtree(&mut result, "", root.identity);
result
}
fn print_subtree(&self, dst: &mut String, indent: &str, pointer: WidgetIdentity) {
dst.push_str(indent);
match self.model.nodes_map.borrow().get(&pointer) {
Some(entry) => {
dst.push_str(entry.node.widget().name());
if entry.node.port().is_some() {
dst.push('*');
}
}
None => {
dst.push_str("<MISSING>");
}
}
dst.push('\n');
let indent = format!("{indent} ");
self.model.iter_children(pointer).for_each(|child| self.print_subtree(dst, &indent, child));
}
}
// === Pretty printing debug adapter ===
/// Debug adapter used for pretty-printing the widget tree. Can be used to print the widget tree
/// hierarchy. This printer is normally too verbose to be a default `Debug` implementation of
/// `Tree`, so it is hidden behind a separate adapter and can be chosen by calling
/// `tree.pretty_printer()`.
pub struct WidgetTreePrettyPrint<'a> {
tree: &'a Tree,
}
impl<'a> Debug for WidgetTreePrettyPrint<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
let printed = self.tree.debug_print();
f.write_str(&printed)
}
}
@ -888,7 +1023,7 @@ impl TreeModel {
start..start + total_descendants
});
std::iter::from_fn(move || {
iter::from_fn(move || {
let index = total_range.next()?;
let entry = hierarchy[index];
// Skip all descendants of the child. The range is now at the next direct child.
@ -926,7 +1061,8 @@ impl TreeModel {
frp: WidgetsFrp,
tree: &span_tree::SpanTree,
node_expression: &str,
styles: &StyleWatch,
layers: &GraphLayers,
styles: &StyleWatchFrp,
) {
self.tree_dirty.set(false);
let app = self.app.clone();
@ -940,11 +1076,14 @@ impl TreeModel {
let mut hierarchy = self.hierarchy.take();
hierarchy.clear();
let shared_extension = scene().extension::<SceneSharedExtension>();
let mut builder = TreeBuilder {
app,
frp,
node_disabled,
node_expression,
layers,
styles,
override_map: &override_map,
connected_map: &connected_map,
@ -958,6 +1097,7 @@ impl TreeModel {
last_ast_depth: default(),
extensions: default(),
node_settings: default(),
shared: &shared_extension.shared,
};
let child = builder.child_widget(tree.root_ref(), default());
@ -1020,10 +1160,12 @@ pub struct NodeInfo {
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,
manage_margins: bool,
/// Whether the widget wants to not have the automatic margin applied.
skip_self_margin: 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>,
custom_child_port_hover_padding: Option<f32>,
}
/// A collection of common data used by all widgets and ports in the widget tree during
@ -1037,6 +1179,11 @@ pub struct ConfigContext<'a, 'b> {
/// Additional state associated with configured widget tree node, such as its depth, connection
/// status or parent node information.
pub(super) info: NodeInfo,
/// The layer partitions that should be used for all of the widget's visual and hover areas. By
/// default, the widget root object is automatically placed on the visual partition. The hover
/// partition needs to be manually assigned to suitable display objects within the widget
/// implementation.
pub layers: CommonLayers,
/// The length of tree extensions vector before the widget was configured. Used to determine
/// which extensions were added by the widget parents, and which are new.
parent_extensions_len: usize,
@ -1063,16 +1210,36 @@ impl<'a, 'b> ConfigContext<'a, 'b> {
&self.builder.node_expression[range]
}
/// Get the `StyleWatch` used by this node.
pub fn styles(&self) -> &StyleWatch {
/// Get the `StyleWatchFrp` used by this node.
pub fn styles(&self) -> &StyleWatchFrp {
self.builder.styles
}
/// Get a style sampler of typed style object. The core FRP nodes of this style object are
/// shared across all widget instances in the scene. Using this method is preferred over
/// manually calling [`FromTheme::from_theme`] in combination with [`ConfigContext::styles`].
pub fn cached_style<T: FromTheme + Any>(&self, network: &frp::Network) -> frp::Stream<T> {
let sampler = self.builder.shared.cached_style::<T>();
// For each widget, create a dedicated FRP node that will be initialized after its FRP
// network has been fully connected.
let per_widget = network.any_mut("cached_style");
per_widget.attach(&sampler);
// Initialize and re-emit the last sampler value in the next tick. Using direct on_event and
// emit to avoid creating unnecessary additional FRP nodes.
use frp::stream::EventEmitter;
network.store(&frp::microtasks::next_microtask(
f!([sampler, per_widget] per_widget.emit_event(&default(), &sampler.value())),
));
per_widget.into()
}
/// Set an extension object of specified type at the current tree position. Any descendant
/// widget will be able to access it, as long as it can name its type. This allows for
/// configure-time communication between any widgets inside the widget tree.
pub fn set_extension<T: Any>(&mut self, val: T) {
let id = std::any::TypeId::of::<T>();
let id = TypeId::of::<T>();
match self.self_extension_index_by_type(id) {
Some(idx) => *self.builder.extensions[idx].downcast_mut().unwrap() = val,
None => {
@ -1087,7 +1254,7 @@ impl<'a, 'b> ConfigContext<'a, 'b> {
///
/// See also: [`ConfigContext::get_extension_or_default`], [`ConfigContext::modify_extension`].
pub fn get_extension<T: Any>(&self) -> Option<&T> {
self.any_extension_index_by_type(std::any::TypeId::of::<T>())
self.any_extension_index_by_type(TypeId::of::<T>())
.map(|idx| self.builder.extensions[idx].downcast_ref().unwrap())
}
@ -1105,7 +1272,7 @@ impl<'a, 'b> ConfigContext<'a, 'b> {
/// See also: [`ConfigContext::get_extension`].
pub fn modify_extension<T>(&mut self, f: impl FnOnce(&mut T))
where T: Any + Default + Clone {
match self.any_extension_index_by_type(std::any::TypeId::of::<T>()) {
match self.any_extension_index_by_type(TypeId::of::<T>()) {
// This extension has been created by this widget, so we can modify it directly.
Some(idx) if idx >= self.parent_extensions_len => {
f(self.builder.extensions[idx].downcast_mut().unwrap());
@ -1125,11 +1292,11 @@ impl<'a, 'b> ConfigContext<'a, 'b> {
}
}
fn any_extension_index_by_type(&self, id: std::any::TypeId) -> Option<usize> {
fn any_extension_index_by_type(&self, id: TypeId) -> Option<usize> {
self.builder.extensions.iter().rposition(|ext| ext.deref().type_id() == id)
}
fn self_extension_index_by_type(&self, id: std::any::TypeId) -> Option<usize> {
fn self_extension_index_by_type(&self, id: TypeId) -> Option<usize> {
let self_extensions = &self.builder.extensions[self.parent_extensions_len..];
self_extensions.iter().rposition(|ext| ext.deref().type_id() == id)
}
@ -1323,7 +1490,8 @@ struct TreeBuilder<'a> {
frp: WidgetsFrp,
node_disabled: bool,
node_expression: &'a str,
styles: &'a StyleWatch,
layers: &'a GraphLayers,
styles: &'a StyleWatchFrp,
/// A list of widget overrides configured on the widget tree. It is persistent between tree
/// builds, and cannot be modified during the tree building process.
override_map: &'a HashMap<OverrideKey, Configuration>,
@ -1342,6 +1510,7 @@ struct TreeBuilder<'a> {
node_settings: NodeSettings,
last_ast_depth: usize,
extensions: Vec<Box<dyn Any>>,
shared: &'a SceneShared,
}
impl<'a> TreeBuilder<'a> {
@ -1352,6 +1521,13 @@ impl<'a> TreeBuilder<'a> {
self.node_settings.manage_margins = true;
}
/// Signal to the builder that this widget manages its own margin. This will prevent the
/// builder from automatically adding margins to the widget based on the offset from previous
/// span.
pub fn manage_margin(&mut self) {
self.node_settings.skip_self_margin = true;
}
/// Set an additional config override for widgets that might be built in the future within the
/// same tree build process. Takes precedence over overrides specified externally. This is
/// useful for applying overrides conditionally, e.g. only when a specific dropdown choice is
@ -1363,7 +1539,7 @@ impl<'a> TreeBuilder<'a> {
/// 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;
self.node_settings.custom_child_port_hover_padding = padding;
}
/// Create a new child widget, along with its whole subtree. The widget type will be
@ -1435,6 +1611,9 @@ impl<'a> TreeBuilder<'a> {
});
let disabled = self.node_disabled;
// TODO: We always use `main_nodes` here right now, as widgets are never visible when the
// node in edit mode. Once this changes, we will need to use conditionally use `edit_nodes`.
let info = NodeInfo {
identity: widget_id,
insertion_index,
@ -1456,14 +1635,28 @@ impl<'a> TreeBuilder<'a> {
let disallowed_configs = ptr_usage.used_configs;
let parent_extensions_len = self.extensions.len();
let layers = self.layers.main_nodes.layers_for_widgets_at_depth(depth);
let ctx = ConfigContext {
builder: &mut *self,
span_node,
info: info.clone(),
parent_extensions_len,
layers: layers.clone(),
};
let config_override = || {
let mut select_configuration_override = || {
let is_applicable = |ctx: &ConfigContext, cfg: &Configuration| {
let flag = cfg.kind.flag();
!disallowed_configs.contains(flag)
&& !matches!(flag.match_node(ctx), Score::Mismatch)
};
if let Some(config) = configuration.filter(|c| is_applicable(&ctx, c)) {
return Some(Cow::Borrowed(config));
}
let kind = &ctx.span_node.kind;
let key = OverrideKey {
call_id: kind.call_id()?,
@ -1472,17 +1665,14 @@ impl<'a> TreeBuilder<'a> {
let local_override = ctx.builder.local_overrides.remove(&key);
let override_map = &ctx.builder.override_map;
let is_applicable = |cfg: &&Configuration| {
let flag = cfg.kind.flag();
!disallowed_configs.contains(flag)
&& !matches!(flag.match_node(&ctx), Score::Mismatch)
};
let local = local_override.filter(|c| is_applicable(&c)).map(Cow::Owned);
local.or_else(|| override_map.get(&key).filter(is_applicable).map(Cow::Borrowed))
let local = local_override.filter(|c| is_applicable(&ctx, c)).map(Cow::Owned);
local.or_else(|| {
override_map.get(&key).filter(|c| is_applicable(&ctx, c)).map(Cow::Borrowed)
})
};
let configuration = match configuration.map(Cow::Borrowed).or_else(config_override) {
let configuration = match select_configuration_override() {
Some(config) => config,
None => Cow::Owned(Configuration::infer_from_context(&ctx, disallowed_configs)),
};
@ -1495,10 +1685,10 @@ impl<'a> TreeBuilder<'a> {
let widget_has_port =
ptr_usage.request_port(&widget_id, ctx.span_node.port_id, can_assign_port);
let port_pad = this.node_settings.custom_port_hover_padding;
let port_pad = this.node_settings.custom_child_port_hover_padding;
let old_node = this.old_nodes.remove(&widget_id).map(|e| e.node);
let parent_info = std::mem::replace(&mut this.parent_info, Some(info.clone()));
let saved_node_settings = std::mem::take(&mut this.node_settings);
let parent_info = mem::replace(&mut this.parent_info, Some(info.clone()));
let saved_node_settings = mem::take(&mut this.node_settings);
// Widget creation/update can recurse into the builder. All borrows must be dropped
@ -1508,14 +1698,13 @@ impl<'a> TreeBuilder<'a> {
// `configure` call has been done, so that the next sibling node will receive correct parent
// data.
let child_node = if widget_has_port {
let app = ctx.app();
let frp = ctx.frp();
let mut port = match old_node {
Some(TreeNode::Port(port)) => port,
Some(TreeNode::Widget(widget)) => Port::new(widget, app, frp),
None => Port::new(DynWidget::new(&configuration.kind, &ctx), app, frp),
Some(TreeNode::Widget(widget)) => Port::new(widget, &ctx),
None => Port::new(DynWidget::new(&configuration.kind, &ctx), &ctx),
};
port.configure(&configuration.kind, ctx, port_pad);
let port_hover_layer = ctx.builder.layers.main_nodes.port_hover_layer(depth);
port.configure(&configuration.kind, ctx, port_pad, &port_hover_layer);
TreeNode::Port(port)
} else {
let mut widget = match old_node {
@ -1536,11 +1725,14 @@ impl<'a> TreeBuilder<'a> {
self.parent_info = parent_info;
self.last_ast_depth = parent_last_ast_depth;
self.extensions.truncate(parent_extensions_len);
let skip_self_margin = self.node_settings.skip_self_margin;
self.node_settings = saved_node_settings;
let child_root = child_node.display_object().clone();
layers.visual.add(&child_root);
if !self.node_settings.manage_margins {
if !(skip_self_margin || self.node_settings.manage_margins) {
// Apply left margin to the widget, based on its offset relative to the previous
// sibling.
let offset = match () {
@ -1585,3 +1777,67 @@ struct Child {
#[deref]
pub root_object: display::object::Instance,
}
// ===================
// === SceneShared ===
// ===================
/// A set of cached objects shared between all widget trees within a given scene. It is currently
/// used to cache all style objects of common types created from within widget instances. That way
/// we can avoid a cost of creating dedicated style FRP nodes for each widget instance.
#[derive(Debug)]
struct SceneShared {
/// An `StyleWatchFrp` instance used by all style objects inside all widget trees within the
/// scene. Using the same instance for all style objects allows it to cache all internal FRP
/// nodes, which would otherwise be created for each style object separately.
cached_styles_frp: StyleWatchFrp,
/// FRP Network shared by all cached style FRP nodes.
///
/// NOTE: Any FRP nodes created within this network will have a lifetime of the whole scene. It
/// is very important to not create any per-widget-instance nodes in it, as that would lead to
/// a memory leak. Use this network only for shared, intentionally long-lived nodes.
network: frp::Network,
/// All shared state used by widgets, including shared style objects. Indexable by type.
objects: RefCell<HashMap<TypeId, Box<dyn Any>>>,
}
/// A scene extension holding [`SceneShared`] object.
#[derive(Debug, Clone, CloneRef)]
struct SceneSharedExtension {
shared: Rc<SceneShared>,
}
impl scene::Extension for SceneSharedExtension {
fn init(scene: &Scene) -> Self {
let shared = SceneShared {
cached_styles_frp: StyleWatchFrp::new(&scene.style_sheet),
network: frp::Network::new("widget::SceneShared"),
objects: default(),
};
Self { shared: Rc::new(shared) }
}
}
impl SceneShared {
/// Retrieve a shared style object that implements `FromTheme`. All calls using the same type
/// will receive a `clone_ref` of the same object instance.
///
/// NOTE: When an existing style instance is returned, its `update` stream is not guaranteed
/// to emit an event in next microtask. The caller must not rely on it.
fn cached_style<T: FromTheme>(&self) -> frp::Sampler<T> {
self.get_or_insert_with(|| T::from_theme(&self.network, &self.cached_styles_frp))
.clone_ref()
}
fn get_or_insert_with<T: Any>(&self, with: impl FnOnce() -> T) -> RefMut<T> {
RefMut::map(self.objects.borrow_mut(), |objects| {
objects
.entry(TypeId::of::<T>())
.or_insert_with(|| Box::new(with()))
.downcast_mut::<T>()
.expect("TypeId key should always be matching stored object type.")
})
}
}

View File

@ -0,0 +1,113 @@
//! A wrapper widget for top-level arguments. Displays the argument name next to the argument
//! widget. It is always requested as a child of a separator widget. This widget's node matching
//! rules determine whether or not the dedicated argument name label will be displayed next to the
//! separator.
use super::prelude::*;
use crate::prelude::*;
use crate::node::input::area::TEXT_SIZE;
use ensogl_component::text;
/// =============
/// === Style ===
/// =============
#[derive(Clone, Debug, Default, PartialEq, FromTheme)]
#[base_path = "theme::widget::argument_name"]
struct Style {
color: color::Rgba,
margin: f32,
weight: f32,
}
// ==============
// === Widget ===
// ==============
/// Separator widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// The widget that prints the argument name next to the argument value.
#[derive(Debug, display::Object)]
pub struct Widget {
display_object: object::Instance,
arg_label_wrapper: object::Instance,
arg_name: frp::Source<ImString>,
}
impl SpanWidget for Widget {
type Config = Config;
fn match_node(ctx: &ConfigContext) -> Score {
let kind = &ctx.span_node.kind;
let matches = ctx.info.subtree_connection.is_none() && kind.is_prefix_argument();
Score::allow_override_if(matches)
}
fn default_config(_: &ConfigContext) -> Configuration<Self::Config> {
Configuration::always(Config)
}
fn new(_: &Config, ctx: &ConfigContext) -> Self {
let root = object::Instance::new_named("widget::ArgumentName");
root.use_auto_layout()
.set_row_flow()
.set_children_alignment_left_center()
.justify_content_center_y();
let arg_label_wrapper = object::Instance::new_named("arg_label_wrapper");
let arg_label = text::Text::new(ctx.app());
arg_label.set_property_default(text::Size(TEXT_SIZE));
arg_label_wrapper.add_child(&arg_label);
let network = &root.network;
let style = ctx.cached_style::<Style>(network);
frp::extend! { network
eval style([arg_label, arg_label_wrapper] (style) {
arg_label.set_property_default(style.color);
let weight = text::Weight::from(style.weight as u16);
arg_label.set_property_default(weight);
arg_label_wrapper.set_margin_right(style.margin);
});
arg_name <- source();
arg_label.set_content <+ arg_name.on_change();
label_width <- arg_label.width.on_change();
label_height <- arg_label.height.on_change();
eval label_width((w) arg_label_wrapper.set_size_x(*w); );
eval label_height([arg_label_wrapper, arg_label] (h) {
arg_label_wrapper.set_size_y(*h);
arg_label.set_y(*h);
});
}
Self { display_object: root, arg_label_wrapper, arg_name }
}
fn configure(&mut self, _: &Config, ctx: ConfigContext) {
ctx.builder.manage_margin();
ctx.builder.manage_child_margins();
let level = ctx.info.nesting_level;
match ctx.span_node.kind.argument_name() {
Some(arg_name) if !arg_name.is_empty() => {
self.arg_name.emit(arg_name);
let child = ctx.builder.child_widget(ctx.span_node, level);
self.display_object
.replace_children(&[&self.arg_label_wrapper, &child.root_object]);
}
_ => {
let child = ctx.builder.child_widget(ctx.span_node, level);
self.display_object.replace_children(&[&child.root_object]);
}
}
}
}

View File

@ -0,0 +1,78 @@
//! A dedicated widget for A blank ('_') token in an argument position.
//!
//! See also [`span_tree::node::InsertionPoint`].
use super::prelude::*;
use crate::prelude::*;
use span_tree::node::Kind;
/// =============
/// === Style ===
/// =============
#[derive(Clone, Copy, Debug, Default, PartialEq, FromTheme)]
#[base_path = "theme::widget::blank"]
struct Style {
size: Vector2,
margin_sides: f32,
margin_top: f32,
corner_radius: f32,
color: color::Rgba,
}
// ==============
// === Widget ===
// ==============
/// Blank widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// Blank widget. Displays a stylized underscore shape.
#[derive(Debug, display::Object)]
pub struct Widget {
display_object: object::Instance,
_rect: Rectangle,
}
impl SpanWidget for Widget {
type Config = Config;
fn match_node(ctx: &ConfigContext) -> Score {
let kind = &ctx.span_node.kind;
let matches =
matches!(kind, Kind::Argument(..) | Kind::Root if ctx.span_expression() == "_");
Score::only_if(matches)
}
fn default_config(_: &ConfigContext) -> Configuration<Self::Config> {
Configuration::always(Config)
}
fn new(_: &Config, ctx: &ConfigContext) -> Self {
let root = object::Instance::new_named("widget::Blank");
root.use_auto_layout();
let rect = Rectangle();
root.add_child(&rect);
let network = &root.network;
let style = ctx.cached_style::<Style>(network);
frp::extend! { network
eval style((style)
rect.set_color(style.color)
.set_size(style.size)
.set_corner_radius(style.corner_radius)
.set_margin_trbl(style.margin_top, style.margin_sides, 0.0, style.margin_sides);
);
}
Self { display_object: root, _rect: rect }
}
fn configure(&mut self, _: &Config, _: ConfigContext) {}
}

View File

@ -1,38 +1,28 @@
//! Definition of default hierarchy widget. This widget expands each child of its span tree into
//! a new widget.
use crate::component::node::input::widget::prelude::*;
use super::prelude::*;
use crate::prelude::*;
use ensogl::display;
use ensogl::display::object;
use span_tree::node::Kind;
// ===============
// === Aliases ===
// ===============
/// A collection type used to collect a temporary list of node child widget roots, so that they can
/// be passed to `replace_children` method in one go. Avoids allocation for small number of
/// children, but also doesn't consume too much stack memory to avoid stack overflow in deep widget
/// hierarchies.
pub type CollectedChildren = SmallVec<[object::Instance; 4]>;
// =================
// === Hierarchy ===
// =================
// ==============
// === Widget ===
// ==============
/// Label widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// Hierarchy widget. This widget expands each child of its span tree into a new widget.
#[derive(Clone, Debug, display::Object)]
#[derive(Debug, display::Object)]
pub struct Widget {
display_object: object::Instance,
/// A temporary list of display object children to insert. Reused across reconfigurations to
/// avoid allocations.
children_vec: SmallVec<[object::Instance; 4]>,
}
impl SpanWidget for Widget {
@ -46,22 +36,37 @@ impl SpanWidget for Widget {
}
fn default_config(ctx: &ConfigContext) -> Configuration<Self::Config> {
let has_port = !ctx.span_node.kind.is_named_argument();
Configuration::maybe_with_port(default(), has_port)
let has_port = !matches!(
ctx.span_node.kind,
Kind::NamedArgument | Kind::ChainedPrefix | Kind::BlockLine
);
Configuration::maybe_with_port(Config, has_port)
}
fn new(_: &Config, _: &ConfigContext) -> Self {
let display_object = object::Instance::new_named("widget::Hierarchy");
display_object.use_auto_layout();
display_object.set_children_alignment_left_center().justify_content_center_y();
Self { display_object }
Self { display_object, children_vec: default() }
}
fn configure(&mut self, _: &Config, ctx: ConfigContext) {
let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument());
let children_iter = ctx.span_node.children_iter();
let children =
children_iter.map(|node| ctx.builder.child_widget(node, child_level).root_object);
self.display_object.replace_children(&children.collect::<CollectedChildren>());
let child_level = ctx.info.nesting_level.next_if(ctx.span_node.kind.is_prefix_argument());
let is_primary = ctx.info.nesting_level.is_primary();
// When this is a top-level (primary) hierarchy widget, request children widgets to have
// separators. The "separator" widget is a wrapper which will display the default argument
// widget next to the separating line. This configuration will only be applied to nodes that
// the separator accepts, which is limited to arguments in prefix chains.
let separator_config = super::separator::Widget::default_config(&ctx).into_dyn();
let child_config = is_primary.then_some(&separator_config);
self.children_vec.clear();
self.children_vec.extend(ctx.span_node.children_iter().map(|node| {
ctx.builder.child_widget_of_type(node, child_level, child_config).root_object
}));
self.display_object.replace_children(&self.children_vec);
self.children_vec.clear();
}
}

View File

@ -5,44 +5,41 @@
//!
//! See also [`span_tree::node::InsertionPoint`].
use crate::component::node::input::widget::prelude::*;
use super::prelude::*;
use crate::prelude::*;
use ensogl::display;
use ensogl::display::object;
// ======================
// === InsertionPoint ===
// ======================
// ==============
// === Widget ===
// ==============
/// Insertion point widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// Insertion point widget. Displays nothing when not connected.
#[derive(Clone, Debug, display::Object)]
#[derive(Debug, display::Object)]
pub struct Widget {
root: object::Instance,
display_object: object::Instance,
}
impl SpanWidget for Widget {
type Config = Config;
fn match_node(ctx: &ConfigContext) -> Score {
ctx.span_node.is_positional_insertion_point().then_val_or_default(Score::Perfect)
Score::only_if(ctx.span_node.is_positional_insertion_point())
}
fn default_config(_: &ConfigContext) -> Configuration<Self::Config> {
Configuration::inert(default())
Configuration::inert(Config)
}
fn new(_: &Config, _: &ConfigContext) -> Self {
let root = object::Instance::new_named("widget::InsertionPoint");
root.set_size(Vector2::<f32>::zero());
Self { root }
let display_object = object::Instance::new_named("widget::InsertionPoint");
display_object.set_size(Vector2::<f32>::zero());
Self { display_object }
}
fn configure(&mut self, _: &Config, _: ConfigContext) {}

View File

@ -1,43 +1,56 @@
//! Definition of static text label widget.
use crate::component::node::input::widget::prelude::*;
use super::prelude::*;
use crate::prelude::*;
use crate::component::node::input::area::TEXT_SIZE;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::object;
use ensogl::display::shape::StyleWatch;
use ensogl_component::text;
use ensogl_hardcoded_theme as theme;
use span_tree::node::Kind;
// =============
// === Label ===
// =============
/// =============
/// === Style ===
/// =============
#[derive(Clone, Debug, Default, PartialEq, FromTheme)]
#[base_path = "theme::widget::label"]
struct Style {
base_color: color::Rgba,
base_weight: f32,
connected_color: color::Rgba,
connected_weight: f32,
disabled_color: color::Rgba,
disabled_weight: f32,
placeholder_color: color::Rgba,
placeholder_weight: f32,
}
// ==============
// === Widget ===
// ==============
/// Label widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
ensogl::define_endpoints_2! {
Input {
content(ImString),
text_color(ColorState),
text_weight(text::Weight),
text_weight(Option<text::Weight>),
text_sdf_weight(f32),
}
}
/// Label widget. Always displays the span tree node's expression as text.
#[derive(Clone, Debug, display::Object)]
#[derive(Debug, display::Object)]
pub struct Widget {
frp: Frp,
#[display_object]
root: object::Instance,
frp: Frp,
display_object: object::Instance,
#[allow(dead_code)]
label: text::Text,
label: text::Text,
}
impl SpanWidget for Widget {
@ -48,9 +61,11 @@ impl SpanWidget for Widget {
}
fn default_config(ctx: &ConfigContext) -> Configuration<Self::Config> {
use span_tree::node::Kind;
let has_port = !matches!(ctx.span_node.kind, Kind::Token | Kind::NamedArgument);
Configuration::maybe_with_port(default(), has_port)
let kind = &ctx.span_node.kind;
let expr = ctx.span_expression();
let not_port = matches!(kind, Kind::Token | Kind::NamedArgument | Kind::Access)
|| matches!(kind, Kind::Operation if expr == ".");
Configuration::maybe_with_port(Config, !not_port)
}
fn new(_: &Config, ctx: &ConfigContext) -> Self {
@ -58,52 +73,65 @@ impl SpanWidget for Widget {
// baseline is properly aligned to center and lines up with other labels in the line.
let app = ctx.app();
let widgets_frp = ctx.frp();
let layers = &ctx.app().display.default_scene.layers;
let root = object::Instance::new_named("widget::Label");
let display_object = object::Instance::new_named("widget::Label");
let label = text::Text::new(app);
label.set_property_default(text::Size(TEXT_SIZE));
layers.label.add(&label);
root.add_child(&label);
display_object.add_child(&label);
let frp = Frp::new();
let network = &frp.network;
let styles = ctx.styles();
let color_anim = color::Animation::new(network);
let weight_anim = ensogl::Animation::new(network);
weight_anim.precision.emit(0.001);
let style = ctx.cached_style::<Style>(network);
frp::extend! { network
let id = ctx.info.identity;
parent_port_hovered <- widgets_frp.hovered_port_children.map(move |h| h.contains(&id));
label_color <- frp.text_color.all_with4(
&parent_port_hovered, &widgets_frp.set_view_mode, &widgets_frp.set_profiling_status,
f!([styles](state, hovered, mode, status) {
state.to_color(*hovered, *mode, *status, &styles)
})
);
parent_port_hovered <- parent_port_hovered.on_change();
text_color <- frp.text_color.on_change();
label_color <- all_with3(
&style, &text_color, &parent_port_hovered,
|style, state, hovered| state.to_color(*hovered, style)
).debounce().on_change();
color_anim.target <+ label_color;
eval color_anim.value((color) label.set_property_default(color));
label_color <- label_color.on_change();
label_weight <- frp.text_weight.on_change();
eval label_color((color) label.set_property_default(color));
weight_anim.target <+ frp.text_sdf_weight.on_change();
eval weight_anim.value((weight) label.set_property_default(text::SdfWeight(*weight)));
label_weight <- all_with4(
&frp.text_color,
&frp.text_weight,
&style,
&parent_port_hovered,
|state, weight, style, hovered| state.to_weight(*hovered, *weight, style)
).debounce().on_change();
eval label_weight((weight) label.set_property_default(weight));
content_change <- frp.content.on_change();
eval content_change((content) label.set_content(content));
width <- label.width.on_change();
height <- label.height.on_change();
eval width((w) root.set_size_x(*w); );
eval height([root, label] (h) {
root.set_size_y(*h);
eval width((w) display_object.set_size_x(*w); );
eval height([display_object, label] (h) {
display_object.set_size_y(*h);
label.set_y(*h);
});
}
Self { frp, root, label }
Self { frp, display_object, label }
}
fn configure(&mut self, _: &Config, ctx: ConfigContext) {
let is_placeholder = ctx.span_node.is_expected_argument();
let content = if is_placeholder {
ctx.span_node.kind.argument_name().unwrap_or_default()
let expr = ctx.span_expression();
let content = if is_placeholder || ctx.info.connection.is_some() {
ctx.span_node.kind.argument_name().unwrap_or(expr)
} else {
ctx.span_expression()
expr
};
let is_connected = ctx.info.subtree_connection.is_some();
@ -111,17 +139,13 @@ impl SpanWidget for Widget {
_ if is_connected => ColorState::Connected,
_ if ctx.info.disabled => ColorState::Disabled,
_ if is_placeholder => ColorState::Placeholder,
_ => {
let span_node_type = ctx.span_node.kind.tp();
let usage_type = ctx.info.usage_type.clone();
let ty = usage_type.or_else(|| span_node_type.map(|t| crate::Type(t.into())));
let color = crate::type_coloring::compute_for_code(ty.as_ref(), ctx.styles());
ColorState::FromType(color)
}
_ => ColorState::Base,
};
let ext = ctx.get_extension_or_default::<Extension>();
let text_weight = if ext.bold { text::Weight::Bold } else { text::Weight::Normal };
let bold = ext.bold || is_placeholder;
let text_weight = bold.then_some(text::Weight::Bold);
let input = &self.frp.public.input;
input.content.emit(content);
input.text_color.emit(color_state);
@ -150,38 +174,41 @@ pub struct Extension {
/// Configured color state of a label widget.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ColorState {
#[default]
Base,
Connected,
Disabled,
Placeholder,
FromType(color::Lcha),
}
impl ColorState {
fn to_color(
fn to_weight(
self,
is_hovered: bool,
view_mode: crate::view::Mode,
status: crate::node::profiling::Status,
styles: &StyleWatch,
) -> color::Lcha {
use theme::code::syntax;
let profiling_mode = view_mode.is_profiling();
let profiled = profiling_mode && status.is_finished();
let color_path = match self {
_ if is_hovered => theme::code::types::selected,
ColorState::Connected => theme::code::types::selected,
ColorState::Disabled if profiled => syntax::profiling::disabled,
ColorState::Placeholder if profiled => syntax::profiling::expected,
ColorState::Disabled => syntax::disabled,
ColorState::Placeholder => syntax::expected,
ColorState::FromType(_) if profiled => syntax::profiling::base,
ColorState::FromType(_) if profiling_mode => syntax::base,
ColorState::FromType(typed) => return typed,
};
styles.get_color(color_path).into()
weight_override: Option<text::Weight>,
style: &Style,
) -> text::Weight {
weight_override.unwrap_or_else(|| {
let weight_num = match self {
_ if is_hovered => style.connected_weight,
ColorState::Base => style.base_weight,
ColorState::Connected => style.connected_weight,
ColorState::Disabled => style.disabled_weight,
ColorState::Placeholder => style.placeholder_weight,
};
text::Weight::from(weight_num as u16)
})
}
fn to_color(self, is_hovered: bool, style: &Style) -> color::Lcha {
match self {
_ if is_hovered => style.connected_color,
ColorState::Base => style.base_color,
ColorState::Connected => style.connected_color,
ColorState::Disabled => style.disabled_color,
ColorState::Placeholder => style.placeholder_color,
}
.into()
}
}

View File

@ -3,15 +3,13 @@
// FIXME[ao]: This code miss important documentation (e.g. for `Element`, `DragData` and `ListItem`)
// and may be unreadable at some places. It should be improved in several next debugging PRs.
use crate::component::node::input::widget::prelude::*;
use super::prelude::*;
use crate::prelude::*;
use crate::component::node::input::area::TEXT_SIZE;
use crate::layers::CommonLayers;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl::display::object;
use ensogl::display::world::with_context;
use ensogl::Animation;
use ensogl_component::list_editor::ListEditor;
use span_tree::node::Kind;
@ -44,7 +42,7 @@ struct Element {
display_object: object::Instance,
content: object::Instance,
#[allow(dead_code)]
background: display::shape::Rectangle,
background: Rectangle,
expr_range: Range<usize>,
item_crumb: usize,
alive: Option<()>,
@ -111,14 +109,14 @@ impl ListItem {
}
impl Element {
fn new() -> Self {
fn new(layers: &CommonLayers) -> Self {
let display_object = object::Instance::new_named("Element");
let content = object::Instance::new_named("Content");
let background = display::shape::Rectangle::new();
let background = Rectangle::new();
background.set_color(display::shape::INVISIBLE_HOVER_COLOR);
background.allow_grow().set_alignment_left_center();
content.use_auto_layout().set_children_alignment_left_center();
with_context(|ctx| ctx.layers.label.add(&background));
layers.hover.add(&background);
display_object.replace_children(&[background.display_object(), &content]);
Self {
display_object,
@ -132,7 +130,7 @@ impl Element {
}
/// A model for the vector editor widget.
#[derive(Clone, CloneRef, Debug, display::Object)]
#[derive(Debug, display::Object)]
pub struct Widget {
display_object: object::Instance,
network: frp::Network,
@ -226,7 +224,7 @@ struct Model {
/// It is stored in the model to allow animating its margins within the FRP network.
append_insertion_point: Option<object::Instance>,
#[allow(dead_code)]
background: display::shape::Rectangle,
background: Rectangle,
elements: HashMap<ElementIdentity, Element>,
default_value: DefaultValue,
expression: String,
@ -242,10 +240,10 @@ impl Model {
list.gap(ITEMS_GAP);
list.set_size_hug_y(TEXT_SIZE).allow_grow_y();
display_object.use_auto_layout().set_children_alignment_left_center();
let background = display::shape::Rectangle::new();
let background = 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);
background.set_margin_xy((-LIST_HOVER_MARGIN, 0.0));
list.add_child(&background);
Self {
@ -357,7 +355,7 @@ impl Model {
Some(new_items_range.map_or(i..i + 1, |r| r.start..i + 1));
}
let element = entry.or_insert_with(Element::new);
let element = entry.or_insert_with(|| Element::new(&ctx.layers));
set_margins(&insert, -INSERTION_OFFSET, INSERTION_OFFSET);
element.alive = Some(());
element.item_crumb = index;
@ -526,7 +524,7 @@ impl Model {
}
}
fn set_margins(object: &display::object::Instance, left: f32, right: f32) {
fn set_margins(object: &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();

View File

@ -0,0 +1,108 @@
//! A widget representing a top-level method call. Displays node background. Instead of `self`
//! argument, it displays an icon as a first port.
use super::prelude::*;
use crate::prelude::*;
use icons::any::View as AnyIcon;
use icons::component_icons::Id as IconId;
use icons::SIZE;
use ide_view_component_list_panel_icons as icons;
// =================
// === Constants ===
// =================
/// Distance between the icon and next widget.
const ICON_GAP: f32 = 10.0;
// ==============
// === Method ===
// ===============
/// Method widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// A widget for selecting a single value from a list of available options. The options can be
/// provided as a static list of strings from argument `tag_values`, or as a dynamic expression.
#[derive(Debug, display::Object)]
#[allow(dead_code)]
pub struct Widget {
display_object: object::Instance,
icon_wrapper: object::Instance,
icon_id: IconId,
icon: AnyIcon,
}
/// Extension which existence in given subtree prevents the widget from being created again.
struct MethodAlreadyInTree;
impl SpanWidget for Widget {
type Config = Config;
fn default_config(_: &ConfigContext) -> Configuration<Self::Config> {
Configuration::always(Config)
}
fn match_node(ctx: &ConfigContext) -> Score {
let matches = ctx.span_node.application.is_some()
&& ctx.get_extension::<MethodAlreadyInTree>().is_none();
Score::only_if(matches)
}
fn new(_: &Config, _: &ConfigContext) -> Self {
// ╭─display_object──────────────────╮
// │ ╭ icon ─╮ ╭ content ──────────╮ │
// │ │ │ │ │ │
// │ │ │ │ │ │
// │ ╰───────╯ ╰───────────────────╯ │
// ╰─────────────────────────────────╯
let display_object = object::Instance::new_named("widget::Method");
display_object
.use_auto_layout()
.set_row_flow()
.set_gap_x(ICON_GAP)
.set_children_alignment_left_center()
.justify_content_center_y();
let icon_wrapper = object::Instance::new_named("icon_wrapper");
icon_wrapper.set_size((SIZE, SIZE));
let mut this = Self { display_object, icon_wrapper, icon_id: default(), icon: default() };
this.set_icon(IconId::default());
this
}
fn configure(&mut self, _: &Config, mut ctx: ConfigContext) {
ctx.set_extension(MethodAlreadyInTree);
let icon_id = ctx
.span_node
.application
.as_ref()
.and_then(|app| app.icon_name.as_ref())
.and_then(|name| name.parse::<IconId>().ok())
.unwrap_or(IconId::Method);
if icon_id != self.icon_id {
self.set_icon(icon_id);
}
let child = ctx.builder.child_widget(ctx.span_node, ctx.info.nesting_level);
self.display_object.replace_children(&[&self.icon_wrapper, &child.root_object]);
}
}
impl Widget {
fn set_icon(&mut self, icon_id: IconId) {
self.icon_id = icon_id;
self.icon = icon_id.cached_view();
self.icon.set_size((SIZE, SIZE));
self.icon.set_xy((SIZE / 2.0, SIZE / 2.0));
self.icon.r_component.set(Vector4(1.0, 1.0, 1.0, 1.0));
self.icon_wrapper.replace_children(&[self.icon.display_object()]);
}
}

View File

@ -0,0 +1,109 @@
//! A wrapper widget for top-level arguments. Displays a separating line next to an argument widget.
//! Handles named argument nodes, creating a child widget only for the argument value.
use super::prelude::*;
use crate::prelude::*;
use crate::component::node::HEIGHT as NODE_HEIGHT;
/// =============
/// === Style ===
/// =============
#[derive(Clone, Debug, Default, PartialEq, FromTheme)]
#[base_path = "theme::widget::separator"]
struct Style {
color: color::Rgba,
margin: f32,
width: f32,
}
// ==============
// === Widget ===
// ==============
/// Separator widget configuration options.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Config;
/// The widget that displays a separating line between top-level argument nodes, as well as prints
/// the argument name when applicable. The name is only printed in cases where it would not be
/// repeated as a label in the child widget.
#[derive(Debug, display::Object)]
pub struct Widget {
display_object: object::Instance,
separator: Rectangle,
}
impl SpanWidget for Widget {
type Config = Config;
fn match_node(ctx: &ConfigContext) -> Score {
let kind = &ctx.span_node.kind;
let matches = ctx.info.nesting_level.is_primary()
&& (kind.is_expected_argument()
|| kind.is_prefix_argument()
|| kind.is_named_argument());
Score::allow_override_if(matches)
}
fn default_config(_: &ConfigContext) -> Configuration<Self::Config> {
Configuration::inert(Config)
}
fn new(_: &Config, ctx: &ConfigContext) -> Self {
let display_object = object::Instance::new_named("widget::Separator");
display_object
.use_auto_layout()
.set_row_flow()
.set_children_alignment_left_center()
.justify_content_center_y();
let separator = Rectangle();
let network = &display_object.network;
let style = ctx.cached_style::<Style>(network);
frp::extend! { network
eval style([separator] (style) {
separator.set_size((style.width, NODE_HEIGHT));
separator.set_margin_xy((style.margin, -NODE_HEIGHT / 2.0));
separator.set_color(style.color);
});
}
Self { display_object, separator }
}
fn configure(&mut self, _: &Config, ctx: ConfigContext) {
ctx.builder.manage_margin();
ctx.builder.manage_child_margins();
let level = ctx.info.nesting_level;
// Request separator children widgets to have argument name displayed. This configuration
// will only apply to nodes that need it, as the `argument_name` widget will only match
// relevant nodes.
let argument_name_config = super::argument_name::Widget::default_config(&ctx).into_dyn();
let child_config = Some(&argument_name_config);
// NOTE: In order to have proper port behavior for named arguments, the separator widget
// must handle the named argument nodes, and create a child only for the value part. It is
// the argument value node that receives the port, and we want that port to be displayed
// over the argument name as well.
let child = if ctx.span_node.kind.is_named_argument() {
let mut child_iter = ctx.span_node.children_iter();
let _name_node = child_iter.next();
let _token = child_iter.next();
let Some(arg_node) = child_iter.next() else { return };
ctx.builder.child_widget_of_type(arg_node, level, child_config)
} else {
ctx.builder.child_widget_of_type(ctx.span_node, level, child_config)
};
let separator = self.separator.display_object();
self.display_object.replace_children(&[separator, &child.root_object]);
}
}

View File

@ -1,58 +1,35 @@
//! Definition of single choice widget.
use crate::component::node::input::widget::prelude::*;
use super::prelude::*;
use crate::prelude::*;
use crate::component::node;
use crate::component::node::input::widget::label;
use enso_frp as frp;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl::display::object::event;
use ensogl::display::shape::SimpleTriangle;
use ensogl_component::button::prelude::INVISIBLE_HOVER_COLOR;
use ensogl_component::drop_down::Dropdown;
use ensogl_hardcoded_theme as theme;
// =================
// === Constants ===
// =================
/// =============
/// === Style ===
/// =============
/// Height of the activation triangle shape.
pub const ACTIVATION_SHAPE_SIZE: Vector2 = Vector2(15.0, 11.0);
/// Gap between activation shape and the dropdown widget content.
pub const ACTIVATION_SHAPE_GAP: f32 = 5.0;
/// Distance between the top of the dropdown list and the bottom of the widget.
const DROPDOWN_Y_OFFSET: f32 = -20.0;
/// Maximum allowed size of the dropdown list. If the list needs to be longer or wider than allowed
/// by these values, it will receive a scroll bar.
const DROPDOWN_MAX_SIZE: Vector2 = Vector2(300.0, 500.0);
// ======================
// === Triangle Shape ===
// ======================
/// Temporary dropdown activation shape definition.
pub mod triangle {
use super::*;
ensogl::shape! {
above = [display::shape::compound::rectangle::shape];
alignment = left_bottom;
(style:Style, color:Vector4) {
let size = Var::canvas_size();
let radius = 1.0.px();
let shrink = &radius * 2.0;
let shape = Triangle(size.x() - &shrink, size.y() - &shrink)
.flip_y()
.grow(radius);
shape.fill(color).into()
}
}
#[derive(Clone, Debug, Default, PartialEq, FromTheme)]
#[base_path = "theme::widget::single_choice"]
struct Style {
triangle_base: color::Lcha,
triangle_connected: color::Lcha,
triangle_size: Vector2,
triangle_offset: Vector2,
/// Offset between the top of the dropdown list and the bottom left corner of the widget.
dropdown_offset: Vector2,
/// Maximum allowed size of the dropdown list. If the list needs to be longer or wider than
/// allowed by these values, it will receive a scroll bar.
dropdown_max_size: Vector2,
dropdown_tint: color::Lcha,
}
@ -111,12 +88,13 @@ ensogl::define_endpoints_2! {
#[allow(dead_code)]
pub struct Widget {
config_frp: Frp,
display_object: display::object::Instance,
content_wrapper: display::object::Instance,
dropdown_wrapper: display::object::Instance,
label_wrapper: display::object::Instance,
display_object: object::Instance,
hover_area: Rectangle,
content_wrapper: object::Instance,
dropdown_wrapper: object::Instance,
triangle_wrapper: object::Instance,
dropdown: Rc<RefCell<LazyDropdown>>,
activation_shape: triangle::View,
triangle: SimpleTriangle,
}
impl SpanWidget for Widget {
@ -130,7 +108,7 @@ impl SpanWidget for Widget {
fn default_config(ctx: &ConfigContext) -> Configuration<Self::Config> {
let kind = &ctx.span_node.kind;
let label = kind.name().map(Into::into);
let label = kind.argument_name().map(Into::into);
let tags = kind.tag_values().unwrap_or_default();
let choices = Rc::new(tags.iter().map(Choice::from).collect());
let default_config = Config { label, choices, arguments: default() };
@ -141,45 +119,36 @@ impl SpanWidget for Widget {
let app = ctx.app();
// ╭─display_object────────────────────╮
// │╭─content_wrapper─────────────────╮│
// ││ ╭ shape ╮ ╭ label_wrapper ────╮ ││
// ││ │ │ │ │ ││
// ││ │ │ │ │ ││
// ││ ╰───────╯ ╰───────────────────╯ ││
// ││ ││
// ││ ││
// │╰─────────────────────────────────╯│
// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
// │ ◎ dropdown_wrapper size=0 │
// │ size=0 ◎ triangle_wrapper │
// │ ◎ dropdown_wrapper │
// ╰───────────────────────────────────╯
let activation_shape = triangle::View::new();
activation_shape.set_size(ACTIVATION_SHAPE_SIZE);
let layers = &app.display.default_scene.layers;
layers.label.add(&activation_shape);
let display_object = display::object::Instance::new_named("widget::SingleChoice");
let content_wrapper = display_object.new_child();
content_wrapper.add_child(&activation_shape);
let label_wrapper = content_wrapper.new_child();
let display_object = object::Instance::new_named("widget::SingleChoice");
let hover_area = Rectangle();
hover_area
.set_color(INVISIBLE_HOVER_COLOR)
.allow_grow_x()
.set_alignment_center()
.set_margin_xy((0.0, -node::HEIGHT / 2.0))
.set_size_y(node::HEIGHT);
display_object.add_child(&hover_area);
let triangle_wrapper = display_object.new_child();
let triangle = SimpleTriangle();
triangle_wrapper.add_child(&triangle);
let dropdown_wrapper = display_object.new_child();
display_object
.use_auto_layout()
.set_column_flow()
.set_children_alignment_left_center()
.justify_content_center_y();
let content_wrapper = display_object.new_child();
content_wrapper
.use_auto_layout()
.set_gap_x(ACTIVATION_SHAPE_GAP)
.set_children_alignment_left_center()
.justify_content_center_y();
label_wrapper
.use_auto_layout()
.set_children_alignment_left_center()
.justify_content_center_y();
dropdown_wrapper.set_size((0.0, 0.0)).set_alignment_left_top();
triangle_wrapper.set_size((0.0, 0.0)).set_alignment_center_bottom();
dropdown_wrapper.set_size((0.0, 0.0)).allow_grow_x().set_alignment_left_bottom();
let config_frp = Frp::new();
let dropdown = LazyDropdown::new(app, &config_frp.network);
@ -188,22 +157,24 @@ impl SpanWidget for Widget {
Self {
config_frp,
display_object,
hover_area,
content_wrapper,
triangle_wrapper,
dropdown_wrapper,
label_wrapper,
dropdown,
activation_shape,
triangle,
}
.init(ctx)
}
fn configure(&mut self, config: &Config, mut ctx: ConfigContext) {
let input = &self.config_frp.public.input;
ctx.layers.hover.add(&self.hover_area);
let has_value = !ctx.span_node.is_insertion_point();
let current_value = has_value.then(|| ctx.span_expression());
let (entry_idx, selected_entry) =
entry_for_current_value(&config.choices, current_value).unzip();
entry_for_current_value(&config.choices[..], current_value).unzip();
input.current_crumbs(ctx.span_node.crumbs.clone());
input.set_entries(config.choices.clone());
@ -227,7 +198,7 @@ impl SpanWidget for Widget {
}
let child = ctx.builder.child_widget(ctx.span_node, ctx.info.nesting_level);
self.label_wrapper.replace_children(&[child.root_object]);
self.content_wrapper.replace_children(&[child.root_object]);
}
}
@ -254,13 +225,18 @@ fn find_call_id(node: &span_tree::Node) -> Option<ast::Id> {
impl Widget {
fn init(self, ctx: &ConfigContext) -> Self {
let is_open = self.init_dropdown_focus(ctx);
let style = ctx.cached_style::<Style>(&self.config_frp.network);
let is_open = self.init_dropdown_focus(ctx, &style);
self.init_dropdown_values(ctx, is_open);
self.init_activation_shape(ctx);
self.init_triangle(ctx, &style);
self
}
fn init_dropdown_focus(&self, ctx: &ConfigContext) -> frp::Stream<bool> {
fn init_dropdown_focus(
&self,
ctx: &ConfigContext,
style: &frp::Stream<Style>,
) -> frp::Stream<bool> {
let widgets_frp = ctx.frp();
let focus_receiver = &self.dropdown_wrapper;
let focus_in = focus_receiver.on_event::<event::FocusIn>();
@ -270,14 +246,17 @@ impl Widget {
let dropdown_frp = &self.dropdown.borrow();
let dropdown_wrapper = &self.dropdown_wrapper;
frp::extend! { network
eval focus_in([dropdown, dropdown_wrapper](_) {
dropdown.borrow_mut().lazy_init(&dropdown_wrapper);
});
_eval <- focus_in.map2(style, f!([dropdown, dropdown_wrapper, style] (_, style_value) {
dropdown.borrow_mut().lazy_init(&dropdown_wrapper, &style, style_value);
}));
readonly_set <- widgets_frp.set_read_only.on_true();
do_open <- focus_in.gate_not(&widgets_frp.set_read_only);
do_close <- any_(focus_out, readonly_set);
is_open <- bool(&do_close, &do_open);
dropdown_frp.set_open <+ is_open.on_change();
dropdown_frp.set_color <+ all_with(style, &widgets_frp.node_base_color,
|style, color| style.dropdown_tint.over(*color)
);
// Close the dropdown after a short delay after selection. Because the dropdown
// value application triggers operations that can introduce a few dropped frames,
@ -318,45 +297,55 @@ impl Widget {
widgets_frp.value_changed <+ dropdown_out_value.map2(&config_frp.current_crumbs,
move |t: &Option<ImString>, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone())
);
};
}
}
fn init_activation_shape(&self, ctx: &ConfigContext) {
fn init_triangle(&self, ctx: &ConfigContext, style: &frp::Stream<Style>) {
let network = &self.config_frp.network;
let config_frp = &self.config_frp;
let widgets_frp = ctx.frp();
let styles = ctx.styles();
let activation_shape = &self.activation_shape;
let display_object = &self.display_object;
let triangle = &self.triangle;
let focus_receiver = &self.dropdown_wrapper;
frp::extend! { network
let id = ctx.info.identity;
parent_port_hovered <- widgets_frp.hovered_port_children.map(move |h| h.contains(&id));
is_connected_or_hovered <- config_frp.is_connected || parent_port_hovered;
activation_shape_theme <- is_connected_or_hovered.map(|is_connected_or_hovered| {
if *is_connected_or_hovered {
Some(theme::widget::activation_shape::connected)
} else {
Some(theme::widget::activation_shape::base)
}
});
activation_shape_theme <- activation_shape_theme.on_change();
eval activation_shape_theme([styles, activation_shape](path) {
if let Some(path) = path {
let color = styles.get_color(path);
activation_shape.color.set(color.into());
}
is_connected <- config_frp.is_connected || parent_port_hovered;
eval *style([triangle] (style) {
let size = style.triangle_size;
triangle.set_xy(style.triangle_offset - Vector2(size.x * 0.5, -size.y));
triangle.set_base_and_altitude(size.x, -size.y);
});
let dot_mouse_down = activation_shape.on_event::<mouse::Down>();
dot_clicked <- dot_mouse_down.filter(mouse::is_primary);
set_focused <- dot_clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused()));
let mouse_down = display_object.on_event::<mouse::Down>();
let mouse_dropdown_down = focus_receiver.on_event::<mouse::Down>();
let mouse_enter = display_object.on_event::<mouse::Enter>();
let mouse_leave = display_object.on_event::<mouse::Leave>();
mouse_dropdown_down_delayed <- mouse_dropdown_down.debounce();
handling_dropdown_down <- bool(&mouse_dropdown_down_delayed, &mouse_dropdown_down);
mouse_down <- mouse_down.gate(&widgets_frp.allow_interaction);
clicked <- mouse_down.filter(mouse::is_primary);
eval clicked([] (event) event.stop_propagation());
clicked <- clicked.gate_not(&handling_dropdown_down);
is_hovered <- bool(&mouse_leave, &mouse_enter).and(&widgets_frp.allow_interaction);
let triangle_color = color::Animation::new(network);
triangle_color.target <+ is_connected.all_with3(style, &is_hovered,
|connected, s, hovered| {
let color = if *connected { s.triangle_connected } else { s.triangle_base };
color.multiply_alpha(if *hovered { 1.0 } else { 0.0 })
}).on_change();
eval triangle_color.value((color) triangle.set_color(color.into()););
set_focused <- clicked.map(f!([focus_receiver](_) !focus_receiver.is_focused()));
eval set_focused([focus_receiver](focus) match focus {
true => focus_receiver.focus(),
false => focus_receiver.blur(),
});
};
}
}
}
@ -400,11 +389,14 @@ struct LazyDropdown {
set_all_entries: frp::Any<Vec<Choice>>,
set_selected_entries: frp::Any<HashSet<Choice>>,
set_open: frp::Any<bool>,
set_color: frp::Any<color::Lcha>,
sampled_set_all_entries: frp::Sampler<Vec<Choice>>,
sampled_set_selected_entries: frp::Sampler<HashSet<Choice>>,
sampled_set_open: frp::Sampler<bool>,
sampled_set_color: frp::Sampler<color::Lcha>,
selected_entries: frp::Any<HashSet<Choice>>,
user_select_action: frp::Any<()>,
network: frp::Network,
dropdown: Option<Dropdown<Choice>>,
}
@ -414,11 +406,13 @@ impl LazyDropdown {
set_all_entries <- any(...);
set_selected_entries <- any(...);
set_open <- any(...);
set_color <- any(...);
selected_entries <- any(...);
user_select_action <- any(...);
sampled_set_all_entries <- set_all_entries.sampler();
sampled_set_selected_entries <- set_selected_entries.sampler();
sampled_set_open <- set_open.sampler();
sampled_set_color <- set_color.sampler();
}
Self {
@ -426,18 +420,26 @@ impl LazyDropdown {
set_all_entries,
set_selected_entries,
set_open,
set_color,
selected_entries,
user_select_action,
sampled_set_all_entries,
sampled_set_selected_entries,
sampled_set_open,
sampled_set_color,
dropdown: None,
network: frp::Network::new("LazyDropdown"),
}
}
/// Perform initialization that actually creates the dropdown. Should be done only once there is
/// a request to open the dropdown.
fn lazy_init(&mut self, parent: &display::object::Instance) {
fn lazy_init(
&mut self,
parent: &object::Instance,
style: &frp::Stream<Style>,
current_style: &Style,
) {
if self.dropdown.is_some() {
return;
}
@ -445,20 +447,31 @@ impl LazyDropdown {
let dropdown = self.dropdown.insert(self.app.new_view::<Dropdown<Choice>>());
parent.add_child(dropdown);
self.app.display.default_scene.layers.above_nodes.add(&*dropdown);
let network = &self.network;
frp::extend! { _network
frp::extend! { network
dropdown.set_all_entries <+ self.sampled_set_all_entries;
dropdown.set_selected_entries <+ self.sampled_set_selected_entries;
dropdown.set_open <+ self.sampled_set_open;
dropdown.set_color <+ self.sampled_set_color;
self.selected_entries <+ dropdown.selected_entries;
self.user_select_action <+ dropdown.user_select_action;
eval* style([dropdown] (style) {
dropdown.set_xy(style.dropdown_offset);
dropdown.set_max_open_size(style.dropdown_max_size);
});
eval_ parent.on_transformed([dropdown, parent] {
dropdown.set_min_open_width(parent.computed_size().x())
});
}
dropdown.set_y(DROPDOWN_Y_OFFSET);
dropdown.set_max_open_size(DROPDOWN_MAX_SIZE);
dropdown.set_xy(current_style.dropdown_offset);
dropdown.set_max_open_size(current_style.dropdown_max_size);
dropdown.set_min_open_width(parent.computed_size().x());
dropdown.allow_deselect_all(true);
dropdown.set_all_entries(self.sampled_set_all_entries.value());
dropdown.set_selected_entries(self.sampled_set_selected_entries.value());
dropdown.set_open(self.sampled_set_open.value());
dropdown.set_color(self.sampled_set_color.value());
}
}

View File

@ -16,6 +16,7 @@ use ensogl::animation::hysteretic::HystereticAnimation;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::shape::Rectangle;
use ensogl::display::shape::StyleWatch;
use ensogl_component::text;
use ensogl_hardcoded_theme as theme;
@ -30,7 +31,6 @@ const HIDE_DELAY_DURATION_MS: f32 = 150.0;
const SHOW_DELAY_DURATION_MS: f32 = 150.0;
// ================
// === SpanTree ===
// ================
@ -112,6 +112,7 @@ ensogl::define_endpoints! {
set_expression (node::Expression),
set_expression_visibility (bool),
set_type_label_visibility (bool),
set_port_color (color::Lcha),
/// Set the expression USAGE type. This is not the definition type, which can be set with
/// `set_expression` instead. In case the usage type is set to None, ports still may be
@ -123,6 +124,7 @@ ensogl::define_endpoints! {
on_port_press (PortId),
on_port_hover (Switch<PortId>),
port_size_multiplier (f32),
port_color (color::Lcha),
body_hover (bool),
type_label_visibility (bool),
expression_label_visibility (bool),
@ -137,6 +139,7 @@ pub struct Model {
app: Application,
display_object: display::object::Instance,
ports: display::object::Instance,
hover_root: display::object::Instance,
port_models: RefCell<Vec<port::Model>>,
label: text::Text,
expression: RefCell<Expression>,
@ -151,6 +154,7 @@ impl Model {
pub fn new(app: &Application, frp: &Frp) -> Self {
let display_object = display::object::Instance::new_named("output");
let ports = display::object::Instance::new();
let hover_root = display::object::Instance::new_named("output hover");
let port_models = default();
let label = app.new_view::<text::Text>();
let id_ports_map = default();
@ -159,10 +163,12 @@ impl Model {
let frp = frp.output.clone_ref();
display_object.add_child(&label);
display_object.add_child(&ports);
display_object.add_child(&hover_root);
Self {
app: app.clone_ref(),
display_object,
ports,
hover_root,
port_models,
label,
expression,
@ -175,12 +181,6 @@ impl Model {
#[profile(Debug)]
fn init(self, app: &Application) -> Self {
// FIXME[WD]: Depth sorting of labels to in front of the mouse pointer. Temporary solution.
// It needs to be more flexible once we have proper depth management.
let scene = &app.display.default_scene;
scene.layers.main.remove(&self.label);
self.label.add_to_scene_layer(&scene.layers.label);
let text_color = self.styles.get_color(theme::graph_editor::node::text);
self.label.set_single_line_mode(true);
app.commands.set_command_enabled(&self.label, "cursor_move_up", false);
@ -188,20 +188,13 @@ impl Model {
self.label.set_property_default(text_color);
self.label.set_property_default(text::Size(input::area::TEXT_SIZE));
self.label.remove_all_cursors();
self.label.set_y(input::area::TEXT_SIZE / 2.0);
self
}
/// Return a list of Node's output ports.
pub fn ports(&self) -> Vec<port::Model> {
self.port_models.borrow().clone()
}
#[profile(Debug)]
fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
/// Return a list of Node's output port hover shapes.
pub fn port_hover_shapes(&self) -> Vec<Rectangle> {
self.port_models.borrow().iter().map(|m| m.shape.hover.clone()).collect()
}
#[profile(Debug)]
@ -228,8 +221,7 @@ impl Model {
let port_models = self.port_models.borrow();
let Some(port) = port_models.get(index) else { return };
let Some(frp) = &port.frp else { return };
frp.set_usage_type(tp);
port.frp.set_usage_type(tp);
}
/// Traverse all span tree nodes that are considered ports.
@ -244,11 +236,6 @@ impl Model {
});
}
#[profile(Debug)]
fn set_size(&self, size: Vector2) {
self.ports.set_x(size.x / 2.0);
}
#[profile(Debug)]
fn set_label_on_new_expression(&self, expression: &Expression) {
self.set_label(expression.code());
@ -288,19 +275,19 @@ impl Model {
node_tp.or_else(|| whole_expr_type.clone())
};
let mut model = port::Model::default();
let span = node.span();
model.index = span.start.into();
model.length = span.size();
let (port_shape, port_frp) =
model.init_shape(&self.app, &self.styles, port_index, port_count);
let index = span.start.into();
let length = span.size();
let model = port::Model::new(&self.app, port_index, port_count, index, length);
let port_frp = &model.frp;
let port_network = &port_frp.network;
let source = &self.frp.source;
let port_id = node.port_id.unwrap_or_default();
frp::extend! { port_network
port_frp.set_size_multiplier <+ self.frp.port_size_multiplier;
// Currently we always use the same color for all ports.
port_frp.set_color <+ self.frp.port_color;
port_frp.set_type_label_visibility <+ self.frp.type_label_visibility;
source.tooltip <+ port_frp.tooltip;
port_frp.set_size <+ self.frp.size;
@ -310,8 +297,11 @@ impl Model {
port_frp.set_type_label_visibility.emit(self.frp.type_label_visibility.value());
port_frp.set_size.emit(self.frp.size.value());
port_frp.set_color.emit(self.frp.port_color.value());
port_frp.set_size_multiplier.emit(self.frp.port_size_multiplier.value());
port_frp.set_definition_type.emit(node_tp);
self.ports.add_child(&port_shape);
self.ports.add_child(&model.shape.root);
self.hover_root.add_child(&model.shape.hover);
models.push(model);
}
});
@ -379,10 +369,10 @@ impl Area {
hysteretic_transition.to_end <+ on_hover_out;
frp.source.port_size_multiplier <+ hysteretic_transition.value;
eval frp.set_size ((t) model.set_size(*t));
frp.source.size <+ frp.set_size;
frp.source.port_color <+ frp.set_port_color;
expr_label_x <- model.label.width.map(|width| -width - input::area::TEXT_OFFSET);
expr_label_x <- frp.set_size.map(|size| size.x + input::area::TEXT_OFFSET);
eval expr_label_x ((x) model.label.set_x(*x));
frp.source.type_label_visibility <+ frp.set_type_label_visibility;
@ -407,7 +397,8 @@ impl Area {
label_color.target_alpha <+ label_alpha_tgt;
label_color_on_change <- label_color.value.sample(&frp.set_expression);
new_label_color <- any(&label_color.value,&label_color_on_change);
eval new_label_color ((color) model.label.set_property(.., color::Rgba::from(color)));
eval new_label_color ((color) model.label.set_property_default(color::Rgba::from(color)));
}
label_color.target_alpha(0.0);
@ -416,9 +407,9 @@ impl Area {
Self { frp, model }
}
/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.model.set_label_layer(layer);
/// Get the display object that is a root of all interactive rectangles in output area.
pub fn hover_root(&self) -> &display::object::Instance {
&self.model.hover_root
}
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
@ -426,7 +417,7 @@ impl Area {
match port {
PortId::Ast(id) => {
let index = *self.model.id_ports_map.borrow().get(&id)?;
self.model.port_models.borrow().get(index)?.frp.as_ref()?.tp.value()
self.model.port_models.borrow().get(index)?.frp.tp.value()
}
_ => None,
}

View File

@ -6,22 +6,15 @@ use enso_text::unit::*;
use crate::application::tooltip;
use crate::application::tooltip::Placement;
use crate::component::node;
use crate::component::type_coloring;
use crate::Type;
use enso_frp as frp;
use ensogl::animation::delayed::DelayedAnimation;
use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::shape::primitive::def::class::ShapeOps;
use ensogl::display::shape::AnyShape;
use ensogl::display::shape::Circle;
use ensogl::display::shape::HalfPlane;
use ensogl::display::shape::PixelDistance;
use ensogl::display::shape::Pixels;
use ensogl::display::shape::Rect;
use ensogl::display::shape::StyleWatch;
use ensogl::display::shape::Var;
use ensogl::display::shape::Rectangle;
use ensogl::gui::text;
use ensogl::Animation;
@ -31,18 +24,20 @@ use ensogl::Animation;
// === Constants ===
// =================
const PORT_SIZE: f32 = 4.0;
const PORT_LINE_WIDTH: f32 = 6.0;
const PORT_OPACITY_HOVERED: f32 = 1.0;
const PORT_OPACITY_NOT_HOVERED: f32 = 0.25;
const SEGMENT_GAP_WIDTH: f32 = 2.0;
const HOVER_AREA_PADDING: f32 = 20.0;
const INFINITE: f32 = 99999.0;
const FULL_TYPE_ONSET_DELAY_MS: f32 = 2000.0;
const LABEL_OFFSET: f32 = 10.0;
const END_CAP_CLIP: f32 = 0.42;
const TOOLTIP_LOCATION: Placement = Placement::Bottom;
// We have currently implemented two possible ways to display the output types of ports on hover:
// as a tooltip next to the mouse coursor or as a label that is fixed right next to the port itself.
// as a tooltip next to the mouse cursor or as a label that is fixed right next to the port itself.
// Right now, there is no final decision, which one we will keep. Therefore, we have the following
// two constants which can be used to turn those methods on or off.
const SHOW_TYPE_AS_TOOLTIP: bool = false;
@ -50,9 +45,9 @@ const SHOW_TYPE_AS_LABEL: bool = true;
// =====================
// === AllPortsShape ===
// =====================
// ==================
// === Shape View ===
// ==================
/// Generic port shape implementation. The shape is of the width of the whole node and is used as a
/// base shape for all port drawing. In case of a multi-port output, the shape is cropped from both
@ -61,366 +56,170 @@ const SHOW_TYPE_AS_LABEL: bool = true;
/// ```text
/// ╭╮ ╭╮
/// │╰────────────────────────────╯│ ▲ height
/// ╰──────────────────────────────╯ ▼ (node_size / 2) + PORT_SIZE
/// ╰──────────────────────────────╯ ▼ (base node size / 2) + PORT_LINE_WIDTH
/// ◄──────────────────────────────►
/// width = node_width + PORT_SIZE
/// total width = node width + PORT_LINE_WIDTH * 2
/// ```
///
/// The corners are rounded with the `radius = inner_radius + port_area_size`. The shape also
/// contains an underlying hover area with a padding defined as `HOVER_AREA_PADDING`.
struct AllPortsShape {
/// The radius of the node, not the outer port radius.
inner_radius: Var<Pixels>,
/// The width of the node, not the outer port area width.
inner_width: Var<Pixels>,
shape: AnyShape,
hover: AnyShape,
}
impl AllPortsShape {
#[profile(Debug)]
fn new(
canvas_width: &Var<Pixels>,
canvas_height: &Var<Pixels>,
size_multiplier: &Var<f32>,
) -> Self {
// === Generic Info ===
let inner_width = canvas_width - HOVER_AREA_PADDING.px() * 2.0;
let inner_height = canvas_height - HOVER_AREA_PADDING.px() * 2.0;
let inner_radius = node::RADIUS.px();
let top_mask = HalfPlane();
// === Main Shape ===
let shrink = 1.px() - 1.px() * size_multiplier;
let port_area_size = PORT_SIZE.px() * size_multiplier;
let port_area_width = &inner_width + (&port_area_size - &shrink) * 2.0;
let port_area_height = &inner_height + (&port_area_size - &shrink) * 2.0;
let outer_radius = &inner_radius + &port_area_size;
let shape = Rect((&port_area_width, &port_area_height));
let shape = shape.corners_radius(outer_radius);
let shape = shape - &top_mask;
let corner_radius = &port_area_size / 2.0;
let corner_offset = &port_area_width / 2.0 - &corner_radius;
let corner = Circle(&corner_radius);
let left_corner = corner.translate_x(-&corner_offset);
let right_corner = corner.translate_x(&corner_offset);
let shape = (shape + left_corner + right_corner).into();
// === Hover Area ===
let hover_radius = &inner_radius + &HOVER_AREA_PADDING.px();
let hover = Rect((canvas_width, canvas_height)).corners_radius(hover_radius);
let hover = (hover - &top_mask).into();
AllPortsShape { inner_radius, inner_width, shape, hover }
}
}
/// The length of the port shape's inner border. (the part that touches the node)
///
/// # Arguments
///
/// * `corner_radius` - The inner radius of the port shape's corner segments. (the round parts)
/// * `width` - The inner width of the port shape. (also the width of the node)
fn shape_border_length<T>(corner_radius: T, width: T) -> T
where T: Clone + Mul<f32, Output = T> + Sub<Output = T> + Add<Output = T> {
let corner_segment_length = corner_segment_length(corner_radius.clone());
let center_segment_length = center_segment_length(corner_radius, width);
center_segment_length + corner_segment_length * 2.0
}
/// The length of the border on the inside of a single corner segment. (the round part at the end of
/// the node)
///
/// # Arguments
///
/// * `corner_radius` - The inner radius of the corner segment.
fn corner_segment_length<T>(corner_radius: T) -> T
where T: Mul<f32, Output = T> {
let corner_circumference = corner_radius * 2.0 * PI;
corner_circumference * 0.25
}
/// The length of the center segment of a port shape. (the straight part in the center of the shape)
///
/// # Arguments
///
/// * `corner_radius` - The inner radius of the port shape's corner segments. (the round parts)
/// * `width` - The inner width of the port shape. (also the width of the node)
fn center_segment_length<T>(corner_radius: T, width: T) -> T
where T: Mul<f32, Output = T> + Sub<Output = T> {
width - corner_radius * 2.0
}
// ======================
// === SinglePortView ===
// ======================
pub use single_port::View as SinglePortView;
/// A single port shape implementation. In contrast to `MultiPortView`, this produces a much faster
/// shader code.
pub mod single_port {
use super::*;
use ensogl::display::shape::*;
ensogl::shape! {
below = [compound::rectangle::shape];
alignment = center;
(style:Style, size_multiplier:f32, opacity:f32, color_rgb:Vector3<f32>) {
let overall_width = Var::<Pixels>::from("input_size.x");
let overall_height = Var::<Pixels>::from("input_size.y");
let ports = AllPortsShape::new(&overall_width,&overall_height,&size_multiplier);
let color = Var::<color::Rgba>::from("srgba(input_color_rgb,input_opacity)");
let shape = ports.shape.fill(color);
let hover = ports.hover.fill(INVISIBLE_HOVER_COLOR);
(shape + hover).into()
}
}
}
// =====================
// === MultiPortView ===
// =====================
use ensogl::animation::animation::delayed::DelayedAnimation;
use ensogl::application::Application;
pub use multi_port::View as MultiPortView;
use std::f32::consts::PI;
/// Implements the shape for a segment of the OutputPort with multiple output ports.
pub mod multi_port {
use super::*;
use ensogl::display::shape::*;
/// Compute the angle perpendicular to the shape border.
fn compute_border_perpendicular_angle(
shape_border_length: &Var<f32>,
corner_segment_length: &Var<f32>,
position: &Var<f32>,
) -> Var<f32> {
// Here we use a trick to use a pseudo-boolean float that is either 0 or 1 to multiply a
// value that should be returned, iff it's case is true. That way we can add return values
// of different "branches" of which exactly one will be non-zero.
// Transform position to be centered in the shape.
// The `distance`s here describe the distance along the shape border, so not straight line
// x coordinate, but the length of the path along the shape.
let center_distance = position - shape_border_length / 2.0;
let center_distance_absolute = center_distance.abs();
let center_distance_sign = center_distance.signum();
let end = shape_border_length / 2.0;
let center_segment_end = &end - corner_segment_length;
let default_rotation = Var::<f32>::from(90.0_f32.to_radians());
// Case 1: The center segment, always zero, so not needed, due to clamping.
// Case 2: The right circle segment.
let relative_position =
(center_distance_absolute - center_segment_end) / corner_segment_length;
let relative_position = relative_position.clamp(0.0.into(), 1.0.into());
let corner_base_rotation = (-90.0_f32).to_radians();
let corner_rotation_delta = relative_position * corner_base_rotation;
corner_rotation_delta * center_distance_sign + default_rotation
}
/// Returns the x position of the crop plane as a fraction of the base shapes center segment.
/// To get the actual x position of the plane, multiply this value by the length of the center
/// segment and apply the appropriate x-offset.
///
/// * `shape_border_length` should be the length of the shapes border path.
/// * `corner_segment_length` should be the quarter circumference of the circles on the sides
/// of base shape.
/// * `position_on_path` should be the position along the shape border (not the pure
/// x-coordinate).
fn calculate_crop_plane_position_relative_to_center_segment(
shape_border_length: &Var<f32>,
corner_segment_length: &Var<f32>,
position_on_path: &Var<f32>,
) -> Var<f32> {
let middle_segment_start_point = corner_segment_length;
let middle_segment_end_point = shape_border_length - corner_segment_length;
// Case 1: The left circle, always 0, achieved through clamping.
// Case 2: The middle segment.
let middle_segment_plane_position_x = (position_on_path - middle_segment_start_point)
/ (&middle_segment_end_point - middle_segment_start_point);
// Case 3: The right circle, always 1, achieved through clamping.
middle_segment_plane_position_x.clamp(0.0.into(), 1.0.into())
}
/// Compute the crop plane at the location of the given port index. Also takes into account an
/// `position_offset` that is given as an offset along the shape boundary.
///
/// The crop plane is a `BottomHalfPlane` that is perpendicular to the border of the shape and
/// can be used to crop the shape at the specified port index.
fn compute_crop_plane(
index: &Var<f32>,
port_num: &Var<f32>,
width: &Var<f32>,
corner_radius: &Var<f32>,
position_offset: &Var<f32>,
) -> AnyShape {
let corner_segment_length = corner_segment_length(corner_radius.clone());
let center_segment_length = center_segment_length(corner_radius.clone(), width.clone());
let shape_border_length = shape_border_length(corner_radius.clone(), width.clone());
let position_relative = index / port_num;
let crop_segment_pos = &position_relative * &shape_border_length + position_offset;
let crop_plane_pos_relative = calculate_crop_plane_position_relative_to_center_segment(
&shape_border_length,
&corner_segment_length,
&crop_segment_pos,
);
let crop_plane_pos = crop_plane_pos_relative * &center_segment_length;
let crop_plane_pos = crop_plane_pos + corner_radius;
let plane_rotation_angle = compute_border_perpendicular_angle(
&shape_border_length,
&corner_segment_length,
&crop_segment_pos,
);
let plane_shape_offset = Var::<Pixels>::from(&crop_plane_pos - width * 0.5);
let crop_shape = BottomHalfPlane();
let crop_shape = crop_shape.rotate(plane_rotation_angle);
let crop_shape = crop_shape.translate_x(plane_shape_offset);
crop_shape.into()
}
ensogl::shape! {
below = [compound::rectangle::shape];
alignment = center;
( style : Style
, size_multiplier : f32
, index : f32
, opacity : f32
, port_count : f32
, padding_left : f32
, padding_right : f32
, color_rgb : Vector3<f32>
) {
let overall_width = Var::<Pixels>::from("input_size.x");
let overall_height = Var::<Pixels>::from("input_size.y");
let ports = AllPortsShape::new(&overall_width,&overall_height,&size_multiplier);
let inner_radius = Var::<f32>::from(ports.inner_radius);
let inner_width = Var::<f32>::from(ports.inner_width);
let next_index = &index + &Var::<f32>::from(1.0);
let left_shape_crop = compute_crop_plane
(&index,&port_count,&inner_width,&inner_radius,&0.0.into());
let right_shape_crop = compute_crop_plane
(&next_index,&port_count,&inner_width,&inner_radius,&0.0.into());
let hover_area = ports.hover.difference(&left_shape_crop);
let hover_area = hover_area.intersection(&right_shape_crop);
let hover_area = hover_area.fill(INVISIBLE_HOVER_COLOR);
let padding_left = Var::<Pixels>::from(padding_left);
let padding_right = Var::<Pixels>::from(padding_right);
let left_shape_crop = left_shape_crop.grow(padding_left);
let right_shape_crop = right_shape_crop.grow(padding_right);
let port_area = ports.shape.difference(&left_shape_crop);
let port_area = port_area.intersection(&right_shape_crop);
let color = Var::<color::Rgba>::from("srgba(input_color_rgb,input_opacity)");
let port_area = port_area.fill(color);
(port_area + hover_area).into()
}
}
}
// ==================
// === Shape View ===
// ==================
/// Abstraction over [`SinglePortView`] and [`MultiPortView`].
#[derive(Clone, CloneRef, Debug)]
/// The corners are rounded to follow the node corner radius. The shape also contains an underlying
/// hover area with a padding defined as `HOVER_AREA_PADDING`.
#[derive(Debug, display::Object)]
#[allow(missing_docs)]
pub enum PortShapeView {
Single(SinglePortView),
Multi(MultiPortView),
pub struct ShapeView {
#[display_object]
pub root: display::object::Instance,
pub main: Rectangle,
/// Interactive shape above the port. Note: It is NOT a child of the `root`. Instead, it is
/// placed within the `hover_root` of the output area.
pub hover: Rectangle,
pub type_label: text::Text,
pub end_cap_left: Option<Rectangle>,
pub end_cap_right: Option<Rectangle>,
pub number_of_ports: usize,
pub port_index: usize,
pub size_multiplier: Cell<f32>,
}
macro_rules! fn_helper {
($( $name:ident($this:ident,$($arg:ident : $arg_tp:ty),*) {$($body1:tt)*} {$($body2:tt)*} )*)
=> {$(
#[allow(unused_variables)]
fn $name(&self, $($arg : $arg_tp),*) {
match self {
Self::Single ($this) => { $($body1)* }
Self::Multi ($this) => { $($body2)* }
}
}
)*};
}
macro_rules! fn_both {
($( $name:ident $args:tt $body:tt )*) => {
fn_helper! {$($name $args $body $body)*}
};
}
macro_rules! fn_multi_only {
($( $name:ident $args:tt $body:tt )*) => {
fn_helper! {$($name $args {{}} $body)*}
};
}
impl PortShapeView {
impl ShapeView {
#[profile(Debug)]
fn new(number_of_ports: usize) -> Self {
if number_of_ports <= 1 {
Self::Single(SinglePortView::new())
} else {
Self::Multi(MultiPortView::new())
fn new(app: &Application, number_of_ports: usize, port_index: usize) -> Self {
let root = display::object::Instance::new_named("Output port shape");
let main = Rectangle();
let type_label = app.new_view::<text::Text>();
type_label.set_y(-LABEL_OFFSET);
root.add_child(&type_label);
// depending on the position of port, keep either the bottom left, bottom right, both or
// neither corners of the main shape.
let is_first = port_index == 0;
let is_last = port_index == number_of_ports - 1;
let main_radius = node::CORNER_RADIUS + PORT_LINE_WIDTH;
match (is_first, is_last) {
(true, true) => main.keep_bottom_half().set_corner_radius(main_radius),
(true, false) => main.keep_bottom_left_quarter().set_corner_radius(main_radius),
(false, true) => main.keep_bottom_right_quarter().set_corner_radius(main_radius),
(false, false) => main.keep_bottom_half(),
};
main.set_color(color::Rgba::transparent())
.set_inset(PORT_LINE_WIDTH)
.set_pointer_events(false);
let make_end_cap = || {
let end_cap = Rectangle();
let half_height = PORT_LINE_WIDTH / 2.0;
let clip_adjusted_height = PORT_LINE_WIDTH / (END_CAP_CLIP * 2.0 + 1.0);
let clip_diff = clip_adjusted_height - half_height;
end_cap
.set_size((PORT_LINE_WIDTH, clip_adjusted_height))
.set_corner_radius(PORT_LINE_WIDTH)
.set_clip(Vector2(0.0, END_CAP_CLIP));
end_cap.set_pointer_events(false);
// End caps are positioned right above the main port line shape.
end_cap.set_y(node::HEIGHT * 0.5 - clip_diff);
root.add_child(&end_cap);
end_cap
};
let end_cap_left = is_first.then(make_end_cap);
let end_cap_right = is_last.then(make_end_cap);
let hover = Rectangle();
let hover_radius = node::CORNER_RADIUS + HOVER_AREA_PADDING;
match (is_first, is_last) {
(true, true) => hover.keep_bottom_half().set_corner_radius(hover_radius),
(true, false) => hover.keep_bottom_left_quarter().set_corner_radius(hover_radius),
(false, true) => hover.keep_bottom_right_quarter().set_corner_radius(hover_radius),
(false, false) => hover.keep_bottom_half(),
};
hover.set_pointer_events(true);
hover.set_color(color::Rgba::transparent()).set_inner_border(HOVER_AREA_PADDING, 0.0);
root.add_child(&main);
Self {
root,
main,
hover,
end_cap_left,
end_cap_right,
number_of_ports,
port_index,
type_label,
size_multiplier: default(),
}
}
fn_both! {
set_size (this,t:Vector2) {this.set_size(t);}
set_size_multiplier (this,t:f32) {this.size_multiplier.set(t)}
set_color (this,t:color::Rgba) {this.color_rgb.set(t.opaque.into())}
set_opacity (this,t:f32) {this.opacity.set(t)}
/// Set the whole node size at which this port shape is rendered. This set size does not equal
/// the size of the port shape itself.
fn set_size(&self, size: Vector2) {
// The center of coordinate space is at the center of the node's left border, but it is more
// convenient to use bottom left corner of the node as reference during layout below.
let origin_offset = Vector2(0.0, -size.y / 2.0);
let corner_radius = node::CORNER_RADIUS.min(min(size.x, size.y) / 2.0);
self.root.set_xy(origin_offset);
// The straight line part of the shape is divided equally between all ports, taking gaps
// into account.
let straight_line_width = size.x - corner_radius * 2.0;
let number_of_gaps = self.number_of_ports - 1;
let total_gap_space =
(number_of_gaps as f32 * SEGMENT_GAP_WIDTH).min(straight_line_width * 0.5);
let single_gap_width = total_gap_space / max(1.0, number_of_gaps as f32);
let space_to_divide = straight_line_width - total_gap_space;
let single_port_width = space_to_divide / self.number_of_ports as f32;
let line_space_before_port =
(single_port_width + single_gap_width) * self.port_index as f32;
// Ports at either end receive additional space to fill the rounded corners. This space
// also includes the width of the port line.
let corner_space = PORT_LINE_WIDTH + corner_radius;
let is_first = self.port_index == 0;
let is_last = self.port_index == self.number_of_ports - 1;
let left_corner = if is_first { corner_space } else { 0.0 };
let right_corner = if is_last { corner_space } else { 0.0 };
let corner_before_port = if is_first { 0.0 } else { corner_space };
let port_left_position = line_space_before_port + corner_before_port - PORT_LINE_WIDTH;
let port_total_width = single_port_width + left_corner + right_corner;
let hover_corner_pad = HOVER_AREA_PADDING - PORT_LINE_WIDTH;
let hover_pad_left = if is_first { hover_corner_pad } else { SEGMENT_GAP_WIDTH * 0.5 };
let hover_pad_right = if is_last { hover_corner_pad } else { SEGMENT_GAP_WIDTH * 0.5 };
let hover_left_position = port_left_position - hover_pad_left;
let hover_total_width = port_total_width + hover_pad_left + hover_pad_right;
// The height of the base port shape is enough to cover half of the single-line node
// height, plus the width of the port line.
let port_total_height = node::HEIGHT * 0.5 + PORT_LINE_WIDTH;
let hover_total_height = node::HEIGHT * 0.5 + HOVER_AREA_PADDING;
// Note that `hover` is not parented to `root`, so we need to translate it manually.
self.hover
.set_size((hover_total_width, hover_total_height))
.set_xy(origin_offset + Vector2(hover_left_position, -HOVER_AREA_PADDING));
self.main
.set_size((port_total_width, port_total_height))
.set_xy((port_left_position, -PORT_LINE_WIDTH));
let label_width = self.type_label.width.value();
let label_x = port_left_position + port_total_width * 0.5 - label_width * 0.5;
self.type_label.set_x(label_x);
self.end_cap_right.for_each_ref(|cap| cap.set_x(size.x));
}
fn_multi_only! {
set_index (this,t:usize) { this.index.set(t as f32) }
set_port_count (this,t:usize) { this.port_count.set(t as f32) }
set_padding_left (this,t:f32) { this.padding_left.set(t) }
set_padding_right (this,t:f32) { this.padding_right.set(t) }
}
}
impl display::Object for PortShapeView {
fn display_object(&self) -> &display::object::Instance {
match self {
Self::Single(view) => view.display_object(),
Self::Multi(view) => view.display_object(),
}
fn set_size_multiplier(&self, multiplier: f32) {
self.size_multiplier.set(multiplier);
let current_width = PORT_LINE_WIDTH * multiplier;
self.main.set_border(current_width);
let cap_size = (current_width, current_width * 0.5);
self.end_cap_left.for_each_ref(|cap| cap.set_size(cap_size).set_x(-current_width));
self.end_cap_right.for_each_ref(|cap| cap.set_size(cap_size));
}
fn focus_receiver(&self) -> &display::object::Instance {
match self {
Self::Single(view) => view.focus_receiver(),
Self::Multi(view) => view.focus_receiver(),
}
fn set_color(&self, color: color::Rgba) {
self.main.set_color(color).set_border_color(color);
self.end_cap_left.for_each_ref(|cap| cap.set_color(color));
self.end_cap_right.for_each_ref(|cap| cap.set_color(color));
}
}
@ -437,6 +236,7 @@ ensogl::define_endpoints! {
set_usage_type (Option<Type>),
set_type_label_visibility (bool),
set_size (Vector2),
set_color (color::Lcha),
}
Output {
@ -448,117 +248,77 @@ ensogl::define_endpoints! {
}
}
#[derive(Clone, Debug, Default)]
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
#[derive(Debug)]
/// The model of a single output port - a shape and a type label, which are made visible on hover. A
/// node can have multiple output ports.
#[allow(missing_docs)]
pub struct Model {
pub frp: Option<Frp>,
pub shape: Option<PortShapeView>,
pub type_label: Option<text::Text>,
pub display_object: Option<display::object::Instance>,
pub index: ByteDiff,
pub length: ByteDiff,
port_count: usize,
port_index: usize,
pub frp: Frp,
pub shape: Rc<ShapeView>,
/// The index of the first byte in node's output expression at which this port starts.
pub index: ByteDiff,
/// The length of the substring of output expression for this port.
pub length: ByteDiff,
}
impl Model {
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn init_shape(
&mut self,
/// Constructor for a single output port model.
pub fn new(
app: &Application,
styles: &StyleWatch,
port_index: usize,
port_count: usize,
) -> (display::object::Instance, Frp) {
let shape = PortShapeView::new(port_count);
index: ByteDiff,
length: ByteDiff,
) -> Self {
let port_count = max(port_count, 1);
let shape = ShapeView::new(app, port_count, port_index);
let is_first = port_index == 0;
let is_last = port_index == port_count.saturating_sub(1);
let padding_left = if is_first { -INFINITE } else { SEGMENT_GAP_WIDTH / 2.0 };
let padding_right = if is_last { INFINITE } else { -SEGMENT_GAP_WIDTH / 2.0 };
shape.set_index(port_index);
shape.set_port_count(port_count);
shape.set_padding_left(padding_left);
shape.set_padding_right(padding_right);
self.shape = Some(shape.clone());
let type_label = app.new_view::<text::Text>();
let offset_y =
styles.get_number(ensogl_hardcoded_theme::graph_editor::node::type_label::offset_y);
type_label.set_y(offset_y);
self.type_label = Some(type_label.clone());
let display_object = display::object::Instance::new();
display_object.add_child(&shape);
display_object.add_child(&type_label);
self.display_object = Some(display_object.clone());
self.port_count = max(port_count, 1);
self.port_index = port_index;
self.init_frp(&shape, &type_label, styles);
(display_object, self.frp.as_ref().unwrap().clone_ref())
let frp = Frp::new();
let mut this = Self { frp, shape: Rc::new(shape), index, length };
this.init_frp();
this
}
fn init_frp(&mut self, shape: &PortShapeView, type_label: &text::Text, styles: &StyleWatch) {
let frp = Frp::new();
fn init_frp(&mut self) {
let frp = &self.frp;
let shape = &self.shape;
let network = &frp.network;
let opacity = Animation::<f32>::new(network);
let color = color::Animation::new(network);
let type_label_opacity = Animation::<f32>::new(network);
let port_count = self.port_count;
let port_index = self.port_index;
let full_type_timer = DelayedAnimation::new(network);
full_type_timer.set_delay(FULL_TYPE_ONSET_DELAY_MS);
full_type_timer.set_duration(0.0);
frp::extend! { network
init <- source_();
// === Mouse Event Handling ===
let mouse_down = shape.on_event::<mouse::Down>();
let mouse_enter = shape.on_event::<mouse::Enter>();
let mouse_leave = shape.on_event::<mouse::Leave>();
let mouse_down = shape.hover.on_event::<mouse::Down>();
let mouse_out = shape.hover.on_event::<mouse::Out>();
let mouse_over = shape.hover.on_event::<mouse::Over>();
mouse_down_primary <- mouse_down.filter(mouse::is_primary);
frp.source.on_hover <+ bool(&mouse_leave, &mouse_enter);
is_hovered <- bool(&mouse_out,&mouse_over);
frp.source.on_hover <+ is_hovered;
frp.source.on_press <+ mouse_down_primary.constant(());
// === Opacity ===
opacity.target <+ mouse_enter.constant(PORT_OPACITY_HOVERED);
opacity.target <+ mouse_leave.constant(PORT_OPACITY_NOT_HOVERED);
eval opacity.value ((t) shape.set_opacity(*t));
// === Size ===
frp.source.size <+ frp.set_size;
eval frp.size ((&s)
shape.set_size(s + Vector2(HOVER_AREA_PADDING,HOVER_AREA_PADDING) * 2.0));
set_type_label_x <- all_with(&frp.size,&type_label.width,
f!([port_count,port_index](port_size,type_label_width) {
let shape_length = shape_border_length(node::RADIUS, port_size.x);
let shape_left = - shape_length / 2.0;
let port_width = shape_length / port_count as f32;
let port_left = shape_left + port_width * port_index as f32;
let port_center_x = port_left + port_width / 2.0;
let label_center_x = port_center_x;
label_center_x - type_label_width / 2.0
}));
eval set_type_label_x ((&t) type_label.set_x(t));
_eval <- all_with(&frp.size,&shape.type_label.width, f!((s, _) shape.set_size(*s)));
eval frp.set_size_multiplier ((t) shape.set_size_multiplier(*t));
// === Type ===
frp.source.tp <+ all_with(&frp.set_usage_type,&frp.set_definition_type,
|usage_tp,def_tp| usage_tp.clone().or_else(|| def_tp.clone())
);
color.target <+ frp.tp.map(f!([styles](t)
type_coloring::compute_for_selection(t.as_ref(),&styles)));
is_hovered <- all(is_hovered, init)._0();
color_opacity <- is_hovered.switch_constant(PORT_OPACITY_NOT_HOVERED, PORT_OPACITY_HOVERED);
color.target <+ frp.set_color.all_with(&color_opacity, |c, a| c.multiply_alpha(*a));
eval color.value ((t) shape.set_color(t.into()));
full_type_timer.start <+ frp.on_hover.on_true();
@ -570,6 +330,8 @@ impl Model {
})
});
}
init.emit(());
shape.set_size_multiplier(0.0);
if SHOW_TYPE_AS_LABEL {
frp::extend! { network
@ -583,9 +345,8 @@ impl Model {
type_label_color <- all_with(&color.value,&type_label_opacity.value,
|color,&opacity| color.opaque.with_alpha(opacity));
type_label.set_property <+ type_label_color.ref_into_some().map(|t| ((..).into(),*t));
type_label.set_property_default <+ type_label_color.ref_into_some();
type_label.set_content <+ type_description.map(|s| s.clone().unwrap_or_default());
shape.type_label.set_property_default <+ type_label_color.ref_into_some();
shape.type_label.set_content <+ type_description.map(|s| s.clone().unwrap_or_default());
}
}
@ -607,10 +368,5 @@ impl Model {
});
}
}
opacity.target.emit(PORT_OPACITY_NOT_HOVERED);
color.target.emit(type_coloring::compute_for_code(None, styles));
self.frp = Some(frp);
}
}

View File

@ -195,8 +195,6 @@ impl ProfilingLabel {
let label = text::Text::new(app);
display_object.add_child(&label);
label.set_y(crate::component::node::input::area::TEXT_SIZE / 2.0);
scene.layers.main.remove(&label);
label.add_to_scene_layer(&scene.layers.label);
let frp = Frp::new();
let network = &frp.network;
@ -239,9 +237,4 @@ impl ProfilingLabel {
ProfilingLabel { display_object, label, frp, styles }
}
/// Set a scene layer for text rendering.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
}
}

View File

@ -63,9 +63,9 @@ mod status_indicator_shape {
(style:Style,color_rgba:Vector4<f32>) {
let width = Var::<Pixels>::from("input_size.x");
let height = Var::<Pixels>::from("input_size.y");
let width = width - node::PADDING.px() * 2.0;
let height = height - node::PADDING.px() * 2.0;
let radius = node::RADIUS.px();
let width = width - node::BACKDROP_INSET.px() * 2.0;
let height = height - node::BACKDROP_INSET.px() * 2.0;
let radius = node::CORNER_RADIUS.px();
let base = Rect((&width,&height)).corners_radius(radius);
let outer = base.grow(INDICATOR_WIDTH_OUTER.px());

View File

@ -501,7 +501,7 @@ impl ContainerModel {
self.action_bar.frp.set_size.emit(action_bar_size);
self.action_bar.set_y((size.y - ACTION_BAR_HEIGHT) / 2.0);
if let Some(viz) = &*self.visualization.borrow() {
if view_state.is_visible() && let Some(viz) = &*self.visualization.borrow() {
viz.frp.set_size.emit(size);
}
}
@ -737,7 +737,7 @@ impl Container {
}));
selection_after_click <- selected_by_click.map(|sel| if *sel {1.0} else {0.0});
selection.target <+ selection_after_click;
_eval <- selection.value.all_with3(&output.size, &selection_style.update,
_eval <- selection.value.all_with3(&output.size, &selection_style,
f!((value, size, style) {
model.set_selection(*size, *value, style);
}
@ -781,6 +781,7 @@ impl Container {
has_data <- input.set_data.is_some();
reset_data <- data.sample(&new_vis_definition).gate(&has_data);
data_update <- any(&data,&reset_data);
data_update <- data_update.buffered_gate(&output.visible);
eval data_update ((t) model.set_visualization_data(t));
}
@ -812,7 +813,6 @@ impl Container {
self.frp.public.set_size(DEFAULT_SIZE);
self.frp.public.set_visualization(None);
init.emit(());
selection_style.init.emit(());
self
}

View File

@ -64,7 +64,7 @@ mod background {
(style:Style) {
let width = Var::<Pixels>::from("input_size.x");
let height = Var::<Pixels>::from("input_size.y");
let radius = node::RADIUS.px() ;
let radius = node::CORNER_RADIUS.px() ;
let background_rounded = Rect((&width,&height)).corners_radius(radius);
let background_sharp = Rect((&width,&height/2.0)).translate_y(-&height/4.0);
let background = background_rounded + background_sharp;

View File

@ -14,6 +14,7 @@ use crate::visualization::foreign::java_script::Sources;
use super::binding;
use super::instance::Instance;
use ensogl::application::Application;
use ensogl::system::web;
use ensogl::system::web::Function;

View File

@ -0,0 +1,277 @@
use crate::prelude::*;
use crate::display;
use crate::display::camera::Camera2d;
use crate::display::scene::layer::LayerSymbolPartition;
use crate::display::scene::HardcodedLayers;
use crate::display::scene::Layer;
use crate::display::shape::compound::rectangle;
use crate::display::shape::system::Shape;
use ensogl::display::scene::layer;
/// An `Rc` handle over [`GraphLayersData`].
#[derive(Debug, Clone, CloneRef, Deref)]
pub struct GraphLayers {
#[deref]
data: Rc<GraphLayersData>,
}
/// All layers used by different parts of the graph editor.
#[derive(Debug)]
pub struct GraphLayersData {
// == Main camera layers ==
/// Layers used for shapes rendered below all nodes, such as node selection.
pub main_backdrop: NodeBackdropLayers,
/// The layer that is used for for all non-active edge shapes. Those must be drawn above node
/// selection, but below backgrounds of all nodes. When an edge becomes active (it is being
/// dragged), it is moved to the `edge_above_nodes` layer.
///
/// Note that small portion of the dragged edge (the very end near the source edge) is always
/// being drawn on this layer, since it must be occluded by its source's node background.
pub edge_below_nodes: Layer,
/// A set of layers used for all node shapes. The nodes at given tree depth are rendered above
pub main_nodes: MainNodeLayers,
/// The layer that is used for for all edge shapes that needs to be above nodes.
pub edge_above_nodes: Layer,
/// An dedicated edge layer that will be masked out with the edge source shape. Sublayer of
/// `edge_above_nodes`.
pub masked_edge_above_nodes: Layer,
/// A cutout layer that removes parts of drawn edge shapes. Mask for `masked_edge_above_nodes`.
pub edge_above_nodes_cutout: Layer,
// == Edited node camera layers ==
/// A set of layers used for all edited shapes, with a dedicated camera.
pub edited_backdrop: NodeBackdropLayers,
pub edited_nodes: MainNodeLayers,
}
/// Layers used by node backdrop shapes. Separated from node layers, so that other layers can
/// be inserted between the backdrop and main node layers (e.g. edges).
#[derive(Debug)]
pub struct NodeBackdropLayers {
/// The actual layer that is used as the base for all backdrop partitions, below any edges.
/// This layer uses a special blend mode, which guarantees that shapes with alpha with
/// identical colors will nicely blend together without visible overdraw. When shapes of
/// different colors are combined together, the result may look a little funky, but
/// acceptable for backdrops. If something needs to be drawn below nodes without special blend,
/// a new layer must be created.
pub backdrop_base: Layer,
/// The visual layer for all rectangle shapes displayed below node background and edges, such
/// as node selection.
///
/// Note: We are using a single partition instead of the layer directly, so that we can easily
/// add more partitions either below or above the backdrop if necessary, and to not mix the
/// types of layers and partitions in different parts of the code.
pub backdrop: LayerSymbolPartition<rectangle::Shape>,
}
#[derive(Debug)]
pub struct MainNodeLayers {
/// The actual layer that is used as the base for all shapes within the node itself.
pub node_base: Layer,
/// Layer below the node body, but above the backdrop.
pub below_body: LayerSymbolPartition<rectangle::Shape>,
/// Hover layer for output port shapes.
pub output_hover: LayerSymbolPartition<rectangle::Shape>,
/// Hover layer below the node body, but above the output port.
pub below_body_hover: LayerSymbolPartition<rectangle::Shape>,
/// Main node's body, including its background color shape and output port.
pub body: LayerSymbolPartition<rectangle::Shape>,
/// The layer used for interactive elements of the main node body, such as output port.
pub body_hover: LayerSymbolPartition<rectangle::Shape>,
/// The layer used for all widget shapes, always above the node body.
pub widget_base: Layer,
/// The stack of partitions used for all widget rectangle shapes. The widgets at given tree
/// depth are rendered above the widgets at lower depths. Every depth has an allocation of two
/// partitions, one for the widget visual elements and one for mouse interaction (hover
/// shapes).
pub widget_rectangles: PartitionStack<rectangle::Shape>,
/// The layer used for elements which should always be rendered above all other node shapes.
pub above_base: Layer,
/// The layer used for the action bar, which is rendered above all other node shapes.
pub action_bar: LayerSymbolPartition<rectangle::Shape>,
/// The stack of hover areas used by the node port. The ports at given tree depth are rendered
/// above the ports at lower depths.
pub port_hover: PartitionStack<rectangle::Shape>,
}
impl GraphLayers {
/// Create a layer stack used by the graph editor.
///
/// Uses `main` and `node_searcher` hardcoded layers. It is assumed that the `node_searcher`
/// layer has an unique camera and is rendered above `main`.
pub fn new(hardcoded: &HardcodedLayers) -> Self {
let base = &hardcoded.main;
let searcher = &hardcoded.node_searcher;
let edit_camera = Camera2d::new();
let main_backdrop = NodeBackdropLayers::new(base, None);
let edge_below_nodes = base.create_sublayer("edge_below_nodes");
let main_nodes = MainNodeLayers::new(base, None);
let edge_above_nodes = base.create_sublayer("edge_above_nodes");
let masked_edge_above_nodes = edge_above_nodes.create_sublayer("masked_edge_above_nodes");
let edge_above_nodes_cutout =
edge_above_nodes.create_mask_sublayer("edge_above_nodes_cutout");
masked_edge_above_nodes.set_inverted_mask(&edge_above_nodes_cutout);
let edited_backdrop = NodeBackdropLayers::new(base, Some(&edit_camera));
let edited_nodes = MainNodeLayers::new(searcher, Some(&edit_camera));
let data = GraphLayersData {
main_backdrop,
edge_below_nodes,
main_nodes,
edge_above_nodes,
masked_edge_above_nodes,
edge_above_nodes_cutout,
edited_backdrop,
edited_nodes,
};
Self { data: Rc::new(data) }
}
}
impl NodeBackdropLayers {
fn new(layer: &Layer, camera: Option<&Camera2d>) -> Self {
let backdrop_base = layer.create_sublayer_with_optional_camera("backdrop_base", camera);
backdrop_base.set_blend_mode(layer::BlendMode::MAX);
Self { backdrop: backdrop_base.create_symbol_partition("backdrop"), backdrop_base }
}
}
impl MainNodeLayers {
fn new(layer: &Layer, camera: Option<&Camera2d>) -> Self {
let node_base = layer.create_sublayer_with_optional_camera("node_base", camera);
let widget_base = layer.create_sublayer_with_optional_camera("widget_base", camera);
let above_base = layer.create_sublayer_with_optional_camera("above", camera);
Self {
below_body: node_base.create_symbol_partition("below_body"),
output_hover: node_base.create_symbol_partition("output_hover"),
below_body_hover: node_base.create_symbol_partition("below_body_hover"),
body: node_base.create_symbol_partition("body"),
body_hover: node_base.create_symbol_partition("body_hover"),
widget_rectangles: PartitionStack::new(&widget_base),
action_bar: above_base.create_symbol_partition("action_bar"),
port_hover: PartitionStack::new(&above_base),
node_base,
widget_base,
above_base,
}
}
/// Camera getter of this set of layers.
pub fn camera(&self) -> Camera2d {
self.node_base.camera()
}
/// Get a set of layer partitions for node widgets at given tree depth. The widgets that are
/// deeper in the tree are rendered above the widgets at lower depths.
pub fn layers_for_widgets_at_depth(&self, depth: usize) -> CommonLayers {
let visual = self.widget_rectangles.get(depth * 2);
let hover = self.widget_rectangles.get(depth * 2 + 1);
CommonLayers { visual, hover }
}
/// Get a layer partition for node port hover areas at given tree depth.
pub fn port_hover_layer(&self, depth: usize) -> LayerSymbolPartition<rectangle::Shape> {
self.port_hover.get(depth)
}
}
/// A combined set of layers with dedicated partitions for visual and interactive elements
#[derive(Debug, Clone)]
pub struct CommonLayers {
/// Layer partition used for visual shapes. Always rendered below the hover partition.
pub visual: LayerSymbolPartition<rectangle::Shape>,
/// Layer partition that is supposed to be used for transparent shapes with mouse interaction.
/// The hover shapes will be rendered above the visual shapes of the same layer, but below the
/// visual shapes of the layer above.
pub hover: LayerSymbolPartition<rectangle::Shape>,
}
// ======================
// === PartitionStack ===
// ======================
// A stack of layer partitions that can be queried by an integer depth. The relative depth order of
// partitions is guaranteed to be the same as their index - the partition of index 0 is the lowest
// one, the partition of index 1 is the one above it, etc.
//
// NOTE: This structure is only suitable for relatively low index values. It will always create
// partitions up to the requested index, even if the lower indices are not used. This is necessary
// to maintain correct rendering order of partitions.
#[derive(Debug)]
pub struct PartitionStack<S> {
base_layer: Layer,
partitions: RefCell<Vec<LayerSymbolPartition<S>>>,
}
impl<S: Shape> PartitionStack<S> {
/// Create a new partition stack on given base layer. All partitions created by this stack will
/// be above all partitions created on that layer so far.
///
/// NOTE: If more partitions are created on the base layer after this stack is created, the
/// order of those relative to the partitions created by this stack is undefined. It is
/// recommended to never create any additional partitions on the base layer after this stack is
/// created.
fn new(base_layer: &Layer) -> Self {
Self { base_layer: base_layer.clone(), partitions: RefCell::new(Vec::new()) }
}
/// Get a partition of given index. Partitions with higher indices are rendered above partitions
/// with lower indices.
pub fn get(&self, index: usize) -> LayerSymbolPartition<S> {
self.get_ref(index).clone()
}
/// Add a shape to a partition of given index. Partitions with higher indices are rendered above
/// partitions with lower indices.
pub fn add(&self, index: usize, shape: &display::object::Instance) {
self.get_ref(index).add(shape);
}
fn get_ref(&self, index: usize) -> RefMut<LayerSymbolPartition<S>> {
let mut partitions = self.partitions.borrow_mut();
if partitions.len() <= index {
self.extend(&mut partitions, index);
}
RefMut::map(partitions, |partitions| &mut partitions[index])
}
/// Separate the cold path into separate non-inline function to avoid codegen bloat.
#[inline(never)]
#[cold]
fn extend(&self, partitions: &mut RefMut<Vec<LayerSymbolPartition<S>>>, index: usize) {
// Sanity check if we aren't creating too many partitions. This is not a hard limit, but
// if we are creating more than 1000 partitions, something is probably wrong.
if index > 1000 {
error!("Unreasonably layer partition index requested. This is probably a bug.");
}
// Ensure that the partitions are always created in order of their index. We cannot skip
// unused indices here.
partitions.resize_with(index + 1, || self.base_layer.create_symbol_partition("stack"));
}
}

View File

@ -18,6 +18,7 @@
#![feature(type_alias_impl_trait)]
#![feature(unboxed_closures)]
#![feature(array_windows)]
#![feature(if_let_guard)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
@ -46,17 +47,16 @@ pub mod profiling;
#[warn(missing_docs)]
pub mod view;
mod layers;
#[warn(missing_docs)]
mod selection;
mod shortcuts;
use crate::application::command::FrpNetworkProvider;
use crate::component::node;
use crate::component::type_coloring;
use crate::component::visualization;
use crate::component::visualization::instance::PreprocessorConfiguration;
use crate::data::enso;
pub use crate::node::profiling::Status as NodeProfilingStatus;
use engine_protocol::language_server::ExecutionEnvironment;
use application::tooltip;
@ -68,7 +68,6 @@ use ensogl::data::color;
use ensogl::display;
use ensogl::display::navigation::navigator::Navigator;
use ensogl::display::object::Id;
use ensogl::display::shape::StyleWatch;
use ensogl::display::shape::StyleWatchFrp;
use ensogl::display::Scene;
use ensogl::gui::cursor;
@ -83,6 +82,17 @@ use ensogl_component::tooltip::Tooltip;
use ensogl_hardcoded_theme as theme;
use span_tree::PortId;
// ==============
// === Export ===
// ==============
pub use crate::node::profiling::Status as NodeProfilingStatus;
pub use layers::GraphLayers;
// ===============
// === Prelude ===
// ===============
@ -108,6 +118,8 @@ const VIZ_PREVIEW_MODE_TOGGLE_TIME_MS: f32 = 300.0;
const VIZ_PREVIEW_MODE_TOGGLE_FRAMES: i32 =
(VIZ_PREVIEW_MODE_TOGGLE_TIME_MS / 1000.0 * 60.0) as i32;
const MAX_ZOOM: f32 = 1.0;
/// The amount of pixels that the dragged target edge overlaps with the cursor.
const CURSOR_EDGE_OVERLAP: f32 = 2.0;
@ -610,7 +622,7 @@ ensogl::define_endpoints_2! {
/// Set whether the output context is explicitly enabled for a node: `Some(true/false)` for
/// enabled/disabled; `None` for no context switch expression.
set_node_context_switch ((NodeId, Option<bool>)),
set_node_comment ((NodeId,node::Comment)),
set_node_comment ((NodeId,ImString)),
set_node_position ((NodeId,Vector2)),
set_expression_usage_type ((NodeId,ast::Id,Option<Type>)),
update_node_widgets ((NodeId,CallWidgetsConfig)),
@ -1587,7 +1599,7 @@ impl GraphEditorModel {
}
fn create_edge(&self, pointer: &EdgePointerFrp) -> Edge {
let edge = Edge::new(component::Edge::new(&self.app));
let edge = Edge::new(component::Edge::new(&self.app, &self.layers));
self.add_child(&edge);
let edge_id = edge.id();
let network = edge.view.network();
@ -1630,7 +1642,7 @@ impl GraphEditorModel {
#[profile(Debug)]
fn new_node(&self, ctx: &NodeCreationContext) -> Node {
let view = component::Node::new(&self.app, self.vis_registry.clone_ref());
let view = component::Node::new(&self.app, &self.layers, self.vis_registry.clone_ref());
let node = Node::new(view);
let node_model = node.model();
let network = node.frp().network();
@ -1783,6 +1795,7 @@ pub struct GraphEditorModel {
pub display_object: display::object::Instance,
// Required for dynamically creating nodes and edges.
pub app: Application,
layers: GraphLayers,
pub nodes: Nodes,
edges: RefCell<Edges>,
pub vis_registry: visualization::Registry,
@ -1796,8 +1809,6 @@ pub struct GraphEditorModel {
frp_public: api::Public,
profiling_statuses: profiling::Statuses,
styles_frp: StyleWatchFrp,
#[deprecated = "StyleWatch was designed as an internal tool for shape system (#795)"]
styles: StyleWatch,
selection_controller: selection::Controller,
}
@ -1823,7 +1834,6 @@ impl GraphEditorModel {
let drop_manager =
ensogl_drop_manager::Manager::new(&scene.dom.root.clone_ref().into(), scene);
let styles_frp = StyleWatchFrp::new(&scene.style_sheet);
let styles = StyleWatch::new(&scene.style_sheet);
let selection_controller = selection::Controller::new(
frp,
&app.cursor,
@ -1832,10 +1842,12 @@ impl GraphEditorModel {
&nodes,
);
#[allow(deprecated)] // `styles`
let layers = GraphLayers::new(&scene.layers);
Self {
display_object,
app,
layers,
nodes,
edges,
vis_registry,
@ -1849,7 +1861,6 @@ impl GraphEditorModel {
frp: frp.private.clone_ref(),
frp_public: frp.public.clone_ref(),
styles_frp,
styles,
selection_controller,
}
.init()
@ -1966,45 +1977,27 @@ impl GraphEditorModel {
fn edit_node_expression(
&self,
node_id: impl Into<NodeId>,
range: impl Into<text::Range<text::Byte>>,
inserted_str: impl Into<ImString>,
node_id: NodeId,
range: &text::Range<text::Byte>,
inserted_str: &ImString,
) {
let node_id = node_id.into();
let range = range.into();
let inserted_str = inserted_str.into();
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
node.edit_expression(range, inserted_str);
}
self.with_node(node_id, |node| node.edit_expression(*range, inserted_str.clone()));
}
fn set_node_skip(&self, node_id: impl Into<NodeId>, skip: &bool) {
let node_id = node_id.into();
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
node.set_skip_macro(*skip);
}
fn set_node_skip(&self, node_id: NodeId, skip: bool) {
self.with_node(node_id, |node| node.set_skip_macro(skip));
}
fn set_node_freeze(&self, node_id: impl Into<NodeId>, freeze: &bool) {
let node_id = node_id.into();
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
node.set_freeze_macro(*freeze);
}
fn set_node_freeze(&self, node_id: NodeId, freeze: bool) {
self.with_node(node_id, |node| node.set_freeze_macro(freeze));
}
fn set_node_context_switch(&self, node_id: impl Into<NodeId>, context_switch: &Option<bool>) {
let node_id = node_id.into();
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
node.set_context_switch(*context_switch);
}
fn set_node_context_switch(&self, node_id: NodeId, context_switch: &Option<bool>) {
self.with_node(node_id, |node| node.set_context_switch(*context_switch));
}
fn set_node_comment(&self, node_id: impl Into<NodeId>, comment: impl Into<node::Comment>) {
let node_id = node_id.into();
let comment = comment.into();
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
node.set_comment.emit(comment);
}
fn set_node_comment(&self, node_id: NodeId, comment: &ImString) {
self.with_node(node_id, |node| node.set_comment(comment.clone()));
}
}
@ -2069,8 +2062,7 @@ impl GraphEditorModel {
/// Return the bounding box of the node identified by `node_id`, or a default bounding box if
/// the node was not found.
pub fn node_bounding_box(&self, node_id: impl Into<NodeId>) -> selection::BoundingBox {
let node_id = node_id.into();
pub fn node_bounding_box(&self, node_id: NodeId) -> selection::BoundingBox {
self.with_node(node_id, |node| node.bounding_box.value()).unwrap_or_default()
}
@ -2089,19 +2081,16 @@ impl GraphEditorModel {
.into_iter()
.filter(|edge_id| {
let Some(edge) = edges.get_mut(edge_id) else { return false };
let source_type = || match edge.target {
Some(target) => self.target_endpoint_type(target),
None => self.hovered_input_type(),
let source_color = || match edge.target {
Some(target) => self.node_color(target.node_id),
None => self.hovered_input_color(),
};
let target_type = || match edge.source {
Some(source) => self.source_endpoint_type(source),
None => self.hovered_output_type(),
let target_color = || match edge.source {
Some(source) => self.node_color(source.node_id),
None => self.hovered_output_color(),
};
let edge_type = source_type().or_else(target_type);
// FIXME: `type_coloring::compute` should be ported to `StyleWatchFrp`.
#[allow(deprecated)] // `self.styles`
let opt_color = edge_type.map(|t| type_coloring::compute(&t, &self.styles));
let color = opt_color.unwrap_or_else(|| self.edge_fallback_color());
let edge_color = source_color().or_else(target_color);
let color = edge_color.unwrap_or_else(|| self.edge_fallback_color());
edge.set_color(color)
})
.collect()
@ -2128,9 +2117,12 @@ impl GraphEditorModel {
}
if let Some(edge_target) = edge.target() {
self.with_node(edge_target.node_id, |node| {
let port_offset = node.model().input.port_offset(edge_target.port);
let input = &node.model().input;
let port_offset = input.port_offset(edge_target.port);
let port_size = input.port_size(edge_target.port);
let node_position = node.position().xy();
edge.view.target_position.emit(node_position + port_offset);
edge.view.target_size.emit(port_size);
});
}
}
@ -2176,22 +2168,18 @@ impl GraphEditorModel {
self.with_edge(id, |edge| edge.target).flatten()
}
fn source_endpoint_type(&self, endpoint: EdgeEndpoint) -> Option<Type> {
self.with_node(endpoint.node_id, |node| node.model().output.port_type(endpoint.port))?
fn node_color(&self, id: NodeId) -> Option<color::Lcha> {
self.with_node(id, |node| node.port_color.value())
}
fn target_endpoint_type(&self, endpoint: EdgeEndpoint) -> Option<Type> {
self.with_node(endpoint.node_id, |node| node.model().input.port_type(endpoint.port))?
}
fn hovered_input_type(&self) -> Option<Type> {
fn hovered_input_color(&self) -> Option<color::Lcha> {
let hover_target = self.frp_public.output.hover_node_input.value();
hover_target.and_then(|tgt| self.target_endpoint_type(tgt))
hover_target.and_then(|tgt| self.node_color(tgt.node_id))
}
fn hovered_output_type(&self) -> Option<Type> {
fn hovered_output_color(&self) -> Option<color::Lcha> {
let hover_target = self.frp_public.output.hover_node_output.value();
hover_target.and_then(|tgt| self.source_endpoint_type(tgt))
hover_target.and_then(|tgt| self.node_color(tgt.node_id))
}
/// Retrieve the color of the edge. Does not recomputes it, but returns the cached value.
@ -2381,7 +2369,7 @@ impl GraphEditor {
eval input.update_node_widgets(((id, widgets)) model.update_node_widgets(*id, widgets));
eval input.set_node_expression(((id, expr)) model.set_node_expression(id, expr));
eval input.edit_node_expression(
((id, range, ins)) model.edit_node_expression(id, range, ins)
((id, range, ins)) model.edit_node_expression(*id, range, ins)
);
}
NodeExpressionFrp { node_with_new_expression_type }
@ -2659,6 +2647,7 @@ impl GraphEditor {
cursor_pos_on_update <- cursor.scene_position.sample(&state.detached_edge);
refresh_cursor_pos <- any(cursor_pos_on_update, cursor.scene_position);
refresh_cursor_data <- all(refresh_cursor_pos, cursor.box_size);
// Only allow hovering output ports of different nodes than the current target node.
is_hovering_valid_output <- out.hover_node_output.all_with(&detached_target_node,
@ -2668,9 +2657,10 @@ impl GraphEditor {
snap_source_to_node <- out.hover_node_output.unwrap().gate(&is_hovering_valid_output);
_eval <- refresh_cursor_pos.map2(&detached_source_edge,
f!((position, &edge_id) model.with_edge(edge_id?, |edge| {
edge.view.target_position.emit(position.xy());
_eval <- refresh_cursor_data.map2(&detached_source_edge,
f!(((position, cursor_size), &edge_id) model.with_edge(edge_id?, |edge| {
let top_of_cursor = Vector2(0.0, cursor_size.y() / 2.0 - CURSOR_EDGE_OVERLAP);
edge.view.target_position.emit(position.xy() + top_of_cursor);
}))
);
_eval <- refresh_source.map2(&detached_target_edge,
@ -2700,12 +2690,6 @@ impl GraphEditor {
active_source_endpoint <- any(...);
active_source_endpoint <+ detached_source_endpoint;
active_source_endpoint <+ out.hover_node_output.gate(&has_detached_target);
active_source_node <- active_source_endpoint.map_some(|e| e.node_id).on_change();
active_node <- active_source_node.unwrap();
rest_node <- active_source_node.previous().unwrap();
eval active_node((id) model.with_node(*id, |n| n.model().set_editing_edge(true)));
eval rest_node((id) model.try_with_node(*id, |n| n.model().set_editing_edge(false)));
// Whenever the detached edge is connected to a target, monitor for that target's port
// existence. If it becomes invalid, the edge must be dropped.
@ -3003,10 +2987,10 @@ fn init_remaining_graph_editor_frp(
// === Set Node SKIP/FREEZE macros and context switch expression ===
frp::extend! { network
eval inputs.set_node_skip(((id, skip)) model.set_node_skip(id, skip));
eval inputs.set_node_freeze(((id, freeze)) model.set_node_freeze(id, freeze));
eval inputs.set_node_skip(((id, skip)) model.set_node_skip(*id, *skip));
eval inputs.set_node_freeze(((id, freeze)) model.set_node_freeze(*id, *freeze));
eval inputs.set_node_context_switch(((id, context_switch))
model.set_node_context_switch(id, context_switch)
model.set_node_context_switch(*id, context_switch)
);
}
@ -3014,7 +2998,7 @@ fn init_remaining_graph_editor_frp(
// === Set Node Comment ===
frp::extend! { network
eval inputs.set_node_comment(((id,comment)) model.set_node_comment(id,comment));
eval inputs.set_node_comment(((id,comment)) model.set_node_comment(*id,comment));
}
// === Set Node Error ===
@ -3511,7 +3495,7 @@ mod tests {
graph_editor.stop_editing();
next_frame();
// Creating edge.
let port = node_1.model().output_port_shape().expect("No output port.");
let port = node_1.model().output_port_hover_shape().expect("No output port.");
mouse.click_on(&port, Vector2::zero());
assert_eq!(graph_editor.num_edges(), 1);
// Dropping edge.
@ -3536,7 +3520,7 @@ mod tests {
graph_editor.stop_editing();
next_frame();
// Creating edge.
let port = node_1.model().output_port_shape().expect("No output port.");
let port = node_1.model().output_port_hover_shape().expect("No output port.");
mouse.click_on(&*port, Vector2::zero());
assert_eq!(graph_editor.num_edges(), 1);
let edge_id = *graph_editor.model.edges.borrow().keys().next().expect("Edge was not added");

View File

@ -91,7 +91,7 @@ pub fn under_selected_nodes(graph_editor: &GraphEditorModel) -> Vector2 {
};
let node_bbox_bottom = |node_id| graph_editor.node_bounding_box(node_id).bottom();
let selected_nodes = graph_editor.nodes.selected.raw.borrow();
let selection_bottom = selected_nodes.iter().map(node_bbox_bottom).reduce(min);
let selection_bottom = selected_nodes.iter().copied().map(node_bbox_bottom).reduce(min);
let selection_bottom_or_zero = selection_bottom.unwrap_or_default();
below_line_and_left_aligned(graph_editor, selection_bottom_or_zero, first_selected_node_x)
}

View File

@ -7,6 +7,7 @@ use crate::breadcrumbs::SharedMethodPointer;
use super::BACKGROUND_HEIGHT;
use super::TEXT_SIZE;
use enso_frp as frp;
use ensogl::application::Application;
use ensogl::data::color;
@ -246,8 +247,6 @@ impl BreadcrumbModel {
scene.layers.panel_overlay.add(&overlay);
scene.layers.panel.add(&separator);
label.add_to_scene_layer(&scene.layers.panel_text);
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
// system (#795)
let style = StyleWatch::new(&scene.style_sheet);

View File

@ -100,8 +100,6 @@ impl ProjectNameModel {
text_field.set_property_default(base_color);
text_field.set_property_default(text_size);
text_field.set_single_line_mode(true);
text_field.add_to_scene_layer(&scene.layers.panel_text);
text_field.hover();
let overlay = Rectangle::new().set_color(INVISIBLE_HOVER_COLOR).clone();

View File

@ -72,8 +72,7 @@ impl View {
let height_fraction = DEPRECATED_Animation::<f32>::new(network);
model.set_x(PADDING_LEFT);
scene.layers.main.remove(&model);
model.add_to_scene_layer(&scene.layers.panel_text);
scene.layers.panel_text.add(&model);
// TODO[ao]: To have code editor usable we treat it as constantly mouse-hovered, but this
// should be changed in the second part of focus management
// (https://github.com/enso-org/ide/issues/823)
@ -111,7 +110,7 @@ impl View {
});
eval position ((pos) model.set_xy(*pos));
let color = styles.get_color(ensogl_hardcoded_theme::code::syntax::base);
let color = styles.get_color(ensogl_hardcoded_theme::code::types::any);
eval color ((color) model.set_property_default(color));
}
init.emit(());

View File

@ -10,9 +10,9 @@ use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::Layer;
use ensogl::display::style::FromTheme;
use ensogl_component::grid_view;
use ensogl_component::shadow;
use ensogl_derive_theme::FromTheme;
use ensogl_hardcoded_theme::application::project_list as theme;
use ensogl_text as text;
@ -87,7 +87,7 @@ impl Data {
display_object.add_child(&text);
if let Some(text_layer) = text_layer {
text.add_to_scene_layer(text_layer);
text_layer.add(&text);
}
Self { display_object, text }
}
@ -124,13 +124,13 @@ impl grid_view::Entry for Entry {
let style = EntryStyle::from_theme(network, &style_frp);
frp::extend! { network
corners_radius <- style.update.map(|s| s.corners_radius);
hover_color <- style.update.map(|s| s.hover_color.into());
selection_color <- style.update.map(|s| s.selection_color.into());
text_padding_left <- style.update.map(|s| s.text_padding_left);
text_padding_bottom <- style.update.map(|s| s.text_padding_bottom);
text_size <- style.update.map(|s| s.text_size);
text_color <- style.update.map(|s| color::Lcha::from(s.text_color));
corners_radius <- style.map(|s| s.corners_radius);
hover_color <- style.map(|s| s.hover_color.into());
selection_color <- style.map(|s| s.selection_color.into());
text_padding_left <- style.map(|s| s.text_padding_left);
text_padding_bottom <- style.map(|s| s.text_padding_bottom);
text_size <- style.map(|s| s.text_size);
text_color <- style.map(|s| color::Lcha::from(s.text_color));
eval input.set_model((text) data.set_text(text.clone_ref()));
contour <- input.set_size.map(|s| grid_view::entry::Contour::rectangular(*s));
@ -144,7 +144,6 @@ impl grid_view::Entry for Entry {
data.text.set_property_default <+ text_size.map(|s| text::Size(*s)).cloned_into_some();
data.text.set_property_default <+ text_color.cloned_into_some();
}
style.init.emit(());
Self { data, frp }
}
@ -220,23 +219,23 @@ impl ProjectList {
display_object.add_child(&grid);
app.display.default_scene.layers.panel.add(&display_object);
caption.set_content("Open Project");
caption.add_to_scene_layer(&app.display.default_scene.layers.panel_text);
app.display.default_scene.layers.panel_text.add(&caption);
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = Style::from_theme(&network, &style_frp);
frp::extend! { network
init <- source::<()>();
width <- style.update.map(|s| s.width);
height <- style.update.map(|s| s.height);
shadow_extent <- style.update.map(|s| s.shadow_extent);
bar_height <- style.update.map(|s| s.bar_height);
label_padding <- style.update.map(|s| s.label_padding);
label_color <- style.update.map(|s| s.bar_label_color);
label_size <- style.update.map(|s| s.bar_label_size);
corners_radius <- style.update.map(|s| s.corners_radius);
entry_height <- style.update.map(|s| s.entry_height);
paddings <- style.update.map(|s| s.paddings);
width <- style.map(|s| s.width);
height <- style.map(|s| s.height);
shadow_extent <- style.map(|s| s.shadow_extent);
bar_height <- style.map(|s| s.bar_height);
label_padding <- style.map(|s| s.label_padding);
label_color <- style.map(|s| s.bar_label_color);
label_size <- style.map(|s| s.bar_label_size);
corners_radius <- style.map(|s| s.corners_radius);
entry_height <- style.map(|s| s.entry_height);
paddings <- style.map(|s| s.paddings);
content_size <- all_with3(&width, &height, &init, |w,h,()| Vector2(*w,*h));
size <- all_with3(&width, &height, &shadow_extent, |w, h, s|
Vector2(w + s * 2.0, h + s * 2.0)
@ -262,7 +261,6 @@ impl ProjectList {
grid_y <- all_with3(&content_size, &bar_height, &paddings, |s,h,p| s.y / 2.0 - *h - *p);
_eval <- all_with(&grid_x, &grid_y, f!((x, y) grid.set_xy(Vector2(*x, *y))));
}
style.init.emit(());
init.emit(());
Self { network, display_object, background, caption, grid }

View File

@ -132,8 +132,7 @@ impl Model {
let camera = scene.camera();
scene.layers.panel.add(&background);
scene.layers.main.remove(&label);
label.add_to_scene_layer(&scene.layers.panel_text);
scene.layers.panel_text.add(&label);
use theme::application::status_bar;
let text_color_path = status_bar::text;

View File

@ -209,17 +209,54 @@ body {
margin: 0;
overflow: hidden;
pointer-events: none;
display: none;
}
#debug-enable-checkbox {
position: absolute;
bottom: 0px;
right: 0px;
color: white;
font-size: 12px;
font-family: "M PLUS 1";
padding: 3px 6px;
background: rgba(15, 0, 77, 0.7);
cursor: none;
user-select: none;
}
#debug-enable-checkbox:has(input:checked) + #debug-root {
display: initial;
}
#debug-root > .debug-layer {
position: absolute;
top: 50vh;
left: 50vw;
width: 0px;
height: 0px;
transform-origin: 0 0;
}
#debug-root > .debug-layer * {
position: absolute;
background: rgba(0, 0, 0, 0.05);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
transform-origin: 0 0;
}
#debug-root > .debug-layer [data-name="Text"] > div {
background: rgba(0, 160, 60, 0.15);
}
#debug-root > .debug-layer [data-name*="compound::rectangle::shape"] {
background: rgba(0, 20, 180, 0.15);
}
#debug-root > .debug-layer .hidden {
display: none;
}
#debug-root > .debug-layer[data-layer-name="DETACHED"] {
display: none;
}

View File

@ -151,6 +151,10 @@ pub enum HeaderToken {
EmptyLine,
ModuleDoc,
Comment,
SuperUse,
SuperUseStar,
SuperPubUse,
SuperPubUseStar,
CrateUse,
CrateUseStar,
CratePubUse,
@ -242,6 +246,10 @@ define_rules! {
CrateUseStar = r"use +crate( *:: *[\w*]+)*";
CratePubUse = r"pub +use +crate( *:: *[\w]+)*( +as +[\w]+)?";
CratePubUseStar = r"pub +use +crate( *:: *[\w*]+)*";
SuperUse = r"use +super( *:: *[\w]+)*( +as +[\w]+)?";
SuperUseStar = r"use +super( *:: *[\w*]+)*";
SuperPubUse = r"pub +use +super( *:: *[\w]+)*( +as +[\w]+)?";
SuperPubUseStar = r"pub +use +super( *:: *[\w*]+)*";
Use = r"use +[\w]+( *:: *[\w]+)*( +as +[\w]+)?";
UseStar = r"use +[\w]+( *:: *[\w*]+)*";
PubUse = r"pub +use +[\w]+( *:: *[\w]+)*( +as +[\w]+)?";
@ -572,13 +580,27 @@ pub fn process_file_content(input: String, is_main_file: bool) -> Result<String>
);
print_section(&mut out, &mut map, &[ModuleAttribAllow, ModuleAttribDeny, ModuleAttribWarn]);
print_section(&mut out, &mut map, &[CrateUseStar, UseStar]);
// Sort `super` before `crate` to match with rustfmt behavior.
print_section(&mut out, &mut map, &[SuperUseStar, CrateUseStar, UseStar]);
print_section(&mut out, &mut map, &[CrateUse]);
print_section(&mut out, &mut map, &[SuperUse]);
print_section(&mut out, &mut map, &[Use]);
print_h1(&mut out, &map, &[PubMod, CratePubUseStar, PubUseStar, CratePubUse, PubUse], "Export");
print_h1(
&mut out,
&map,
&[PubMod, SuperPubUseStar, CratePubUseStar, PubUseStar, SuperPubUse, CratePubUse, PubUse],
"Export",
);
print_section(&mut out, &mut map, &[PubMod]);
print_section(&mut out, &mut map, &[CratePubUseStar, PubUseStar, CratePubUse, PubUse]);
print_section(&mut out, &mut map, &[
SuperPubUseStar,
CratePubUseStar,
PubUseStar,
SuperPubUse,
CratePubUse,
PubUse,
]);
out.push_str("\n\n");
out.push_str(&input[total_len..]);
Ok(out)
@ -608,7 +630,9 @@ use crate::prelude::*;
use crate::lib_b;
use lib_c;
pub use crate::lib_e;
use super::prelude::*;
use crate::lib_a;
use super::item_2;
use lib_d::item_1;
use logger::traits::*;
pub mod mod2;
@ -632,12 +656,15 @@ pub struct Struct1 {}
// === Non-Standard Linter Configuration ===
#![warn(missing_copy_implementations)]
use super::prelude::*;
use crate::prelude::*;
use logger::traits::*;
use crate::lib_b;
use crate::lib_a;
use super::item_2;
use lib_c;
use lib_d::item_1;

View File

@ -167,7 +167,7 @@ async fn adding_node_by_clicking_on_the_output_port() {
let (node_1_id, _, node_1) = add_node_with_internal_api(&graph_editor, "1 + 1").await;
let method = |editor: &GraphEditor| {
let port = node_1.model().output_port_shape().expect("No output port");
let port = node_1.model().output_port_hover_shape().expect("No output port");
port.events_deprecated.mouse_over.emit(());
editor.start_node_creation_from_port();
};
@ -293,7 +293,7 @@ async fn mouse_oriented_node_placement() {
}
fn check_edge_drop(&self) {
let port = self.source_node.view.model().output_port_shape().unwrap();
let port = self.source_node.view.model().output_port_hover_shape().unwrap();
port.events_deprecated.emit_mouse_down(PrimaryButton);
port.events_deprecated.emit_mouse_up(PrimaryButton);
self.scene.mouse.frp_deprecated.position.emit(self.mouse_position);

View File

@ -105,27 +105,13 @@ pub trait AddToBuilder<T> {
fn add_to_builder(&mut self, t: T) -> &mut Self;
}
impl<T: HasCodeRepr> AddToBuilder<&T> for CodeBuilder {
default fn add_to_builder(&mut self, t: &T) -> &mut Self {
impl<T: HasCodeRepr + ?Sized> AddToBuilder<&T> for CodeBuilder {
fn add_to_builder(&mut self, t: &T) -> &mut Self {
t.build(self);
self
}
}
impl AddToBuilder<&String> for CodeBuilder {
fn add_to_builder(&mut self, t: &String) -> &mut Self {
self.add_str(t);
self
}
}
impl AddToBuilder<&str> for CodeBuilder {
fn add_to_builder(&mut self, t: &str) -> &mut Self {
self.add_str(t);
self
}
}
// === Instances ===
impl Default for CodeBuilder {
@ -178,3 +164,15 @@ impl HasCodeRepr for usize {
write!(builder, "{self}").unwrap();
}
}
impl HasCodeRepr for String {
fn build(&self, builder: &mut CodeBuilder) {
builder.add_str(self);
}
}
impl HasCodeRepr for str {
fn build(&self, builder: &mut CodeBuilder) {
builder.add_str(self);
}
}

View File

@ -11,3 +11,4 @@ proc-macro = true
proc-macro2 = { workspace = true }
syn_1 = { workspace = true }
quote = { workspace = true }
proc-macro-crate = "1.3"

View File

@ -3,13 +3,12 @@
//! if any of the values changes.
use proc_macro2::Ident;
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use quote::ToTokens;
use syn::DeriveInput;
use syn::Path;
use syn::Type;
use syn_1 as syn;
@ -21,39 +20,6 @@ use syn_1 as syn;
const BASE_PATH_ATTRIBUTE_NAME: &str = "base_path";
// Sadly we can't use `path` here, because it conflicts with Rust's builtin attribute.
const PATH_ATTRIBUTE_NAME: &str = "theme_path";
const ACCESSOR_ATTRIBUTE_NAME: &str = "accessor";
// ==================
// === ThemeTypes ===
// ==================
/// Provides a convenient way to reason about the three types that have special theme support.
enum ThemeTypes {
Color,
String,
Number,
/// The type is not directly supported by the macro, but can be accessed using a custom
/// accessor.
Unknown,
}
impl ThemeTypes {
/// Return the corresponging [`ThemeType`] for the given [`Type`].
fn from_ty(ty: &Type) -> Self {
match ty {
Type::Path(type_path)
if type_path.clone().into_token_stream().to_string().contains("ImString") =>
Self::String,
Type::Path(type_path) if type_path.clone().into_token_stream().to_string() == "f32" =>
Self::Number,
Type::Path(type_path)
if type_path.clone().into_token_stream().to_string().contains("Rgba") =>
Self::Color,
_ => Self::Unknown,
}
}
}
@ -68,87 +34,39 @@ fn build_frp(
struct_name: &Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> TokenStream {
let mut struct_init = TokenStream::new();
let mut frp_content = TokenStream::new();
let mut prev_value: Option<Ident> = None;
for field in fields.iter() {
let field_name =
field.ident.as_ref().expect("Encountered unnamed struct field. This cannot happen.");
let field_update = format_ident!("{}_update", field_name);
let update = if let Some(prev_value) = prev_value {
quote! {
#field_update <- all(&#prev_value, &#field_name);
}
} else {
quote! {
#field_update <- all(&init, &#field_name);
}
};
frp_content.extend(update);
prev_value = Some(field_update);
frp_content.extend(quote! {
layout_update_init_debounced <+ #field_name;
});
struct_init.extend(quote! {
#field_name : #field_name.value(),
});
}
let destruct_pattern = make_destruct_pattern(fields);
let struct_init = make_struct_inits(fields);
let prev_value = prev_value.expect("Struct did not contain any fields.");
let struct_generation = quote! {
layout_update <- #prev_value.map(|#destruct_pattern|{
#struct_name {
quote! {
__ensogl_core::frp::extend! { network
layout_update_init_debounced <- any_(...);
layout_update_needed <- any_(...);
layout_update_needed <+ layout_update_init_debounced.debounce();
#frp_content
layout_sampler <- layout_update_needed.map(move |()| {
#struct_name {
#struct_init
}
});
};
frp_content.extend(struct_generation);
quote! {
frp::extend! { network
init <- source_();
#frp_content;
}).sampler();
}
// In order to make sure that the style value is initialized on first access, we need to
// force an update immediately. Then, in order to fire the update after FRP network
// initialization, we need to emit the init value in the next microtask.
layout_update_needed.emit();
layout_update_init_debounced.emit();
}
}
/// Create the field initializers for the struct holding the theme values.
fn make_struct_inits(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> TokenStream {
let mut combined = TokenStream::new();
for field in fields.iter() {
let field_name = field
.ident
.as_ref()
.expect("Encountered unnamed struct field. This cannot not happen.");
// Keep in mind that all [`Copy`] types also implement [`Clone`].
let init = quote! {
#field_name : #field_name.clone(),
};
combined.extend(init);
}
combined
}
/// Return a token stream that allows the destructuring of the tuple type that is created by the
/// FRP created in `build_frp`.
fn make_destruct_pattern(
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> TokenStream {
let mut combined = quote! { _ }; // Discard init argument, which is the innermost one.
for field in fields.iter() {
let field_name = field
.ident
.as_ref()
.expect("Encountered unnamed struct field. This cannot not happen.");
combined = quote! {
( #combined, #field_name)
}
}
combined
}
/// Iterate the metadata in the list of attributes and return the first path that was supplied with
/// the `attribute_name` attribute.
fn get_path_from_metadata(metadata: &[syn::Attribute], attribute_name: &str) -> Option<Path> {
@ -190,6 +108,7 @@ fn make_theme_getters(
.iter()
.map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
let field_path = get_path_metadata_for_field(f, PATH_ATTRIBUTE_NAME);
let field_path = match (base_path, field_path) {
(Some(base_path), None) => quote! { #base_path::#field_name },
@ -197,28 +116,8 @@ fn make_theme_getters(
(None, None) => panic!("Neither `base_path` nor `path` attributes were set."),
};
let accessor = get_path_from_metadata(&f.attrs, ACCESSOR_ATTRIBUTE_NAME)
.map(|accessor| {
quote! { #accessor(&network, style, #field_path) }
})
.unwrap_or_else(|| match ThemeTypes::from_ty(&f.ty) {
ThemeTypes::Color => quote! {
StyleWatchFrp::get_color(style, #field_path)
},
ThemeTypes::String => quote! {
StyleWatchFrp::get_text(style, #field_path)
},
ThemeTypes::Number => quote! {
StyleWatchFrp::get_number(style, #field_path)
},
ThemeTypes::Unknown => panic!(
"Unknown type for theme value, but no accessor was provided. \
Use the `#[accessor]` attribute to provide a custom accessor function"
),
});
quote! {
let #field_name = #accessor;
let #field_name = style.access::<#field_type>(#field_path);
}
})
.collect()
@ -233,6 +132,7 @@ pub fn expand(input: DeriveInput) -> TokenStream {
use syn::DataStruct;
use syn::Fields;
let ensogl_core = ensogl_core_crate();
let base_path = get_path_metadata_for_struct(&input, BASE_PATH_ATTRIBUTE_NAME);
let fields = match input.data {
@ -243,28 +143,32 @@ pub fn expand(input: DeriveInput) -> TokenStream {
let theme_getters = make_theme_getters(&fields, base_path.as_ref());
let st_name = input.ident;
let frp_network_description = build_frp(&st_name, &fields);
let update_struct_name = format_ident!("{}FromThemeFRP", st_name);
let out = quote! {
#[automatically_derived]
/// FRP endpoints with [`#st_name`] derived from style FRP. Generated by [`FromTheme`]
/// macro.
pub struct #update_struct_name {
/// Event emitted each time the style will be updated with the sructure derived from it.
pub update: enso_frp::Stream<#st_name>,
/// Emit this event when you want to initialize all nodes dependant on the `update`
/// field.
pub init: enso_frp::stream::WeakNode<enso_frp::SourceData>
}
#[automatically_derived]
impl #st_name {
/// Create FRP endpoints used to obtain [`#st_name`] from a style sheet.
pub fn from_theme(network: &enso_frp::Network, style: &StyleWatchFrp) -> #update_struct_name {
impl #ensogl_core::display::style::FromTheme for #st_name {
fn from_theme(
network: &#ensogl_core::frp::Network,
style: &#ensogl_core::display::shape::StyleWatchFrp,
) -> #ensogl_core::frp::Sampler<#st_name> {
use #ensogl_core as __ensogl_core;
#theme_getters
#frp_network_description
#update_struct_name {update:layout_update,init}
layout_sampler
}
}
};
out
}
fn ensogl_core_crate() -> Ident {
use proc_macro_crate::crate_name;
use proc_macro_crate::FoundCrate;
match crate_name("ensogl-core").or_else(|_| crate_name("ensogl")) {
Ok(FoundCrate::Itself) => Ident::new("crate", Span::call_site()),
Ok(FoundCrate::Name(name)) => Ident::new(&name, Span::call_site()),
Err(proc_macro_crate::Error::CrateNotFound { .. }) =>
panic!("Could not find 'ensogl-core' or 'ensogl' in dependencies."),
Err(e) => panic!("{e:?}"),
}
}

View File

@ -7,24 +7,24 @@
//! appended to `base_path` to get the full path to the value in the theme. It can be overridden
//! using the `theme_path` attribute on the field.
//!
//! A custom accessor function can be provided for each field using the `accessor` attribute. This
//! function should have a following signature:
//! A custom accessor function can be provided for each type by implementing the [`ThemeAccess`]
//! trait. This implementation should have a following signature:
//!
//! ```ignore
//! fn accessor<P: Into<ensogl_core::display::style::Path>>(
//! network: &frp::Network,
//! style: &StyleWatchFrp,
//! path: P,
//! ) -> frp::Sampler<T>
//! impl ThemeAccess for CustomType {
//! fn from_style_data(path_str: &str, data: &Option<style::Data>) -> Self {
//! // ...
//! }
//! }
//! ```
//! where `T` is the type of the field. This accessor will be used to retrieve the value from the
//! stylesheet. If no accessor is provided, a standard getters of the [`StyleWatchFrp`] are used
//! instead.
//!
//! This implementation will be used to retrieve the value from the stylesheet. By default, the
//! types `f32`, `ImString`, `color::Rgba` and `color::Lcha` are supported.
//!
//! Example usage
//!```no_compile
//! use ensogl_core::data::color;
//! use ensogl_derive_theme::FromTheme;
//! use ensogl_core::display::style::FromTheme;
//!
//! #[derive(FromTheme)]
//! #[base_path = "ensogl_hardcoded_theme"]
@ -32,8 +32,7 @@
//! some_number: f32,
//! some_color: color::Rgba,
//! #[theme_path = "ensogl_hardcoded_theme::some_path::label"]
//! some_label: String,
//! #[accessor = "my_custom_accessor"]
//! some_label: ImString,
//! some_custom_value: CustomType,
//! }
//! ```
@ -87,7 +86,7 @@ mod from_theme;
/// Implements the `FromTheme` derive macro. See thr crate docs for more information.
#[proc_macro_derive(FromTheme, attributes(base_path, theme_path, accessor))]
pub fn derive_from_thee(input: TokenStream) -> TokenStream {
pub fn derive_from_theme(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
from_theme::expand(input).into()
}

View File

@ -16,9 +16,9 @@
#![warn(missing_debug_implementations)]
use enso_prelude::*;
use ensogl_core::prelude::*;
use enso_shapely::before_main;
use ensogl_core::prelude::ImString;
use ensogl_text::font::DEFAULT_FONT;
use ensogl_text::font::DEFAULT_FONT_MONO;
@ -37,11 +37,11 @@ macro_rules! _define_theme_literals {
$id $theme [$($path)*] $qual { $($var).+ = $($e),* } $($($rest)*)?
}
};
($id:tt $theme:ident [$($path:ident)*] $var:ident = $($e:expr),* $(;$($rest:tt)*)?) => {
($id:tt $theme:ident [$($path:ident)*] $(#[$meta:meta])* $var:ident = $($e:expr),* $(;$($rest:tt)*)?) => {
$theme.set(stringify!($($path.)*$var), _select_theme_expr!{$id $($e),*});
_define_theme_literals!{$id $theme [$($path)*] $($($rest)*)?}
};
($id:tt $theme:ident [$($path:ident)*] $path_segment:ident {$($t:tt)*} $($rest:tt)*) => {
($id:tt $theme:ident [$($path:ident)*] $(#[$meta:meta])* $path_segment:ident {$($t:tt)*} $($rest:tt)*) => {
_define_theme_literals!{$id $theme [$($path)* $path_segment] $($t)*}
_define_theme_literals!{$id $theme [$($path)*] $($rest)*}
};
@ -56,11 +56,13 @@ macro_rules! _define_theme_modules {
[$($path)*] $qual {$($var).+ = $($e),*} $($($rest)*)?
}
};
([$($path:ident)*] $var:ident = $($e:expr),* $(;$($rest:tt)*)?) => {
([$($path:ident)*] $(#[$meta:meta])* $var:ident = $($e:expr),* $(;$($rest:tt)*)?) => {
$(#[$meta])*
pub const $var : StaticPath = StaticPath::new(stringify!($($path.)*$var));
_define_theme_modules!{[$($path)*] $($($rest)*)?}
};
([$($path:ident)*] $path_segment:ident {$($t:tt)*} $($rest:tt)*) => {
([$($path:ident)*] $(#[$meta:meta])* $path_segment:ident {$($t:tt)*} $($rest:tt)*) => {
$(#[$meta])*
pub mod $path_segment {
use ensogl_core::display::style::StaticPath;
pub const HERE : StaticPath = StaticPath::new(stringify!($($path.)*$path_segment));
@ -72,6 +74,10 @@ macro_rules! _define_theme_modules {
/// Select the theme expression by its number.
macro_rules! _select_theme_expr {
// when only one expression is specified, use it for all numbers.
($_id:tt $e0:expr) => {
$e0
};
(0 $e0:expr $(,$rest:tt)*) => {
$e0
};
@ -487,24 +493,12 @@ define_themes! { [light:0, dark:1]
}
}
code {
syntax {
base = Lcha(0.09,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,0.7);
disabled = Lcha(0.7,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,0.2);
expected = Lcha(0.7,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,0.3);
selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7);
profiling {
base = Lcha(1.0,0.0,0.0,0.9) , Lcha(0.0,0.0,0.0,0.7);
disabled = Lcha(1.0,0.0,0.0,0.5) , Lcha(0.0,0.0,0.0,0.2);
expected = Lcha(1.0,0.0,0.0,0.5) , Lcha(0.0,0.0,0.0,0.3);
selection = Lcha(1.0,0.0,0.0,1.0) , Lcha(0.0,0.0,0.0,1.0);
}
}
types {
hue_steps = 512.0 , 512.0;
hue_shift = 0.0, 0.0;
lightness = 0.72 , 0.7;
chroma = 0.7 , 0.4;
any = code::syntax::base , code::syntax::base;
any = Lcha(0.09,0.0,0.0,1.0) , Lcha(1.0,0.0,0.0,0.7);
any.selection = Lcha(0.8,0.0,0.0,1.0) , Lcha(0.5,0.0,0.0,1.0);
selected = graph_editor::node::background , graph_editor::node::background;
overriden {
@ -545,21 +539,14 @@ define_themes! { [light:0, dark:1]
right = 300.0, 300.0;
}
node {
// Original RGB values (for reference after fixing color-conversion issues)
// light: rgb(253,254,255), old-dark: Lcha(0.2,0.014,0.18,1.0), dark: rgb(47,48,50)
background = Rgba(0.992,0.996,1.0,1.0), Rgba(0.182,0.188,0.196,1.0);
background.skipped = graph_editor::node::background , graph_editor::node::background;
selection = selection, selection;
port_color_tint = Rgba(1.0,1.0,1.0,0.15), Rgba(1.0,1.0,1.0,0.15);
text = Lcha(0.09,0.0,0.0,1.0), Lcha(1.0,0.0,0.0,0.7);
corner_radius = 14.0, 14.0;
selection {
size = 3.5 , 3.5;
offset = 3.75 , 3.75;
}
text = Rgba(0.078,0.067,0.137,0.85) , Lcha(1.0,0.0,0.0,0.7);
text {
missing_arg = Rgba(0.078,0.067,0.137,0.25) , Lcha(1.0,0.0,0.0,0.3);
variant.dimmed = Lcha(0.7,0.0,0.0,0.7) , Lcha(0.25,0.014,0.18,1.0);
selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7);
size = 20.0 , 20.0;
opacity = 0.2 , 0.2;
hover_opacity = 0.1 , 0.1;
}
actions {
context_switch {
@ -573,14 +560,13 @@ define_themes! { [light:0, dark:1]
edited = Lcha::yellow(0.9,1.0), Lcha::yellow(0.9,1.0);
}
error {
dataflow = Rgba(1.0,0.341,0.125,1.0), Rgba(1.0,0.341,0.125,1.0);
dataflow = Lcha(0.566,0.564,0.082,1.0), Lcha(0.566,0.564,0.082,1.0);
panic = Rgba(0.7,0.235,0.08,1.0), Rgba(0.7,0.235,0.08,1.0);
warning = Rgba(1.0,0.655,0.141,1.0), Rgba(1.0,0.655,0.141,1.0);
width = 4.0 , 4.0;
repeat_x = 20.0 , 20.0;
repeat_y = 20.0 , 20.0;
stripe_width = 10.0 , 10.0;
stripe_angle = 45.0 , 45.0;
stripe_gap = 20.0 , 20.0;
stripe_angle = 135.0 , 135.0;
}
profiling {
lightness = code::types::lightness , code::types::lightness;
@ -591,6 +577,12 @@ define_themes! { [light:0, dark:1]
type_label {
offset_y = -23.0, -23.0;
}
temp_colors {
color_0 = Lch(0.491, 0.339, 0.727), Lch(0.491, 0.339, 0.727);
color_1 = Lch(0.447, 0.379, 0.968), Lch(0.447, 0.379, 0.968);
color_2 = Lch(0.444, 0.124, 0.701), Lch(0.444, 0.124, 0.701);
}
}
visualization {
background = graph_editor::node::background, graph_editor::node::background;
@ -624,6 +616,7 @@ define_themes! { [light:0, dark:1]
}
}
edge {
disabled_color = Lcha(0.95,0.0,0.0,1.0), Lcha(0.95,0.0,0.0,1.0);
split {
lightness_factor = 1.2 , 0.2;
chroma_factor = 0.8 , 1.0;
@ -669,35 +662,79 @@ define_themes! { [light:0, dark:1]
}
}
}
/// Styles dedicated for each individual node widget kind. The name of the style group should
/// match the widget module name in `node::input::widget`.
widget {
activation_shape {
base = Lcha(0.56708, 0.23249, 0.71372, 1.0), Lcha(0.56708, 0.23249, 0.71372, 1.0);
connected = graph_editor::node::background , graph_editor::node::background;
single_choice {
triangle_base = Lcha(1.0,0.0,0.0,0.5);
triangle_connected = Lcha(1.0,0.0,0.0,1.0);
triangle_size = Vector2(8.0, 6.0);
/// Additional space around the triangle shape that will detect mouse hover.
triangle_offset = Vector2(0.0, -7.0);
dropdown_offset = Vector2(0.0, -20.0);
dropdown_max_size = Vector2(300.0, 500.0);
dropdown_tint = Rgba(0.0,0.0,0.0,0.1);
}
list_view {
background = graph_editor::node::background , graph_editor::node::background;
highlight = Rgba(0.906,0.914,0.922,1.0) , Lcha(1.0,0.0,0.0,0.15); // rgb(231,233,235)
text = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7);
background = graph_editor::node::background;
highlight = Rgba(0.906,0.914,0.922,1.0), Lcha(1.0,0.0,0.0,0.15); // rgb(231,233,235)
text = Lcha(0.0,0.0,0.0,0.7), Lcha(1.0,0.0,0.0,0.7);
text {
selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7);
font = DEFAULT_FONT_MONO, DEFAULT_FONT_MONO;
size = 12.0, 12.0;
highlight_bold = 0.02, 0.02;
selection = Lcha(0.7,0.0,0.125,0.7);
font = DEFAULT_FONT_MONO;
size = 12.0;
highlight_bold = 0.02;
}
entry {
padding = 10.0, 10.0;
padding = 10.0;
}
highlight {
height = 24.0, 24.0;
corner_radius = 12.0, 12.0;
height = 24.0;
corner_radius = 12.0;
}
padding = 5.0, 5.0;
padding = 5.0;
}
blank {
color = Lcha(0.0,0.0,0.0,0.33);
size = Vector2(20.0, 4.0);
margin_sides = 2.0;
margin_top = 8.0;
corner_radius = 4.0;
}
label {
/// Base label style, used when the label doesn't belong to any of the groups defined
/// below.
base_color = Lcha(1.0,0.0,0.0,1.0), Lcha(1.0,0.0,0.0,0.7);
base_weight = 400.0;
/// Label style when the node is disabled. For disabled nodes, all labels are rendered
/// with this style.
disabled_color = Lcha(0.95,0.0,0.0,0.9);
disabled_weight = 400.0;
/// Label style for placeholder argument names. Implies that the argument value is
/// using default value.
placeholder_color = Lcha(1.0,0.0,0.0,0.7);
placeholder_weight = 700.0;
/// Label style for connected ports. In connected ports, all labels are rendered with
/// this style.
connected_color = Lcha(1.0,0.0,0.0,1.0);
connected_weight = 400.0;
}
separator {
color = Rgba(0.0, 0.0, 0.0, 0.12);
margin = 7.5;
width = 1.0;
}
argument_name {
/// Label style for argument names.
color = Lcha(1.0,0.0,0.0,0.7);
margin = widget::separator::margin;
weight = 400.0;
}
}
colors {
dimming {
lightness_factor = 1.1 , 1.1;
chroma_factor = 0.2 , 0.2;
lightness_factor = 1.1;
chroma_factor = 0.2;
}
}
component {

View File

@ -249,7 +249,7 @@ impl DropDownMenu {
eval frp.input.set_entries ([model](entries) {
let entries:list_view::entry::SingleMaskedProvider<Entry> = entries.clone_ref().into();
model.content.set(entries.clone());
model.content.replace(Some(entries.clone()));
let entries = list_view::entry::AnyModelProvider::<Entry>::new(entries);
model.selection_menu.frp.set_entries.emit(entries);
});
@ -424,6 +424,6 @@ impl DropDownMenu {
/// Set the layer of all text labels.
pub fn set_label_layer(&self, layer: &display::scene::Layer) {
self.model.selection_menu.set_label_layer(layer);
self.model.label.add_to_scene_layer(layer);
layer.add(&self.model.label);
}
}

View File

@ -101,8 +101,8 @@ impl EntryData {
label_bold.set_property_default(text::Weight::Bold);
display_object.add_child(&label_thin);
if let Some(layer) = text_layer {
label_thin.add_to_scene_layer(layer);
label_bold.add_to_scene_layer(layer);
layer.add(&label_thin);
layer.add(&label_bold);
}
let selected = default();
let deferred_label = default();
@ -138,7 +138,7 @@ impl EntryData {
fn set_content(&self, text: &ImString) {
self.selected_label().set_content(text.clone_ref());
self.deferred_label.set(text.clone_ref());
self.deferred_label.replace(Some(text.clone_ref()));
}
}

View File

@ -106,7 +106,7 @@ impl EntryData {
display_object.add_child(&label);
display_object.add_child(&background);
if let Some(layer) = text_layer {
label.add_to_scene_layer(layer);
layer.add(&label);
}
Self { display_object, label, background }
}

View File

@ -85,7 +85,7 @@ impl Model {
// Temporary solution. The depth management needs to allow defining relative position of
// the text and background and let the whole component to be set to am an arbitrary layer.
background_layer.add(&self.background);
self.label.add_to_scene_layer(text_layer);
text_layer.add(&self.label);
}
/// Change the size based on the size of the contained text, returning the new size including
@ -149,9 +149,7 @@ impl Label {
Label { model, frp }.init()
}
/// Set layers for Label's background and text respectively. This is needed because
/// `text::Text` uses its own `add_to_scene_layer` method instead of utilizing more common
/// [`Layer::add_exclusive`].
/// Set layers for Label's background and text respectively.
pub fn set_layers(&self, background_layer: &Layer, text_layer: &Layer) {
self.model.set_layers(background_layer, text_layer);
}

View File

@ -71,9 +71,7 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
/// Resize the entry's view to fit a new width.
fn set_max_width(&self, max_width_px: f32);
/// Set the layer of all [`text::Text`] components inside. The [`text::Text`] component is
/// handled in a special way, and is often in different layer than shapes. See TODO comment
/// in [`text::Text::add_to_scene_layer`] method.
/// Set the layer of all [`text::Text`] components inside.
fn set_label_layer(&self, label_layer: &display::scene::Layer);
}
@ -152,7 +150,7 @@ impl Entry for Label {
}
fn set_label_layer(&self, label_layer: &display::scene::Layer) {
self.label.add_to_scene_layer(label_layer);
label_layer.add(&self.label);
}
}

View File

@ -199,14 +199,14 @@ struct Model {
impl Model {
fn resize(&self, size: Vector2) {
self.h_scrollbar.set_y(-size.y + scrollbar::WIDTH / 2.0);
let scrollbar_y = size.x - scrollbar::WIDTH / 2.0 + scrollbar::PADDING / 2.0 + 1.0;
self.v_scrollbar.set_x(scrollbar_y);
self.h_scrollbar.set_x(size.x / 2.0);
self.v_scrollbar.set_y(-size.y / 2.0);
self.v_scrollbar.set_xy((size.x - scrollbar::WIDTH, 0.0));
self.h_scrollbar.set_xy((0.0, -size.y));
self.v_scrollbar.set_length(size.y);
self.h_scrollbar.set_length(size.x);
self.v_scrollbar.set_thumb_size(size.y);
self.h_scrollbar.set_thumb_size(size.x);
self.mask.set_size(size);
self.mask.set_x(size.x / 2.0);
self.mask.set_y(-size.y / 2.0);
self.mask.set_xy((size.x / 2.0, -size.y / 2.0));
}
}
@ -281,10 +281,6 @@ impl ScrollArea {
model.h_scrollbar.set_max <+ frp.set_content_width;
model.v_scrollbar.set_max <+ frp.set_content_height;
model.h_scrollbar.set_thumb_size <+ frp.resize.map(|size| size.x);
model.v_scrollbar.set_thumb_size <+ frp.resize.map(|size| size.y);
model.h_scrollbar.set_length <+ frp.resize.map(|size| size.x);
model.v_scrollbar.set_length <+ frp.resize.map(|size| size.y);
frp.source.scroll_area_height <+ frp.resize.map(|size| size.y);
eval frp.resize((size) model.resize(*size));
@ -341,7 +337,6 @@ impl ScrollArea {
}
});
frp.source.viewport <+ viewport;
}

View File

@ -25,16 +25,35 @@ use ensogl_core::animation::delayed::DelayedAnimation;
use ensogl_core::animation::overshoot::OvershootAnimation;
use ensogl_core::application;
use ensogl_core::application::Application;
use ensogl_core::control::io::mouse;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::shape::Rectangle;
use ensogl_core::display::shape::StyleWatchFrp;
use ensogl_hardcoded_theme as theme;
use ensogl_selector as selector;
use ensogl_selector::model::Model;
use ensogl_selector::Bounds;
// =============
// === Style ===
// =============
#[derive(Debug, Clone, Copy, Default, FromTheme)]
struct Style {
#[theme_path = "theme::component::slider::overshoot_limit"]
overshoot_limit: f32,
#[theme_path = "theme::component::slider::track::color"]
default_color: color::Lcha,
#[theme_path = "theme::component::slider::track::hover_color"]
hover_color: color::Lcha,
#[theme_path = "theme::component::slider::background::color"]
bg_default_color: color::Lcha,
#[theme_path = "theme::component::slider::background::hover_color"]
bg_hover_color: color::Lcha,
}
// =================
// === Constants ===
// =================
@ -98,38 +117,16 @@ ensogl_core::define_endpoints! {
impl Frp {
/// Initialize the FRP network.
pub fn init(&self, app: &Application, model: &Model, style: &StyleWatchFrp) {
fn init(&self, app: &Application, model: &Rc<Model>, style: &StyleWatchFrp) {
let frp = &self;
let network = &frp.network;
let scene = &app.display.default_scene;
let mouse = &scene.mouse.frp_deprecated;
let thumb_position = OvershootAnimation::new(network);
let thumb_color = color::Animation::new(network);
let background_color = color::Animation::new(network);
let activity_cool_off = DelayedAnimation::new(network);
activity_cool_off.frp.set_delay(HIDE_DELAY);
activity_cool_off.frp.set_duration(0.0);
frp::extend! { network
resize <- frp.set_length.map(|&length| Vector2::new(length,WIDTH));
}
let base_frp = selector::Frp::new(app, model, style, network, resize.clone(), mouse);
model.use_track_handles(false);
model.set_track_corner_round(true);
model.show_background(true);
model.show_shadow(false);
model.show_left_overflow(false);
model.show_right_overflow(false);
model.set_padding(PADDING);
let overshoot_limit = style.get_number(theme::component::slider::overshoot_limit);
let default_color = style.get_color(theme::component::slider::track::color);
let hover_color = style.get_color(theme::component::slider::track::hover_color);
let bg_default_color = style.get_color(theme::component::slider::background::color);
let bg_hover_color = style.get_color(theme::component::slider::background::hover_color);
let style = Style::from_theme(network, style);
let click_and_hold_timer = frp::io::timer::DelayedInterval::new(network);
let click_and_hold_config = frp::io::timer::DelayedIntervalConfig::new(
@ -139,14 +136,28 @@ impl Frp {
frp::extend! { network
init_theme <- any_mut::<()>();
// == Mouse events ==
let track_over = model.track.on_event::<mouse::Over>();
let track_out = model.track.on_event::<mouse::Out>();
let track_down = model.track.on_event::<mouse::Down>();
let background_down = model.background.on_event::<mouse::Down>();
let scene_up = scene.on_event::<mouse::Up>();
let mouse_move = scene.on_event::<mouse::Move>();
track_hover <- bool(&track_out, &track_over).on_change();
is_dragging_track <- bool(&scene_up, &track_down).on_change();
is_dragging_background <- bool(&scene_up, &background_down).on_change();
// == Thumb position ==
// Overshoot control
bar_not_filled <- all_with(&frp.set_thumb_size, &frp.set_max, |&size, &max| size < max);
overshoot_enabled <- frp.set_overshoot_enabled && bar_not_filled;
// Scrolling and Jumping
thumb_position.set_overshoot_limit <+ all(&overshoot_limit, &init_theme)._0();
thumb_position.set_overshoot_limit <+ style.map(|s| s.overshoot_limit);
thumb_position.soft_change_by <+ frp.scroll_by.gate(&overshoot_enabled);
thumb_position.hard_change_by <+ frp.scroll_by.gate_not(&overshoot_enabled);
thumb_position.hard_change_to <+ any(&frp.scroll_to,&frp.jump_to);
@ -159,8 +170,9 @@ impl Frp {
// === Mouse position in local coordinates ===
mouse_position <- mouse.position.map(f!([scene,model](pos)
scene.screen_to_object_space(&model,*pos)));
mouse_position <- mouse_move.map(f!([scene, model] (event)
scene.screen_to_object_space(&model.display_object, event.client_centered())
));
// We will initialize the mouse position with `Vector2(f32::NAN,f32::NAN)`, because the
// default `Vector2(0.0,0.0)` would reveal the scrollbar before we get the actual mouse
@ -169,19 +181,6 @@ impl Frp {
mouse_position <- any(&mouse_position,&init_mouse_position);
// === Color ===
default_color <- all(&default_color,&init_theme)._0().map(|c| color::Lcha::from(*c));
hover_color <- all(&hover_color,&init_theme)._0().map(|c| color::Lcha::from(*c));
bg_default_color <- all(&bg_default_color,&init_theme)._0().map(|c| color::Lcha::from(*c));
bg_hover_color <- all(&bg_hover_color,&init_theme)._0().map(|c| color::Lcha::from(*c));
engaged <- base_frp.track_hover || base_frp.is_dragging_track;
thumb_color.target_color <+ engaged.switch(&default_color,&hover_color).map(|c| c.opaque);
background_color.target_color <+ engaged.switch(&bg_default_color,&bg_hover_color).map(|c| c.opaque);;
eval thumb_color.value((c) model.set_track_color(color::Rgba::from(*c)));
eval background_color.value((c) model.set_background_color(color::Rgba::from(*c)));
// === Hiding ===
// We start a delayed animation whenever the bar is scrolled to a new place (it is
@ -203,19 +202,31 @@ impl Frp {
// from the sides. This could be handled differently, but the solution was chosen for
// the simplicity of the implementation and the feeling of the interaction.
vert_mouse_distance <- all_with(&mouse_position,&frp.set_length,|&pos,&length| {
let scrollbar_x_range = (-length/2.0)..=(length/2.0);
let scrollbar_x_range = 0.0..=length;
if scrollbar_x_range.contains(&pos.x) {
pos.y.abs() - WIDTH / 2.0
(pos.y - WIDTH / 2.0).abs() - WIDTH / 2.0
} else {
f32::INFINITY
}
});
target_alpha <- all_with5(&recently_active,&base_frp.is_dragging_track,
&vert_mouse_distance,&frp.set_thumb_size,&frp.set_max,Self::compute_target_alpha);
thumb_color.target_alpha <+ target_alpha.map2(&default_color, |target_alpha,base_color| target_alpha*base_color.alpha);
background_color.target_alpha <+ target_alpha.map2(&bg_default_color, |target_alpha,base_color| target_alpha*base_color.alpha);;
// === Color ===
engaged <- track_hover || is_dragging_track;
alpha <- all_with5(&recently_active,&is_dragging_track,
&vert_mouse_distance,&frp.set_thumb_size,&frp.set_max,Self::compute_target_alpha);
target_colors <- all_with3(&style, &engaged, &alpha, |s, &engaged, &alpha| {
let thumb = if engaged { s.hover_color } else { s.default_color };
let bg = if engaged { s.bg_hover_color } else { s.bg_default_color };
(thumb.multiply_alpha(alpha), bg.multiply_alpha(alpha))
});
let thumb_color = color::Animation::new(network);
let background_color = color::Animation::new(network);
thumb_color.target <+ target_colors._0();
background_color.target <+ target_colors._1();
eval thumb_color.value((c) model.set_track_color(c.into()));
eval background_color.value((c) model.set_background_color(c.into()));
}
frp::extend! { network
@ -233,28 +244,30 @@ impl Frp {
// The size at which we render the thumb on screen, in normalized units. Can differ from
// the actual thumb size if the thumb is smaller than the min.
visual_size <- all_with(&normalized_size,&min_visual_size,|&size,&min|
size.clamp(min, 1.0));
size.min(1.0).max(min));
// The position at which we render the thumb on screen, in normalized units.
visual_start <- all_with(&normalized_position,&visual_size,|&pos,&size|
pos * (1.0 - size));
visual_bounds <- all_with(&visual_start,&visual_size,|&start,&size|
Bounds::new(start,start+size));
visual_center <- visual_bounds.map(|bounds| bounds.center());
thumb_center_px <- all_with(&visual_center,&inner_length, |normalized,length|
(normalized - 0.5) * length);
thumb_center_px <- all_with(&visual_center,&inner_length,
|normalized,length| normalized * length);
// Because of overshoot, we want to further clamp true visual bounds, so the thumb does
// not go outside the bar. We want to only limit the bounds we use for drawing the thumb
// itself, without influencing other logic that depends on true thumb size or position.
clamped_visual_bounds <- visual_bounds.map(|bounds|
Bounds::new(bounds.start.max(0.0), bounds.end.min(1.0)));
update_slider <- all(&clamped_visual_bounds,&resize);
eval update_slider(((value,size)) model.set_background_range(*value,*size));
size <- frp.set_length.map(|&length| Vector2::new(length,WIDTH));
update_slider <- all(&clamped_visual_bounds,&size);
eval update_slider(((value,size)) model.update_layout(*value,*size));
// === Clicking ===
frp.scroll_by <+ base_frp.background_click.map3(&thumb_center_px,&frp.set_thumb_size,
background_click <- mouse_position.sample(&background_down);
frp.scroll_by <+ background_click.map3(&thumb_center_px,&frp.set_thumb_size,
|click_position,thumb_center,thumb_size| {
let direction = if click_position.x > *thumb_center { 1.0 } else { -1.0 };
direction * thumb_size * CLICK_JUMP_PERCENTAGE
@ -263,9 +276,9 @@ impl Frp {
// === Click and hold repeated scrolling ===
background_drag_start <- base_frp.is_dragging_background.on_true();
background_drag_start <- is_dragging_background.on_true();
click_and_hold_timer.restart <+ background_drag_start.constant(click_and_hold_config);
click_and_hold_timer.stop <+ base_frp.is_dragging_background.on_false();
click_and_hold_timer.stop <+ is_dragging_background.on_false();
mouse_pos_at_timer_trigger <- mouse_position.sample(&click_and_hold_timer.on_trigger);
offset_from_thumb_px <- mouse_pos_at_timer_trigger.map2(&thumb_center_px,
@ -284,7 +297,7 @@ impl Frp {
// === Dragging ===
drag_started <- base_frp.is_dragging_track.on_change().on_true().constant(());
drag_started <- is_dragging_track.on_change().on_true().constant(());
x <- all4(&mouse_position,&inner_length,&frp.set_max,&frp.thumb_position);
x <- x.sample(&drag_started);
drag_offset <- x.map(|(mouse_px,length_px,max,thumb_pos)| {
@ -292,7 +305,7 @@ impl Frp {
mouse_px.x - thumb_position_px
});
x <- all4(&mouse_position,&drag_offset,&inner_length,&frp.set_max);
x <- x.gate(&base_frp.is_dragging_track);
x <- x.gate(&is_dragging_track);
frp.jump_to <+ x.map(|(mouse_px,offset_px,length_px,max)| {
let target_px = mouse_px.x - offset_px;
target_px / length_px * max
@ -307,7 +320,6 @@ impl Frp {
frp.set_thumb_size(0.2);
frp.set_max(1.0);
init_mouse_position.emit(Vector2(f32::NAN, f32::NAN));
init_theme.emit(());
}
fn compute_target_alpha(
@ -364,7 +376,7 @@ pub struct Scrollbar {
impl Scrollbar {
/// Constructor.
pub fn new(app: &Application) -> Self {
let model = Rc::new(Model::new(app));
let model = Rc::new(Model::new());
let frp = Frp::default();
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
frp.init(app, &model, &style);
@ -395,3 +407,40 @@ impl application::View for Scrollbar {
Scrollbar::new(app)
}
}
#[derive(Debug, display::Object)]
struct Model {
display_object: display::object::Instance,
background: Rectangle,
track: Rectangle,
}
impl Model {
fn new() -> Self {
let display_object = display::object::Instance::new_named("Scrollbar");
let track = Rectangle();
let background = Rectangle();
background.allow_grow().set_alignment_center();
track.set_inset(PADDING).set_corner_radius_max();
display_object.add_child(&background);
display_object.add_child(&track);
Self { display_object, background, track }
}
fn set_track_color(&self, color: color::Rgba) {
self.track.set_color(color);
}
fn set_background_color(&self, color: color::Rgba) {
self.background.set_color(color);
}
fn update_layout(&self, value: Bounds, size: Vector2) {
let start_px = value.start * size.x;
let end_px = value.end * size.x;
let length_px = end_px - start_px;
self.display_object.set_size(size);
self.track.set_size(Vector2(length_px, size.y));
self.track.set_xy((start_px, 0.0));
}
}

View File

@ -57,7 +57,7 @@ impl Model {
let label_left = app.new_view::<text::Text>();
app.display.default_scene.layers.main.remove(&label_full);
label_full.add_to_scene_layer(&app.display.default_scene.layers.label);
app.display.default_scene.layers.label.add(&label_full);
display_object.add_child(&label_full);
display_object.add_child(&label_left);

View File

@ -104,14 +104,10 @@ impl Model {
display_object.add_child(&track);
display_object.add_child(&right_overflow);
scene.layers.main.remove(&label_left);
label_left.add_to_scene_layer(&scene.layers.label);
scene.layers.main.remove(&label_right);
label_right.add_to_scene_layer(&scene.layers.label);
scene.layers.main.remove(&caption_left);
caption_left.add_to_scene_layer(&scene.layers.label);
scene.layers.main.remove(&caption_center);
caption_center.add_to_scene_layer(&scene.layers.label);
scene.layers.label.add(&label_left);
scene.layers.label.add(&label_right);
scene.layers.label.add(&caption_left);
scene.layers.label.add(&caption_center);
Self {
background,

View File

@ -126,8 +126,8 @@ impl<Metric: Default, Str: Default> Default for Change<Metric, Str> {
ensogl_core::define_endpoints! {
Input {
cursors_move (Option<Transform>),
cursors_select (Option<Transform>),
cursors_move (Transform),
cursors_select (Transform),
set_cursor (Location),
add_cursor (Location),
set_single_selection (selection::Shape),

View File

@ -13,9 +13,10 @@ use crate::buffer::selection::Selection;
// =================
/// Selection transformation patterns. Used for the needs of keyboard and mouse interaction.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Transform {
/// Select all text.
#[default]
All,
/// Move to the left by one grapheme cluster.
Left,
@ -98,18 +99,14 @@ impl BufferModel {
/// If `modify` is `true`, the selections are modified, otherwise the results of individual
/// region movements become cursors. Modify is often mapped to the `shift` button in text
/// editors.
pub fn moved_selection(&self, movement: Option<Transform>, modify: bool) -> selection::Group {
movement
.map(|transform| {
let mut result = selection::Group::new();
let selections = self.selection.borrow().clone();
for &selection in selections.iter() {
let new_selection = self.moved_selection_region(transform, selection, modify);
result.merge(new_selection);
}
result
})
.unwrap_or_default()
pub fn moved_selection(&self, transform: Transform, modify: bool) -> selection::Group {
let mut result = selection::Group::new();
let selections = self.selection.borrow().clone();
for &selection in selections.iter() {
let new_selection = self.moved_selection_region(transform, selection, modify);
result.merge(new_selection);
}
result
}
/// Compute the result of movement on one selection region.

View File

@ -65,6 +65,7 @@ pub mod shape {
use super::*;
ensogl_core::shape! {
above = [ensogl_core::display::shape::compound::rectangle];
pointer_events = false;
alignment = center;
(style:Style, selection:f32, start_time:f32, not_blinking:f32, color_rgb:Vector3<f32>) {

View File

@ -418,6 +418,8 @@ impl Text {
let scene = &m.app.display.default_scene;
let mouse = &scene.mouse.frp_deprecated;
let buf = &m.buffer.frp;
frp::extend! { network
// === Setting Cursors ===
@ -433,63 +435,63 @@ impl Text {
loc_on_mouse_set <- mouse_on_set.map(f!((p) m.screen_to_text_location(*p)));
loc_on_mouse_add <- mouse_on_add.map(f!((p) m.screen_to_text_location(*p)));
loc_on_set_at_front <- input.set_cursor_at_text_start.map(f_!([] default()));
loc_on_set_at_front <- input.set_cursor_at_text_start.constant(default());
loc_on_set_at_end <- input.set_cursor_at_text_end.map(f_!(m.last_line_last_location()));
loc_on_add_at_front <- input.add_cursor_at_front.map(f_!([] default()));
loc_on_add_at_front <- input.add_cursor_at_front.constant(default());
loc_on_add_at_end <- input.add_cursor_at_end.map(f_!(m.last_line_last_location()));
loc_on_set <- any(loc_on_set,loc_on_mouse_set,loc_on_set_at_front,loc_on_set_at_end);
loc_on_add <- any(loc_on_add,loc_on_mouse_add,loc_on_add_at_front,loc_on_add_at_end);
m.buffer.frp.set_cursor <+ loc_on_set;
m.buffer.frp.add_cursor <+ loc_on_add;
m.buffer.frp.set_single_selection <+ shape_on_select;
buf.set_cursor <+ loc_on_set;
buf.add_cursor <+ loc_on_add;
buf.set_single_selection <+ shape_on_select;
// === Cursor Transformations ===
eval_ input.remove_all_cursors (m.buffer.frp.remove_all_cursors());
buf.remove_all_cursors <+ input.remove_all_cursors;
eval_ input.keep_first_selection_only (m.buffer.frp.keep_first_selection_only());
eval_ input.keep_last_selection_only (m.buffer.frp.keep_last_selection_only());
eval_ input.keep_first_cursor_only (m.buffer.frp.keep_first_cursor_only());
eval_ input.keep_last_cursor_only (m.buffer.frp.keep_last_cursor_only());
buf.keep_first_selection_only <+ input.keep_first_selection_only;
buf.keep_last_selection_only <+ input.keep_last_selection_only;
buf.keep_first_cursor_only <+ input.keep_first_cursor_only;
buf.keep_last_cursor_only <+ input.keep_last_cursor_only;
eval_ input.keep_newest_selection_only (m.buffer.frp.keep_newest_selection_only());
eval_ input.keep_oldest_selection_only (m.buffer.frp.keep_oldest_selection_only());
eval_ input.keep_newest_cursor_only (m.buffer.frp.keep_newest_cursor_only());
eval_ input.keep_oldest_cursor_only (m.buffer.frp.keep_oldest_cursor_only());
buf.keep_newest_selection_only <+ input.keep_newest_selection_only;
buf.keep_oldest_selection_only <+ input.keep_oldest_selection_only;
buf.keep_newest_cursor_only <+ input.keep_newest_cursor_only;
buf.keep_oldest_cursor_only <+ input.keep_oldest_cursor_only;
eval_ input.cursor_move_left (m.buffer.frp.cursors_move(Transform::Left));
eval_ input.cursor_move_right (m.buffer.frp.cursors_move(Transform::Right));
eval_ input.cursor_move_up (m.buffer.frp.cursors_move(Transform::Up));
eval_ input.cursor_move_down (m.buffer.frp.cursors_move(Transform::Down));
buf.cursors_move <+ input.cursor_move_left.constant(Transform::Left);
buf.cursors_move <+ input.cursor_move_right.constant(Transform::Right);
buf.cursors_move <+ input.cursor_move_up.constant(Transform::Up);
buf.cursors_move <+ input.cursor_move_down.constant(Transform::Down);
eval_ input.cursor_move_left_word (m.buffer.frp.cursors_move(Transform::LeftWord));
eval_ input.cursor_move_right_word (m.buffer.frp.cursors_move(Transform::RightWord));
buf.cursors_move <+ input.cursor_move_left_word.constant(Transform::LeftWord);
buf.cursors_move <+ input.cursor_move_right_word.constant(Transform::RightWord);
eval_ input.cursor_move_left_of_line (m.buffer.frp.cursors_move(Transform::LeftOfLine));
eval_ input.cursor_move_right_of_line (m.buffer.frp.cursors_move(Transform::RightOfLine));
buf.cursors_move <+ input.cursor_move_left_of_line.constant(Transform::LeftOfLine);
buf.cursors_move <+ input.cursor_move_right_of_line.constant(Transform::RightOfLine);
eval_ input.cursor_move_to_text_start (m.buffer.frp.cursors_move(Transform::StartOfDocument));
eval_ input.cursor_move_to_text_end (m.buffer.frp.cursors_move(Transform::EndOfDocument));
buf.cursors_move <+ input.cursor_move_to_text_start.constant(Transform::StartOfDocument);
buf.cursors_move <+ input.cursor_move_to_text_end.constant(Transform::EndOfDocument);
eval_ input.cursor_select_left (m.buffer.frp.cursors_select(Transform::Left));
eval_ input.cursor_select_right (m.buffer.frp.cursors_select(Transform::Right));
eval_ input.cursor_select_up (m.buffer.frp.cursors_select(Transform::Up));
eval_ input.cursor_select_down (m.buffer.frp.cursors_select(Transform::Down));
buf.cursors_select <+ input.cursor_select_left.constant(Transform::Left);
buf.cursors_select <+ input.cursor_select_right.constant(Transform::Right);
buf.cursors_select <+ input.cursor_select_up.constant(Transform::Up);
buf.cursors_select <+ input.cursor_select_down.constant(Transform::Down);
eval_ input.cursor_select_left_word (m.buffer.frp.cursors_select(Transform::LeftWord));
eval_ input.cursor_select_right_word (m.buffer.frp.cursors_select(Transform::RightWord));
buf.cursors_select <+ input.cursor_select_left_word.constant(Transform::LeftWord);
buf.cursors_select <+ input.cursor_select_right_word.constant(Transform::RightWord);
eval_ input.cursor_select_left_of_line (m.buffer.frp.cursors_select(Transform::LeftOfLine));
eval_ input.cursor_select_right_of_line (m.buffer.frp.cursors_select(Transform::RightOfLine));
buf.cursors_select <+ input.cursor_select_left_of_line.constant(Transform::LeftOfLine);
buf.cursors_select <+ input.cursor_select_right_of_line.constant(Transform::RightOfLine);
eval_ input.cursor_select_to_text_start (m.buffer.frp.cursors_select(Transform::StartOfDocument));
eval_ input.cursor_select_to_text_end (m.buffer.frp.cursors_select(Transform::EndOfDocument));
buf.cursors_select <+ input.cursor_select_to_text_start.constant(Transform::StartOfDocument);
buf.cursors_select <+ input.cursor_select_to_text_end.constant(Transform::EndOfDocument);
eval_ input.select_all (m.buffer.frp.cursors_select(Transform::All));
eval_ input.select_word_at_cursor (m.buffer.frp.cursors_select(Transform::Word));
buf.cursors_select <+ input.select_all.constant(Transform::All);
buf.cursors_select <+ input.select_word_at_cursor.constant(Transform::Word);
}
}
@ -545,7 +547,7 @@ impl Text {
copy_whole_lines <- sels_on_copy.gate(&all_empty_sels_on_copy);
copy_regions_only <- sels_on_copy.gate_not(&all_empty_sels_on_copy);
eval_ copy_whole_lines (m.buffer.frp.cursors_select(Some(Transform::Line)));
eval_ copy_whole_lines (m.buffer.frp.cursors_select(Transform::Line));
sels_on_copy_whole_lines <- copy_whole_lines.map(f_!(m.buffer.selections_contents()));
text_chubks_to_copy <- any(&sels_on_copy_whole_lines, &copy_regions_only);
eval text_chubks_to_copy ((s) m.copy(s));
@ -557,7 +559,7 @@ impl Text {
cut_whole_lines <- sels_on_cut.gate(&all_empty_sels_on_cut);
cut_regions_only <- sels_on_cut.gate_not(&all_empty_sels_on_cut);
eval_ cut_whole_lines (m.buffer.frp.cursors_select(Some(Transform::Line)));
eval_ cut_whole_lines (m.buffer.frp.cursors_select(Transform::Line));
sels_on_cut_whole_lines <- cut_whole_lines.map(f_!(m.buffer.selections_contents()));
sels_to_cut <- any(&sels_on_cut_whole_lines,&cut_regions_only);
eval sels_to_cut ((s) m.copy(s));
@ -691,25 +693,6 @@ impl Text {
// ========================
// === Layer Management ===
// ========================
impl Text {
/// Add the text area to a specific scene layer.
// TODO https://github.com/enso-org/ide/issues/1576
// You should use this function to add text to a layer instead of just `layer.add` because
// currently the text needs to store reference to the layer to perform screen to object
// space position conversion (it needs to know the camera transformation). This should be
// improved in the future and normal `layer.add` should be enough.
pub fn add_to_scene_layer(&self, layer: &display::scene::Layer) {
self.data.layer.set(layer.clone_ref());
layer.add(self);
}
}
// =================
// === TextModel ===
// =================
@ -735,9 +718,6 @@ pub struct TextModelData {
height_dirty: Cell<bool>,
/// Cache of shaped lines.
shaped_lines: RefCell<BTreeMap<Line, ShapedLine>>,
// FIXME[ao]: this is a temporary solution to handle properly areas in different views. Should
// be replaced with proper object management.
layer: CloneRefCell<display::scene::Layer>,
}
impl TextModel {
@ -751,7 +731,6 @@ impl TextModel {
frp.private.output.glyph_system.emit(Some(glyph_system.clone()));
let glyph_system = RefCell::new(glyph_system);
let buffer = buffer::Buffer::new(buffer::BufferModel::new());
let layer = CloneRefCell::new(scene.layers.main.clone_ref());
let default_size = buffer.formatting.font_size().default.value;
let first_line = Self::new_line_helper(
@ -770,7 +749,6 @@ impl TextModel {
let frp = frp.downgrade();
let data = TextModelData {
app,
layer,
frp,
buffer,
display_object,
@ -840,10 +818,12 @@ impl TextModel {
impl TextModel {
/// Transforms screen position to the object (display object) coordinate system.
fn screen_to_object_space(&self, screen_pos: Vector2) -> Vector2 {
let camera = self.layer.get().camera();
let Some(display_layer) = self.display_layer() else { return Vector2::zero() };
let camera = display_layer.camera();
let origin_world_space = Vector4(0.0, 0.0, 0.0, 1.0);
let origin_clip_space = camera.view_projection_matrix() * origin_world_space;
let inv_object_matrix = self.transformation_matrix().try_inverse().unwrap();
let inv_object_matrix = self.transformation_matrix().try_inverse();
let Some(inv_object_matrix) = inv_object_matrix else { return Vector2::zero() };
let shape = self.app.display.default_scene.frp.shape.value();
let clip_space_z = origin_clip_space.z;

View File

@ -516,11 +516,11 @@ impl Font {
}
}
/// Get number of rows in MSDF texture.
pub fn msdf_texture_rows(&self) -> usize {
/// Get the font MSDF atlas texture.
pub fn msdf_texture(&self) -> &msdf::Texture {
match self {
Font::NonVariable(font) => font.msdf_texture_rows(),
Font::Variable(font) => font.msdf_texture_rows(),
Font::NonVariable(font) => &font.atlas,
Font::Variable(font) => &font.atlas,
}
}
@ -694,11 +694,6 @@ impl<F: Family> FontTemplate<F> {
pub fn with_borrowed_msdf_texture_data<R>(&self, operation: impl FnOnce(&[u8]) -> R) -> R {
self.atlas.with_borrowed_data(operation)
}
/// Get number of rows in MSDF texture.
pub fn msdf_texture_rows(&self) -> usize {
self.atlas.rows()
}
}

View File

@ -4,26 +4,18 @@ highp float median(highp vec3 v) {
return max(min(v.x, v.y), min(max(v.x, v.y), v.z));
}
highp vec2 uv_to_texture_coord(vec2 uv) {
highp vec2 texture_glyph_offset = input_msdf_size / vec2(textureSize(input_atlas, 0));
highp vec2 offset = vec2(0.0, input_atlas_index) * texture_glyph_offset;
return offset + uv * texture_glyph_offset;
}
highp float get_fatting() {
highp vec2 local_to_px_ratio = 1.0 / fwidth(input_local.xy);
highp float font_size_px = input_font_size * (local_to_px_ratio.x + local_to_px_ratio.y) / 2.0;
highp float font_size_px = input_font_size * (local_to_px_ratio.x + local_to_px_ratio.y) * 0.5;
highp float fatting = input_sdf_weight;
return font_size_px * fatting;
}
highp float get_alpha(vec2 uv) {
highp vec2 tex_coord = uv_to_texture_coord(uv);
highp vec2 msdf_unit_tex = input_msdf_range / vec2(textureSize(input_atlas, 0));
highp vec2 msdf_unit_px = msdf_unit_tex / fwidth(tex_coord);
highp float avg_msdf_unit_px = (msdf_unit_px.x + msdf_unit_px.y) / 2.0;
highp vec2 msdf_unit_px = input_msdf_range / (fwidth(uv) * vec2(input_msdf_size));
highp float avg_msdf_unit_px = (msdf_unit_px.x + msdf_unit_px.y) * 0.5;
highp vec3 msdf_sample = texture(input_atlas,tex_coord).rgb;
highp vec3 msdf_sample = texture(input_atlas,vec3(uv, input_atlas_index)).rgb;
highp float sig_dist = median(msdf_sample) - 0.5;
highp float sig_dist_px = sig_dist * avg_msdf_unit_px + get_fatting();
highp float opacity = 0.5 + sig_dist_px;

Some files were not shown because too many files have changed in this diff Show More