Do not immediately modify code when disconnecting the edge at target side. (#7041)

Fixes #6772

When detaching an existing edge by grabbing by a source port, the node's code is no longer immediately modified. It is only changed once the edge has been either connected or destroyed. When grabbing on the source side, the existing behavior is preserved. That way, we always have guaranteed place to keep the edge connected to.

https://github.com/enso-org/enso/assets/919491/49e560cb-0a29-4c6a-97ec-4370185b8c89

In general, the detached edges are now more stable, resilient to all kinds of expression modifications during the drag.

https://github.com/enso-org/enso/assets/919491/e62450ff-46b2-466f-ac33-f4f19e66ee1d


In case there is a situation where the currently dragged edge's port is destroyed (e.g. by Undo/Redo), instead of showing glitched port position it is simply dropped.

https://github.com/enso-org/enso/assets/919491/8fb089aa-a4a5-4a8c-92eb-23aeff9867b8

# Important Notes

The whole edge connection and view handling at the graph-editor view level has been completely rewritten. The edge endpoints are now identified using new `PortId` structure, that is not dependant on the span-tree. This prepares us for eventual removal of the span-tree in favour of manipulating AST directly. Right now those `PortId`s are still stored within the span-tree nodes, but it will be easy to eventually generate them on the fly from the AST itself. The widget tree has also already been switched to that representation where appropriate.

Additionally, I have started splitting the graph editor FRP network into smaller methods. Due to its absolutely enormous size and complexity of it, I haven't finished the split completely, and mostly edge-related part is refactored. I don't want to block this PR on this any longer though, as the merge conflicts are getting a bit unwieldy to deal with.
This commit is contained in:
Paweł Grabarz 2023-06-20 23:27:39 +02:00 committed by GitHub
parent 31956aa603
commit fcaa7510c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 2672 additions and 2994 deletions

View File

@ -10,6 +10,7 @@ use crate::node::MainLine;
use ast::crumbs::Crumb;
use ast::crumbs::Crumbs;
use ast::crumbs::Located;
@ -21,10 +22,10 @@ use ast::crumbs::Crumbs;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Endpoint {
/// Id of the node where the endpoint is located.
pub node: Id,
/// Crumbs to the AST creating this endpoint. These crumbs are relative to the node's AST,
/// not just expression, if the node is binding, there'll crumb for left/right operand first.
pub crumbs: Crumbs,
pub node: Id,
/// The AST ID and location of a port to which this endpoint is connected. The location is
/// relative to the entire node's AST, including both its expression and assignment pattern.
pub port: Located<Id>,
}
impl Endpoint {
@ -33,61 +34,45 @@ impl Endpoint {
///
/// Returns None if first crumb is not present or does not denote a valid node.
fn new_in_block(block: &ast::Block<Ast>, mut crumbs: Crumbs) -> Option<Endpoint> {
let line_crumb = crumbs.pop_front()?;
let line_crumb = match line_crumb {
Crumb::Block(block_crumb) => Some(block_crumb),
_ => None,
}?;
let Some(Crumb::Block(line_crumb)) = crumbs.pop_front() else { return None };
let line_ast = block.get(&line_crumb).ok()?;
let definition = DefinitionInfo::from_line_ast(line_ast, ScopeKind::NonRoot, block.indent);
let is_non_def = definition.is_none();
let node = is_non_def.and_option_from(|| MainLine::from_ast(line_ast))?.id();
Some(Endpoint { node, crumbs })
let port_id = line_ast.get_traversing(&crumbs).ok()?.id?;
Some(Endpoint { node, port: Located::new(crumbs, port_id) })
}
}
/// Connection source, i.e. the port generating the data / identifier introducer.
pub type Source = Endpoint;
/// Connection destination, i.e. the port receiving data / identifier user.
pub type Destination = Endpoint;
// ==================
// === Connection ===
// ==================
/// Describes a connection between two endpoints: from `source` to `destination`.
/// Describes a connection between two endpoints: from `source` to `target`.
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Connection {
pub source: Source,
pub destination: Destination,
pub source: Endpoint,
pub target: Endpoint,
}
/// Lists all the connection in the graph for the given code block.
pub fn list_block(block: &ast::Block<Ast>) -> Vec<Connection> {
let identifiers = analyze_crumbable(block);
let introduced_iter = identifiers.introduced.into_iter();
type NameMap = HashMap<String, Endpoint>;
let introduced_names = introduced_iter
.flat_map(|name| {
let endpoint = Endpoint::new_in_block(block, name.crumbs)?;
Some((name.item, endpoint))
})
.collect::<NameMap>();
identifiers
.used
.into_iter()
.flat_map(|name| {
// If name is both introduced and used in the graph's scope; and both of these
// occurrences can be represented as endpoints, then we have a connection.
let source = introduced_names.get(&name.item).cloned()?;
let destination = Endpoint::new_in_block(block, name.crumbs)?;
Some(Connection { source, destination })
})
.collect()
let introduced = identifiers.introduced.into_iter();
let used = identifiers.used.into_iter();
let introduced_names: HashMap<String, Endpoint> = introduced
.flat_map(|ident| Some((ident.item, Endpoint::new_in_block(block, ident.crumbs)?)))
.collect();
used.flat_map(|ident| {
// If name is both introduced and used in the graph's scope; and both of these
// occurrences can be represented as endpoints, then we have a connection.
let source = introduced_names.get(&ident.item)?.clone();
let target = Endpoint::new_in_block(block, ident.crumbs)?;
Some(Connection { source, target })
})
.collect()
}
/// Lists all the connection in the single-expression definition body.
@ -134,7 +119,7 @@ pub fn dependent_nodes_in_def(body: &Ast, node: Id) -> HashSet<Id> {
while let Some(current_node) = to_visit.pop() {
let opt_out_connections = node_out_connections.get(&current_node);
let out_connections = opt_out_connections.iter().flat_map(|v| v.iter());
let out_nodes = out_connections.map(|c| c.destination.node);
let out_nodes = out_connections.map(|c| c.target.node);
let new_nodes_in_result = out_nodes.filter(|n| result.insert(*n));
for node in new_nodes_in_result {
to_visit.push(node)
@ -171,7 +156,7 @@ mod tests {
let repr_of = |connection: &Connection| {
let endpoint = &connection.source;
let node = graph.find_node(endpoint.node).unwrap();
let ast = node.ast().get_traversing(&endpoint.crumbs).unwrap();
let ast = node.ast().get_traversing(&endpoint.port.crumbs).unwrap();
ast.repr()
};
@ -216,27 +201,27 @@ f = fun 2";
let run = TestRun::from_block(code_block);
let c = &run.connections[0];
assert_eq!(run.endpoint_node_repr(&c.source), "a = d");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, LeftOperand]);
assert_eq!(&c.source.port.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.target), "c = a + b");
assert_eq!(&c.target.port.crumbs, &crumbs![RightOperand, LeftOperand]);
let c = &run.connections[1];
assert_eq!(run.endpoint_node_repr(&c.source), "b = d");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "c = a + b");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand, RightOperand]);
assert_eq!(&c.source.port.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.target), "c = a + b");
assert_eq!(&c.target.port.crumbs, &crumbs![RightOperand, RightOperand]);
let c = &run.connections[2];
assert_eq!(run.endpoint_node_repr(&c.source), "d = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "a = d");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
assert_eq!(&c.source.port.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.target), "a = d");
assert_eq!(&c.target.port.crumbs, &crumbs![RightOperand]);
let c = &run.connections[3];
assert_eq!(run.endpoint_node_repr(&c.source), "d = p");
assert_eq!(&c.source.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.destination), "b = d");
assert_eq!(&c.destination.crumbs, &crumbs![RightOperand]);
assert_eq!(&c.source.port.crumbs, &crumbs![LeftOperand]);
assert_eq!(run.endpoint_node_repr(&c.target), "b = d");
assert_eq!(&c.target.port.crumbs, &crumbs![RightOperand]);
// Note that line `fun a = a b` des not introduce any connections, as it is a definition.

View File

@ -191,18 +191,18 @@ impl Hash for Identifier {
/// Generate an identifier name that is not present in the given sequence.
///
/// The name is generated by taking `base` string and appending subsequent integers.
pub fn generate_name(
base: impl AsRef<str>,
unavailable: impl IntoIterator<Item = String>,
pub fn generate_name<'a>(
base: &str,
unavailable_names: impl IntoIterator<Item = &'a str>,
) -> FallibleResult<Identifier> {
let base = base.as_ref();
let is_relevant = |name: &String| name.starts_with(base);
let unavailable = unavailable.into_iter().filter(is_relevant).collect::<HashSet<_>>();
let unavailable_suffixes = unavailable_names
.into_iter()
.filter_map(|name| name.strip_prefix(base).and_then(|suffix| suffix.parse::<usize>().ok()))
.collect::<HashSet<_>>();
let name = (1..)
.find_map(|i| {
let candidate = format!("{base}{i}");
let available = !unavailable.contains(&candidate);
available.as_some(candidate)
let available = !unavailable_suffixes.contains(&i);
available.then(|| format!("{base}{i}"))
})
.unwrap(); // It never yields `None`, as we iterate infinite sequence until we find match.
Identifier::from_text(name)

View File

@ -130,7 +130,7 @@ impl Info {
/// The name shall be generated by appending number to the given base string.
pub fn generate_name(&self, base: &str) -> FallibleResult<Identifier> {
let used_names = self.used_names();
let used_names = used_names.into_iter().map(|name| name.item);
let used_names = used_names.iter().map(|name| name.item.as_str());
identifier::generate_name(base, used_names)
}

View File

@ -117,15 +117,15 @@ impl GraphHelper {
/// Get the information about node described byt the given ID.
pub fn lookup_node(&self, id: node::Id) -> FallibleResult<&NodeInfo> {
let err = CannotResolveEndpointNode(id).into();
self.nodes.iter().find(|node| node.id() == id).ok_or(err)
let found = self.nodes.iter().find(|node| node.id() == id);
found.ok_or_else(|| CannotResolveEndpointNode(id).into())
}
/// Get the identifier constituting a connection's endpoint.
pub fn endpoint_identifier(&self, endpoint: &Endpoint) -> FallibleResult<Identifier> {
let node = self.lookup_node(endpoint.node)?;
let err = || EndpointIdentifierCannotBeResolved(endpoint.clone()).into();
let endpoint_ast = node.ast().get_traversing(&endpoint.crumbs)?.clone_ref();
let endpoint_ast = node.ast().get_traversing(&endpoint.port.crumbs)?.clone_ref();
Identifier::new(endpoint_ast).ok_or_else(err)
}
@ -206,7 +206,7 @@ impl Extracted {
let mut output = None;
for connection in graph.info.connections() {
let starts_inside = extracted_nodes_set.contains(&connection.source.node);
let ends_inside = extracted_nodes_set.contains(&connection.destination.node);
let ends_inside = extracted_nodes_set.contains(&connection.target.node);
let identifier = graph.connection_variable(&connection)?;
leaves.remove(&connection.source.node);

View File

@ -1128,11 +1128,6 @@ impl<T> Block<T> {
leading_empty_lines.chain(first_line).chain(lines)
}
/// Calculate absolute indentation of lines in this block.
pub fn indent(&self, parent_indent: usize) -> usize {
parent_indent + self.indent
}
/// Iterate over non-empty lines, while keeping their absolute indices.
pub fn enumerate_non_empty_lines(&self) -> impl Iterator<Item = (usize, BlockLine<&T>)> + '_ {
self.iter_all_lines().enumerate().filter_map(

View File

@ -5,9 +5,7 @@
use crate::prelude::*;
use ast::crumbs::*;
use crate::generate::Context;
use crate::node;
use crate::SpanTree;
use ast::opr::match_named_argument;
use ast::opr::ArgWithOffset;
@ -66,14 +64,14 @@ pub trait Actions {
/// Erase element pointed by this node from operator or prefix chain.
///
/// It returns new ast root with performed action.
fn erase(&self, root: &Ast, context: &impl Context) -> FallibleResult<(Ast, crate::Crumbs)>;
fn erase(&self, root: &Ast) -> FallibleResult<Ast>;
}
impl<T: Implementation> Actions for T {
fn is_action_available(&self, action: Action) -> bool {
match action {
Action::Set => self.set_impl().is_some(),
Action::Erase => self.erase_impl::<crate::generate::context::Empty>().is_some(),
Action::Erase => self.erase_impl().is_some(),
}
}
@ -83,10 +81,10 @@ impl<T: Implementation> Actions for T {
action(root, to)
}
fn erase(&self, root: &Ast, context: &impl Context) -> FallibleResult<(Ast, crate::Crumbs)> {
fn erase(&self, root: &Ast) -> FallibleResult<Ast> {
let operation = Action::Erase;
let action = self.erase_impl().ok_or(ActionNotAvailable { operation })?;
action(root, context)
action(root)
}
}
@ -104,15 +102,14 @@ pub type SetOperation<'a> = Box<dyn FnOnce(&Ast, Ast) -> FallibleResult<Ast> + '
/// A concrete function for "set" operations on specific SpanTree node. It takes root ast
/// as argument and returns new root with action performed.
pub type EraseOperation<'a, C> =
Box<dyn FnOnce(&Ast, &C) -> FallibleResult<(Ast, crate::Crumbs)> + 'a>;
pub type EraseOperation<'a> = Box<dyn FnOnce(&Ast) -> FallibleResult<Ast> + 'a>;
/// Implementation of actions - this is for keeping in one place checking of actions availability
/// and the performing the action.
#[allow(missing_docs)]
pub trait Implementation {
fn set_impl(&self) -> Option<SetOperation>;
fn erase_impl<C: Context>(&self) -> Option<EraseOperation<C>>;
fn erase_impl(&self) -> Option<EraseOperation>;
}
impl<'a> Implementation for node::Ref<'a> {
@ -245,38 +242,29 @@ impl<'a> Implementation for node::Ref<'a> {
Ok(new_root)
})),
node::Kind::Token => None,
_ => {
match &self.ast_crumbs.last() {
// Operators should be treated in a special way - setting functions in place in
// a operator should replace Infix with Prefix with two applications.
// TODO[ao] Maybe some day...
Some(Crumb::Infix(InfixCrumb::Operator))
| Some(Crumb::SectionLeft(SectionLeftCrumb::Opr))
| Some(Crumb::SectionRight(SectionRightCrumb::Opr))
| Some(Crumb::SectionSides(SectionSidesCrumb)) => None,
_ => Some(Box::new(move |root, new| {
modify_preserving_id(root, |root| {
root.set_traversing(&self.ast_crumbs, new)
})
})),
}
}
_ => match &self.ast_crumbs.last() {
// Operators should be treated in a special way - setting functions in place in
// a operator should replace Infix with Prefix with two applications.
// TODO[ao] Maybe some day...
Some(Crumb::Infix(InfixCrumb::Operator))
| Some(Crumb::SectionLeft(SectionLeftCrumb::Opr))
| Some(Crumb::SectionRight(SectionRightCrumb::Opr))
| Some(Crumb::SectionSides(SectionSidesCrumb)) => None,
_ => Some(Box::new(move |root, new| {
modify_preserving_id(root, |root| root.set_traversing(&self.ast_crumbs, new))
})),
},
}
}
fn erase_impl<C: Context>(&self) -> Option<EraseOperation<C>> {
fn erase_impl(&self) -> Option<EraseOperation> {
if self.node.kind.removable() {
Some(Box::new(move |root, context| {
Some(Box::new(move |root| {
let (mut last_crumb, mut parent_crumbs) =
self.ast_crumbs.split_last().expect("Erase target must have parent AST node");
let mut ast = root.get_traversing(parent_crumbs)?;
let is_named_argument = match_named_argument(ast).is_some();
// When an element is removed, we have to find an adequate span tree node that
// could become a new temporary target of dragged edge. It should be a node that
// has an reverse set operation to the erase we are performing now.
let mut reinsert_crumbs = None;
if is_named_argument {
// When erasing named argument, we need to remove the whole argument, not only
// the value part. The named argument AST is always a single infix expression,
@ -309,15 +297,12 @@ impl<'a> Implementation for node::Ref<'a> {
let is_child = |span: &SpanSeed<Ast>| span.is_child();
let child_after_offset = after.iter().position(is_child);
let child_before_offset = before.iter().rposition(is_child);
let (insertion_point_offset, removed_range) =
match (child_after_offset, child_before_offset) {
(Some(after), _) => (-1, index..=index + after),
(None, Some(before)) => (-2, before + 1..=index),
(None, None) => (-1, index..=index),
};
let removed_range = match (child_after_offset, child_before_offset) {
(Some(after), _) => index..=index + after,
(None, Some(before)) => before + 1..=index,
(None, None) => index..=index,
};
reinsert_crumbs =
Some(self.crumbs.relative_sibling(insertion_point_offset));
span_info.drain(removed_range);
Ok(ast.with_shape(tree))
} else {
@ -406,67 +391,7 @@ impl<'a> Implementation for node::Ref<'a> {
}
}
let new_span_tree = SpanTree::new(&new_root, context)?;
// For resolved arguments, the valid insertion point of this argument is its
// placeholder. The position of placeholder is not guaranteed to be in the same
// place as the removed argument, as it might have been out of order. To find
// the correct placeholder position, we need to search regenerated span-tree.
let reinsert_crumbs = reinsert_crumbs.or_else(|| {
self.kind.definition_index().and_then(|_| {
let call_id = self.kind.call_id();
let name = self.kind.argument_name();
let found = new_span_tree.root_ref().find_node(|node| {
node.kind.is_insertion_point()
&& node.kind.call_id() == call_id
&& node.kind.argument_name() == name
});
found.map(|found| found.crumbs)
})
});
// For non-resolved arguments, use the preceding insertion point. After the
// span-tree is regenerated, it will contain an insertion point before the next
// argument, or an append if there are no more arguments. Both are correct as
// reinsertion points. We can find them by scanning new span-tree for the insertion
// point at correct location.
let reinsert_crumbs = reinsert_crumbs.or_else(|| {
let call_id = self.kind.call_id();
let call_root =
new_span_tree.root_ref().find_node(|node| node.ast_id == call_id)?;
let removed_offset = if is_named_argument {
self.parent().ok()??.span_offset
} else {
self.span_offset
};
// If removed offset is past the tree, use last Append as reinsertion point.
let max_span = call_root.span().end;
let removed_offset = removed_offset.min(max_span);
let found = call_root.find_by_span(&(removed_offset..removed_offset).into());
let found = found.filter(|node| node.is_insertion_point());
found.map(|found| found.crumbs)
});
// If all fails, use the root of the span-tree as reinsertion point as a fallback,
// and log it as an error.
let reinsert_crumbs = reinsert_crumbs.unwrap_or_else(|| {
let printed_tree = new_span_tree.debug_print(&new_root.repr());
let offset = self.span_offset;
error!(
"Failed to find reinsertion point for erased argument. This is a bug.\n\
Removed offset: {offset}\n\
Span tree: \n{printed_tree}"
);
new_span_tree.root_ref().crumbs
});
Ok((new_root, reinsert_crumbs))
Ok(new_root)
}))
} else {
None
@ -534,7 +459,7 @@ mod test {
let case = format!("{self:?}");
let result = match &self.action {
Set => node.set(&ast, arg),
Erase => node.erase(&ast, &context).map(|(ast, _)| ast),
Erase => node.erase(&ast),
}
.expect(&case);
let result_repr = result.repr();

View File

@ -6,10 +6,12 @@ use enso_text::unit::*;
use crate::generate::context::CalledMethodInfo;
use crate::node;
use crate::node::InsertionPointType;
use crate::node::PortId;
use crate::ArgumentInfo;
use crate::Node;
use crate::SpanTree;
use ast::crumbs::BlockCrumb;
use ast::crumbs::InfixCrumb;
use ast::crumbs::Located;
use ast::crumbs::PrefixCrumb;
@ -49,6 +51,8 @@ pub trait SpanTreeGenerator {
/// Generate tree for this AST treated as root for the whole expression.
fn generate_tree(&self, context: &impl Context) -> FallibleResult<SpanTree> {
let root = self.generate_node(node::Kind::Root, context)?;
let port_id = root.port_id.or(Some(PortId::Root));
let root = root.with_port_id(port_id);
Ok(SpanTree { root })
}
}
@ -103,7 +107,7 @@ impl ChildGenerator {
fn generate_ast_node(
&mut self,
child_ast: Located<Ast>,
child_ast: Located<&Ast>,
kind: impl Into<node::Kind>,
context: &impl Context,
) -> FallibleResult<&mut node::Child> {
@ -140,7 +144,7 @@ impl ChildGenerator {
let kind = kind.into();
let size = self.current_offset;
let children = self.children;
Node { kind, size, children, ast_id, ..default() }
Node::new().with_kind(kind).with_size(size).with_children(children).with_ast_id(ast_id)
}
}
@ -342,18 +346,22 @@ fn generate_node_for_ast(
ast::prefix::Chain::from_ast(ast).unwrap().generate_node(kind, context),
ast::Shape::Tree(tree) if tree.type_info != ast::TreeType::Lambda =>
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 ast_id = ast.id;
if let Some(info) = ast.id.and_then(|id| context.call_info(id)) {
let node = { Node { kind: node::Kind::Operation, size, ast_id, ..default() } };
if let Some(info) = ast_id.and_then(|id| context.call_info(id)) {
let node = Node::new()
.with_kind(node::Kind::Operation)
.with_size(size)
.with_ast_id(ast_id);
// Note that in this place it is impossible that Ast is in form of
// `this.method` -- it is covered by the former if arm. As such, we don't
// need to use `ApplicationBase` here as we do elsewhere.
let params = info.with_call_id(ast.id).parameters.into_iter();
let params = info.with_call_id(ast_id).parameters.into_iter();
Ok(generate_trailing_expected_arguments(node, params).with_kind(kind))
} else {
Ok(Node { kind, size, ast_id, ..default() })
Ok(Node::new().with_kind(kind).with_size(size).with_ast_id(ast_id))
}
}
}
@ -439,7 +447,7 @@ fn generate_node_for_opr_chain(
let is_last = i + 1 == this.args.len();
let has_left = !node.is_insertion_point();
let opr_crumbs = elem.crumb_to_operator(has_left);
let opr_ast = Located::new(opr_crumbs, elem.operator.ast().clone_ref());
let opr_ast = Located::new(opr_crumbs, elem.operator.ast());
let left_crumbs = if has_left { vec![elem.crumb_to_previous()] } else { vec![] };
let mut gen = ChildGenerator::default();
@ -491,7 +499,7 @@ fn generate_node_for_opr_chain(
gen.generate_ast_node(opr_ast, node::Kind::Operation, context)?;
if let Some(operand) = &elem.operand {
let arg_crumbs = elem.crumb_to_operand(has_left);
let arg_ast = Located::new(arg_crumbs, operand.arg.clone_ref());
let arg_ast = Located::new(arg_crumbs, &operand.arg);
gen.spacing(operand.offset);
if has_left {
@ -515,16 +523,8 @@ fn generate_node_for_opr_chain(
gen.reverse_children();
}
Ok((
Node {
kind: if is_last { kind.clone() } else { node::Kind::chained().into() },
size: gen.current_offset,
children: gen.children,
ast_id: elem.infix_id,
..default()
},
elem.offset,
))
let kind = if is_last { kind.clone() } else { node::Kind::chained().into() };
Ok((gen.into_node(kind, elem.infix_id), elem.offset))
})?;
Ok(node)
}
@ -631,7 +631,7 @@ fn generate_node_for_prefix_chain(
gen.add_node(vec![PrefixCrumb::Arg.into()], node);
}
None => {
let arg_ast = Located::new(PrefixCrumb::Arg, arg.sast.wrapped.clone());
let arg_ast = Located::new(PrefixCrumb::Arg, &arg.sast.wrapped);
gen.generate_ast_node(arg_ast, arg_kind, context)?;
}
}
@ -657,19 +657,11 @@ fn generate_node_for_named_argument(
let NamedArgumentDef { id, larg, loff, opr, roff, rarg, .. } = this;
let mut gen = ChildGenerator::default();
gen.generate_ast_node(
Located::new(InfixCrumb::LeftOperand, larg.clone()),
node::Kind::Token,
context,
)?;
gen.generate_ast_node(Located::new(InfixCrumb::LeftOperand, larg), node::Kind::Token, context)?;
gen.spacing(loff);
gen.generate_ast_node(
Located::new(InfixCrumb::Operator, opr.clone()),
node::Kind::Token,
context,
)?;
gen.generate_ast_node(Located::new(InfixCrumb::Operator, opr), node::Kind::Token, context)?;
gen.spacing(roff);
let arg_ast = Located::new(InfixCrumb::RightOperand, rarg.clone());
let arg_ast = Located::new(InfixCrumb::RightOperand, rarg);
gen.generate_ast_node(arg_ast, arg_kind, context)?;
Ok(gen.into_node(node::Kind::NamedArgument, id))
}
@ -800,10 +792,11 @@ fn generate_expected_argument(
let mut gen = ChildGenerator::default();
let extended_ast_id = node.ast_id.or(node.extended_ast_id);
gen.add_node(ast::Crumbs::new(), node);
let port_id = argument_info.call_id.map(|id| PortId::ArgPlaceholder { application: id, index });
let arg_node = gen.generate_empty_node(InsertionPointType::ExpectedArgument { index, named });
arg_node.node.set_argument_info(argument_info);
let kind = node::Kind::chained().into();
Node { kind, size: gen.current_offset, children: gen.children, extended_ast_id, ..default() }
arg_node.node.set_port_id(port_id);
gen.into_node(node::Kind::chained(), None).with_extended_ast_id(extended_ast_id)
}
/// Build a prefix application-like span tree structure where no prefix argument has been provided
@ -842,6 +835,10 @@ fn tree_generate_node(
let first_token_or_child =
tree.span_info.iter().find(|span| !matches!(span, SpanSeed::Space(_)));
let is_array = matches!(first_token_or_child, Some(SpanSeed::Token(ast::SpanSeedToken { token })) if token == "[");
let array_id = ast_id.filter(|_| is_array);
let mut insert_port_iter = (0..)
.map_while(|insert_at| array_id.map(|array| PortId::ArrayInsert { array, insert_at }));
let last_token_index =
tree.span_info.iter().rposition(|span| matches!(span, SpanSeed::Token(_)));
for (index, raw_span_info) in tree.span_info.iter().enumerate() {
@ -852,9 +849,10 @@ fn tree_generate_node(
}
SpanSeed::Token(ast::SpanSeedToken { token }) => {
if is_array && Some(index) == last_token_index {
let kind = InsertionPointType::Append;
children.push(node::Child {
node: Node::new().with_kind(kind),
node: Node::new()
.with_kind(InsertionPointType::Append)
.with_port_id(insert_port_iter.next()),
parent_offset,
sibling_offset,
ast_crumbs: vec![],
@ -864,16 +862,17 @@ fn tree_generate_node(
let kind = node::Kind::Token;
let size = ByteDiff::from(token.len());
let ast_crumbs = vec![TreeCrumb { index }.into()];
let node = Node { kind, size, ..default() };
let node = Node::new().with_kind(kind).with_size(size);
children.push(node::Child { node, parent_offset, sibling_offset, ast_crumbs });
parent_offset += size;
sibling_offset = 0.byte_diff();
}
SpanSeed::Child(ast::SpanSeedChild { node }) => {
if is_array {
let kind = InsertionPointType::BeforeArgument(index);
children.push(node::Child {
node: Node::new().with_kind(kind),
node: Node::new()
.with_kind(InsertionPointType::BeforeArgument(index))
.with_port_id(insert_port_iter.next()),
parent_offset,
sibling_offset,
ast_crumbs: vec![],
@ -895,7 +894,37 @@ fn tree_generate_node(
}
let tree_type = Some(tree.type_info.clone());
Ok(Node { kind, tree_type, size, children, ast_id, ..default() })
Ok(Node { kind, tree_type, size, children, ..default() }.with_ast_id(ast_id))
}
fn block_generate_node(
block: &ast::Block<Ast>,
kind: node::Kind,
context: &impl Context,
ast_id: Option<Id>,
) -> FallibleResult<Node> {
let mut gen = ChildGenerator::default();
let newline = Node::new().with_kind(node::Kind::Token).with_size(1.byte_diff());
gen.add_node(vec![], newline.clone());
for empty_line_space in &block.empty_lines {
gen.spacing(*empty_line_space);
gen.add_node(vec![], newline.clone());
}
gen.spacing(block.indent);
let first_line = Located::new(BlockCrumb::HeadLine, &block.first_line.elem);
gen.generate_ast_node(first_line, node::Kind::BlockLine, context)?;
gen.spacing(block.first_line.off);
for (tail_index, line) in block.lines.iter().enumerate() {
gen.add_node(vec![], newline.clone());
if let Some(elem) = &line.elem {
gen.spacing(block.indent);
let line = Located::new(BlockCrumb::TailLine { tail_index }, elem);
gen.generate_ast_node(line, node::Kind::BlockLine, context)?;
}
gen.spacing(line.off);
}
Ok(gen.into_node(kind, ast_id))
}
@ -961,6 +990,7 @@ mod test {
fn clear_expression_ids(node: &mut Node) {
node.ast_id = None;
node.extended_ast_id = None;
node.port_id = None;
node.application = None;
for child in &mut node.children {
clear_expression_ids(&mut child.node);

View File

@ -40,6 +40,7 @@ pub mod node;
pub use node::Crumb;
pub use node::Crumbs;
pub use node::Node;
pub use node::PortId;
@ -269,6 +270,7 @@ impl SpanTree {
pub fn debug_print(&self, code: &str) -> String {
use std::fmt::Write;
let max_length = code.lines().map(|l| l.len()).max().unwrap_or(0);
let mut code = code.to_string();
let code_padding = self.root.size.as_usize().saturating_sub(code.len());
for _ in 0..code_padding {
@ -276,7 +278,7 @@ impl SpanTree {
}
let mut buffer = String::new();
let span_padding = " ".repeat(code.len() + 2);
let spaces = " ".repeat(max_length + 3);
struct PrintState {
indent: String,
@ -285,20 +287,41 @@ impl SpanTree {
let state = PrintState { indent: String::new(), num_children: 1 };
self.root_ref().dfs_with_layer_data(state, |node, state| {
let span = node.span();
let offset_in_line = code[..span.start.value].lines().last().unwrap_or("").len();
let node_code = &code[span];
buffer.push_str(&span_padding[0..node.span_offset.value]);
buffer.push_str(&spaces[0..offset_in_line]);
buffer.push('▷');
buffer.push_str(node_code);
buffer.push('◁');
let written = node.span_offset.value + node_code.len() + 2;
buffer.push_str(&span_padding[written..]);
let mut written_in_line = offset_in_line + 1;
let mut span_lines = node_code.lines();
// Make sure that empty spans are still printed as single line.
let mut next_line = Some(span_lines.next().unwrap_or(""));
while let Some(line) = next_line {
buffer.push_str(line);
written_in_line += line.len();
next_line = span_lines.next();
let is_last = next_line.is_none();
if is_last {
buffer.push('◁');
written_in_line += 1;
}
buffer.push_str(&spaces[written_in_line..]);
buffer.push_str(&state.indent);
if !is_last {
if !node.crumbs.is_empty() {
buffer.push_str("");
}
buffer.push_str("\n ");
written_in_line = 1;
}
}
let indent = if let Some(index) = node.crumbs.last() {
let is_last = *index == state.num_children - 1;
let indent_targeted = if is_last { " ╰─" } else { " ├─" };
let indent_continue = if is_last { " " } else { "" };
buffer.push_str(&state.indent);
buffer.push_str(indent_targeted);
format!("{}{}", state.indent, indent_continue)
} else {
@ -324,10 +347,6 @@ impl SpanTree {
write!(buffer, " ext_id={ext_id:?}").unwrap();
}
if let Some(tt) = node.tree_type.as_ref() {
write!(buffer, " tt={tt:?}").unwrap();
}
buffer.push('\n');
let num_children = node.children.len();

View File

@ -37,6 +37,7 @@ pub struct Node {
pub size: ByteDiff,
pub children: Vec<Child>,
pub ast_id: Option<ast::Id>,
pub port_id: Option<PortId>,
/// When this `Node` is a part of an AST extension (a virtual span that only exists in
/// span-tree, but not in AST), this field will contain the AST ID of the expression it extends
/// (e.g. the AST of a function call with missing arguments, extended with expected arguments).
@ -124,8 +125,17 @@ impl Node {
self.children = ts;
self
}
pub fn with_ast_id(mut self, id: ast::Id) -> Self {
self.ast_id = Some(id);
pub fn with_ast_id(mut self, id: Option<ast::Id>) -> Self {
self.ast_id = id;
self.port_id = id.map(PortId::Ast);
self
}
pub fn with_extended_ast_id(mut self, id: Option<ast::Id>) -> Self {
self.extended_ast_id = id;
self
}
pub fn with_port_id(mut self, id: Option<PortId>) -> Self {
self.port_id = id;
self
}
pub fn with_application(mut self, application: ApplicationData) -> Self {
@ -154,6 +164,9 @@ impl Node {
pub fn set_definition_index(&mut self, definition_index: usize) {
self.kind.set_definition_index(definition_index);
}
pub fn set_port_id(&mut self, id: Option<PortId>) {
self.port_id = id;
}
}
@ -192,6 +205,40 @@ impl DerefMut for Child {
// ==============
// === PortId ===
// ==============
/// Identification for a port that can be hovered and connected to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PortId {
/// An unique existing AST within the expression. Whenever a node has an assigned AST ID, this
/// port ID variant will be used.
Ast(ast::Id),
/// The root node of the expression without assigned ID.
#[default]
Root,
/// An argument that doesn't exist yet, but will be created when the connection is made.
ArgPlaceholder {
/// ID of method application containing this argument.
application: ast::Id,
/// The positional index of the argument within the method application that this
/// placeholder represents.
index: usize,
},
/// A position within an array expression where a connection variable will be inserted.
ArrayInsert {
/// ID of array expression containing this insertion point.
array: ast::Id,
/// Insert position within the array, counting only array elements, not commas or brackets.
/// Index 0 means the variable will be inserted before the first element, index 1 means it
/// will be inserted after the first element, etc.
insert_at: usize,
},
}
// ==============
// === Crumbs ===
// ==============

View File

@ -32,6 +32,8 @@ pub enum Kind {
/// between AST tokens. For example, given expression `foo bar`, the span assigned to the
/// `InsertionPoint` between `foo` and `bar` should be set to 3.
InsertionPoint(InsertionPoint),
/// A single line within a block.
BlockLine,
}
@ -106,6 +108,17 @@ impl Kind {
pub fn is_function_parameter(&self) -> bool {
self.is_this() || self.is_argument() || self.is_expected_argument()
}
/// If this kind is an expected argument, return its argument index.
pub fn expected_argument_index(&self) -> Option<usize> {
match self {
Self::InsertionPoint(InsertionPoint {
kind: InsertionPointType::ExpectedArgument { index, .. },
..
}) => Some(*index),
_ => None,
}
}
}
@ -258,6 +271,7 @@ impl Kind {
Self::NamedArgument => "NamedArgument",
Self::Token => "Token",
Self::InsertionPoint(_) => "InsertionPoint",
Self::BlockLine => "BlockLine",
}
}
}

View File

@ -8,7 +8,6 @@ use crate::prelude::*;
use crate::model::module::NodeMetadata;
use ast::crumbs::InfixCrumb;
use ast::crumbs::Located;
use ast::macros::DocumentationCommentInfo;
use double_representation::connection;
@ -21,7 +20,6 @@ use double_representation::module;
use double_representation::node;
use double_representation::node::MainLine;
use double_representation::node::NodeAst;
use double_representation::node::NodeAstInfo;
use double_representation::node::NodeInfo;
use double_representation::node::NodeLocation;
use engine_protocol::language_server;
@ -29,6 +27,7 @@ use parser::Parser;
use span_tree::action::Action;
use span_tree::action::Actions;
use span_tree::generate::Context as SpanTreeContext;
use span_tree::PortId;
use span_tree::SpanTree;
@ -48,11 +47,16 @@ pub use double_representation::graph::LocationHint;
// === Errors ===
// ==============
/// Error raised when node with given Id was not found in the graph's body.
/// Error raised when node with given ID was not found in the graph's body.
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Node with Id {} was not found.", _0)]
pub struct NodeNotFound(ast::Id);
/// Error raised when an endpoint with given Node ID and Port ID was not found in the graph's body.
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "Port with ID {:?} was not found.", _0)]
pub struct EndpointNotFound(Endpoint);
/// Error raised when an attempt to set node's expression to a binding has been made.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Illegal string `{}` given for node expression. It must not be a binding.", _0)]
@ -70,6 +74,11 @@ pub struct NoPatternOnNode {
pub node: node::Id,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Fail)]
#[fail(display = "AST node is missing ID.")]
pub struct MissingAstId;
// ====================
@ -171,45 +180,46 @@ impl NewNodeInfo {
/// Reference to the port (i.e. the span tree node).
pub type PortRef<'a> = span_tree::node::Ref<'a>;
// === Connection ===
/// Connection within the graph, described using a pair of ports at given nodes.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct Connection {
pub source: Endpoint,
pub target: Endpoint,
}
// === Endpoint
/// Connection endpoint - a port on a node, described using span-tree crumbs.
/// Connection endpoint - a port within a node.
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct Endpoint {
pub node: double_representation::node::Id,
pub port: span_tree::Crumbs,
/// Crumbs which locate the Var in the `port` ast node.
///
/// In normal case this is an empty crumb (which means that the whole span of `port` is the
/// mentioned Var. However, span tree does not cover all the possible ast of node expression
/// (e.g. it does not decompose Blocks), but still we want to pass information about connection
/// to such port and be able to remove it.
pub var_crumbs: ast::Crumbs,
pub node: node::Id,
pub port: PortId,
}
impl Endpoint {
/// Create endpoint with empty `var_crumbs`.
pub fn new(node: double_representation::node::Id, port: impl Into<span_tree::Crumbs>) -> Self {
let var_crumbs = default();
let port = port.into();
Endpoint { node, port, var_crumbs }
pub fn new(node: double_representation::node::Id, port: PortId) -> Self {
Endpoint { node, port }
}
/// Create an endpoint pointing to whole node pattern or expression.
pub fn root(node_id: node::Id) -> Self {
Endpoint { node: node_id, port: PortId::Root }
}
/// Create a target endpoint from given node info, pointing at a port under given AST crumbs.
/// Returns error if the crumbs do not point at any valid AST node.
pub fn target_at(node: &Node, crumbs: impl ast::crumbs::IntoCrumbs) -> FallibleResult<Self> {
let expression = node.info.expression();
let port_ast = expression.get_traversing(&crumbs.into_crumbs())?;
Ok(Endpoint { node: node.info.id(), port: PortId::Ast(port_ast.id.ok_or(MissingAstId)?) })
}
}
// === Connection ===
/// Connection described using span-tree crumbs.
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Connection {
pub source: Endpoint,
pub destination: Endpoint,
}
// === NodeTrees ===
/// Stores node's span trees: one for inputs (expression) and optionally another one for outputs
@ -220,50 +230,14 @@ pub struct NodeTrees {
pub inputs: SpanTree,
/// Describes node outputs, i.e. its pattern. `None` if a node is not an assignment.
pub outputs: Option<SpanTree>,
/// Info about macros used in the node's expression.
ast_info: NodeAstInfo,
}
impl NodeTrees {
#[allow(missing_docs)]
pub fn new(node: &NodeInfo, context: &impl SpanTreeContext) -> Option<NodeTrees> {
let inputs = SpanTree::new(&node.expression(), context).ok()?;
let ast_info = node.main_line.ast_info.clone();
let outputs = if let Some(pat) = node.pattern() {
Some(SpanTree::new(pat, context).ok()?)
} else {
None
};
Some(NodeTrees { inputs, outputs, ast_info })
}
/// Converts AST crumbs (as obtained from double rep's connection endpoint) into the
/// appropriate span-tree node reference.
pub fn get_span_tree_node<'a, 'b>(
&'a self,
ast_crumbs: &'b [ast::Crumb],
) -> Option<span_tree::node::NodeFoundByAstCrumbs<'a, 'b>> {
use ast::crumbs::Crumb::Infix;
// We can display only a part of the expression to the user. We hide [`SKIP`] and [`FREEZE`]
// macros and context switch expressions. In this case, we skip an additional
// number of AST crumbs.
let expression_crumbs_to_skip = self.ast_info.ast_crumbs_to_skip();
if let Some(outputs) = self.outputs.as_ref() {
// Node in assignment form. First crumb decides which span tree to use.
let first_crumb = ast_crumbs.get(0);
let is_input = matches!(first_crumb, Some(Infix(InfixCrumb::RightOperand)));
let tree = match first_crumb {
Some(Infix(InfixCrumb::LeftOperand)) => Some(outputs),
Some(Infix(InfixCrumb::RightOperand)) => Some(&self.inputs),
_ => None,
};
let skip = if is_input { expression_crumbs_to_skip + 1 } else { 1 };
tree.and_then(|tree| tree.root_ref().get_descendant_by_ast_crumbs(&ast_crumbs[skip..]))
} else {
let skip = expression_crumbs_to_skip;
// Expression node - there is only inputs span tree.
self.inputs.root_ref().get_descendant_by_ast_crumbs(&ast_crumbs[skip..])
}
let outputs = node.pattern().map(|pat| SpanTree::new(pat, context)).transpose().ok()?;
Some(NodeTrees { inputs, outputs })
}
}
@ -288,36 +262,23 @@ impl Connections {
.iter()
.filter_map(|node| Some((node.id(), NodeTrees::new(node, context)?)))
.collect();
let mut ret = Connections { trees, connections: default() };
let connections =
graph.connections().into_iter().filter_map(|c| ret.convert_connection(&c)).collect();
ret.connections = connections;
ret
let connections = graph.connections().iter().map(Self::convert_connection).collect();
Connections { trees, connections }
}
/// Converts Endpoint from double representation to the span tree crumbs.
pub fn convert_endpoint(
&self,
endpoint: &double_representation::connection::Endpoint,
) -> Option<Endpoint> {
let tree = self.trees.get(&endpoint.node)?;
let span_tree_node = tree.get_span_tree_node(&endpoint.crumbs)?;
Some(Endpoint {
node: endpoint.node,
port: span_tree_node.node.crumbs,
var_crumbs: span_tree_node.ast_crumbs.into(),
})
/// Converts Endpoint from double representation to port-based representation.
fn convert_endpoint(endpoint: &double_representation::connection::Endpoint) -> Endpoint {
Endpoint { node: endpoint.node, port: PortId::Ast(endpoint.port.item) }
}
/// Converts Connection from double representation to the span tree crumbs.
pub fn convert_connection(
&self,
/// Converts Connection from double representation to port-based representation.
fn convert_connection(
connection: &double_representation::connection::Connection,
) -> Option<Connection> {
let source = self.convert_endpoint(&connection.source)?;
let destination = self.convert_endpoint(&connection.destination)?;
Some(Connection { source, destination })
) -> Connection {
Connection {
source: Self::convert_endpoint(&connection.source),
target: Self::convert_endpoint(&connection.target),
}
}
}
@ -370,94 +331,53 @@ pub fn name_for_ast(ast: &Ast) -> String {
/// Also provides a number of utility functions for connection operations.
#[derive(Clone, Debug)]
pub struct EndpointInfo {
/// The endpoint descriptor.
pub endpoint: Endpoint,
/// Ast of the relevant node piece (expression or the pattern).
pub ast: Ast,
/// Span tree for the relevant node side (outputs or inputs).
///
/// TODO[PG]: Replace span-tree operations (set/erase) with direct AST operations using PortId.
/// That way we can get rid of crumbs/span-trees completely.
/// https://github.com/enso-org/enso/issues/6834
pub span_tree: SpanTree,
/// The endpoint port location within the span tree.
/// TODO[PG]: Replace with PortId, see above.
pub crumbs: span_tree::Crumbs,
}
impl EndpointInfo {
/// Construct information about endpoint. Ast must be the node's expression or pattern.
pub fn new(
endpoint: &Endpoint,
ast: &Ast,
ast: Ast,
context: &impl SpanTreeContext,
) -> FallibleResult<EndpointInfo> {
Ok(EndpointInfo {
endpoint: endpoint.clone(),
ast: ast.clone(),
span_tree: SpanTree::new(ast, context)?,
})
}
/// Obtains a reference to the port (span tree node) of this endpoint.
pub fn port(&self) -> FallibleResult<span_tree::node::Ref> {
self.span_tree.get_node(&self.endpoint.port)
}
/// Obtain reference to the parent of the port identified by given crumbs slice.
pub fn parent_port_of(&self, crumbs: &[span_tree::node::Crumb]) -> Option<PortRef> {
let parent_crumbs = span_tree::node::parent_crumbs(crumbs);
parent_crumbs.and_then(|cr| self.span_tree.get_node(cr.iter()).ok())
}
/// Iterates over sibling ports located after this endpoint in its chain.
pub fn chained_ports_after(&self) -> impl Iterator<Item = PortRef> + '_ {
let parent_port = self.parent_chain_port();
let ports_after = parent_port.map(move |parent_port| {
parent_port
.chain_children_iter()
.skip_while(move |port| port.crumbs != self.endpoint.port)
.skip(1)
});
ports_after.into_iter().flatten()
}
/// Obtains parent port. If this port is part of chain, the parent port will be the parent of
/// the whole chain.
pub fn parent_chain_port(&self) -> Option<PortRef> {
// TODO [mwu]
// Unpleasant. Likely there should be something in span tree that allows obtaining
// sequence of nodes between root and given crumb. Or sth.
let mut parent_port = self.parent_port_of(&self.endpoint.port);
while parent_port.contains_if(|p| p.node.is_chained()) {
parent_port = parent_port.and_then(|p| self.parent_port_of(&p.crumbs));
}
parent_port
let span_tree = SpanTree::new(&ast, context)?;
let node = span_tree
.root_ref()
.find_node(|n| n.port_id == Some(endpoint.port))
.ok_or(EndpointNotFound(*endpoint))?;
let crumbs = node.crumbs;
Ok(EndpointInfo { ast, span_tree, crumbs })
}
/// Ast being the exact endpoint target. Might be more granular than a span tree port.
pub fn target_ast(&self) -> FallibleResult<&Ast> {
self.ast.get_traversing(&self.full_ast_crumbs()?)
self.ast.get_traversing(&self.span_tree_node()?.ast_crumbs)
}
/// Full sequence of Ast crumbs identifying endpoint target.
pub fn full_ast_crumbs(&self) -> FallibleResult<ast::Crumbs> {
let port = self.port()?;
let mut crumbs = port.ast_crumbs;
crumbs.extend(self.endpoint.var_crumbs.iter().cloned());
Ok(crumbs)
/// Obtains a reference to the span tree node of this endpoint.
pub fn span_tree_node(&self) -> FallibleResult<span_tree::node::Ref> {
self.span_tree.get_node(&self.crumbs)
}
/// Sets AST at the given port. Returns new root Ast.
pub fn set(&self, ast_to_set: Ast) -> FallibleResult<Ast> {
self.port()?.set(&self.ast, ast_to_set)
self.span_tree_node()?.set(&self.ast, ast_to_set)
}
/// Sets AST at the endpoint target. Returns new root Ast. Does not use span tree logic.
pub fn set_ast(&self, ast_to_set: Ast) -> FallibleResult<Ast> {
self.ast.set_traversing(&self.full_ast_crumbs()?, ast_to_set)
}
/// Erases given port. Returns new root Ast and crumbs pointing to the nearest insertion point.
pub fn erase(
&self,
context: &impl SpanTreeContext,
) -> FallibleResult<(Ast, span_tree::Crumbs)> {
self.port()?.erase(&self.ast, context)
/// Erases given port. Returns new root Ast.
pub fn erase(&self) -> FallibleResult<Ast> {
self.span_tree_node()?.erase(&self.ast)
}
}
@ -609,7 +529,8 @@ impl Handle {
/// appended to avoid conflicts with other identifiers used in the graph.
pub fn variable_name_for(&self, node: &NodeInfo) -> FallibleResult<ast::known::Var> {
let base_name = Self::variable_name_base_for(node);
let used_names = self.used_names()?.into_iter().map(|located_name| located_name.item);
let used_names = self.used_names()?;
let used_names = used_names.iter().map(|name| name.item.as_str());
let name = generate_name(base_name.as_str(), used_names)?.as_var()?;
Ok(ast::known::Var::new(name, None))
}
@ -632,30 +553,37 @@ impl Handle {
})
}
/// Obtains information for connection's destination endpoint.
pub fn destination_info(
/// Obtains information for new connection's target endpoint.
pub fn target_info(
&self,
connection: &Connection,
context: &impl SpanTreeContext,
) -> FallibleResult<EndpointInfo> {
let destination_node = self.node_info(connection.destination.node)?;
let target_node_ast = destination_node.expression();
EndpointInfo::new(&connection.destination, &target_node_ast, context)
let target_node = self.node_info(connection.target.node)?;
let target_node_ast = target_node.expression();
EndpointInfo::new(&connection.target, target_node_ast, context)
}
/// Obtains information about connection's source endpoint.
/// Obtains information about new connection's source endpoint.
pub fn source_info(
&self,
connection: &Connection,
context: &impl SpanTreeContext,
) -> FallibleResult<EndpointInfo> {
let source_node = self.node_info(connection.source.node)?;
if let Some(pat) = source_node.pattern() {
EndpointInfo::new(&connection.source, pat, context)
let mut source = connection.source;
let use_whole_pattern = source.port == PortId::Root;
let pattern = if use_whole_pattern {
let pattern = self.introduce_pattern_if_missing(connection.source.node)?;
let id = pattern.id.ok_or(EndpointNotFound(source))?;
source.port = PortId::Ast(id);
pattern
} else {
let source_node = self.node_info(connection.source.node)?;
// For subports we would not have any idea what pattern to introduce. So we fail.
Err(NoPatternOnNode { node: connection.source.node }.into())
}
source_node.pattern().ok_or(NoPatternOnNode { node: connection.source.node })?.clone()
};
EndpointInfo::new(&source, pattern, context)
}
/// If the node has no pattern, introduces a new pattern with a single variable name.
@ -739,51 +667,35 @@ impl Handle {
context: &impl SpanTreeContext,
) -> FallibleResult {
let _transaction_guard = self.get_or_open_transaction("Connect");
if connection.source.port.is_empty() {
// If we create connection from node's expression root, we are able to introduce missing
// pattern with a new variable.
self.introduce_pattern_if_missing(connection.source.node)?;
}
let source_info = self.source_info(connection, context)?;
let destination_info = self.destination_info(connection, context)?;
let target_info = self.target_info(connection, context)?;
let source_identifier = source_info.target_ast()?.clone();
let updated_target_node_expr = destination_info.set(source_identifier.with_new_id())?;
self.set_expression_ast(connection.destination.node, updated_target_node_expr)?;
let updated_target_node_expr = target_info.set(source_identifier.with_new_id())?;
self.set_expression_ast(connection.target.node, updated_target_node_expr)?;
// Reorder node lines, so the connection target is after connection source.
let source_node = connection.source.node;
let destination_node = connection.destination.node;
self.place_node_and_dependencies_lines_after(source_node, destination_node)
let target_node = connection.target.node;
self.place_node_and_dependencies_lines_after(source_node, target_node)
}
/// Remove the connections from the graph. Returns an updated edge destination endpoint for
/// disconnected edge, in case it is still used as destination-only edge. When `None` is
/// returned, no update is necessary.
/// Remove the connections from the graph.
pub fn disconnect(
&self,
connection: &Connection,
context: &impl SpanTreeContext,
) -> FallibleResult<Option<span_tree::Crumbs>> {
) -> FallibleResult {
let _transaction_guard = self.get_or_open_transaction("Disconnect");
let info = self.destination_info(connection, context)?;
let mut new_destination_crumbs = None;
let updated_expression = if connection.destination.var_crumbs.is_empty() {
let port = info.port()?;
if port.is_action_available(Action::Erase) {
let (ast, crumbs) = info.erase(context)?;
new_destination_crumbs = Some(crumbs);
Ok(ast)
} else {
info.set(Ast::blank())
}
let info = self.target_info(connection, context)?;
let port = info.span_tree_node()?;
let updated_expression = if port.is_action_available(Action::Erase) {
info.erase()
} else {
info.set_ast(Ast::blank())
info.set(Ast::blank())
}?;
self.set_expression_ast(connection.destination.node, updated_expression)?;
Ok(new_destination_crumbs)
self.set_expression_ast(connection.target.node, updated_expression)?;
Ok(())
}
/// Obtain the definition information for this graph from the module's AST.
@ -902,8 +814,7 @@ impl Handle {
let mut graph = GraphInfo::from_definition(definition);
graph.edit_node(id, expression)?;
Ok(graph.source)
})?;
Ok(())
})
}
/// Updates the given node's expression by rewriting a part of it, as specified by span crumbs.
@ -924,8 +835,7 @@ impl Handle {
let port = node_span_tree.get_node(crumbs)?;
let new_node_ast = if expression_text.as_ref().is_empty() {
if port.is_action_available(Action::Erase) {
let (ast, _) = port.erase(&node_ast, context)?;
ast
port.erase(&node_ast)?
} else {
port.set(&node_ast, Ast::blank())?
}
@ -1078,7 +988,7 @@ pub mod tests {
use crate::model::suggestion_database;
use crate::test::mock::data;
use ast::crumbs;
use ast::crumbs::*;
use ast::test_utils::expect_shape;
use double_representation::name::project;
use engine_protocol::language_server::MethodPointer;
@ -1086,7 +996,7 @@ pub mod tests {
use parser::Parser;
use span_tree::generate::context::CalledMethodInfo;
use span_tree::generate::MockContext;
use span_tree::PortId;
/// Returns information about all the connections between graph's nodes.
pub fn connections(graph: &Handle) -> FallibleResult<Connections> {
@ -1100,6 +1010,7 @@ pub mod tests {
pub graph_id: Id,
pub project_name: project::QualifiedName,
pub code: String,
pub id_map: ast::IdMap,
pub suggestions: HashMap<suggestion_database::entry::Id, suggestion_database::Entry>,
}
@ -1112,6 +1023,7 @@ pub mod tests {
graph_id: data::graph_id(),
project_name: data::project_qualified_name(),
code: data::CODE.to_owned(),
id_map: default(),
suggestions: default(),
}
}
@ -1131,6 +1043,7 @@ pub mod tests {
model::module::test::MockData {
code: self.code.clone(),
path: self.module_path.clone(),
id_map: self.id_map.clone(),
..default()
}
}
@ -1484,21 +1397,30 @@ main =
}
#[test]
#[ignore] // FIXME (https://github.com/enso-org/enso/issues/5574)
fn graph_controller_connections_listing() {
let mut test = Fixture::set_up();
const PROGRAM: &str = r"
main =
x,y = get_pos
[x,y] = get_pos
print x
z = print $ foo y
print z
foo
print z";
test.data.code = PROGRAM.into();
test.run(|graph| async move {
let id_map = &mut test.data.id_map;
let x_src = id_map.generate(13..14);
let y_src = id_map.generate(15..16);
let z_src = id_map.generate(44..45);
let x_dst = id_map.generate(38..39);
let y_dst = id_map.generate(60..61);
let z_dst1 = id_map.generate(72..73);
let z_dst2 = id_map.generate(96..97);
test.run(move |graph| async move {
let connections = connections(&graph).unwrap();
let (node0, node1, node2, node3, node4) = graph.nodes().unwrap().expect_tuple();
assert_eq!(node0.info.expression().repr(), "get_pos");
assert_eq!(node1.info.expression().repr(), "print x");
@ -1507,29 +1429,27 @@ main =
let c = &connections.connections[0];
assert_eq!(c.source.node, node0.info.id());
assert_eq!(c.source.port, span_tree::node::Crumbs::new(vec![1]));
assert_eq!(c.destination.node, node1.info.id());
assert_eq!(c.destination.port, span_tree::node::Crumbs::new(vec![2]));
assert_eq!(c.source.port, PortId::Ast(x_src));
assert_eq!(c.target.node, node1.info.id());
assert_eq!(c.target.port, PortId::Ast(x_dst));
let c = &connections.connections[1];
assert_eq!(c.source.node, node0.info.id());
assert_eq!(c.source.port, span_tree::node::Crumbs::new(vec![4]));
assert_eq!(c.destination.node, node2.info.id());
assert_eq!(c.destination.port, span_tree::node::Crumbs::new(vec![4, 2]));
assert_eq!(c.source.port, PortId::Ast(y_src));
assert_eq!(c.target.node, node2.info.id());
assert_eq!(c.target.port, PortId::Ast(y_dst));
let c = &connections.connections[2];
assert_eq!(c.source.node, node2.info.id());
assert_eq!(c.source.port, span_tree::node::Crumbs::default());
assert_eq!(c.destination.node, node3.info.id());
assert_eq!(c.destination.port, span_tree::node::Crumbs::new(vec![2]));
assert_eq!(c.source.port, PortId::Ast(z_src));
assert_eq!(c.target.node, node3.info.id());
assert_eq!(c.target.port, PortId::Ast(z_dst1));
use ast::crumbs::*;
let c = &connections.connections[3];
assert_eq!(c.source.node, node2.info.id());
assert_eq!(c.source.port, span_tree::node::Crumbs::default());
assert_eq!(c.destination.node, node4.info.id());
assert_eq!(c.destination.port, span_tree::node::Crumbs::new(vec![2]));
assert_eq!(c.destination.var_crumbs, crumbs!(BlockCrumb::HeadLine, PrefixCrumb::Arg));
assert_eq!(c.source.port, PortId::Ast(z_src));
assert_eq!(c.target.node, node4.info.id());
assert_eq!(c.target.port, PortId::Ast(z_dst2));
})
}
@ -1541,32 +1461,35 @@ main =
struct Case {
/// A pattern (the left side of assignment operator) of source node.
src: &'static str,
/// An expression of destination node.
/// An expression of target node.
dst: &'static str,
/// Crumbs of source and destination ports (i.e. SpanTree nodes)
ports: (&'static [usize], &'static [usize]),
/// Expected destination expression after connecting.
/// Crumbs of source and target ports (i.e. SpanTree nodes)
ports: (Range<usize>, Range<usize>),
/// Expected target expression after connecting.
expected: &'static str,
}
impl Case {
fn run(&self) {
let mut test = Fixture::set_up();
let main_prefix = format!("main = \n {} = foo\n ", self.src);
let src_prefix = "main = \n ";
let main_prefix = format!("{src_prefix}{} = foo\n ", self.src);
let main = format!("{}{}", main_prefix, self.dst);
let expected = format!("{}{}", main_prefix, self.expected);
let this = self.clone();
let (src_port, dst_port) = self.ports;
let src_port = src_port.to_vec();
let dst_port = dst_port.to_vec();
let (src_port, dst_port) = self.ports.clone();
let src_port = src_port.start + src_prefix.len()..src_port.end + src_prefix.len();
let dst_port = dst_port.start + main_prefix.len()..dst_port.end + main_prefix.len();
let src_port = PortId::Ast(test.data.id_map.generate(src_port));
let dst_port = PortId::Ast(test.data.id_map.generate(dst_port));
test.data.code = main;
test.run(|graph| async move {
test.run(move |graph| async move {
let (node0, node1) = graph.nodes().unwrap().expect_tuple();
let source = Endpoint::new(node0.info.id(), src_port.to_vec());
let destination = Endpoint::new(node1.info.id(), dst_port.to_vec());
let connection = Connection { source, destination };
let source = Endpoint::new(node0.info.id(), src_port);
let target = Endpoint::new(node1.info.id(), dst_port);
let connection = Connection { source, target };
graph.connect(&connection, &span_tree::generate::context::Empty).unwrap();
let new_main = graph.definition().unwrap().ast.repr();
assert_eq!(new_main, expected, "Case {this:?}");
@ -1574,11 +1497,11 @@ main =
}
}
let cases = &[Case { src: "x", dst: "foo", expected: "x", ports: (&[], &[]) }, Case {
let cases = &[Case { src: "x", dst: "foo", expected: "x", ports: (0..1, 0..3) }, Case {
src: "Vec x y",
dst: "1 + 2 + 3",
expected: "x + 2 + 3",
ports: (&[0, 2], &[0, 1]),
ports: (4..5, 0..1),
}];
for case in cases {
case.run()
@ -1601,16 +1524,8 @@ main =
assert!(connections(&graph).unwrap().connections.is_empty());
let (node0, _node1, node2) = graph.nodes().unwrap().expect_tuple();
let connection_to_add = Connection {
source: Endpoint {
node: node2.info.id(),
port: default(),
var_crumbs: default(),
},
destination: Endpoint {
node: node0.info.id(),
port: vec![4].into(),
var_crumbs: default(),
},
source: Endpoint::root(node2.info.id()),
target: Endpoint::target_at(&node0, [InfixCrumb::RightOperand]).unwrap(),
};
graph.connect(&connection_to_add, &span_tree::generate::context::Empty).unwrap();
let new_main = graph.definition().unwrap().ast.repr();
@ -1637,19 +1552,10 @@ main =
d = 4";
test.data.code = PROGRAM.into();
test.run(|graph| async move {
let (node0, _node1, _node2, _node3, node4, _node5) =
graph.nodes().unwrap().expect_tuple();
let (node0, _node1, _node2, _node3, node4, _) = graph.nodes().unwrap().expect_tuple();
let connection_to_add = Connection {
source: Endpoint {
node: node4.info.id(),
port: default(),
var_crumbs: default(),
},
destination: Endpoint {
node: node0.info.id(),
port: vec![4].into(),
var_crumbs: default(),
},
source: Endpoint::root(node4.info.id()),
target: Endpoint::target_at(&node0, [InfixCrumb::RightOperand]).unwrap(),
};
graph.connect(&connection_to_add, &span_tree::generate::context::Empty).unwrap();
let new_main = graph.definition().unwrap().ast.repr();
@ -1677,16 +1583,8 @@ main =
assert!(connections(&graph).unwrap().connections.is_empty());
let (node0, node1, _) = graph.nodes().unwrap().expect_tuple();
let connection_to_add = Connection {
source: Endpoint {
node: node0.info.id(),
port: default(),
var_crumbs: default(),
},
destination: Endpoint {
node: node1.info.id(),
port: vec![2].into(), // `_` in `print _`
var_crumbs: default(),
},
source: Endpoint::root(node0.info.id()),
target: Endpoint::target_at(&node1, [PrefixCrumb::Arg]).unwrap(),
};
graph.connect(&connection_to_add, &span_tree::generate::context::Empty).unwrap();
let new_main = graph.definition().unwrap().ast.repr();
@ -1789,7 +1687,7 @@ main =
Case {
info: None,
dest_node_expr: "f\n bar a var",
dest_node_expected: "f\n bar a _",
dest_node_expected: "f\n bar a",
},
];
for case in cases {

View File

@ -430,13 +430,11 @@ impl Handle {
}
}
/// Remove the connections from the graph. Returns an updated edge destination endpoint for
/// disconnected edge, in case it is still used as destination-only edge. When `None` is
/// returned, no update is necessary.
/// Remove the connections from the graph.
///
/// ### Errors
/// - Fails if the project is in read-only mode.
pub fn disconnect(&self, connection: &Connection) -> FallibleResult<Option<span_tree::Crumbs>> {
pub fn disconnect(&self, connection: &Connection) -> FallibleResult {
if self.project.read_only() {
Err(ReadOnly.into())
} else {
@ -515,7 +513,8 @@ pub mod tests {
use crate::test;
use crate::test::mock::Fixture;
use controller::graph::SpanTree;
use ast::crumbs::InfixCrumb;
use controller::graph::Endpoint;
use engine_protocol::language_server::types::test::value_update_with_type;
use wasm_bindgen_test::wasm_bindgen_test_configure;
@ -681,15 +680,10 @@ main =
assert_eq!(sum_node.expression().to_string(), "2 + 2");
assert_eq!(product_node.expression().to_string(), "5 * 5");
let context = &span_tree::generate::context::Empty;
let sum_tree = SpanTree::new(&sum_node.expression(), context).unwrap();
let sum_input =
sum_tree.root_ref().leaf_iter().find(|n| n.is_argument()).unwrap().crumbs;
let connection = Connection {
source: controller::graph::Endpoint::new(product_node.id(), []),
destination: controller::graph::Endpoint::new(sum_node.id(), sum_input),
source: Endpoint::root(product_node.id()),
target: Endpoint::target_at(sum_node, [InfixCrumb::LeftOperand]).unwrap(),
};
assert!(executed.connect(&connection).is_err());
});
}

View File

@ -549,7 +549,9 @@ mod tests {
use super::*;
use crate::test::mock::Fixture;
use crate::test::mock::Unified;
use span_tree::SpanTree;
use ast::crumbs::InfixCrumb;
use controller::graph::Connection;
use controller::graph::Endpoint;
fn check_atomic_undo(fixture: Fixture, action: impl FnOnce()) {
let Fixture { project, module, searcher, .. } = fixture;
@ -622,16 +624,11 @@ main =
assert_eq!(sum_node.expression().to_string(), "2 + 2");
assert_eq!(product_node.expression().to_string(), "5 * 5");
let context = &span_tree::generate::context::Empty;
let sum_tree = SpanTree::new(&sum_node.expression(), context).unwrap();
let sum_input =
sum_tree.root_ref().leaf_iter().find(|n| n.is_argument()).unwrap().crumbs;
let connection = controller::graph::Connection {
source: controller::graph::Endpoint::new(product_node.id(), []),
destination: controller::graph::Endpoint::new(sum_node.id(), sum_input),
let connection = Connection {
source: Endpoint::root(product_node.id()),
target: Endpoint::target_at(sum_node, [InfixCrumb::LeftOperand]).unwrap(),
};
graph.connect(&connection, context).unwrap();
graph.connect(&connection, &span_tree::generate::context::Empty).unwrap();
});
}

View File

@ -17,7 +17,6 @@ use futures::future::LocalBoxFuture;
use ide_view as view;
use ide_view::graph_editor::component::node as node_view;
use ide_view::graph_editor::component::visualization as visualization_view;
use ide_view::graph_editor::EdgeEndpoint;
use span_tree::generate::Context as _;
use view::graph_editor::CallWidgetsConfig;
@ -46,11 +45,14 @@ pub type ViewNodeId = view::graph_editor::NodeId;
pub type AstNodeId = ast::Id;
/// The connection identifier used by view.
pub type ViewConnection = view::graph_editor::EdgeId;
pub type ViewConnection = view::graph_editor::Connection;
/// The connection identifier used by controllers.
pub type AstConnection = controller::graph::Connection;
/// The connection endpoint used by controllers.
pub type AstEndpoint = controller::graph::Endpoint;
// =================
// === Constants ===
@ -166,6 +168,26 @@ impl Model {
);
}
fn connection_made(&self, connection: &ViewConnection) {
self.log_action(
|| {
let update = self.state.update_from_view();
Some(self.controller.connect(&update.view_to_ast_connection(connection)?))
},
"make connection",
);
}
fn connection_broken(&self, connection: &ViewConnection) {
self.log_action(
|| {
let update = self.state.update_from_view();
Some(self.controller.disconnect(&update.view_to_ast_connection(connection)?))
},
"break connection",
);
}
/// Sets or clears a context switch expression for the specified node.
///
/// A context switch expression allows enabling or disabling the execution of a particular node
@ -275,40 +297,6 @@ impl Model {
)
}
/// Connection was created in view.
fn new_connection_created(&self, id: ViewConnection) {
self.log_action(
|| {
let connection = self.view.model.edges.get_cloned_ref(&id)?;
let ast_to_create = self.state.update_from_view().create_connection(connection)?;
Some(self.controller.connect(&ast_to_create))
},
"create connection",
);
}
/// Connection was removed in view.
fn connection_removed(&self, id: ViewConnection) {
self.log_action(
|| {
let update = self.state.update_from_view();
let ast_to_remove = update.remove_connection(id)?;
Some(self.controller.disconnect(&ast_to_remove).map(|target_crumbs| {
if let Some(crumbs) = target_crumbs {
trace!(
"Updating edge target after disconnecting it. New crumbs: {crumbs:?}"
);
// If we are still using this edge (e.g. when dragging it), we need to
// update its target endpoint. Otherwise it will not reflect expression
// update performed on the target node.
self.view.replace_detached_edge_target((id, crumbs));
};
}))
},
"delete connection",
);
}
fn nodes_collapsed(&self, collapsed: &[ViewNodeId]) {
self.log_action(
|| {
@ -541,7 +529,7 @@ struct ViewUpdate {
state: Rc<State>,
nodes: Vec<controller::graph::Node>,
trees: HashMap<AstNodeId, controller::graph::NodeTrees>,
connections: HashSet<AstConnection>,
connections: Vec<AstConnection>,
}
impl ViewUpdate {
@ -551,7 +539,7 @@ impl ViewUpdate {
let state = model.state.clone_ref();
let nodes = model.controller.graph().nodes()?;
let connections_and_trees = model.controller.connections()?;
let connections = connections_and_trees.connections.into_iter().collect();
let connections = connections_and_trees.connections;
let trees = connections_and_trees.trees;
Ok(Self { state, nodes, trees, connections })
}
@ -628,21 +616,11 @@ impl ViewUpdate {
.collect()
}
/// Remove connections from the state and return views to be removed.
/// Get all current connections from the updated state, and return them in a form suitable for
/// passing to the Graph Editor view.
#[profile(Debug)]
fn remove_connections(&self) -> Vec<ViewConnection> {
self.state.update_from_controller().retain_connections(&self.connections)
}
/// Add connections to the state and return endpoints of connections to be created in views.
#[profile(Debug)]
fn add_connections(&self) -> Vec<(EdgeEndpoint, EdgeEndpoint)> {
let ast_conns = self.connections.iter();
ast_conns
.filter_map(|connection| {
self.state.update_from_controller().set_connection(connection.clone())
})
.collect()
fn map_all_connections(&self) -> Vec<ViewConnection> {
self.state.update_from_controller().map_connections(&self.connections)
}
#[profile(Debug)]
@ -738,11 +716,7 @@ impl Graph {
// === Refreshing Connections ===
remove_connection <= update_data.map(|update| update.remove_connections());
add_connection <= update_data.map(|update| update.add_connections());
view.remove_edge <+ remove_connection;
view.connect_nodes <+ add_connection;
view.set_connections <+ update_data.map(|update| update.map_all_connections());
// === Refreshing Expressions ===
@ -761,11 +735,11 @@ impl Graph {
eval view.node_position_set_batched(((node_id, position)) model.node_position_changed(*node_id, *position));
eval view.node_removed((node_id) model.node_removed(*node_id));
eval view.on_edge_endpoints_set((edge_id) model.new_connection_created(*edge_id));
eval view.on_edge_endpoint_unset(((edge_id,_)) model.connection_removed(*edge_id));
eval view.nodes_collapsed(((nodes, _)) model.nodes_collapsed(nodes));
eval view.enabled_visualization_path(((node_id, path)) model.node_visualization_changed(*node_id, path.clone()));
eval view.node_expression_span_set(((node_id, crumbs, expression)) model.node_expression_span_set(*node_id, crumbs, expression.clone_ref()));
eval view.connection_made((connection) model.connection_made(connection));
eval view.connection_broken((connection) model.connection_broken(connection));
eval view.node_action_context_switch(((node_id, active)) model.node_action_context_switch(*node_id, *active));
eval view.node_action_skip(((node_id, enabled)) model.node_action_skip(*node_id, *enabled));
eval view.node_action_freeze(((node_id, enabled)) model.node_action_freeze(*node_id, *enabled));

View File

@ -3,12 +3,11 @@
use crate::prelude::*;
use crate::presenter::graph::AstConnection;
use crate::presenter::graph::AstEndpoint;
use crate::presenter::graph::AstNodeId;
use crate::presenter::graph::ViewConnection;
use crate::presenter::graph::ViewNodeId;
use bimap::BiMap;
use bimap::Overwritten;
use double_representation::context_switch::ContextSwitchExpression;
use engine_protocol::language_server::ExpressionUpdatePayload;
use engine_protocol::language_server::SuggestionId;
@ -165,73 +164,6 @@ impl Nodes {
}
// ===================
// === Connections ===
// ===================
/// A structure keeping pairs of AST connections with their views (and list of AST connections
/// without view).
#[derive(Clone, Debug, Default)]
pub struct Connections {
connections: BiMap<AstConnection, ViewConnection>,
connections_without_view: HashSet<AstConnection>,
}
impl Connections {
/// Remove all connections not belonging to the given set.
///
/// Returns the views of removed connections.
pub fn retain_connections(
&mut self,
connections: &HashSet<AstConnection>,
) -> Vec<ViewConnection> {
self.connections_without_view.retain(|x| connections.contains(x));
let to_remove = self.connections.iter().filter(|(con, _)| !connections.contains(con));
let to_remove_vec = to_remove.map(|(_, edge_id)| *edge_id).collect_vec();
self.connections.retain(|con, _| connections.contains(con));
to_remove_vec
}
/// Add a new AST connection without view.
pub fn add_ast_connection(&mut self, connection: AstConnection) -> bool {
if !self.connections.contains_left(&connection) {
self.connections_without_view.insert(connection)
} else {
false
}
}
/// Add a connection with view.
///
/// Returns `true` if the new connection was added, and `false` if it already existed. In the
/// latter case, the new `view` is assigned to it (replacing possible previous view).
pub fn add_connection_view(&mut self, connection: AstConnection, view: ViewConnection) -> bool {
let existed_without_view = self.connections_without_view.remove(&connection);
match self.connections.insert(connection, view) {
Overwritten::Neither => !existed_without_view,
Overwritten::Left(_, _) => false,
Overwritten::Right(previous, _) => {
self.connections_without_view.insert(previous);
!existed_without_view
}
Overwritten::Pair(_, _) => false,
Overwritten::Both(_, (previous, _)) => {
self.connections_without_view.insert(previous);
false
}
}
}
/// Remove the connection by view (if any), and return it.
pub fn remove_connection(&mut self, connection: ViewConnection) -> Option<AstConnection> {
let (ast_connection, _) = self.connections.remove_by_right(&connection)?;
Some(ast_connection)
}
}
// ===================
// === Expressions ===
// ===================
@ -313,7 +245,6 @@ impl Expressions {
#[derive(Clone, Debug, Default)]
pub struct State {
nodes: RefCell<Nodes>,
connections: RefCell<Connections>,
expressions: RefCell<Expressions>,
}
@ -328,38 +259,6 @@ impl State {
self.nodes.borrow().ast_id_of_view(node)
}
/// Convert the AST connection to pair of [`EdgeEndpoint`]s.
pub fn view_edge_targets_of_ast_connection(
&self,
connection: AstConnection,
) -> Option<(EdgeEndpoint, EdgeEndpoint)> {
let convertible_source = connection.source.var_crumbs.is_empty();
let convertible_dest = connection.destination.var_crumbs.is_empty();
(convertible_source && convertible_dest).and_option_from(|| {
let nodes = self.nodes.borrow();
let src_node = nodes.get(connection.source.node)?.view_id?;
let dst_node = nodes.get(connection.destination.node)?.view_id?;
let src = EdgeEndpoint::new(src_node, connection.source.port);
let data = EdgeEndpoint::new(dst_node, connection.destination.port);
Some((src, data))
})
}
/// Convert the pair of [`EdgeEndpoint`]s to AST connection.
pub fn ast_connection_from_view_edge_targets(
&self,
source: EdgeEndpoint,
target: EdgeEndpoint,
) -> Option<AstConnection> {
let nodes = self.nodes.borrow();
let src_node = nodes.ast_id_of_view(source.node_id)?;
let dst_node = nodes.ast_id_of_view(target.node_id)?;
Some(controller::graph::Connection {
source: controller::graph::Endpoint::new(src_node, source.port),
destination: controller::graph::Endpoint::new(dst_node, target.port),
})
}
/// Get id of all node's expressions (ids of the all corresponding line AST nodes).
pub fn expressions_of_node(&self, node: ViewNodeId) -> Vec<ast::Id> {
let ast_node = self.nodes.borrow().ast_id_of_view(node);
@ -615,22 +514,20 @@ impl<'a> ControllerChange<'a> {
// === Connections ===
impl<'a> ControllerChange<'a> {
/// If given connection does not exists yet, add it and return the endpoints of the
/// to-be-created edge.
pub fn set_connection(
&self,
connection: AstConnection,
) -> Option<(EdgeEndpoint, EdgeEndpoint)> {
self.connections
.borrow_mut()
.add_ast_connection(connection.clone())
.and_option_from(move || self.view_edge_targets_of_ast_connection(connection))
}
/// Remove all connection not belonging to the given set. Returns the list of to-be-removed
/// views.
pub fn retain_connections(&self, connections: &HashSet<AstConnection>) -> Vec<ViewConnection> {
self.connections.borrow_mut().retain_connections(connections)
/// Map controller connections to view connections. Only creates connections where nodes on both
/// endpoints are currently represented in the view.
pub fn map_connections(&self, connections: &[AstConnection]) -> Vec<ViewConnection> {
let nodes = self.nodes.borrow();
connections
.iter()
.filter_map(|connection| {
let src_node = nodes.get(connection.source.node)?.view_id?;
let dst_node = nodes.get(connection.target.node)?.view_id?;
let source = EdgeEndpoint::new(src_node, connection.source.port);
let target = EdgeEndpoint::new(dst_node, connection.target.port);
Some(ViewConnection { source, target })
})
.collect()
}
}
@ -829,43 +726,20 @@ impl<'a> ViewChange<'a> {
let expression_has_changed = span_expression != new_span_expression;
expression_has_changed.then_some(ast_id)
}
}
// === Connections ===
impl<'a> ViewChange<'a> {
/// If the connections does not already exist, it is created and corresponding to-be-created
/// Ast connection is returned.
pub fn create_connection(&self, connection: view::graph_editor::Edge) -> Option<AstConnection> {
let source = connection.source()?;
let target = connection.target()?;
self.create_connection_from_endpoints(connection.id(), source, target)
}
/// If the connections with provided endpoints does not already exist, it is created and
/// corresponding to-be-created Ast connection is returned.
pub fn create_connection_from_endpoints(
&self,
connection: ViewConnection,
source: EdgeEndpoint,
target: EdgeEndpoint,
) -> Option<AstConnection> {
let ast_connection = self.ast_connection_from_view_edge_targets(source, target)?;
let mut connections = self.connections.borrow_mut();
let should_update_controllers =
connections.add_connection_view(ast_connection.clone(), connection);
should_update_controllers.then_some(ast_connection)
}
/// Remove the connection and return the corresponding AST connection which should be removed.
pub fn remove_connection(&self, id: ViewConnection) -> Option<AstConnection> {
self.connections.borrow_mut().remove_connection(id)
/// Map a connection on view side to a connection on controller side. Returns `None` if view
/// node on either connection endpoint is not represented in the controller.
pub fn view_to_ast_connection(&self, connection: &ViewConnection) -> Option<AstConnection> {
let source_node = self.state.ast_node_id_of_view(connection.source.node_id)?;
let target_node = self.state.ast_node_id_of_view(connection.target.node_id)?;
Some(AstConnection {
source: AstEndpoint::new(source_node, connection.source.port),
target: AstEndpoint::new(target_node, connection.target.port),
})
}
}
// =============
// === Tests ===
// =============
@ -957,58 +831,6 @@ mod tests {
assert_eq!(state.view_id_of_ast_node(node1.id()), None)
}
#[wasm_bindgen_test]
fn adding_and_removing_connections() {
use controller::graph::Endpoint;
let Fixture { state, nodes } = Fixture::setup_nodes(&["node1 = 2", "node1 + node1"]);
let src = Endpoint {
node: nodes[0].node.id(),
port: default(),
var_crumbs: default(),
};
let dest1 = Endpoint {
node: nodes[1].node.id(),
port: span_tree::Crumbs::new(vec![0]),
var_crumbs: default(),
};
let dest2 = Endpoint {
node: nodes[1].node.id(),
port: span_tree::Crumbs::new(vec![2]),
var_crumbs: default(),
};
let ast_con1 = AstConnection { source: src.clone(), destination: dest1.clone() };
let ast_con2 = AstConnection { source: src.clone(), destination: dest2.clone() };
let view_con1 = ensogl::display::object::Id::from(1).into();
let view_con2 = ensogl::display::object::Id::from(2).into();
let view_src = EdgeEndpoint { node_id: nodes[0].view, port: src.port };
let view_tgt1 = EdgeEndpoint { node_id: nodes[1].view, port: dest1.port };
let view_tgt2 = EdgeEndpoint { node_id: nodes[1].view, port: dest2.port };
let view_pair1 = (view_src.clone(), view_tgt1.clone());
let from_controller = state.update_from_controller();
let from_view = state.update_from_view();
assert_eq!(from_controller.set_connection(ast_con1.clone()), Some(view_pair1));
assert_eq!(
from_view.create_connection_from_endpoints(view_con1, view_src.clone(), view_tgt1),
None
);
assert_eq!(
from_view.create_connection_from_endpoints(view_con2, view_src, view_tgt2),
Some(ast_con2.clone())
);
let all_connections = [ast_con1, ast_con2.clone()].into_iter().collect();
assert_eq!(from_controller.retain_connections(&all_connections), vec![]);
assert_eq!(
from_controller.retain_connections(&[ast_con2.clone()].into_iter().collect()),
vec![view_con1]
);
assert_eq!(from_view.remove_connection(view_con2), Some(ast_con2));
}
#[wasm_bindgen_test]
fn refreshing_node_expression() {
let Fixture { state, nodes } = Fixture::setup_nodes(&["foo bar"]);

View File

@ -411,10 +411,8 @@ impl Project {
eval_ graph.nodes_collapsed([]analytics::remote_log_event("graph_editor::nodes_collapsed"));
eval_ graph.node_entered([]analytics::remote_log_event("graph_editor::node_enter_request"));
eval_ graph.node_exited([]analytics::remote_log_event("graph_editor::node_exit_request"));
eval_ graph.on_edge_endpoints_set([]analytics::remote_log_event("graph_editor::edge_endpoints_set"));
eval_ graph.visualization_shown([]analytics::remote_log_event("graph_editor::visualization_shown"));
eval_ graph.visualization_hidden([]analytics::remote_log_event("graph_editor::visualization_hidden"));
eval_ graph.on_edge_endpoint_unset([]analytics::remote_log_event("graph_editor::connection_removed"));
eval_ project.editing_committed([]analytics::remote_log_event("project::editing_committed"));
}
self

View File

@ -178,15 +178,6 @@ fn init(app: &Application) {
let expression_4 = expression_mock_trim();
graph_editor.frp.set_node_expression.emit((node4_id, expression_4));
// === Connections ===
let src = graph_editor::EdgeEndpoint::new(node1_id, span_tree::Crumbs::new(default()));
let tgt =
graph_editor::EdgeEndpoint::new(node2_id, span_tree::Crumbs::new(vec![0, 0, 0, 0, 1]));
graph_editor.frp.connect_nodes.emit((src, tgt));
// === VCS ===
let dummy_node_added_id = graph_editor.model.add_node();
@ -268,14 +259,14 @@ fn init(app: &Application) {
graph_editor.frp.set_node_position.emit((node_id, Vector2(-300.0, -100.0)));
let expression = expression_mock_string("Click me to show a pop-up");
graph_editor.frp.set_node_expression.emit((node_id, expression));
let node = graph_editor.nodes().all.get_cloned_ref(&node_id).unwrap();
let popup = project_view.popup();
let network = node.network();
let node_clicked = node.on_event::<mouse::Down>();
frp::extend! { network
eval_ node_clicked (popup.set_label.emit("This is a test pop-up."));
}
graph_editor.model.with_node(node_id, |node| {
let popup = project_view.popup();
let network = node.network();
let node_clicked = node.on_event::<mouse::Down>();
frp::extend! { network
eval_ node_clicked (popup.set_label.emit("This is a test pop-up."));
}
});
// === Rendering ===

View File

@ -26,10 +26,13 @@ pub async fn add_node(
let node_added = graph_editor.node_added.next_event();
method(graph_editor);
let (node_id, source_node, _) = node_added.expect();
let node = graph_editor.nodes().get_cloned_ref(&node_id).expect("Node was not added");
node.set_expression(node::Expression::new_plain(expression));
let node = graph_editor.model.with_node(node_id, |node| {
node.set_expression(node::Expression::new_plain(expression));
node.clone_ref()
});
graph_editor.stop_editing();
(node_id, source_node, node)
(node_id, source_node, node.expect("Node was not added"))
}
/// Create a new node directly.
@ -79,7 +82,7 @@ impl InitialNodes {
/// Find the initial nodes expected in a default project. Panics if the project state is not
/// as expected.
pub fn obtain_from_graph_editor(graph_editor: &GraphEditor) -> Self {
let mut nodes = graph_editor.nodes().all.entries();
let mut nodes = graph_editor.model.nodes.all.entries();
let y = |n: &Node| n.position().y;
nodes.sort_unstable_by(|(_, a), (_, b)| y(a).total_cmp(&y(b)));
let two_nodes = "Expected two nodes in initial Graph Editor.";

View File

@ -47,13 +47,15 @@ define_endpoints_2! {
/// dragged by the mouse.)
source_attached(bool),
set_disabled(bool),
/// Whether the edge should stop responding to mouse movement.
set_hover_disabled(bool),
/// The typical color of the node; also used to derive the focus color.
set_color(color::Lcha),
}
Output {
/// The mouse has clicked to detach the source end of the edge.
/// The edge was clicked close to the source end.
source_click(),
/// The mouse has clicked to detach the target end of the edge.
/// The edge was clicked close to the target end.
target_click(),
}
}
@ -102,17 +104,23 @@ impl Edge {
eval frp.set_disabled ((t) model.inputs.set_disabled(*t));
// Mouse events.
eval mouse_move ([model] (e) {
gated_mouse_move <- mouse_move.gate_not(&frp.set_hover_disabled);
gated_mouse_down <- mouse_down.gate_not(&frp.set_hover_disabled);
gated_mouse_out <- mouse_out.gate_not(&frp.set_hover_disabled);
hover_disabled <- frp.set_hover_disabled.on_true();
clear_focus <- any_(gated_mouse_out, hover_disabled);
eval gated_mouse_move ([model] (e) {
let pos = model.screen_pos_to_scene_pos(e.client_centered());
model.inputs.set_mouse_position(pos);
});
eval_ mouse_out (model.inputs.clear_focus.set(true));
eval mouse_down ([model, output] (e) {
eval_ clear_focus (model.inputs.clear_focus.set(true));
eval gated_mouse_down ([model, output] (e) {
let pos = model.screen_pos_to_scene_pos(e.client_centered());
let pos = model.scene_pos_to_parent_pos(pos);
match model.closer_end(pos) {
Some(EndPoint::Source) => output.target_click.emit(()),
Some(EndPoint::Target) => output.source_click.emit(()),
Some(EndPoint::Source) => output.source_click.emit(()),
Some(EndPoint::Target) => output.target_click.emit(()),
// Ignore click events that were delivered to our display object inaccurately.
None => (),
}
@ -123,16 +131,16 @@ impl Edge {
eval edge_color.value ((color) model.inputs.set_color(color.into()));
// Invalidation.
redraw_needed <- any(...);
redraw_needed <+ frp.target_position.constant(());
redraw_needed <+ frp.source_attached.constant(());
redraw_needed <+ frp.target_attached.constant(());
redraw_needed <+ frp.source_size.constant(());
redraw_needed <+ frp.set_disabled.constant(());
redraw_needed <+ mouse_move.constant(());
redraw_needed <+ mouse_out.constant(());
redraw_needed <+ edge_color.value.constant(());
redraw_needed <+ display_object.on_transformed.constant(());
redraw_needed <- any_(...);
redraw_needed <+ frp.target_position;
redraw_needed <+ frp.source_attached;
redraw_needed <+ frp.target_attached;
redraw_needed <+ frp.source_size;
redraw_needed <+ frp.set_disabled;
redraw_needed <+ gated_mouse_move;
redraw_needed <+ gated_mouse_out;
redraw_needed <+ edge_color.value;
redraw_needed <+ display_object.on_transformed;
redraw <- redraw_needed.debounce();
eval_ redraw (model.redraw());
}

View File

@ -188,47 +188,6 @@ pub mod error_shape {
}
// ==============
// === Crumbs ===
// ==============
#[derive(Clone, Copy, Debug)]
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub enum Endpoint {
Input,
Output,
}
#[derive(Clone, Debug)]
#[allow(missing_docs)] // FIXME[everyone] Public-facing API should be documented.
pub struct Crumbs {
pub endpoint: Endpoint,
pub crumbs: span_tree::Crumbs,
}
impl Crumbs {
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn input(crumbs: span_tree::Crumbs) -> Self {
let endpoint = Endpoint::Input;
Self { endpoint, crumbs }
}
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn output(crumbs: span_tree::Crumbs) -> Self {
let endpoint = Endpoint::Output;
Self { endpoint, crumbs }
}
}
impl Default for Crumbs {
fn default() -> Self {
Self::output(default())
}
}
// ============
// === Node ===
// ============
@ -242,7 +201,7 @@ ensogl::define_endpoints_2! {
disable_visualization (),
set_visualization (Option<visualization::Definition>),
set_disabled (bool),
set_input_connected (span_tree::Crumbs,Option<color::Lcha>),
set_connections (HashMap<span_tree::PortId, color::Lcha>),
set_expression (Expression),
edit_expression (text::Range<text::Byte>, ImString),
set_skip_macro (bool),
@ -692,16 +651,16 @@ impl Node {
filtered_usage_type <- input.set_expression_usage_type.filter(
move |(_,tp)| *tp != unresolved_symbol_type
);
eval filtered_usage_type (((a,b)) model.set_expression_usage_type(*a,b));
eval input.set_expression ((a) model.set_expression(a));
eval filtered_usage_type(((a,b)) model.set_expression_usage_type(*a,b));
eval input.set_expression((a) model.set_expression(a));
model.input.edit_expression <+ input.edit_expression;
out.on_expression_modified <+ model.input.frp.on_port_code_update;
out.requested_widgets <+ model.input.frp.requested_widgets;
out.request_import <+ model.input.frp.request_import;
out.on_expression_modified <+ model.input.frp.on_port_code_update;
out.requested_widgets <+ model.input.frp.requested_widgets;
out.request_import <+ model.input.frp.request_import;
model.input.set_connected <+ input.set_input_connected;
model.input.set_disabled <+ input.set_disabled;
model.input.update_widgets <+ input.update_widgets;
model.input.set_connections <+ input.set_connections;
model.input.set_disabled <+ input.set_disabled;
model.input.update_widgets <+ input.update_widgets;
model.output.set_expression_visibility <+ input.set_output_expression_visibility;

View File

@ -33,7 +33,11 @@ const ANIMATION_LENGTH_COEFFIENT: f32 = 15.0;
/// Initialize edited node growth/shrink animator. It would handle scene layer change for the edited
/// node as well.
pub fn initialize_edited_node_animator(model: &GraphEditorModel, frp: &crate::Frp, scene: &Scene) {
pub fn initialize_edited_node_animator(
model: &Rc<GraphEditorModel>,
frp: &crate::Frp,
scene: &Scene,
) {
let network = &frp.network();
let out = &frp.output;
let searcher_cam = scene.layers.node_searcher.camera();
@ -112,16 +116,16 @@ impl GraphEditorModel {
/// Move node to the `edited_node` scene layer, so that it is rendered by the separate camera.
#[profile(Debug)]
fn move_node_to_edited_node_layer(&self, node_id: NodeId) {
if let Some(node) = self.nodes.get_cloned(&node_id) {
self.nodes.with(&node_id, |node| {
node.model().set_editing_expression(true);
}
});
}
/// Move node to the `main` scene layer, so that it is rendered by the main camera.
#[profile(Debug)]
fn move_node_to_main_layer(&self, node_id: NodeId) {
if let Some(node) = self.nodes.get_cloned(&node_id) {
self.nodes.with(&node_id, |node| {
node.model().set_editing_expression(false);
}
});
}
}

View File

@ -5,7 +5,6 @@ use enso_text::index::*;
use ensogl::display::shape::*;
use ensogl::display::traits::*;
use crate::component::type_coloring;
use crate::node;
use crate::node::input::widget;
use crate::node::input::widget::OverrideKey;
@ -50,6 +49,7 @@ pub const TEXT_SIZE: f32 = 12.0;
pub use span_tree::Crumb;
pub use span_tree::Crumbs;
pub use span_tree::PortId;
pub use span_tree::SpanTree;
@ -62,8 +62,12 @@ pub use span_tree::SpanTree;
#[derive(Clone, Default)]
#[allow(missing_docs)]
pub struct Expression {
pub code: ImString,
pub span_tree: SpanTree,
pub code: ImString,
pub span_tree: SpanTree,
/// The map containing either first or "this" argument for each unique `call_id` in the tree.
pub target_map: HashMap<ast::Id, ast::Id>,
/// Map from Port ID to its span-tree node crumbs.
pub ports_map: HashMap<PortId, Crumbs>,
}
impl Deref for Expression {
@ -85,6 +89,13 @@ impl Debug for Expression {
}
}
impl Expression {
fn port_node(&self, port: PortId) -> Option<span_tree::node::Ref> {
let crumbs = self.ports_map.get(&port)?;
self.span_tree.get_node(crumbs).ok()
}
}
// === Pretty printing debug adapter ===
@ -120,7 +131,26 @@ impl Expression {
impl From<node::Expression> for Expression {
#[profile(Debug)]
fn from(t: node::Expression) -> Self {
Self { code: t.code, span_tree: t.input_span_tree }
let span_tree = t.input_span_tree;
let mut target_map: HashMap<ast::Id, ast::Id> = HashMap::new();
let mut ports_map: HashMap<PortId, Crumbs> = HashMap::new();
span_tree.root_ref().dfs(|node| {
if let Some((ast_id, call_id)) = node.ast_id.zip(node.kind.call_id()) {
let entry = target_map.entry(call_id);
let is_target_argument = node.kind.is_this();
entry
.and_modify(|target_id| {
if is_target_argument {
*target_id = ast_id;
}
})
.or_insert(ast_id);
}
if let Some(port_id) = node.port_id {
ports_map.insert(port_id, node.crumbs.clone());
}
});
Self { code: t.code, span_tree, target_map, ports_map }
}
}
@ -255,8 +285,8 @@ impl Model {
}
}
fn set_connected(&self, crumbs: &Crumbs, status: Option<color::Lcha>) {
self.widget_tree.set_connected(crumbs, status);
fn set_connections(&self, map: &HashMap<PortId, color::Lcha>) {
self.widget_tree.set_connections(map);
}
fn set_expression_usage_type(&self, id: ast::Id, usage_type: Option<Type>) {
@ -267,19 +297,18 @@ impl Model {
hovered.then(cursor::Style::cursor).unwrap_or_default()
}
fn port_hover_pointer_style(&self, hovered: &Switch<Crumbs>) -> Option<cursor::Style> {
let crumbs = hovered.on()?;
let expr = self.expression.borrow();
let port = expr.span_tree.get_node(crumbs).ok()?;
let display_object = self.widget_tree.get_port_display_object(&port)?;
let tp = port.tp().map(|t| t.into());
let color = tp.as_ref().map(|tp| type_coloring::compute(tp, &self.styles));
fn port_hover_pointer_style(&self, hovered: &Switch<PortId>) -> Option<cursor::Style> {
let display_object = self.widget_tree.get_port_display_object(hovered.into_on()?)?;
let pad_x = node::input::port::PORT_PADDING_X * 2.0;
let min_y = node::input::port::BASE_PORT_HEIGHT;
let computed_size = display_object.computed_size();
let size = Vector2(computed_size.x + pad_x, computed_size.y.max(min_y));
let radius = size.y / 2.0;
Some(cursor::Style::new_highlight(display_object, size, radius, color))
Some(cursor::Style::new_highlight(display_object, size, min_y / 2.0))
}
fn port_type(&self, port: PortId) -> Option<Type> {
let expression = self.expression.borrow();
Some(Type::from(expression.port_node(port)?.kind.tp()?))
}
/// Configure widgets associated with single Enso call expression, overriding default widgets
@ -308,11 +337,8 @@ impl Model {
/// See also: [`controller::graph::widget`] module of `enso-gui` crate.
#[profile(Debug)]
fn request_widget_config_overrides(&self, expression: &Expression, area_frp: &FrpEndpoints) {
let call_info = CallInfoMap::scan_expression(&expression.span_tree);
for (call_id, info) in call_info.iter() {
if let Some(target_id) = info.target_id {
area_frp.source.requested_widgets.emit((*call_id, target_id));
}
for (call_id, target_id) in expression.target_map.iter() {
area_frp.source.requested_widgets.emit((*call_id, *target_id));
}
}
@ -323,13 +349,16 @@ impl Model {
let new_expression = Expression::from(new_expression.into());
debug!("Set expression: \n{:?}", new_expression.tree_pretty_printer());
// Request widget configuration before rebuilding the widget tree, so that in case there are
// any widget responses already cached, they can be immediately used during the first build.
// Otherwise the tree would often be rebuilt twice immediately after setting the expression.
self.request_widget_config_overrides(&new_expression, area_frp);
self.widget_tree.rebuild_tree(
&new_expression.span_tree,
&new_expression.code,
&self.styles,
);
self.request_widget_config_overrides(&new_expression, area_frp);
*self.expression.borrow_mut() = new_expression;
}
@ -375,17 +404,14 @@ ensogl::define_endpoints! {
/// Set read-only mode for input ports.
set_read_only (bool),
/// Set the connection status of the port indicated by the breadcrumbs. For connected ports,
/// contains the color of connected edge.
set_connected (Crumbs, Option<color::Lcha>),
/// Provide a map of edge colors for all connected ports.
set_connections (HashMap<PortId, color::Lcha>),
/// Update widget configuration for widgets already present in this input area.
update_widgets (CallWidgetsConfig),
/// Enable / disable port hovering. The optional type indicates the type of the active edge
/// if any. It is used to highlight ports if they are missing type information or if their
/// types are polymorphic.
set_ports_active (bool,Option<Type>),
/// Enable / disable port hovering
set_ports_active (bool),
set_view_mode (view::Mode),
set_profiling_status (profiling::Status),
@ -405,8 +431,8 @@ ensogl::define_endpoints! {
editing (bool),
ports_visible (bool),
body_hover (bool),
on_port_press (Crumbs),
on_port_hover (Switch<Crumbs>),
on_port_press (PortId),
on_port_hover (Switch<PortId>),
on_port_code_update (Crumbs,ImString),
view_mode (view::Mode),
/// A set of widgets attached to a method requests their definitions to be queried from an
@ -416,6 +442,8 @@ ensogl::define_endpoints! {
request_import (ImString),
/// A connected port within the node has been moved. Some edges might need to be updated.
input_edges_need_refresh (),
/// The widget tree has been rebuilt. Some ports might have been added or removed.
widget_tree_rebuilt (),
}
}
@ -476,8 +504,8 @@ impl Area {
let ports_active = &frp.set_ports_active;
edit_or_ready <- frp.set_edit_ready_mode || set_editing;
reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, (a, _)| *e && !a);
port_vis <- all_with(&set_editing, ports_active, |e, (a, _)| !e && *a);
reacts_to_hover <- all_with(&edit_or_ready, ports_active, |e, a| *e && !a);
port_vis <- all_with(&set_editing, ports_active, |e, a| !e && *a);
frp.output.source.ports_visible <+ port_vis;
frp.output.source.editing <+ set_editing;
model.widget_tree.set_ports_visible <+ frp.ports_visible;
@ -547,10 +575,12 @@ impl Area {
// === Widgets ===
eval frp.update_widgets((a) model.apply_widget_configuration(a));
eval frp.set_connected(((crumbs,status)) model.set_connected(crumbs,*status));
eval frp.set_connections((conn) model.set_connections(conn));
eval frp.set_expression_usage_type(((id,tp)) model.set_expression_usage_type(*id,tp.clone()));
eval frp.set_disabled ((disabled) model.widget_tree.set_disabled(*disabled));
eval_ model.widget_tree.rebuild_required(model.rebuild_widget_tree_if_dirty());
frp.output.source.widget_tree_rebuilt <+ model.widget_tree.on_rebuild_finished;
// === View Mode ===
@ -578,26 +608,32 @@ impl Area {
Self { frp, model }
}
/// Check if the node currently contains a port of given ID.
pub fn has_port(&self, port: PortId) -> bool {
self.model.widget_tree.get_port_display_object(port).is_some()
}
/// An offset from node position to a specific port.
pub fn port_offset(&self, crumbs: &[Crumb]) -> Vector2<f32> {
let expr = self.model.expression.borrow();
let port = expr
.get_node(crumbs)
.ok()
.and_then(|node| self.model.widget_tree.get_port_display_object(&node));
pub fn port_offset(&self, port: PortId) -> Vector2<f32> {
let object = self.model.widget_tree.get_port_display_object(port);
let initial_position = Vector2(TEXT_OFFSET, NODE_HEIGHT / 2.0);
port.map_or(initial_position, |port| {
let pos = port.global_position();
object.map_or(initial_position, |object| {
let pos = object.global_position();
let node_pos = self.model.display_object.global_position();
let size = port.computed_size();
let size = object.computed_size();
pos.xy() - node_pos.xy() + size * 0.5
})
}
/// A type of the specified port.
pub fn port_type(&self, crumbs: &Crumbs) -> Option<Type> {
let expression = self.model.expression.borrow();
expression.span_tree.get_node(crumbs).ok().and_then(|t| t.tp().map(|t| t.into()))
pub fn port_type(&self, port: PortId) -> Option<Type> {
self.model.port_type(port)
}
/// Get the span-tree crumbs for the specified port.
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.
@ -605,38 +641,3 @@ impl Area {
self.model.set_label_layer(layer);
}
}
// ===================
// === CallInfoMap ===
// ===================
#[derive(Debug, Deref)]
struct CallInfoMap {
/// The map from node's call_id to call information.
call_info: HashMap<ast::Id, CallInfo>,
}
/// Information about the call expression, which are derived from the span tree.
#[derive(Debug, Default)]
struct CallInfo {
/// The AST ID associated with `self` argument span of the call expression.
target_id: Option<ast::Id>,
}
impl CallInfoMap {
fn scan_expression(expression: &SpanTree) -> Self {
let mut call_info: HashMap<ast::Id, CallInfo> = HashMap::new();
expression.root_ref().dfs(|node| {
if let Some(call_id) = node.kind.call_id() {
let mut entry = call_info.entry(call_id).or_default();
if entry.target_id.is_none() || node.kind.is_this() {
entry.target_id = node.ast_id;
}
}
});
Self { call_info }
}
}

View File

@ -15,6 +15,7 @@ use ensogl::data::color;
use ensogl::display;
use ensogl::display::scene::layer::LayerSymbolPartition;
use ensogl::display::shape;
use span_tree::PortId;
@ -111,7 +112,7 @@ impl PortLayers {
pub struct Port {
/// Drop source must be kept at the top of the struct, so it will be dropped first.
_on_cleanup: frp::DropSource,
crumbs: Rc<RefCell<span_tree::Crumbs>>,
port_id: frp::Source<PortId>,
port_root: display::object::Instance,
widget_root: display::object::Instance,
widget: DynWidget,
@ -149,7 +150,6 @@ impl Port {
let mouse_leave = hover_shape.on_event::<mouse::Leave>();
let mouse_down = hover_shape.on_event::<mouse::Down>();
let crumbs: Rc<RefCell<span_tree::Crumbs>> = default();
if frp.set_ports_visible.value() {
port_root.add_child(&hover_shape);
@ -160,16 +160,14 @@ impl Port {
frp::extend! { network
on_cleanup <- on_drop();
port_id <- source();
hovering <- bool(&mouse_leave, &mouse_enter);
cleanup_hovering <- on_cleanup.constant(false);
hovering <- any(&hovering, &cleanup_hovering);
hovering <- hovering.on_change();
frp.on_port_hover <+ hovering.map(
f!([crumbs](t) Switch::new(crumbs.borrow().clone(), *t))
);
frp.on_port_press <+ mouse_down.map(f_!(crumbs.borrow().clone()));
frp.on_port_hover <+ hovering.map2(&port_id, |t, id| Switch::new(*id, *t));
frp.on_port_press <+ port_id.sample(&mouse_down);
eval frp.set_ports_visible([port_root_weak, hover_shape] (active) {
if let Some(port_root) = port_root_weak.upgrade() {
if *active {
@ -194,7 +192,7 @@ impl Port {
widget,
widget_root,
port_root,
crumbs,
port_id,
current_depth: 0,
}
}
@ -210,7 +208,11 @@ impl Port {
ctx: ConfigContext,
pad_x_override: Option<f32>,
) {
self.crumbs.replace(ctx.span_node.crumbs.clone());
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."),
};
self.set_connected(ctx.info.connection);
self.set_port_layout(&ctx, pad_x_override.unwrap_or(HOVER_PADDING_X));
self.widget.configure(config, ctx);

View File

@ -57,6 +57,7 @@ use ensogl::display::shape::StyleWatch;
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 text::index::Byte;
@ -92,8 +93,6 @@ pub const WIDGET_SPACING_PER_OFFSET: f32 = 7.224_609_4;
/// the hover area of the port.
pub const PRIMARY_PORT_MAX_NESTING_LEVEL: usize = 0;
// ===========
// === FRP ===
// ===========
@ -110,8 +109,8 @@ ensogl::define_endpoints_2! {
Output {
value_changed (span_tree::Crumbs, Option<ImString>),
request_import (ImString),
on_port_hover (Switch<span_tree::Crumbs>),
on_port_press (span_tree::Crumbs),
on_port_hover (Switch<PortId>),
on_port_press (PortId),
pointer_style (cursor::Style),
/// Any of the connected port's display object within the widget tree has been updated. This
/// signal is generated using the `on_updated` signal of the `display_object` of the widget,
@ -122,6 +121,8 @@ ensogl::define_endpoints_2! {
/// Dirty flag has been marked. This signal is fired immediately after the update that
/// caused it. Prefer using `rebuild_required` signal instead, which is debounced.
marked_dirty_sync (),
/// The widget tree has been rebuilt. Its port structure has potentially been updated.
on_rebuild_finished (),
}
}
@ -475,6 +476,7 @@ pub struct WidgetsFrp {
pub(super) set_read_only: 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>>,
/// Remove given tree node's reference from the widget tree, and send its only remaining strong
/// reference to a new widget owner using [`SpanWidget::receive_ownership`] method. This will
/// effectively give up tree's ownership of that node, and will prevent its view from being
@ -484,8 +486,8 @@ pub struct WidgetsFrp {
pub(super) transfer_ownership: frp::Any<TransferRequest>,
pub(super) value_changed: frp::Any<(span_tree::Crumbs, Option<ImString>)>,
pub(super) request_import: frp::Any<ImString>,
pub(super) on_port_hover: frp::Any<Switch<span_tree::Crumbs>>,
pub(super) on_port_press: frp::Any<span_tree::Crumbs>,
pub(super) on_port_hover: frp::Any<Switch<PortId>>,
pub(super) on_port_press: frp::Any<PortId>,
pub(super) pointer_style: frp::Any<cursor::Style>,
pub(super) connected_port_updated: frp::Any<()>,
}
@ -552,6 +554,12 @@ impl Tree {
on_port_press <- any(...);
frp.private.output.on_port_hover <+ on_port_hover;
frp.private.output.on_port_press <+ on_port_press;
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)))
).sampler();
}
let value_changed = frp.private.output.value_changed.clone_ref();
@ -570,6 +578,7 @@ impl Tree {
on_port_hover,
on_port_press,
pointer_style,
hovered_port_children,
connected_port_updated,
};
@ -592,11 +601,10 @@ impl Tree {
self.notify_dirty(self.model.set_usage_type(ast_id, usage_type));
}
/// Set connection status for given span crumbs. The connected nodes will be highlighted with a
/// different color, and the widgets might change behavior depending on the connection
/// status.
pub fn set_connected(&self, crumbs: &span_tree::Crumbs, status: Option<color::Lcha>) {
self.notify_dirty(self.model.set_connected(crumbs, status));
/// Set all currently active connections. The connected nodes will be highlighted with a
/// different color, and the widgets might change behavior depending on the connection status.
pub fn set_connections(&self, map: &HashMap<PortId, color::Lcha>) {
self.notify_dirty(self.model.set_connections(map));
}
/// Set disabled status for given span tree node. The disabled nodes will be grayed out.
@ -608,7 +616,7 @@ impl Tree {
/// Rebuild tree if it has been marked as dirty. The dirty flag is marked whenever more data
/// external to the span-tree is provided, using `set_config_override`, `set_usage_type`,
/// `set_connected` or `set_disabled` methods of the widget tree.
/// `set_connections` or `set_disabled` methods of the widget tree.
pub fn rebuild_tree_if_dirty(
&self,
tree: &span_tree::SpanTree,
@ -630,17 +638,14 @@ impl Tree {
node_expression: &str,
styles: &StyleWatch,
) {
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, styles);
self.frp.private.output.on_rebuild_finished.emit(());
}
/// Get the root display object of the widget port for given span tree node. Not all nodes must
/// have a distinct widget, so the returned value might be [`None`].
pub fn get_port_display_object(
&self,
span_node: &SpanRef,
) -> Option<display::object::Instance> {
let pointer = self.model.get_node_widget_pointer(span_node);
self.model.with_port(pointer, |w| w.display_object().clone())
pub fn get_port_display_object(&self, port_id: PortId) -> Option<display::object::Instance> {
self.model.with_port(port_id, |w| w.display_object().clone())
}
/// Get hover shapes for all ports in the tree. Used in tests to manually dispatch mouse events.
@ -762,9 +767,9 @@ struct TreeModel {
/// be used to quickly find the parent of a node, or iterate over all children or descendants
/// of a node.
hierarchy: RefCell<Vec<NodeHierarchy>>,
ports_map: RefCell<HashMap<StableSpanIdentity, usize>>,
ports_map: RefCell<HashMap<PortId, WidgetIdentity>>,
override_map: Rc<RefCell<HashMap<OverrideKey, Configuration>>>,
connected_map: Rc<RefCell<HashMap<span_tree::Crumbs, color::Lcha>>>,
connected_map: Rc<RefCell<HashMap<PortId, color::Lcha>>>,
usage_type_map: Rc<RefCell<HashMap<ast::Id, crate::Type>>>,
node_disabled: Cell<bool>,
tree_dirty: Cell<bool>,
@ -814,10 +819,13 @@ impl TreeModel {
}
/// Set the connection status under given widget. It may cause the tree to be marked as dirty.
fn set_connected(&self, crumbs: &span_tree::Crumbs, status: Option<color::Lcha>) -> bool {
let mut map = self.connected_map.borrow_mut();
let dirty = map.synchronize_entry(crumbs.clone(), status);
self.mark_dirty_flag(dirty)
fn set_connections(&self, map: &HashMap<PortId, color::Lcha>) -> bool {
let mut prev_map = self.connected_map.borrow_mut();
let modified = &*prev_map != map;
if modified {
*prev_map = map.clone();
}
self.mark_dirty_flag(modified)
}
/// Set the usage type of an expression. It may cause the tree to be marked as dirty.
@ -952,30 +960,24 @@ impl TreeModel {
self.hierarchy.replace(builder.hierarchy);
let mut ports_map_borrow = self.ports_map.borrow_mut();
ports_map_borrow.clear();
ports_map_borrow.extend(
builder.pointer_usage.into_iter().filter_map(|(k, v)| Some((k, v.port_index?))),
);
}
/// Convert span tree node to a representation with stable identity across rebuilds. Every node
/// in the span tree has a unique representation in the form of a [`StableSpanIdentity`], which
/// is more stable across changes in the span tree than [`span_tree::Crumbs`]. The pointer is
/// used to identify the widgets or ports in the widget tree.
pub fn get_node_widget_pointer(&self, span_node: &SpanRef) -> StableSpanIdentity {
StableSpanIdentity::from_node(span_node)
ports_map_borrow.extend(builder.pointer_usage.into_iter().filter_map(|(k, v)| {
let (port_id, index) = v.assigned_port?;
Some((port_id, WidgetIdentity { main: k, index }))
}));
}
/// Perform an operation on a shared reference to a tree port under given pointer. When there is
/// no port under provided pointer, the operation will not be performed and `None` will be
/// returned.
pub fn with_port<T>(
&self,
pointer: StableSpanIdentity,
f: impl FnOnce(&Port) -> T,
) -> Option<T> {
let index = *self.ports_map.borrow().get(&pointer)?;
let unique_ptr = WidgetIdentity { main: pointer, index };
self.nodes_map.borrow().get(&unique_ptr).and_then(|n| n.node.port()).map(f)
pub fn with_port<T>(&self, port: PortId, f: impl FnOnce(&Port) -> T) -> Option<T> {
let identity = *self.ports_map.borrow().get(&port)?;
self.nodes_map.borrow().get(&identity).and_then(|n| n.node.port()).map(f)
}
/// Compute a set of descendant widgets of a given port.
fn port_child_widgets(&self, port: PortId) -> HashSet<WidgetIdentity> {
let identity = self.ports_map.borrow().get(&port).copied();
identity.map_or_default(|id| self.iter_subtree(id).collect())
}
}
@ -1271,12 +1273,12 @@ impl WidgetIdentity {
#[derive(Debug, Default)]
struct PointerUsage {
/// Next sequence index that will be assigned to a widget created for the same span tree node.
next_index: usize,
next_index: usize,
/// The pointer index of a widget on this span tree that received a port, if any exist already.
port_index: Option<usize>,
assigned_port: Option<(PortId, usize)>,
/// The widget configuration kinds that were already used for this span tree node. Those will
/// be excluded from config possibilities of the next widget created for this node.
used_configs: DynKindFlags,
used_configs: DynKindFlags,
}
impl PointerUsage {
@ -1285,9 +1287,15 @@ impl PointerUsage {
self.next_index - 1
}
fn request_port(&mut self, identity: &WidgetIdentity, wants_port: bool) -> bool {
let will_receive_port = wants_port && self.port_index.is_none();
will_receive_port.then(|| self.port_index = Some(identity.index));
fn request_port(
&mut self,
identity: &WidgetIdentity,
port_id: Option<PortId>,
wants_port: bool,
) -> bool {
let Some(port_id) = port_id else { return false; };
let will_receive_port = wants_port && self.assigned_port.is_none();
will_receive_port.then(|| self.assigned_port = Some((port_id, identity.index)));
will_receive_port
}
}
@ -1317,7 +1325,7 @@ struct TreeBuilder<'a> {
/// selected. This is a temporary map that is cleared and created from scratch for
/// each tree building process.
local_overrides: HashMap<OverrideKey, Configuration>,
connected_map: &'a HashMap<span_tree::Crumbs, color::Lcha>,
connected_map: &'a HashMap<PortId, color::Lcha>,
usage_type_map: &'a HashMap<ast::Id, crate::Type>,
old_nodes: HashMap<WidgetIdentity, TreeEntry>,
new_nodes: HashMap<WidgetIdentity, TreeEntry>,
@ -1406,7 +1414,7 @@ impl<'a> TreeBuilder<'a> {
let usage_type = span_node.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned();
// Prepare the widget node info and build context.
let connection_color = self.connected_map.get(&span_node.crumbs);
let connection_color = span_node.port_id.as_ref().and_then(|p| self.connected_map.get(p));
let connection = connection_color.map(|&color| EdgeData { color, depth });
let parent_connection = self.parent_info.as_ref().and_then(|info| info.connection);
let subtree_connection = connection.or(parent_connection);
@ -1476,8 +1484,9 @@ impl<'a> TreeBuilder<'a> {
let this = &mut *ctx.builder;
let ptr_usage = this.pointer_usage.entry(main_ptr).or_default();
ptr_usage.used_configs |= configuration.kind.flag();
let can_assign_port = configuration.has_port && !is_extended_ast;
let widget_has_port =
ptr_usage.request_port(&widget_id, configuration.has_port && !is_extended_ast);
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 old_node = this.old_nodes.remove(&widget_id).map(|e| e.node);

View File

@ -26,7 +26,6 @@ ensogl::define_endpoints_2! {
content(ImString),
text_color(ColorState),
text_weight(text::Weight),
crumbs(span_tree::Crumbs),
}
}
@ -72,9 +71,8 @@ impl SpanWidget for Widget {
let styles = ctx.styles();
frp::extend! { network
parent_port_hovered <- widgets_frp.on_port_hover.map2(&frp.crumbs, |h, crumbs| {
h.on().map_or(false, |h| crumbs.starts_with(h))
});
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) {
@ -130,7 +128,6 @@ impl SpanWidget for Widget {
input.content.emit(content);
input.text_color.emit(color_state);
input.text_weight(text_weight);
input.crumbs.emit(ctx.span_node.crumbs.clone());
}
}

View File

@ -334,10 +334,9 @@ impl Widget {
let activation_shape = &self.activation_shape;
let focus_receiver = &self.dropdown_wrapper;
frp::extend! { network
is_hovered <- widgets_frp.on_port_hover.map2(&config_frp.current_crumbs, |h, crumbs| {
h.on().map_or(false, |h| crumbs.starts_with(h))
});
is_connected_or_hovered <- config_frp.is_connected || is_hovered;
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)

View File

@ -21,7 +21,6 @@ use ensogl::display::shape::StyleWatch;
use ensogl::display::shape::StyleWatchFrp;
use ensogl_component::text;
use ensogl_hardcoded_theme as theme;
use span_tree;
@ -39,7 +38,7 @@ const SHOW_DELAY_DURATION_MS: f32 = 150.0;
// ================
use span_tree::node::Ref as PortRef;
use span_tree::Crumbs;
use span_tree::PortId;
use span_tree::SpanTree;
@ -124,9 +123,8 @@ ensogl::define_endpoints! {
}
Output {
on_port_press (Crumbs),
on_port_hover (Switch<Crumbs>),
on_port_type_change (Crumbs,Option<Type>),
on_port_press (PortId),
on_port_hover (Switch<PortId>),
port_size_multiplier (f32),
body_hover (bool),
type_label_visibility (bool),
@ -297,27 +295,30 @@ impl Model {
node_tp.or_else(|| whole_expr_type.clone())
};
let crumbs = node.crumbs.clone_ref();
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,&self.styles_frp,port_index,port_count);
let (port_shape, port_frp) = model.init_shape(
&self.app,
&self.styles,
&self.styles_frp,
port_index,
port_count,
);
let port_network = &port_frp.network;
let source = &self.frp.source;
let port_id = node.port_id.unwrap_or_default();
frp::extend! { port_network
self.frp.source.on_port_hover <+ port_frp.on_hover.map
(f!([crumbs](t) Switch::new(crumbs.clone(),*t)));
self.frp.source.on_port_press <+ port_frp.on_press.constant(crumbs.clone());
port_frp.set_size_multiplier <+ self.frp.port_size_multiplier;
self.frp.source.on_port_type_change <+ port_frp.tp.map(move |t|(crumbs.clone(),t.clone()));
port_frp.set_type_label_visibility <+ self.frp.type_label_visibility;
self.frp.source.tooltip <+ port_frp.tooltip;
port_frp.set_view_mode <+ self.frp.view_mode;
port_frp.set_size <+ self.frp.size;
port_frp.set_size_multiplier <+ self.frp.port_size_multiplier;
port_frp.set_type_label_visibility <+ self.frp.type_label_visibility;
source.tooltip <+ port_frp.tooltip;
port_frp.set_view_mode <+ self.frp.view_mode;
port_frp.set_size <+ self.frp.size;
source.on_port_hover <+ port_frp.on_hover.map(move |&t| Switch::new(port_id,t));
source.on_port_press <+ port_frp.on_press.constant(port_id);
}
port_frp.set_type_label_visibility.emit(self.frp.type_label_visibility.value());
@ -441,14 +442,29 @@ impl Area {
}
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn port_type(&self, crumbs: &Crumbs) -> Option<Type> {
let expression = self.model.expression.borrow();
let node = expression.span_tree.root_ref().get_descendant(crumbs).ok()?;
let id = node.ast_id?;
let index = *self.model.id_ports_map.borrow().get(&id)?;
self.model.port_models.borrow().get(index)?.frp.as_ref()?.tp.value()
pub fn port_type(&self, port: PortId) -> Option<Type> {
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()
}
_ => None,
}
}
/// Get the expression code for the specified port.
pub fn port_expression(&self, port: PortId) -> Option<String> {
match port {
PortId::Ast(id) => {
let index = *self.model.id_ports_map.borrow().get(&id)?;
let port_models = self.model.port_models.borrow();
let model = port_models.get(index)?;
let span = enso_text::Range::new(model.index, model.index + model.length);
Some(self.model.expression.borrow().code.as_ref()?[span].to_owned())
}
_ => None,
}
}
#[allow(missing_docs)] // FIXME[everyone] All pub functions should have docs.
pub fn whole_expr_id(&self) -> Option<ast::Id> {

View File

@ -11,6 +11,7 @@ use crate::view;
use crate::Type;
use enso_frp as frp;
use ensogl::control::io::mouse;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::shape::primitive::def::class::ShapeOps;
@ -23,7 +24,6 @@ use ensogl::display::shape::Rect;
use ensogl::display::shape::StyleWatch;
use ensogl::display::shape::StyleWatchFrp;
use ensogl::display::shape::Var;
use ensogl::gui::component;
use ensogl::gui::text;
use ensogl::Animation;
@ -408,13 +408,6 @@ impl PortShapeView {
set_padding_left (this,t:f32) { this.padding_left.set(t) }
set_padding_right (this,t:f32) { this.padding_right.set(t) }
}
fn events(&self) -> &component::PointerTarget_DEPRECATED {
match self {
Self::Single(t) => &t.events_deprecated,
Self::Multi(t) => &t.events_deprecated,
}
}
}
impl display::Object for PortShapeView {
@ -513,7 +506,6 @@ impl Model {
) {
let frp = Frp::new();
let network = &frp.network;
let events = shape.events();
let opacity = Animation::<f32>::new(network);
let color = color::Animation::new(network);
let type_label_opacity = Animation::<f32>::new(network);
@ -527,14 +519,19 @@ impl Model {
// === Mouse Event Handling ===
frp.source.on_hover <+ bool(&events.mouse_out,&events.mouse_over);
frp.source.on_press <+ events.mouse_down_primary;
let mouse_down = shape.on_event::<mouse::Down>();
let mouse_out = shape.on_event::<mouse::Out>();
let mouse_over = shape.on_event::<mouse::Over>();
mouse_down_primary <- mouse_down.filter(mouse::is_primary);
frp.source.on_hover <+ bool(&mouse_out,&mouse_over);
frp.source.on_press <+ mouse_down_primary.constant(());
// === Opacity ===
opacity.target <+ events.mouse_over.constant(PORT_OPACITY_HOVERED);
opacity.target <+ events.mouse_out.constant(PORT_OPACITY_NOT_HOVERED);
opacity.target <+ mouse_over.constant(PORT_OPACITY_HOVERED);
opacity.target <+ mouse_out.constant(PORT_OPACITY_NOT_HOVERED);
eval opacity.value ((t) shape.set_opacity(*t));

View File

@ -11,7 +11,7 @@ use crate::Frp;
// =============================
/// Initialise the FRP logic for the execution environment selector.
pub fn init_frp(frp: &Frp, model: &GraphEditorModelWithNetwork) {
pub fn init_frp(frp: &Frp, model: &Rc<GraphEditorModel>) {
let out = &frp.private.output;
let network = frp.network();
let inputs = &frp.private.input;

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ use crate::component::node;
use crate::new_node_position::free_place_finder::find_free_place;
use crate::new_node_position::free_place_finder::OccupiedArea;
use crate::selection::BoundingBox;
use crate::EdgeId;
use crate::GraphEditorModel;
use crate::Node;
use crate::NodeId;
@ -69,8 +68,8 @@ pub fn new_node_position(
let pos = on_ray(graph_editor, screen_center, Vector2(0.0, -1.0)).unwrap();
magnet_alignment(graph_editor, pos, HorizontallyAndVertically)
}
DroppingEdge { edge_id } =>
at_mouse_aligned_to_source_node(graph_editor, *edge_id, mouse_position),
DroppingEdge { endpoint } =>
at_mouse_aligned_to_source_node(graph_editor, endpoint.node_id, mouse_position),
StartCreationFromPortEvent { endpoint } => under(graph_editor, endpoint.node_id),
}
}
@ -150,11 +149,10 @@ pub fn at_mouse_aligned_to_close_nodes(
/// To learn more about the align algorithm, see the docs of [`aligned_if_close_to_node`].
pub fn at_mouse_aligned_to_source_node(
graph_editor: &GraphEditorModel,
edge_id: EdgeId,
node_id: NodeId,
mouse_position: Vector2,
) -> Vector2 {
let source_node_id = graph_editor.edge_source_node_id(edge_id);
let source_node = source_node_id.and_then(|id| graph_editor.nodes.get_cloned_ref(&id));
let source_node = graph_editor.nodes.get_cloned_ref(&node_id);
aligned_if_close_to_node(graph_editor, mouse_position, source_node)
}

View File

@ -243,50 +243,65 @@ fn get_nodes_in_bounding_box(bounding_box: &BoundingBox, nodes: &Nodes) -> Vec<N
/// Return an FRP endpoint that indicates the current selection mode. This method sets up the logic
/// for deriving the selection mode from the graph editor FRP.
pub fn get_mode(network: &frp::Network, editor: &crate::Frp) -> frp::stream::Stream<Mode> {
frp::extend! { network
let multi_select_flag = crate::enable_disable_toggle
( network
, &editor.enable_node_multi_select
, &editor.disable_node_multi_select
, &editor.toggle_node_multi_select
);
let merge_select_flag = crate::enable_disable_toggle
( network
, &editor.enable_node_merge_select
, &editor.disable_node_merge_select
, &editor.toggle_node_merge_select
);
let subtract_select_flag = crate::enable_disable_toggle
( network
, &editor.enable_node_subtract_select
, &editor.disable_node_subtract_select
, &editor.toggle_node_subtract_select
);
let inverse_select_flag = crate::enable_disable_toggle
( network
, &editor.enable_node_inverse_select
, &editor.disable_node_inverse_select
, &editor.toggle_node_inverse_select
);
selection_mode <- all_with4
(&multi_select_flag,&merge_select_flag,&subtract_select_flag,&inverse_select_flag,
|multi,merge,subtract,inverse| {
if *multi { Mode::Multi }
else if *merge { Mode::Merge }
else if *subtract { Mode::Subtract }
else if *inverse { Mode::Inverse }
else { Mode::Normal }
}
let multi_select_flag = enable_disable_toggle(
network,
&editor.enable_node_multi_select,
&editor.disable_node_multi_select,
&editor.toggle_node_multi_select,
);
let merge_select_flag = enable_disable_toggle(
network,
&editor.enable_node_merge_select,
&editor.disable_node_merge_select,
&editor.toggle_node_merge_select,
);
let subtract_select_flag = enable_disable_toggle(
network,
&editor.enable_node_subtract_select,
&editor.disable_node_subtract_select,
&editor.toggle_node_subtract_select,
);
let inverse_select_flag = enable_disable_toggle(
network,
&editor.enable_node_inverse_select,
&editor.disable_node_inverse_select,
&editor.toggle_node_inverse_select,
);
frp::extend! { network
selection_mode <- all_with4
(&multi_select_flag,&merge_select_flag,&subtract_select_flag,&inverse_select_flag,
|multi,merge,subtract,inverse| {
if *multi { Mode::Multi }
else if *merge { Mode::Merge }
else if *subtract { Mode::Subtract }
else if *inverse { Mode::Inverse }
else { Mode::Normal }
}
);
}
selection_mode
}
/// Return the toggle status of the given enable/disable/toggle inputs as a stream of booleans.
fn enable_disable_toggle(
network: &frp::Network,
enable: &frp::Any,
disable: &frp::Any,
toggle: &frp::Any,
) -> frp::Stream<bool> {
frp::extend! { network
out <- any(...);
let on_toggle = network.map2("on_toggle", toggle, &out, |_,t| !t);
let on_enable = network.to_true("on_enable", enable);
let on_disable = network.to_false("on_disable", disable);
out <+ on_toggle;
out <+ on_enable;
out <+ on_disable;
}
out.into()
}
// ==================
@ -296,7 +311,7 @@ pub fn get_mode(network: &frp::Network, editor: &crate::Frp) -> frp::stream::Str
/// Selection Controller that handles the logic for selecting and deselecting nodes in the graph
/// editor.
#[derive(Debug, Clone, CloneRef)]
pub struct Controller {
pub(super) struct Controller {
network: frp::Network,
cursor_selection_nodes: node_set::Set,
@ -307,7 +322,7 @@ pub struct Controller {
}
impl Controller {
pub fn new(
pub(super) fn new(
editor: &crate::Frp,
cursor: &Cursor,
mouse: &frp::io::Mouse_DEPRECATED,
@ -415,7 +430,7 @@ impl Controller {
// === Single Node Selection Box & Mouse IO ===
should_not_select <- edit_mode || editor.output.some_edge_endpoints_unset;
should_not_select <- edit_mode || editor.output.has_detached_edge;
node_to_select_non_edit <- touch.nodes.selected.gate_not(&should_not_select);
node_to_select_edit <- touch.nodes.down.gate(&edit_mode);
node_to_select <- any(node_to_select_non_edit,

View File

@ -91,8 +91,6 @@ pub const SHORTCUTS: &[(ensogl::application::shortcut::ActionType, &str, &str, &
(Release, "!read_only", "cmd", "edit_mode_off"),
(Press, "!read_only", "cmd left-mouse-button", "edit_mode_on"),
(Release, "!read_only", "cmd left-mouse-button", "edit_mode_off"),
// === Profiling Mode ===
(Press, "", "cmd p", "toggle_profiling_mode"),
// === Debug ===
(Press, "debug_mode", "ctrl d", "debug_set_test_visualization_data_for_selected_node"),
(Press, "debug_mode", "ctrl shift enter", "debug_push_breadcrumb"),

View File

@ -212,24 +212,18 @@ impl Model {
}
fn searcher_anchor_next_to_node(&self, node_id: NodeId) -> Vector2<f32> {
if let Some(node) = self.graph_editor.nodes().get_cloned_ref(&node_id) {
node.position().xy()
} else {
error!("Trying to show searcher under non existing node");
default()
}
self.graph_editor.model.with_node(node_id, |node| node.position().xy()).unwrap_or_default()
}
fn show_fullscreen_visualization(&self, node_id: NodeId) {
let node = self.graph_editor.nodes().get_cloned_ref(&node_id);
if let Some(node) = node {
self.graph_editor.model.with_node(node_id, |node| {
let visualization =
node.view.model().visualization.fullscreen_visualization().clone_ref();
self.display_object.remove_child(&*self.graph_editor);
self.display_object.remove_child(&self.project_view_top_bar);
self.display_object.add_child(&visualization);
*self.fullscreen_vis.borrow_mut() = Some(visualization);
}
});
}
fn hide_fullscreen_visualization(&self) {

View File

@ -30,15 +30,15 @@ async fn create_new_project_and_add_nodes() {
let test = Fixture::setup_new_project().await;
let graph_editor = test.graph_editor();
assert_eq!(graph_editor.nodes().all.len(), 2);
assert_eq!(graph_editor.model.nodes.len(), 2);
let expect_node_added = graph_editor.node_added.next_event();
graph_editor.add_node();
let (added_node_id, source_node, _) = expect_node_added.expect();
assert_eq!(source_node, None);
assert_eq!(graph_editor.nodes().all.len(), 3);
assert_eq!(graph_editor.model.nodes.len(), 3);
let added_node =
graph_editor.nodes().get_cloned_ref(&added_node_id).expect("Added node is not added");
graph_editor.model.nodes.get_cloned_ref(&added_node_id).expect("Added node is not added");
assert_eq!(added_node.view.set_expression.value().code, "");
}
@ -129,7 +129,7 @@ async fn adding_node_with_add_node_button() {
let (first_node_id, node_source, first_node) =
add_node_with_add_node_button(&graph_editor, "1 + 1").await;
assert!(node_source.is_none());
assert_eq!(graph_editor.nodes().all.len(), INITIAL_NODE_COUNT + 1);
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 1);
let node_position = first_node.position();
assert!(
first_node.position().y < bottom_node_pos.y,
@ -137,20 +137,20 @@ async fn adding_node_with_add_node_button() {
);
// Selected node is used as a `source` node.
graph_editor.nodes().deselect_all();
graph_editor.nodes().select(first_node_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(first_node_id);
let (_, node_source, _) = add_node_with_add_node_button(&graph_editor, "+ 1").await;
assert_eq!(node_source, Some(NodeSource { node: first_node_id }));
assert_eq!(graph_editor.nodes().all.len(), INITIAL_NODE_COUNT + 2);
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 2);
// If there is a free space, the new node is created in the center of screen.
let camera = scene.layers.main.camera();
camera.update_xy(|pos| pos + Vector2(1000.0, 1000.0));
wait_a_frame().await;
graph_editor.nodes().deselect_all();
graph_editor.model.nodes.deselect_all();
let (node_id, node_source, _) = add_node_with_add_node_button(&graph_editor, "1").await;
assert!(node_source.is_none());
assert_eq!(graph_editor.nodes().all.len(), INITIAL_NODE_COUNT + 3);
assert_eq!(graph_editor.model.nodes.len(), INITIAL_NODE_COUNT + 3);
let node_position = graph_editor.model.get_node_position(node_id).expect(
"Node was not
added",
@ -185,7 +185,7 @@ async fn new_nodes_placement_with_nodes_selected() {
InitialNodes::obtain_from_graph_editor(&graph_editor);
// Scenario 1. Creating a new node with one node selected.
graph_editor.nodes().select(node_2_id);
graph_editor.model.nodes.select(node_2_id);
let (node_3_id, _, node_3) = add_node_with_add_node_button(&graph_editor, "+ 1").await;
assert_eq!(
node_3.position().x,
@ -194,8 +194,8 @@ async fn new_nodes_placement_with_nodes_selected() {
);
assert!(node_3.position().y < node_2.position().y, "New node is not below the selected one.");
graph_editor.nodes().deselect_all();
graph_editor.nodes().select(node_2_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_2_id);
let (node_4_id, _, node_4) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
node_4.position().y,
@ -209,13 +209,13 @@ async fn new_nodes_placement_with_nodes_selected() {
graph_editor.remove_node(node_3_id);
graph_editor.remove_node(node_4_id);
graph_editor.nodes().deselect_all();
graph_editor.model.nodes.deselect_all();
// Scenario 2. Creating a new node with multiple nodes selected.
node_1.set_position(Vector3(-100.0, 0.0, 0.0));
wait_a_frame().await;
graph_editor.nodes().select(node_1_id);
graph_editor.nodes().select(node_2_id);
graph_editor.model.nodes.select(node_1_id);
graph_editor.model.nodes.select(node_2_id);
let (.., node_5) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
@ -225,9 +225,9 @@ async fn new_nodes_placement_with_nodes_selected() {
);
assert!(node_5.position().y < node_2.position().y, "New node is not below the bottom node.");
graph_editor.nodes().deselect_all();
graph_editor.nodes().select(node_1_id);
graph_editor.nodes().select(node_2_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_1_id);
graph_editor.model.nodes.select(node_2_id);
let (node_6_id, _, node_6) = add_node_with_shortcut(&graph_editor, "+ 1").await;
assert_eq!(
@ -241,13 +241,13 @@ async fn new_nodes_placement_with_nodes_selected() {
);
// Scenario 3. Creating a new node with enabled visualization.
graph_editor.nodes().deselect_all();
graph_editor.nodes().select(node_6_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_6_id);
let (node_7_id, _, node_7) = add_node_with_shortcut(&graph_editor, "+ 1").await;
let pos_without_visualization = node_7.position().y;
graph_editor.remove_node(node_7_id);
graph_editor.nodes().deselect_all();
graph_editor.nodes().select(node_6_id);
graph_editor.model.nodes.deselect_all();
graph_editor.model.nodes.select(node_6_id);
node_6.enable_visualization();
wait_a_frame().await;
let (.., node_7) = add_node_with_shortcut(&graph_editor, "+ 1").await;
@ -282,7 +282,7 @@ async fn mouse_oriented_node_placement() {
self.graph_editor.model.get_node_position(new_node_id).map(|v| v.xy());
assert_eq!(new_node_pos, Some(self.expected_position));
self.graph_editor.stop_editing();
assert_eq!(self.graph_editor.nodes().all.len(), 2);
assert_eq!(self.graph_editor.model.nodes.len(), 2);
}
fn check_tab_key(&self) {
@ -302,7 +302,7 @@ async fn mouse_oriented_node_placement() {
"No detached edge after clicking port"
);
let added_node = self.graph_editor.node_added.next_event();
self.scene.mouse.click_on_background();
self.scene.mouse.click_on_background(Vector2::zero());
enso_web::simulate_sleep((enso_shortcuts::DOUBLE_EVENT_TIME_MS + 10.0) as f64);
self.check_searcher_opening_place(added_node);
}

View File

@ -794,8 +794,16 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
let shape = scene.frp.shape.value();
let clip_space_z = origin_clip_space.z;
let clip_space_x = origin_clip_space.w * 2.0 * screen_pos.x / shape.width;
let clip_space_y = origin_clip_space.w * 2.0 * screen_pos.y / shape.height;
let clip_space_x = if shape.width.is_finite() && shape.width != 0.0 {
origin_clip_space.w * 2.0 * screen_pos.x / shape.width
} else {
0.0
};
let clip_space_y = if shape.height.is_finite() && shape.height != 0.0 {
origin_clip_space.w * 2.0 * screen_pos.y / shape.height
} else {
0.0
};
let clip_space = Vector4(clip_space_x, clip_space_y, clip_space_z, origin_clip_space.w);
let world_space = camera.inversed_view_projection_matrix() * clip_space;
(inv_object_matrix * world_space).xy()

View File

@ -369,10 +369,8 @@ impl ScrollArea {
hovering <- hovering.sampler();
let on_scroll = model.display_object.on_event::<mouse::Wheel>();
on_scroll_when_hovering <- on_scroll.gate(&hovering);
model.h_scrollbar.scroll_by <+ on_scroll_when_hovering
.map(|event| event.delta_x() as f32);
model.v_scrollbar.scroll_by <+ on_scroll_when_hovering
.map(|event| event.delta_y() as f32);
model.h_scrollbar.scroll_by <+ on_scroll_when_hovering.map(|event| event.delta_x());
model.v_scrollbar.scroll_by <+ on_scroll_when_hovering.map(|event| event.delta_y());
}

View File

@ -487,3 +487,28 @@ impl Default for FixedFrameRateSampler {
Self::new(60.0)
}
}
// ==================
// === Test Utils ===
// ==================
/// Test-specific API.
pub mod test_utils {
use super::*;
use std::sync::Mutex;
static FRAME_TIME: Mutex<Cell<f64>> = Mutex::new(Cell::new(0.0));
const FRAME_TIME_STEP: f64 = 1000.0 / 60.0;
/// Run single animation loop frame and flush all queued tasks.
pub fn next_frame() {
LOOP_REGISTRY.with(|registry| {
frp::microtasks::flush_microtasks();
let time = FRAME_TIME.lock().unwrap().update(|t| t + FRAME_TIME_STEP);
(registry.animation_loop.data.borrow_mut().on_frame)((time as f32).ms());
frp::microtasks::flush_microtasks();
});
}
}

View File

@ -140,8 +140,11 @@ impl display::Object for Application {
pub mod test_utils {
use super::*;
/// Screen size for unit and integration tests.
const TEST_SCREEN_SIZE: (f32, f32) = (1920.0, 1080.0);
use crate::system::web::dom::Shape;
/// Screen shape for unit and integration tests.
pub const TEST_SCREEN_SHAPE: Shape =
Shape { width: 1920.0, height: 1080.0, pixel_ratio: 1.5 };
/// Extended API for tests.
pub trait ApplicationExt {
@ -152,14 +155,8 @@ pub mod test_utils {
impl ApplicationExt for Application {
fn set_screen_size_for_tests(&self) {
let (screen_width, screen_height) = TEST_SCREEN_SIZE;
let scene = &self.display.default_scene;
scene.layers.iter_sublayers_and_masks_nested(|layer| {
let camera = layer.camera();
camera.set_screen(screen_width, screen_height);
camera.reset_zoom();
camera.update(scene);
});
scene.dom.root.override_shape(TEST_SCREEN_SHAPE);
}
}
}

View File

@ -6,6 +6,7 @@ use web::traits::*;
use crate::system::web;
use enso_frp::io::mouse;
use std::borrow::Borrow;
use web::dom::Shape;
@ -17,8 +18,9 @@ use web::dom::Shape;
/// Mouse event wrapper.
#[derive(Clone, Derivative)]
#[derivative(Default(bound = ""))]
pub struct Event<EventType, JsEvent> {
pub struct Event<EventType, JsEvent: ToEventData> {
js_event: Option<JsEvent>,
data: JsEvent::Data,
shape: Shape,
event_type: ZST<EventType>,
}
@ -26,13 +28,12 @@ pub struct Event<EventType, JsEvent> {
impl<EventType, JsEvent> Debug for Event<EventType, JsEvent>
where
EventType: TypeDisplay,
JsEvent: AsRef<web::MouseEvent>,
JsEvent: ToEventData,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(&type_display::<EventType>())
.field("button", &self.button())
.field("client_x", &self.client_x())
.field("client_y", &self.client_y())
.field("data", &self.data)
.field("shape", &self.shape)
.finish()
}
}
@ -42,7 +43,7 @@ where
pub trait IsEvent {
type PhantomType;
}
impl<EventType, JsEvent> IsEvent for Event<EventType, JsEvent> {
impl<EventType, JsEvent: ToEventData> IsEvent for Event<EventType, JsEvent> {
type PhantomType = EventType;
}
@ -50,22 +51,37 @@ impl<EventType, JsEvent> IsEvent for Event<EventType, JsEvent> {
pub type EventPhantomType<T> = <T as IsEvent>::PhantomType;
impl<EventType, JsEvent> Event<EventType, JsEvent>
where JsEvent: AsRef<web::MouseEvent>
where JsEvent: ToEventData
{
/// Constructor.
pub fn new(js_event: JsEvent, shape: Shape) -> Self {
let js_event = Some(js_event);
let event_type = default();
Self { js_event, shape, event_type }
Self {
data: js_event.to_data(shape),
js_event: Some(js_event),
shape,
event_type: default(),
}
}
/// Constructor for simulated event. Those events have provided data, but no actual JS event
/// object. This is useful for testing.
pub fn simulated(data: JsEvent::Data, shape: Shape) -> Self {
Self { data, js_event: None, shape, event_type: default() }
}
}
impl<EventType, JsEvent> Event<EventType, JsEvent>
where
JsEvent: ToEventData + AsRef<web::Event>,
JsEvent::Data: Borrow<MouseEventData>,
{
/// The horizontal coordinate within the application's viewport at which the event occurred (as
/// opposed to the coordinate within the page).
///
/// For example, clicking on the left edge of the viewport will always result in a mouse event
/// with a [`client_x`] value of 0, regardless of whether the page is scrolled horizontally.
pub fn client_x(&self) -> i32 {
self.js_event.as_ref().map(|t| t.as_ref().client_x()).unwrap_or_default()
pub fn client_x(&self) -> f32 {
self.client().x
}
/// The vertical coordinate within the application's viewport at which the event occurred (as
@ -73,9 +89,8 @@ where JsEvent: AsRef<web::MouseEvent>
///
/// For example, clicking on the bottom edge of the viewport will always result in a mouse event
/// with a [`client_y`] value of 0, regardless of whether the page is scrolled horizontally.
pub fn client_y(&self) -> i32 {
self.shape.height as i32
- self.js_event.as_ref().map(|t| t.as_ref().client_y()).unwrap_or_default()
pub fn client_y(&self) -> f32 {
self.client().y
}
/// The coordinate within the application's viewport at which the event occurred (as opposed to
@ -83,60 +98,57 @@ where JsEvent: AsRef<web::MouseEvent>
///
/// For example, clicking on the bottom edge of the viewport will always result in a mouse event
/// with a [`client`] value of (0,0), regardless of whether the page is scrolled horizontally.
pub fn client(&self) -> Vector2<i32> {
Vector2(self.client_x(), self.client_y())
pub fn client(&self) -> Vector2 {
self.data.borrow().client
}
/// The coordinate within the application's viewport at which the event occurred (as opposed to
/// the coordinate within the page), measured from the center of the viewport.
pub fn client_centered(&self) -> Vector2 {
let x = self.client_x() as f32 - self.shape.width / 2.0;
let y = self.client_y() as f32 - self.shape.height / 2.0;
let x = self.client_x() - self.shape.width / 2.0;
let y = self.client_y() - self.shape.height / 2.0;
Vector2(x, y)
}
/// The horizontal coordinate (offset) of the mouse pointer in global (screen) coordinates.
pub fn screen_x(&self) -> f32 {
self.js_event.as_ref().map(|t| t.as_ref().screen_x()).unwrap_or_default() as f32
self.screen().x
}
/// The vertical coordinate (offset) of the mouse pointer in global (screen) coordinates.
pub fn screen_y(&self) -> f32 {
self.shape.height
- self.js_event.as_ref().map(|t| t.as_ref().screen_y()).unwrap_or_default() as f32
self.screen().y
}
/// The coordinate (offset) of the mouse pointer in global (screen) coordinates.
pub fn screen(&self) -> Vector2 {
Vector2(self.screen_x(), self.screen_y())
self.data.borrow().screen
}
/// The difference in the X coordinate of the mouse pointer between the given event and the
/// previous mousemove event. In other words, the value of the property is computed like this:
/// `current_event.movement_x = current_event.screen_x() - previous_event.screen_x()`.
pub fn movement_x(&self) -> i32 {
self.js_event.as_ref().map(|t| t.as_ref().movement_x()).unwrap_or_default()
pub fn movement_x(&self) -> f32 {
self.movement().x
}
/// The difference in the Y coordinate of the mouse pointer between the given event and the
/// previous mousemove event. In other words, the value of the property is computed like this:
/// `current_event.movement_y = current_event.screen_y() - previous_event.screen_y()`.
pub fn movement_y(&self) -> i32 {
-self.js_event.as_ref().map(|t| t.as_ref().movement_y()).unwrap_or_default()
pub fn movement_y(&self) -> f32 {
self.movement().y
}
/// The difference in the coordinate of the mouse pointer between the given event and the
/// previous mousemove event. In other words, the value of the property is computed like this:
/// `current_event.movement = current_event.screen() - previous_event.screen()`.
pub fn movement(&self) -> Vector2<i32> {
Vector2(self.movement_x(), self.movement_y())
pub fn movement(&self) -> Vector2 {
self.data.borrow().movement
}
/// Indicates which button was pressed on the mouse to trigger the event.
pub fn button(&self) -> mouse::Button {
mouse::Button::from_code(
self.js_event.as_ref().map(|t| t.as_ref().button().into()).unwrap_or_default(),
)
self.data.borrow().button
}
/// Return the position relative to the event handler that was used to catch the event. If the
@ -144,11 +156,11 @@ where JsEvent: AsRef<web::MouseEvent>
/// the viewport. This can happen if the event handler is, for example, the window.
///
/// Note: may cause reflow of the JS layout.
pub fn position_relative_to_event_handler(&self) -> Vector2<f32> {
pub fn position_relative_to_event_handler(&self) -> Vector2 {
if let Some(element) = self.try_get_current_target_element() {
self.relative_position_with_reflow(&element)
} else {
Vector2::new(self.client_x() as f32, self.client_y() as f32)
Vector2::new(self.client_x(), self.client_y())
}
}
@ -162,16 +174,16 @@ where JsEvent: AsRef<web::MouseEvent>
/// Return the position relative to the given element.
///
/// Note: causes reflow of the JS layout.
pub fn relative_position_with_reflow(&self, element: &web::Element) -> Vector2<f32> {
pub fn relative_position_with_reflow(&self, element: &web::Element) -> Vector2 {
let rect = element.get_bounding_client_rect();
let x = self.client_x() as f64 - rect.left();
let y = self.client_y() as f64 - rect.top();
Vector2::new(x as f32, y as f32)
let x = self.client_x() - rect.left() as f32;
let y = self.client_y() - rect.top() as f32;
Vector2(x, y)
}
/// Check whether the `ctrl` key was pressed when the event was triggered.
pub fn ctrl_key(&self) -> bool {
self.js_event.as_ref().map(|t| t.as_ref().ctrl_key()).unwrap_or_default()
self.data.borrow().ctrl_key
}
/// Prevent the default action of the event.
@ -184,9 +196,10 @@ where JsEvent: AsRef<web::MouseEvent>
self,
) -> Event<EventPhantomType<NewEventType>, JsEvent> {
let js_event = self.js_event;
let data = self.data;
let shape = self.shape;
let event_type = default();
Event { js_event, shape, event_type }
Event { js_event, data, shape, event_type }
}
}
@ -236,6 +249,79 @@ macro_rules! define_events {
}};
}
/// An JS event type that is convertible to a light copyable struct containing its associated data.
#[allow(missing_docs)]
pub trait ToEventData {
type Data: Default + Debug + Copy;
fn to_data(&self, shape: Shape) -> Self::Data;
}
/// The data associated with a mouse event. In web environment, it is derived from the mouse event
/// itself. In test environment, it is passed as a parameter in order to simulate a particular
/// mouse event.
#[derive(Copy, Clone, Debug, Default)]
pub struct MouseEventData {
/// Mouse client position. See [`Event<EventType,JsEvent>::client()`].
pub client: Vector2,
/// Mouse screen position. See [`Event<EventType,JsEvent>::screen()`].
pub screen: Vector2,
/// Mouse movement. See [`Event<EventType,JsEvent>::movement()`].
pub movement: Vector2,
/// See [`Event<EventType,JsEvent>::button()`].
pub button: mouse::Button,
/// See [`Event<EventType,JsEvent>::ctrl_key()`].
pub ctrl_key: bool,
}
impl MouseEventData {
/// Convenience constructor for primary mouse button events. Used in testing.
pub fn primary_at(pos: Vector2) -> Self {
Self { client: pos, screen: pos, ..default() }
}
}
/// The data associated with a mouse wheel event. See [`MouseEventData`] for more information.
#[derive(Copy, Clone, Debug, Default, Deref)]
pub struct WheelEventData {
/// Each wheel event is also a mouse event, therefore it also contains the mouse event data.
#[deref]
pub base: MouseEventData,
/// The amount of scrolling that was performed in the x and y direction. The unit is not
/// specified and browser dependent, but the sign will always match the direction of scroll.
pub delta: Vector2,
}
impl Borrow<MouseEventData> for WheelEventData {
fn borrow(&self) -> &MouseEventData {
&self.base
}
}
impl ToEventData for web::MouseEvent {
type Data = MouseEventData;
fn to_data(&self, shape: Shape) -> Self::Data {
MouseEventData {
client: Vector2(self.client_x() as f32, shape.height - self.client_y() as f32),
screen: Vector2(self.screen_x() as f32, shape.height - self.screen_y() as f32),
movement: Vector2(self.movement_x() as f32, -self.movement_y() as f32),
button: mouse::Button::from_code(self.button().into()),
ctrl_key: self.ctrl_key(),
}
}
}
impl ToEventData for web::WheelEvent {
type Data = WheelEventData;
fn to_data(&self, shape: Shape) -> Self::Data {
WheelEventData {
base: web::MouseEvent::to_data(self, shape),
delta: Vector2(self.delta_x() as f32, self.delta_y() as f32),
}
}
}
define_events! {
// ======================
// === JS-like Events ===
@ -330,12 +416,12 @@ define_events! {
impl Wheel {
/// The horizontal scroll amount.
pub fn delta_x(&self) -> f64 {
self.js_event.as_ref().map(|t| t.delta_x()).unwrap_or_default()
pub fn delta_x(&self) -> f32 {
self.data.delta.x
}
/// The vertical scroll amount.
pub fn delta_y(&self) -> f64 {
self.js_event.as_ref().map(|t| t.delta_y()).unwrap_or_default()
pub fn delta_y(&self) -> f32 {
self.data.delta.y
}
}

View File

@ -35,7 +35,11 @@ impl Screen {
/// Get Screen's aspect ratio.
pub fn aspect(self) -> f32 {
self.width / self.height
if self.height.is_finite() && self.width.is_finite() && self.height != 0.0 {
self.width / self.height
} else {
1.0
}
}
/// Check whether the screen size is zero or negative.

View File

@ -245,14 +245,14 @@ impl NavigatorEvents {
event.prevent_default();
let position = data.mouse_position();
let zoom_speed = data.zoom_speed();
let movement = Vector2::new(event.delta_x() as f32, -event.delta_y() as f32);
let movement = Vector2::new(event.delta_x(), -event.delta_y());
let amount = movement_to_zoom(movement);
if let Some(event) = ZoomEvent::new(position, amount, zoom_speed) {
data.on_zoom(event);
}
} else if data.is_wheel_panning_enabled() {
let x = -event.delta_x() as f32;
let y = event.delta_y() as f32;
let x = -event.delta_x();
let y = event.delta_y();
let pan_speed = data.pan_speed();
let movement = Vector2::new(x, y) * pan_speed;
let pan_event = PanEvent::new(movement);

View File

@ -129,35 +129,39 @@ impl PointerTargetRegistry {
#[derive(Clone, CloneRef, Debug)]
pub struct Mouse {
pub mouse_manager: MouseManager,
pub last_position: Rc<Cell<Vector2<i32>>>,
pub position: Uniform<Vector2<i32>>,
pub click_count: Uniform<i32>,
pub mouse_manager: MouseManager,
pub pointer_target_registry: PointerTargetRegistry,
pub last_position: Rc<Cell<Vector2>>,
pub position: Uniform<Vector2<i32>>,
pub click_count: Uniform<i32>,
/// The encoded value of pointer target ID. The offscreen canvas containing the encoded IDs is
/// sampled and the most recent sample is stored here. Please note, that this sample may be
/// from a few frames back, as it is not guaranteed when we will receive the data from GPU.
pub pointer_target_encoded: Uniform<Vector4<u32>>,
pub target: Rc<Cell<PointerTargetId>>,
pub handles: Rc<[callback::Handle; 6]>,
pub scene_frp: Frp,
pub pointer_target_encoded: Uniform<Vector4<u32>>,
pub target: Rc<Cell<PointerTargetId>>,
pub handles: Rc<[callback::Handle; 6]>,
pub scene_frp: Frp,
/// Stored in order to be converted to [`mouse::Over`], [`mouse::Out`], [`mouse::Enter`], and
/// [`mouse::Leave`] when the mouse enters or leaves an element.
pub last_move_event: Rc<RefCell<Option<mouse::Move>>>,
pub last_move_event: Rc<RefCell<Option<mouse::Move>>>,
/// # Deprecated
/// This API is deprecated. Instead, use the display object's event API. For example, to get an
/// FRP endpoint for mouse event, you can use the [`crate::display::Object::on_event`]
/// function.
pub frp_deprecated: enso_frp::io::Mouse_DEPRECATED,
pub frp_deprecated: enso_frp::io::Mouse_DEPRECATED,
pub background: PointerTarget_DEPRECATED,
}
impl Mouse {
pub fn new(
scene_frp: &Frp,
scene_object: &display::object::Instance,
root: &web::dom::WithKnownShape<web::HtmlDivElement>,
variables: &UniformScope,
display_mode: &Rc<Cell<glsl::codes::DisplayModes>>,
pointer_target_registry: &PointerTargetRegistry,
) -> Self {
let background = PointerTarget_DEPRECATED::new();
let pointer_target_registry = PointerTargetRegistry::new(&background, scene_object);
let last_pressed_elem: Rc<RefCell<HashMap<mouse::Button, PointerTargetId>>> = default();
let scene_frp = scene_frp.clone_ref();
@ -177,16 +181,14 @@ impl Mouse {
last_move_event.borrow_mut().replace(event.clone());
let shape = scene_frp.shape.value();
let pixel_ratio = shape.pixel_ratio;
let screen_x = event.client_x();
let screen_y = event.client_y();
let new_pos = Vector2::new(screen_x,screen_y);
let new_pos = event.client();
let pos_changed = new_pos != last_position.get();
if pos_changed {
last_position.set(new_pos);
let new_canvas_position = new_pos.map(|v| (v as f32 * pixel_ratio) as i32);
let new_canvas_position = new_pos.map(|v| (v * pixel_ratio) as i32);
position.set(new_canvas_position);
let position_bottom_left = Vector2(new_pos.x as f32, new_pos.y as f32);
let position_top_left = Vector2(new_pos.x as f32, shape.height - new_pos.y as f32);
let position_bottom_left = new_pos;
let position_top_left = Vector2(new_pos.x, shape.height - new_pos.y);
let position = position_bottom_left - shape.center();
frp_deprecated.position_bottom_left.emit(position_bottom_left);
frp_deprecated.position_top_left.emit(position_top_left);
@ -256,6 +258,7 @@ impl Mouse {
let handles = Rc::new([on_move, on_down, on_up, on_wheel, on_leave, on_enter]);
Self {
pointer_target_registry,
mouse_manager,
last_position,
position,
@ -266,6 +269,48 @@ impl Mouse {
frp_deprecated,
scene_frp,
last_move_event,
background,
}
}
/// Discover what object the mouse pointer is on.
fn handle_over_and_out_events(&self) {
let opt_new_target = PointerTargetId::decode_from_rgba(self.pointer_target_encoded.get());
let new_target = opt_new_target.unwrap_or_else(|err| {
error!("{err}");
default()
});
self.switch_target(new_target);
}
/// Set mouse target and emit hover events if necessary.
fn switch_target(&self, new_target: PointerTargetId) {
let current_target = self.target.get();
if new_target != current_target {
self.target.set(new_target);
if let Some(event) = (*self.last_move_event.borrow()).clone() {
self.pointer_target_registry.with_mouse_target(current_target, |t, d| {
t.mouse_out.emit(());
let out_event = event.clone().unchecked_convert_to::<mouse::Out>();
let leave_event = event.clone().unchecked_convert_to::<mouse::Leave>();
d.emit_event(out_event);
d.emit_event_without_bubbling(leave_event);
});
self.pointer_target_registry.with_mouse_target(new_target, |t, d| {
t.mouse_over.emit(());
let over_event = event.clone().unchecked_convert_to::<mouse::Over>();
let enter_event = event.clone().unchecked_convert_to::<mouse::Enter>();
d.emit_event(over_event);
d.emit_event_without_bubbling(enter_event);
});
// Re-emitting position event. See the docs of [`re_emit_position_event`] to learn
// why.
self.pointer_target_registry.with_mouse_target(new_target, |_, d| {
d.emit_event(event.clone());
});
self.re_emit_position_event();
}
}
}
@ -292,7 +337,7 @@ impl Mouse {
pub fn re_emit_position_event(&self) {
let shape = self.scene_frp.shape.value();
let new_pos = self.last_position.get();
let position = Vector2(new_pos.x as f32, new_pos.y as f32) - shape.center();
let position = new_pos - shape.center();
self.frp_deprecated.position.emit(position);
}
}
@ -832,8 +877,6 @@ pub struct SceneData {
pub mouse: Mouse,
pub keyboard: Keyboard,
pub uniforms: Uniforms,
pub background: PointerTarget_DEPRECATED,
pub pointer_target_registry: PointerTargetRegistry,
pub stats: Stats,
pub dirty: Dirty,
pub renderer: Renderer,
@ -866,14 +909,11 @@ impl SceneData {
let dirty = Dirty::new(on_mut);
let layers = world::with_context(|t| t.layers.clone_ref());
let stats = stats.clone();
let background = PointerTarget_DEPRECATED::new();
let pointer_target_registry = PointerTargetRegistry::new(&background, &display_object);
let uniforms = Uniforms::new(&variables);
let renderer = Renderer::new(&dom, &variables);
let style_sheet = world::with_context(|t| t.style_sheet.clone_ref());
let frp = Frp::new(&dom.root.shape);
let mouse =
Mouse::new(&frp, &dom.root, &variables, &display_mode, &pointer_target_registry);
let mouse = Mouse::new(&frp, &display_object, &dom.root, &variables, &display_mode);
let disable_context_menu = Rc::new(web::ignore_context_menu(&dom.root));
let keyboard = Keyboard::new(&web::window);
let network = &frp.network;
@ -905,8 +945,6 @@ impl SceneData {
mouse,
keyboard,
uniforms,
pointer_target_registry,
background,
stats,
dirty,
renderer,
@ -1047,7 +1085,11 @@ impl SceneData {
}
pub fn screen_to_scene_coordinates(&self, position: Vector3<f32>) -> Vector3<f32> {
let position = position / self.camera().zoom();
let zoom = self.camera().zoom();
if zoom == 0.0 {
return Vector3::zero();
}
let position = position / zoom;
let position = Vector4::new(position.x, position.y, position.z, 1.0);
(self.camera().inversed_view_matrix() * position).xyz()
}
@ -1091,43 +1133,6 @@ impl SceneData {
eval_ self.mouse.frp_deprecated.position (pointer_position_changed.set(true));
}
}
/// Discover what object the mouse pointer is on.
fn handle_mouse_over_and_out_events(&self) {
let opt_new_target =
PointerTargetId::decode_from_rgba(self.mouse.pointer_target_encoded.get());
let new_target = opt_new_target.unwrap_or_else(|err| {
error!("{err}");
default()
});
let current_target = self.mouse.target.get();
if new_target != current_target {
self.mouse.target.set(new_target);
if let Some(event) = (*self.mouse.last_move_event.borrow()).clone() {
self.pointer_target_registry.with_mouse_target(current_target, |t, d| {
t.mouse_out.emit(());
let out_event = event.clone().unchecked_convert_to::<mouse::Out>();
let leave_event = event.clone().unchecked_convert_to::<mouse::Leave>();
d.emit_event(out_event);
d.emit_event_without_bubbling(leave_event);
});
self.pointer_target_registry.with_mouse_target(new_target, |t, d| {
t.mouse_over.emit(());
let over_event = event.clone().unchecked_convert_to::<mouse::Over>();
let enter_event = event.clone().unchecked_convert_to::<mouse::Enter>();
d.emit_event(over_event);
d.emit_event_without_bubbling(enter_event);
});
// Re-emitting position event. See the docs of [`re_emit_position_event`] to learn
// why.
self.pointer_target_registry.with_mouse_target(new_target, |_, d| {
d.emit_event(event.clone());
});
self.mouse.re_emit_position_event();
}
}
}
}
@ -1315,21 +1320,17 @@ impl Scene {
/// during this frame.
#[profile(Debug)]
pub fn update_layout(&self, time: animation::TimeInfo) -> UpdateStatus {
if self.context.borrow().is_some() {
debug_span!("Early update.").in_scope(|| {
world::with_context(|t| t.theme_manager.update());
debug_span!("Early update.").in_scope(|| {
world::with_context(|t| t.theme_manager.update());
let mut scene_was_dirty = false;
self.frp.frame_time_source.emit(time.since_animation_loop_started.unchecked_raw());
// Please note that `update_camera` is called first as it may trigger FRP events
// which may change display objects layout.
scene_was_dirty |= self.update_camera(self);
self.display_object.update(self);
UpdateStatus { scene_was_dirty, pointer_position_changed: false }
})
} else {
default()
}
let mut scene_was_dirty = false;
self.frp.frame_time_source.emit(time.since_animation_loop_started.unchecked_raw());
// Please note that `update_camera` is called first as it may trigger FRP events
// which may change display objects layout.
scene_was_dirty |= self.update_camera(self);
self.display_object.update(self);
UpdateStatus { scene_was_dirty, pointer_position_changed: false }
})
}
/// Perform rendering phase of scene update. At this point, all display object state is being
@ -1341,30 +1342,26 @@ impl Scene {
time: animation::TimeInfo,
early_status: UpdateStatus,
) -> UpdateStatus {
if let Some(context) = &*self.context.borrow() {
debug_span!("Late update.").in_scope(|| {
let UpdateStatus { mut scene_was_dirty, mut pointer_position_changed } =
early_status;
scene_was_dirty |= self.layers.update();
scene_was_dirty |= self.update_shape();
debug_span!("Late update.").in_scope(|| {
let UpdateStatus { mut scene_was_dirty, mut pointer_position_changed } = early_status;
scene_was_dirty |= self.layers.update();
scene_was_dirty |= self.update_shape();
if let Some(context) = &*self.context.borrow() {
context.profiler.measure_data_upload(|| {
scene_was_dirty |= self.update_symbols();
});
self.handle_mouse_over_and_out_events();
self.mouse.handle_over_and_out_events();
scene_was_dirty |= self.shader_compiler.run(context, time);
}
pointer_position_changed |= self.pointer_position_changed.get();
self.pointer_position_changed.set(false);
pointer_position_changed |= self.pointer_position_changed.get();
self.pointer_position_changed.set(false);
// FIXME: setting it to true for now in order to make cursor blinking work.
// Text cursor animation is in GLSL. To be handled properly in this PR:
// #183406745
scene_was_dirty |= true;
UpdateStatus { scene_was_dirty, pointer_position_changed }
})
} else {
default()
}
// FIXME: setting it to true for now in order to make cursor blinking work.
// Text cursor animation is in GLSL. To be handled properly in this PR:
// #183406745
scene_was_dirty |= true;
UpdateStatus { scene_was_dirty, pointer_position_changed }
})
}
}
@ -1443,18 +1440,97 @@ enum TaskState {
/// Extended API for tests.
pub mod test_utils {
use super::*;
use display::shape::ShapeInstance;
use enso_callback::traits::*;
pub trait MouseExt {
/// Emulate click on background for testing purposes.
fn click_on_background(&self);
/// Set the pointer target and emit mouse enter/leave and over/out events.
fn set_hover_target(&self, target: PointerTargetId) -> &Self;
/// Simulate mouse down event.
fn emit_down(&self, event: mouse::Down) -> &Self;
/// Simulate mouse up event.
fn emit_up(&self, event: mouse::Up) -> &Self;
/// Simulate mouse move event.
fn emit_move(&self, event: mouse::Move) -> &Self;
/// Simulate mouse wheel event.
fn emit_wheel(&self, event: mouse::Wheel) -> &Self;
/// Get current screen shape.
fn screen_shape(&self) -> Shape;
/// Convert main camera scene position to needed event position in test environment.
fn scene_to_event_position(&self, scene_pos: Vector2) -> Vector2 {
scene_pos + self.screen_shape().center()
}
/// Simulate mouse move and hover with manually specified event data and target.
fn hover_raw(&self, data: mouse::MouseEventData, target: PointerTargetId) -> &Self {
self.emit_move(mouse::Move::simulated(data, self.screen_shape()))
.set_hover_target(target)
}
/// Simulate mouse hover and click with manually specified event data and target.
fn click_on_raw(&self, data: mouse::MouseEventData, target: PointerTargetId) -> &Self {
self.hover_raw(data, target)
.emit_down(mouse::Down::simulated(data, self.screen_shape()))
.emit_up(mouse::Up::simulated(data, self.screen_shape()))
}
/// Simulate mouse hover on a on given shape.
fn hover<S>(&self, instance: &ShapeInstance<S>, scene_pos: Vector2) -> &Self {
let pos = self.scene_to_event_position(scene_pos);
self.hover_raw(mouse::MouseEventData::primary_at(pos), PointerTargetId::Symbol {
id: instance.sprite.borrow().global_instance_id,
})
}
/// Simulate mouse hover on background.
fn hover_background(&self, scene_pos: Vector2) -> &Self {
let pos = self.scene_to_event_position(scene_pos);
self.hover_raw(mouse::MouseEventData::primary_at(pos), PointerTargetId::Background)
}
/// Simulate mouse hover and click on given shape.
fn click_on<S>(&self, instance: &ShapeInstance<S>, scene_pos: Vector2) {
let pos = self.scene_to_event_position(scene_pos);
self.click_on_raw(mouse::MouseEventData::primary_at(pos), PointerTargetId::Symbol {
id: instance.sprite.borrow().global_instance_id,
});
}
/// Simulate mouse click on background.
fn click_on_background(&self, scene_pos: Vector2) {
let pos = self.scene_to_event_position(scene_pos);
self.click_on_raw(mouse::MouseEventData::primary_at(pos), PointerTargetId::Background);
}
}
impl MouseExt for Mouse {
fn click_on_background(&self) {
self.target.set(PointerTargetId::Background);
let left_mouse_button = frp::io::mouse::Button::Button0;
self.frp_deprecated.down.emit(left_mouse_button);
self.frp_deprecated.up.emit(left_mouse_button);
fn set_hover_target(&self, target: PointerTargetId) -> &Self {
self.switch_target(target);
self
}
fn emit_down(&self, event: mouse::Down) -> &Self {
self.mouse_manager.on_down.run_all(&event);
self
}
fn emit_up(&self, event: mouse::Up) -> &Self {
self.mouse_manager.on_up.run_all(&event);
self
}
fn emit_move(&self, event: mouse::Move) -> &Self {
self.mouse_manager.on_move.run_all(&event);
self
}
fn emit_wheel(&self, event: mouse::Wheel) -> &Self {
self.mouse_manager.on_wheel.run_all(&event);
self
}
fn screen_shape(&self) -> Shape {
self.scene_frp.shape.value()
}
}
}

View File

@ -231,7 +231,7 @@ impl<S: Shape> ShapeViewModel<S> {
.and_then(|assignment| assignment.partition_id(shape_system))
.unwrap_or_default();
let (shape, instance) = layer.instantiate(&*self.data.borrow(), symbol_partition);
scene.pointer_target_registry.insert(
scene.mouse.pointer_target_registry.insert(
instance.global_instance_id,
self.events_deprecated.clone_ref(),
self.display_object(),
@ -247,7 +247,7 @@ impl<S: Shape> ShapeViewModel<S> {
fn unregister_existing_mouse_targets(&self) {
for global_instance_id in mem::take(&mut *self.pointer_targets.borrow_mut()) {
scene().pointer_target_registry.remove(global_instance_id);
scene().mouse.pointer_target_registry.remove(global_instance_id);
}
}
}

View File

@ -64,25 +64,13 @@ define_style! {
#[allow(missing_docs)]
impl Style {
pub fn new_highlight<H, Color: Into<color::Lcha>>(
host: H,
size: Vector2<f32>,
radius: f32,
color: Option<Color>,
) -> Self
where
H: display::Object,
{
let host = Some(StyleValue::new(host.display_object().clone_ref()));
pub fn new_highlight(host: display::object::Instance, size: Vector2<f32>, radius: f32) -> Self {
let host = Some(StyleValue::new(host));
let size = Some(StyleValue::new(size));
let radius = Some(StyleValue::new(radius));
let press = Some(StyleValue::new(0.0));
let color = color.map(|color| {
let color = color.into();
StyleValue::new(color)
});
let port_selection_layer = Some(StyleValue::new_no_animation(true));
Self { host, size, radius, color, port_selection_layer, press, ..default() }
Self { host, size, radius, port_selection_layer, press, ..default() }
}
pub fn new_color(color: color::Lcha) -> Self {
@ -541,10 +529,13 @@ impl Cursor {
// ╲│ z = camera.z
screen_position <- position.map(f!([model](position) {
let cam_pos = model.scene.layers.cursor.camera().position();
let coeff = cam_pos.z / (cam_pos.z - position.z);
let x = position.x * coeff;
let y = position.y * coeff;
Vector3(x,y,0.0)
let z_diff = cam_pos.z - position.z;
if z_diff == 0.0 {
Vector3::zero()
} else {
let coeff = cam_pos.z / z_diff;
(position.xy() * coeff).push(0.0)
}
}));
scene_position <- screen_position.map(f!((p) scene.screen_to_scene_coordinates(*p)));

View File

@ -141,6 +141,12 @@ impl<T> WithKnownShape<T> {
}
}
/// Treat this object as if id had the provided shape. Note that this function does not cause
/// the actual DOM object to change its shape. Useful for testing.
pub fn override_shape(&self, shape: Shape) {
self.shape_source.emit(shape);
}
/// Get the current shape of the object.
pub fn shape(&self) -> Shape {
self.shape.value()

View File

@ -20,9 +20,10 @@ use nalgebra::Vector2;
/// JS supports up to 5 mouse buttons currently:
/// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
/// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Button {
#[default]
Button0,
Button1,
Button2,
@ -82,12 +83,6 @@ impl Button {
}
}
impl Default for Button {
fn default() -> Self {
Self::Button0
}
}
// ==================

View File

@ -298,6 +298,28 @@ impl Network {
self.register(OwnedBatch::new(label, input))
}
/// Batch unique incoming events emitted within a single microtask. The batch will be emitted
/// after the current program execution finishes, but before returning to the event loop. All
/// emitted batches are guaranteed to be non-empty. Since the batch is emitted as a hash set,
/// the order of incoming events is not preserved.
///
/// Use [`Network::debounce`] if you want to receive only the latest value emitted within a
/// microtask.
///
/// ```text
/// Input: ───────1───3─2─3───────────
/// Microtasks: ── ▶───── ▶───── ▶───── ▶──
/// Output: ──────────{1}────{2,3}─────
/// ```
///
/// Note: See documentation of [`crate::microtasks`] module for more details about microtasks.
pub fn batch_unique<T>(&self, label: Label, input: &T) -> Stream<HashSet<Output<T>>>
where
T: EventOutput,
Output<T>: Hash + Eq, {
self.register(OwnedBatchUnique::new(label, input))
}
/// Fold the incoming value using [`Monoid`] implementation.
pub fn fold<T1, X>(&self, label: Label, event: &T1) -> Stream<X>
@ -1029,6 +1051,29 @@ impl Network {
self.register(OwnedMap::new(label, src, f))
}
/// A version of map that operates on [`Some`] values. The provided function will only be called
/// when incoming stream's value is `Some(In)`. If you need the provided function to return an
/// optional type, use [`Network::and_then`].
pub fn map_some<T, In, F, Out>(&self, label: Label, src: &T, f: F) -> Stream<Option<Out>>
where
T: EventOutput<Output = Option<In>>,
Out: Data,
F: 'static + Fn(&In) -> Out, {
self.map(label, src, move |value| value.as_ref().map(&f))
}
/// A version of map that operates on optionals. The provided function will only be called when
/// incoming stream's value is [`Some`], and then the provided function can return an optional
/// itself. If you want a non-optional return type, use [`Network::map_some`].
pub fn and_then<T, In, F, Out>(&self, label: Label, src: &T, f: F) -> Stream<Option<Out>>
where
T: EventOutput<Output = Option<In>>,
Out: Data,
F: 'static + Fn(&In) -> Option<Out>, {
self.map(label, src, move |value| value.as_ref().and_then(&f))
}
pub fn map_<'a, T, F, Out>(&self, label: Label, src: &T, f: F) -> Stream<()>
where
T: EventOutput,
@ -2791,7 +2836,7 @@ impl<T: EventOutput> stream::EventConsumer<Output<T>> for OwnedBatch<T> {
let this = weak.upgrade();
let this = this.expect("BatchData holds callback handle, so it must be alive.");
this.scheduled_task.take();
let batch = this.collected_batch.borrow_mut().drain(..).collect();
let batch = this.collected_batch.take();
this.emit_event(&default(), &batch);
});
scheduled_task.replace(handle);
@ -2806,6 +2851,66 @@ impl<T: EventOutput> stream::InputBehaviors for BatchData<T> {
}
// ===================
// === BatchUnique ===
// ===================
#[derive(Debug)]
pub struct BatchUniqueData<T: HasOutput> {
_input: T,
collected_batch: RefCell<HashSet<Output<T>>>,
scheduled_task: RefCell<Option<enso_callback::Handle>>,
}
pub type OwnedBatchUnique<T> = stream::Node<BatchUniqueData<T>>;
pub type BatchUnique<T> = stream::WeakNode<BatchUniqueData<T>>;
impl<T: HasOutput> HasOutput for BatchUniqueData<T> {
type Output = HashSet<Output<T>>;
}
impl<T: EventOutput> OwnedBatchUnique<T>
where Output<T>: Eq + Hash
{
/// Constructor.
pub fn new(label: Label, input: &T) -> Self {
let definition = BatchUniqueData {
_input: input.clone_ref(),
collected_batch: default(),
scheduled_task: default(),
};
Self::construct_and_connect(label, input, definition)
}
}
impl<T: EventOutput> stream::EventConsumer<Output<T>> for OwnedBatchUnique<T>
where Output<T>: Eq + Hash
{
fn on_event(&self, _stack: CallStack, value: &Output<T>) {
self.collected_batch.borrow_mut().insert(value.clone());
let mut scheduled_task = self.scheduled_task.borrow_mut();
if scheduled_task.is_none() {
let weak = self.downgrade();
let handle = next_microtask(move || {
let this = weak.upgrade();
let this =
this.expect("BatchUniqueData holds callback handle, so it must be alive.");
this.scheduled_task.take();
let batch = this.collected_batch.take();
this.emit_event(&default(), &batch);
});
scheduled_task.replace(handle);
}
}
}
impl<T: EventOutput> stream::InputBehaviors for BatchUniqueData<T> {
fn input_behaviors(&self) -> Vec<Link> {
vec![]
}
}
// ================
// === Debounce ===
// ================
@ -2836,12 +2941,14 @@ impl<T: EventOutput> stream::EventConsumer<Output<T>> for OwnedDebounce<T> {
let mut next_value = self.next_value.borrow_mut();
let not_scheduled = next_value.is_none();
next_value.replace(value.clone());
drop(next_value);
if not_scheduled {
let weak = self.downgrade();
let handler = next_microtask(move || {
if let Some(node) = weak.upgrade() {
if let Some(value) = node.next_value.borrow_mut().take() {
let taken_value = node.next_value.borrow_mut().take();
if let Some(value) = taken_value {
node.emit_event(&default(), &value);
}
}