Finishing Vector Editor (#6470)

This commit is contained in:
Adam Obuchowicz 2023-05-02 11:24:20 +02:00 committed by GitHub
parent cd92d90f9f
commit d6fa36d793
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1129 additions and 309 deletions

View File

@ -135,7 +135,9 @@
- [Added capability to create node widgets with complex UI][6347]. Node widgets
such as dropdown can now be placed in the node and affect the code text flow.
- [The IDE UI element for selecting the execution mode of the project is now
sending messages to the backend.][6341].
sending messages to the backend][6341].
- [List Editor Widget][6470]. Now you can edit lists by clicking buttons on
nodes or by dragging the elements.
#### EnsoGL (rendering engine)
@ -198,7 +200,8 @@
[5895]: https://github.com/enso-org/enso/pull/6130
[6035]: https://github.com/enso-org/enso/pull/6035
[6097]: https://github.com/enso-org/enso/pull/6097
[6097]: https://github.com/enso-org/enso/pull/6341
[6341]: https://github.com/enso-org/enso/pull/6341
[6470]: https://github.com/enso-org/enso/pull/6470
#### Enso Standard Library

View File

@ -623,6 +623,14 @@ impl<T> SpanSeed<T> {
pub fn child(node: T) -> Self {
Self::Child(SpanSeedChild { node })
}
pub fn token(token: String) -> Self {
Self::Token(SpanSeedToken { token })
}
pub fn is_child(&self) -> bool {
matches!(self, SpanSeed::Child { .. })
}
}

View File

@ -12,6 +12,7 @@ use crate::SpanTree;
use ast::opr::match_named_argument;
use ast::opr::ArgWithOffset;
use ast::Ast;
use ast::SpanSeed;
@ -142,6 +143,33 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
),
};
Ok(infix.into_ast())
} else if let ast::Shape::Tree(tree) = ast.shape() {
let mut tree = tree.clone();
let span_info = &mut tree.span_info;
let has_children =
span_info.iter().any(|span| matches!(span, ast::SpanSeed::Child(_)));
match *kind {
BeforeArgument(index) => {
span_info.insert(index, ast::SpanSeed::child(new));
span_info.insert(index + 1, ast::SpanSeed::token(",".to_owned()));
span_info.insert(index + 2, ast::SpanSeed::space(1).unwrap());
}
Append => {
let last_token_index = span_info
.iter()
.rposition(|span| matches!(span, ast::SpanSeed::Token(_)));
let index = last_token_index.unwrap_or(0);
if has_children {
span_info.insert(index, ast::SpanSeed::token(",".to_owned()));
span_info.insert(index + 1, ast::SpanSeed::space(1).unwrap());
span_info.insert(index + 2, ast::SpanSeed::child(new));
} else {
span_info.insert(index, ast::SpanSeed::child(new));
}
}
_ => unreachable!("Wrong insertion point in tree."),
}
Ok(ast.with_shape(tree))
} else {
let mut prefix = ast::prefix::Chain::from_ast_non_strict(&ast);
let item = ast::prefix::Argument::new(new, DEFAULT_OFFSET, None);
@ -236,7 +264,6 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
}
}
fn erase_impl<C: Context>(&self) -> Option<EraseOperation<C>> {
if self.node.kind.removable() {
Some(Box::new(move |root, context| {
@ -245,6 +272,11 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
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,
@ -266,6 +298,28 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
infix.args.pop();
}
Ok(infix.into_ast())
} else if let (Crumb::Tree(crumb), ast::Shape::Tree(tree)) =
(last_crumb, ast.shape())
{
let index = crumb.index;
let mut tree = tree.clone();
let span_info: &mut Vec<_> = &mut tree.span_info;
let after = &span_info[index + 1..];
let before = &span_info[..index];
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),
};
reinsert_crumbs =
Some(self.crumbs.relative_sibling(insertion_point_offset));
span_info.drain(removed_range);
Ok(ast.with_shape(tree))
} else {
let mut prefix = ast::prefix::Chain::from_ast_non_strict(&ast);
prefix.args.pop();
@ -327,17 +381,19 @@ impl<'a, T> Implementation for node::Ref<'a, T> {
// 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 = self.kind.definition_index().and_then(|_| {
let call_id = self.kind.call_id();
let name = self.kind.argument_name();
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
});
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)
found.map(|found| found.crumbs)
})
});
// For non-resolved arguments, use the preceding insertion point. After the

View File

@ -819,6 +819,11 @@ fn tree_generate_node<T: Payload>(
} else {
let mut parent_offset = ByteDiff::from(0);
let mut sibling_offset = ByteDiff::from(0);
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 last_token_index =
tree.span_info.iter().rposition(|span| matches!(span, SpanSeed::Token(_)));
for (index, raw_span_info) in tree.span_info.iter().enumerate() {
match raw_span_info {
SpanSeed::Space(ast::SpanSeedSpace { space }) => {
@ -826,6 +831,16 @@ fn tree_generate_node<T: Payload>(
sibling_offset += ByteDiff::from(space);
}
SpanSeed::Token(ast::SpanSeedToken { token }) => {
if is_array && Some(index) == last_token_index {
let kind = InsertionPointType::Append;
children.push(node::Child {
node: Node::<T>::new().with_kind(kind),
parent_offset,
sibling_offset,
ast_crumbs: vec![],
});
sibling_offset = 0.byte_diff();
}
let kind = node::Kind::Token;
let size = ByteDiff::from(token.len());
let ast_crumbs = vec![TreeCrumb { index }.into()];
@ -835,7 +850,18 @@ fn tree_generate_node<T: Payload>(
sibling_offset = 0.byte_diff();
}
SpanSeed::Child(ast::SpanSeedChild { node }) => {
let kind = node::Kind::argument();
if is_array {
let kind = InsertionPointType::BeforeArgument(index);
children.push(node::Child {
node: Node::<T>::new().with_kind(kind),
parent_offset,
sibling_offset,
ast_crumbs: vec![],
});
sibling_offset = 0.byte_diff();
}
let kind = node::Kind::argument().with_removable(is_array);
let node = node.generate_node(kind, context)?;
let child_size = node.size;
let ast_crumbs = vec![TreeCrumb { index }.into()];

View File

@ -255,6 +255,15 @@ impl Crumbs {
vec.push(child);
self
}
/// Create crumbs to the sibling node, which is `offset` nodes away from the current node.
pub fn relative_sibling(&self, offset: isize) -> Self {
let mut vec = self.vec.deref().clone();
if let Some(last) = vec.last_mut() {
*last = last.saturating_add_signed(offset)
}
Self { vec: Rc::new(vec) }
}
}
impl<T: IntoIterator<Item = Crumb>> From<T> for Crumbs {
@ -391,7 +400,7 @@ impl<'a, T> Ref<'a, T> {
}
/// Iterator over all direct children producing `Ref`s.
pub fn children_iter(self) -> impl DoubleEndedIterator<Item = Ref<'a, T>> {
pub fn children_iter(self) -> impl DoubleEndedIterator<Item = Ref<'a, T>> + Clone {
let children_count = self.node.children.len();
(0..children_count).map(move |i| self.clone().child(i).unwrap())
}

View File

@ -300,18 +300,15 @@ impl Model {
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| {
target_crumbs.and_then(|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.
let edge = self.view.model.edges.get_cloned_ref(&id)?;
let outdated_target = edge.target()?;
edge.set_target(EdgeEndpoint::new(outdated_target.node_id, crumbs));
Some(())
});
self.view.replace_detached_edge_target((id, crumbs));
};
}))
},
"delete connection",

View File

@ -166,7 +166,7 @@ impl BreadcrumbsModel {
pub fn new(app: Application, frp: &Frp) -> Self {
let scene = &app.display.default_scene;
let project_name = app.new_view();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Breadcrumbs");
let root = display::object::Instance::new();
let breadcrumbs_container = display::object::Instance::new();
let scene = scene.clone_ref();

View File

@ -779,7 +779,7 @@ macro_rules! define_components {
/// Constructor.
#[allow(clippy::vec_init_then_push)]
pub fn new() -> Self {
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named(stringify!($name));
$(let $field = <$field_type>::new();)*
$(display_object.add_child(&$field);)*
let mut shape_view_events:Vec<PointerTarget_DEPRECATED> = Vec::default();
@ -1304,7 +1304,7 @@ impl EdgeModelData {
/// Constructor.
#[profile(Debug)]
pub fn new(scene: &Scene, network: &frp::Network) -> Self {
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Edge");
let front = Front::new();
let back = Back::new();
let joint = joint::View::new();

View File

@ -513,7 +513,7 @@ impl NodeModel {
let background = background::View::new();
let drag_area = drag_area::View::new();
let vcs_indicator = vcs::StatusIndicator::new(app);
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Node");
display_object.add_child(&profiling_label);
display_object.add_child(&drag_area);

View File

@ -19,6 +19,7 @@ use enso_frp;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display;
use ensogl::display::world::with_context;
use ensogl::gui::cursor;
use ensogl::Animation;
use ensogl_component::text;
@ -152,13 +153,14 @@ impl Model {
#[profile(Debug)]
pub fn new(app: &Application) -> Self {
let app = app.clone_ref();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("input");
let edit_mode_label = app.new_view::<text::Text>();
let expression = default();
let styles = StyleWatch::new(&app.display.default_scene.style_sheet);
let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let widget_tree = widget::Tree::new(&app);
with_context(|ctx| ctx.layers.widget.add(&widget_tree));
Self { app, display_object, edit_mode_label, expression, styles, styles_frp, widget_tree }
.init()
}

View File

@ -23,10 +23,14 @@ use ensogl::display::shape;
// === Constants ===
// =================
/// The horizontal padding of ports. It affects how the port hover should extend the target text
/// The horizontal padding of ports. It affects how the port shape should extend the target text
/// boundary on both sides.
pub const PORT_PADDING_X: f32 = 4.0;
/// The horizontal padding of port hover areas. It affects how the port hover should extend the
/// target text boundary on both sides.
pub const HOVER_PADDING_X: f32 = 2.0;
/// The minimum size of the port visual area.
pub const BASE_PORT_HEIGHT: f32 = 18.0;
@ -122,7 +126,7 @@ impl Port {
/// Create a new port for given widget. The widget will be placed as a child of the port's root
/// display object, and its layout size will be used to determine the port's size.
pub fn new(widget: DynWidget, app: &Application, frp: &WidgetsFrp) -> Self {
let port_root = display::object::Instance::new();
let port_root = display::object::Instance::new_named("Port");
let widget_root = widget.root_object().clone_ref();
let port_shape = PortShape::new();
let hover_shape = HoverShape::new();
@ -140,8 +144,8 @@ impl Port {
hover_shape
.set_size_y(BASE_PORT_HEIGHT)
.allow_grow()
.set_margin_left(-PORT_PADDING_X)
.set_margin_right(-PORT_PADDING_X)
.set_margin_left(-HOVER_PADDING_X)
.set_margin_right(-HOVER_PADDING_X)
.set_alignment_left_center();
let layers = app.display.default_scene.extension::<PortLayers>();
@ -235,6 +239,7 @@ impl Port {
self.port_root.remove_child(&self.widget_root);
self.port_root.add_child(new_root);
self.widget_root = new_root.clone_ref();
self.widget_root.set_margin_left(0.0);
}
}
@ -264,6 +269,18 @@ impl Port {
self.widget
}
/// Get a reference to a widget currently wrapped by the port. The widget may change during
/// the next tree rebuild.
pub(super) fn widget(&self) -> &DynWidget {
&self.widget
}
/// Get a mutable reference to a widget currently wrapped by the port. The widget may change
/// during the next tree rebuild.
pub(super) fn widget_mut(&mut self) -> &mut DynWidget {
&mut self.widget
}
/// Get the port's hover shape. Used for testing to simulate mouse events.
pub fn hover_shape(&self) -> &HoverShape {
&self.hover_shape

View File

@ -134,6 +134,15 @@ pub trait SpanWidget {
fn new(config: &Self::Config, ctx: &ConfigContext) -> Self;
/// Update configuration for existing widget.
fn configure(&mut self, config: &Self::Config, ctx: ConfigContext);
/// Receive a reference of the tree items for which the ownership transfer was requested. Called
/// by the tree when [`WidgetsFrp::transfer_ownership`] signal is used.
fn receive_ownership(
&mut self,
original_request: TransferRequest,
nodes: Vec<(WidgetIdentity, TreeNode)>,
) {
_ = (original_request, nodes);
}
}
@ -209,6 +218,15 @@ macro_rules! define_widget_modules(
},
}
}
fn receive_ownership(&mut self,
req: TransferRequest,
nodes: Vec<(WidgetIdentity, TreeNode)>,
) {
match (self) {
$(DynWidget::$name(model) => model.receive_ownership(req, nodes),)*
}
}
}
};
);
@ -252,7 +270,12 @@ impl Configuration {
/// Derive widget configuration from Enso expression, node data in span tree and inferred value
/// type. When no configuration is provided with an override, this function will be used to
/// create a default configuration.
fn from_node(span_node: &SpanRef, usage_type: Option<crate::Type>, expression: &str) -> Self {
fn from_node(
span_node: &SpanRef,
usage_type: Option<crate::Type>,
expression: &str,
is_directly_connected: bool,
) -> Self {
use span_tree::node::Kind;
let kind = &span_node.kind;
@ -260,46 +283,51 @@ impl Configuration {
const VECTOR_TYPE: &str = "Standard.Base.Data.Vector.Vector";
let is_list_editor_enabled = ARGS.groups.feature_preview.options.vector_editor.value;
let is_vector = |arg: &span_tree::node::Argument| {
let type_matches = usage_type
let node_expr = &expression[span_node.span()];
let looks_like_vector = node_expr.starts_with('[') && node_expr.ends_with(']');
let type_is_vector = |tp: &Option<String>| {
usage_type
.as_ref()
.map(|t| t.as_str())
.or(arg.tp.as_deref())
.map_or(false, |tp| tp.contains(VECTOR_TYPE));
if type_matches {
let node_expr = &expression[span_node.span()];
node_expr.starts_with('[') && node_expr.ends_with(']')
} else {
false
}
.or(tp.as_deref())
.map_or(false, |tp| tp.contains(VECTOR_TYPE))
};
match kind {
Kind::Argument(arg) if !arg.tag_values.is_empty() =>
Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values),
Kind::Argument(arg) if is_list_editor_enabled && is_vector(arg) => Self::list_editor(),
Kind::Argument(_) if is_list_editor_enabled && looks_like_vector => Self::list_editor(),
Kind::Root if is_list_editor_enabled && looks_like_vector => Self::list_editor(),
Kind::InsertionPoint(arg) if arg.kind.is_expected_argument() =>
if !arg.tag_values.is_empty() {
if is_list_editor_enabled && (type_is_vector(&arg.tp) || looks_like_vector) {
Self::list_editor()
} else if !arg.tag_values.is_empty() {
Self::static_dropdown(arg.name.as_ref().map(Into::into), &arg.tag_values)
} else {
Self::always(label::Config::default())
},
Kind::Token | Kind::Operation if !has_children => Self::inert(label::Config::default()),
Kind::NamedArgument => Self::inert(hierarchy::Config),
Kind::InsertionPoint(_) => Self::inert(insertion_point::Config),
Kind::InsertionPoint(_) =>
Self::maybe_with_port(insertion_point::Config, is_directly_connected),
_ if has_children => Self::always(hierarchy::Config),
_ => Self::always(label::Config::default()),
}
}
const fn maybe_with_port<C>(kind: C, has_port: bool) -> Self
where C: ~const Into<DynConfig> {
Self { display: Display::Always, kind: kind.into(), has_port }
}
const fn always<C>(kind: C) -> Self
where C: ~const Into<DynConfig> {
Self { display: Display::Always, kind: kind.into(), has_port: true }
Self::maybe_with_port(kind, true)
}
const fn inert<C>(kind: C) -> Self
where C: ~const Into<DynConfig> {
Self { display: Display::Always, kind: kind.into(), has_port: false }
Self::maybe_with_port(kind, false)
}
/// Widget configuration for static dropdown, based on the tag values provided by suggestion
@ -391,6 +419,13 @@ 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>,
/// 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
/// reused.
///
/// NOTE: Calling this during rebuild will have no effect.
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>>,
@ -399,6 +434,20 @@ pub struct WidgetsFrp {
pub(super) connected_port_updated: frp::Any<()>,
}
/// A request for widget tree item ownership transfer. See [`WidgetsFrp::transfer_ownership`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct TransferRequest {
/// The widget ID that will receive the ownership of the node. Usually this is the ID of the
/// widget that sends the request, which can be obtained from [`NodeInfo::identity`], which is
/// provided on [`ConfigContext`].
pub new_owner: WidgetIdentity,
/// The ID of the node that should be transferred. Usually one of the node's children, which
/// can be obtained from [`Child`] instance returned from [`TreeBuilder::child_widget`].
pub to_transfer: WidgetIdentity,
/// Whether the whole subtree should be transferred, or just the node itself.
pub whole_subtree: bool,
}
// ============
@ -434,6 +483,8 @@ impl Tree {
frp::extend! { network
frp.private.output.rebuild_required <+ frp.marked_dirty_sync.debounce();
transfer_ownership <- any(...);
eval transfer_ownership((request) model.transfer_ownership(*request));
set_ports_visible <- frp.set_ports_visible.sampler();
set_read_only <- frp.set_read_only.sampler();
@ -455,6 +506,7 @@ impl Tree {
set_read_only,
set_view_mode,
set_profiling_status,
transfer_ownership,
value_changed,
request_import,
on_port_hover,
@ -561,7 +613,7 @@ impl Tree {
/// `Port` struct and stored in `Port` variant. Otherwise, the widget will be stored directly using
/// the `Widget` node variant.
#[derive(Debug)]
pub(super) enum TreeNode {
pub enum TreeNode {
/// A tree node that contains a port. The port wraps a widget.
Port(Port),
/// A tree node without a port, directly containing a widget.
@ -569,12 +621,29 @@ pub(super) enum TreeNode {
}
impl TreeNode {
fn port(&self) -> Option<&Port> {
#[allow(missing_docs)]
pub fn port(&self) -> Option<&Port> {
match self {
TreeNode::Port(port) => Some(port),
TreeNode::Widget(_) => None,
}
}
#[allow(missing_docs)]
pub fn widget(&self) -> &DynWidget {
match self {
TreeNode::Port(port) => port.widget(),
TreeNode::Widget(widget) => widget,
}
}
#[allow(missing_docs)]
pub fn widget_mut(&mut self) -> &mut DynWidget {
match self {
TreeNode::Port(port) => port.widget_mut(),
TreeNode::Widget(widget) => widget,
}
}
}
impl display::Object for TreeNode {
@ -648,7 +717,7 @@ impl TreeModel {
/// argument info.
fn new(app: &Application) -> Self {
let app = app.clone_ref();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Tree");
display_object.use_auto_layout();
display_object.set_children_alignment_left_center().justify_content_center_y();
display_object.set_size_y(NODE_HEIGHT);
@ -716,6 +785,22 @@ impl TreeModel {
Some(hierarchy[parent_index].identity)
}
/// Iterate over a node with given pointer and all its descendants, if it exists in the tree.
#[allow(dead_code)]
pub fn iter_subtree(
&self,
pointer: WidgetIdentity,
) -> impl Iterator<Item = WidgetIdentity> + '_ {
let hierarchy = self.hierarchy.borrow();
let nodes = self.nodes_map.borrow();
let total_range = nodes.get(&pointer).map_or(0..0, |entry| {
let start = entry.index;
let total_descendants = hierarchy[entry.index].total_descendants;
start..start + 1 + total_descendants
});
total_range.map(move |index| hierarchy[index].identity)
}
/// Iterate children of a node under given pointer, if any exist.
#[allow(dead_code)]
pub fn iter_children(
@ -741,6 +826,27 @@ impl TreeModel {
})
}
/// Prevent a widget from being reused in future rebuild. Send its only remaining strong
/// reference to the new owner.
fn transfer_ownership(&self, request: TransferRequest) {
let mut nodes = Vec::new();
if request.whole_subtree {
let iter = self.iter_subtree(request.to_transfer);
let mut nodes_map = self.nodes_map.borrow_mut();
nodes.extend(iter.filter_map(move |id| Some((id, nodes_map.remove(&id)?.node))));
} else {
let mut nodes_map = self.nodes_map.borrow_mut();
if let Some(entry) = nodes_map.remove(&request.to_transfer) {
nodes.push((request.to_transfer, entry.node));
}
}
let mut nodes_map = self.nodes_map.borrow_mut();
if let Some(owner) = nodes_map.get_mut(&request.new_owner) {
owner.node.widget_mut().receive_ownership(request, nodes);
}
}
#[profile(Task)]
fn rebuild_tree(
&self,
@ -780,7 +886,7 @@ impl TreeModel {
};
let child = builder.child_widget(tree.root_ref(), default());
self.display_object.replace_children(&[child]);
self.display_object.replace_children(&[child.root_object]);
self.nodes_map.replace(builder.new_nodes);
self.hierarchy.replace(builder.hierarchy);
@ -1014,7 +1120,7 @@ impl NestingLevel {
/// A stable identifier to a span tree node. Uniquely determines a main widget of specific node in
/// the span tree. It is a base of a widget stable identity, and allows widgets to be reused when
/// rebuilding the tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct StableSpanIdentity {
/// AST ID of either the node itself, or the closest ancestor node which has one. Is [`None`]
/// when there is no such parent with assigned AST id.
@ -1049,7 +1155,7 @@ impl StableSpanIdentity {
/// create a child widget on the same span tree node, so we need to be able to distinguish between
/// them. Note that only one widget created for a given span tree node will be able to receive a
/// port. The port is assigned to the first widget at given span that wants to receive it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deref)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deref, Default)]
pub struct WidgetIdentity {
/// The pointer to the main widget of this widget's node.
#[deref]
@ -1139,7 +1245,7 @@ impl<'a> TreeBuilder<'a> {
&mut self,
span_node: span_tree::node::Ref<'_>,
nesting_level: NestingLevel,
) -> display::object::Instance {
) -> Child {
self.child_widget_of_type(span_node, nesting_level, None)
}
@ -1159,7 +1265,7 @@ impl<'a> TreeBuilder<'a> {
span_node: span_tree::node::Ref<'_>,
nesting_level: NestingLevel,
configuration: Option<&Configuration>,
) -> display::object::Instance {
) -> Child {
// This call can recurse into itself within the widget configuration logic. We need to save
// the current layer's state, so it can be restored later after visiting the child node.
let parent_last_ast_depth = self.last_ast_depth;
@ -1188,6 +1294,12 @@ impl<'a> TreeBuilder<'a> {
let sibling_offset = span_node.sibling_offset.as_usize();
let usage_type = main_ptr.ast_id.and_then(|id| self.usage_type_map.get(&id)).cloned();
// Prepare the widget node info and build context.
let connection_color = self.connected_map.get(&span_node.crumbs);
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);
// Get widget configuration. There are three potential sources for configuration, that are
// used in order, whichever is available first:
// 1. The `config_override` argument, which can be set by the parent widget if it wants to
@ -1208,7 +1320,9 @@ impl<'a> TreeBuilder<'a> {
Some(config) => config,
None => {
let ty = usage_type.clone();
inferred_config = Configuration::from_node(&span_node, ty, self.node_expression);
let expr = &self.node_expression;
let connected = connection.is_some();
inferred_config = Configuration::from_node(&span_node, ty, expr, connected);
&inferred_config
}
};
@ -1225,12 +1339,6 @@ impl<'a> TreeBuilder<'a> {
let old_node = self.old_nodes.remove(&widget_id).map(|e| e.node);
// Prepare the widget node info and build context.
let connection_color = self.connected_map.get(&span_node.crumbs);
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);
let disabled = self.node_disabled;
let info = NodeInfo {
identity: widget_id,
@ -1298,7 +1406,7 @@ impl<'a> TreeBuilder<'a> {
let entry = TreeEntry { node: child_node, index: insertion_index };
self.new_nodes.insert(widget_id, entry);
child_root
Child { id: widget_id, root_object: child_root }
}
}

View File

@ -41,7 +41,7 @@ impl super::SpanWidget for Widget {
}
fn new(_: &Config, _: &super::ConfigContext) -> Self {
let display_object = object::Instance::new();
let display_object = object::Instance::new_named("widget::Hierarchy");
display_object.use_auto_layout();
display_object.set_children_alignment_left_center().justify_content_center_y();
Self { display_object }
@ -50,7 +50,8 @@ impl super::SpanWidget for Widget {
fn configure(&mut self, _: &Config, ctx: super::ConfigContext) {
let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument());
let children_iter = ctx.span_node.children_iter();
let children = children_iter.map(|node| ctx.builder.child_widget(node, child_level));
let children =
children_iter.map(|node| ctx.builder.child_widget(node, child_level).root_object);
self.display_object.replace_children(&children.collect::<CollectedChildren>());
}
}

View File

@ -20,7 +20,7 @@ use ensogl::display::object;
pub struct Config;
/// Insertion point widget. Displays nothing.
/// Insertion point widget. Displays nothing when not connected.
#[derive(Clone, Debug)]
pub struct Widget {
root: object::Instance,
@ -34,7 +34,8 @@ impl super::SpanWidget for Widget {
}
fn new(_: &Config, _: &super::ConfigContext) -> Self {
let root = object::Instance::new();
let root = object::Instance::new_named("widget::InsertionPoint");
root.set_size(Vector2::<f32>::zero());
Self { root }
}

View File

@ -51,11 +51,9 @@ impl super::SpanWidget for Widget {
let app = ctx.app();
let widgets_frp = ctx.frp();
let layers = &ctx.app().display.default_scene.layers;
let root = object::Instance::new();
root.set_size_y(TEXT_SIZE);
let root = object::Instance::new_named("widget::Label");
let label = text::Text::new(app);
label.set_property_default(text::Size(TEXT_SIZE));
label.set_y(TEXT_SIZE);
layers.label.add(&label);
root.add_child(&label);
let frp = Frp::new();
@ -81,7 +79,12 @@ impl super::SpanWidget for Widget {
eval content_change((content) label.set_content(content));
width <- label.width.on_change();
height <- label.height.on_change();
eval width((w) root.set_size_x(*w); );
eval height([root, label] (h) {
root.set_size_y(*h);
label.set_y(*h);
});
}
Self { frp, root, label }

View File

@ -1,195 +1,356 @@
//! Module dedicated to the List Editor widget. The main structure is [`Model`] which is one of
//! the [KindModel](crate::component::node::widget::KindModel) variants.
//!
//! Currently the view is a simple [`Elements`] component, which will be replaced with a rich
//! view in [future tasks](https://github.com/enso-org/enso/issues/5631).
//! Module dedicated to the [List Editor widget](Widget).
// FIXME[ao]: This code miss important documentation (e.g. for `Element`, `DragData` and `ListItem`)
// and may be unreadable at some places. It should be improved in several next debugging PRs.
use crate::prelude::*;
use crate::component::node::input::widget::single_choice::triangle;
use crate::component::node::input::widget::single_choice::ACTIVATION_SHAPE_SIZE;
use crate::component::node::input::area::TEXT_SIZE;
use crate::component::node::input::widget::Configuration;
use crate::component::node::input::widget::WidgetsFrp;
use crate::component::node::input::widget::TransferRequest;
use crate::component::node::input::widget::TreeNode;
use crate::component::node::input::widget::WidgetIdentity;
use ensogl::application::Application;
use ensogl::control::io::mouse;
use ensogl::display;
use ensogl::display::object::event;
use ensogl::display::shape::StyleWatch;
use ensogl::display::object;
use ensogl::display::world::with_context;
use ensogl_component::list_editor::ListEditor;
use ensogl_component::text::Text;
use ensogl_hardcoded_theme as theme;
use span_tree::node::Kind;
use std::fmt::Write;
// ==============
// === Widget ===
// ==============
// ===============
// === Element ===
// ===============
ensogl::define_endpoints_2! {
Input {
current_value(Option<ImString>),
current_crumbs(span_tree::Crumbs),
#[derive(Debug)]
struct Element {
display_object: object::Instance,
content: object::Instance,
#[allow(dead_code)]
background: display::shape::Rectangle,
expr_range: Range<usize>,
item_crumb: usize,
alive: Option<()>,
}
#[derive(Debug)]
struct DragData {
element_id: WidgetIdentity,
element: Element,
expression: String,
#[allow(dead_code)]
owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
}
#[derive(Debug, Clone, CloneRef)]
struct ListItem {
element_id: Immutable<WidgetIdentity>,
display_object: object::Instance,
drag_data: Rc<RefCell<Option<DragData>>>,
}
impl PartialEq for ListItem {
fn eq(&self, other: &Self) -> bool {
self.element_id == other.element_id
}
}
impl ListItem {
fn take_drag_data(&self) -> Option<DragData> {
let mut borrow = self.drag_data.borrow_mut();
let can_take = matches!(&*borrow, Some(data) if data.element_id == *self.element_id);
can_take.and_option_from(|| borrow.take())
}
}
impl display::Object for ListItem {
fn display_object(&self) -> &object::Instance {
&self.display_object
}
}
impl Element {
fn new() -> Self {
let display_object = object::Instance::new_named("Element");
let content = object::Instance::new_named("Content");
let background = display::shape::Rectangle::new();
background.set_color(display::shape::INVISIBLE_HOVER_COLOR);
background.allow_grow().set_alignment_left_center();
content.use_auto_layout().set_children_alignment_left_center();
with_context(|ctx| ctx.layers.label.add(&background));
display_object.replace_children(&[background.display_object(), &content]);
Self {
display_object,
content,
background,
expr_range: default(),
alive: default(),
item_crumb: default(),
}
}
}
impl display::Object for Element {
fn display_object(&self) -> &object::Instance {
&self.display_object
}
}
/// A model for the vector editor widget.
///
/// Currently it displays an activation shape (a triangle) which, on click, displays the widget
/// view. The view is a [`ListEditor`] - see its documentation for available GUI actions. Currently
/// only adding new elements is supported.
///
/// The component does not handle nested arrays well. They should be fixed once [integrated into
/// new widget hierarchy](https://github.com/enso-org/enso/issues/5923).
#[derive(Clone, CloneRef, Debug)]
pub struct Widget {
config_frp: Frp,
display_object: display::object::Instance,
child_container: display::object::Instance,
list_container: display::object::Instance,
activation_shape: triangle::View,
list: ListEditor<Text>,
display_object: object::Instance,
network: frp::Network,
model: Rc<RefCell<Model>>,
}
impl Widget {
/// A gap between the `activation_shape` and `elements` view.
const GAP: f32 = 3.0;
/// Create Model for Vector Editor widget.
pub fn new(app: &Application, widgets_frp: &WidgetsFrp, styles: &StyleWatch) -> Self {
let display_object = display::object::Instance::new();
let list_container = display::object::Instance::new();
let child_container = display::object::Instance::new();
let activation_shape = triangle::View::new();
let list = ListEditor::new(&app.cursor);
let toggle_color = styles.get_color(theme::widget::activation_shape::connected);
activation_shape.set_size(ACTIVATION_SHAPE_SIZE);
activation_shape.color.set(toggle_color.into());
display_object.add_child(&child_container);
display_object.add_child(&list_container);
display_object.add_child(&activation_shape);
display_object
.use_auto_layout()
.set_column_count(1)
.set_gap_y(Self::GAP)
.set_children_alignment_center();
display_object.set_size_hug();
let config_frp = Frp::new();
Self { config_frp, display_object, child_container, list_container, activation_shape, list }
.init_toggle(widgets_frp)
.init_list_updates(app, widgets_frp)
}
fn init_toggle(self, widgets_frp: &WidgetsFrp) -> Self {
let network = &self.config_frp.network;
let display_object = &self.display_object;
let list_container = &self.list_container;
let list = &self.list;
let dot_clicked = self.activation_shape.on_event::<mouse::Down>();
let focus_in = self.display_object.on_event::<event::FocusIn>();
let focus_out = self.display_object.on_event::<event::FocusOut>();
fn init_list_updates(self, ctx: &super::ConfigContext) -> Self {
let widgets_frp = ctx.frp();
let network = &self.network;
let model = &self.model;
let list = self.model.borrow().list.clone_ref();
frp::extend! { network
init <- source_();
set_focused <- dot_clicked.map(f!([display_object](_) !display_object.is_focused()));
eval set_focused([display_object](focus) match focus {
true => display_object.focus(),
false => display_object.blur(),
});
// Adding elements.
requested_new <- list.request_new_item.filter_map(|resp| resp.gui_interaction_payload());
widgets_frp.value_changed <+ requested_new.filter_map(f!((idx) model.borrow_mut().on_new_item(*idx)));
readonly_set <- widgets_frp.set_read_only.on_true();
do_open <- focus_in.gate_not(&widgets_frp.set_read_only);
do_close <- any_(focus_out, readonly_set);
is_open <- bool(&do_close, &do_open).on_change();
eval is_open([list_container, list](open) match open {
true => list_container.add_child(&list),
false => list_container.remove_child(&list),
});
}
init.emit(());
self
}
fn init_list_updates(self, app: &Application, widgets_frp: &WidgetsFrp) -> Self {
let config_frp = &self.config_frp;
let network = &config_frp.network;
let list = &self.list;
frp::extend! { network
init <- source_();
value <- all(config_frp.current_value, init)._0();
non_empty_value <- value.filter_map(|v| v.clone());
empty_value <- value.filter_map(|v| v.is_none().then_some(()));
eval non_empty_value ([list, app](val) Self::update_list(&app, val.as_str(), &list));
eval_ empty_value ([list] Self::clear_list(&list));
code_changed_by_user <-
list.request_new_item.map(f_!([app, list] Self::push_new_element(&app, &list)));
value_changed <- code_changed_by_user.map(f_!([list] {
Some(ImString::new(Self::construct_code(&list)))
// Inserting dragged elements.
inserted_by_user <- list.on_item_added.filter_map(|resp| resp.gui_interaction_payload());
widgets_frp.value_changed <+ inserted_by_user.filter_map(f!([list, model](index) {
let item = list.item_at(*index)?;
model.borrow_mut().on_item_added(item, *index)
}));
widgets_frp.value_changed <+ value_changed.map2(&config_frp.current_crumbs,
move |t: &Option<ImString>, crumbs: &span_tree::Crumbs| (crumbs.clone(), t.clone())
);
// Removing dragged elements.
removed_by_user <- list.on_item_removed.filter_map(|resp| resp.clone().gui_interaction_payload());
remove_request <- removed_by_user.filter_map(f!([model] ((_, item)) {
let item = item.borrow_mut().take()?;
model.borrow_mut().on_item_removed(item)
}));
widgets_frp.transfer_ownership <+ remove_request._1();
widgets_frp.value_changed <+ remove_request._0().map(|crumb| (crumb.clone(), None));
}
init.emit(());
self
}
}
fn clear_list(list: &ListEditor<Text>) {
for _ in 0..list.items().len() {
list.remove(0);
#[derive(Debug)]
struct Model {
self_id: WidgetIdentity,
list: ListEditor<ListItem>,
elements: HashMap<WidgetIdentity, Element>,
default_value: String,
expression: String,
crumbs: span_tree::Crumbs,
drag_data_rc: Rc<RefCell<Option<DragData>>>,
received_drag: Option<DragData>,
insertion_indices: Vec<usize>,
}
impl Model {
fn new(ctx: &super::ConfigContext, display_object: &object::Instance) -> Self {
let list = ListEditor::new(&ctx.app().cursor);
list.set_size_hug_y(TEXT_SIZE).allow_grow_y();
display_object.use_auto_layout().set_children_alignment_left_center();
Self {
self_id: ctx.info.identity,
list,
elements: default(),
default_value: default(),
expression: default(),
crumbs: default(),
drag_data_rc: default(),
received_drag: default(),
insertion_indices: default(),
}
}
fn update_list(app: &Application, code: &str, list: &ListEditor<Text>) {
let mut codes = Self::parse_array_code(code).fuse();
let mut widgets_kept = 0;
for widget in list.items() {
match codes.next() {
Some(code) => {
widgets_kept += 1;
if widget.content.value().to_string() != code {
widget.set_content(ImString::new(code));
}
fn configure(&mut self, root: &object::Instance, cfg: &Config, mut ctx: super::ConfigContext) {
self.expression.clear();
self.default_value.clear();
self.expression.push_str(ctx.expression_at(ctx.span_node.span()));
self.crumbs = ctx.span_node.crumbs.clone();
// Right now, nested list editors are broken. Prevent them from being created. Whenever
// a nested list editor is requested, we instead use a hierarchical widget to display the
// child list items as ordinary expressions.
let ext = ctx.get_extension_or_default::<Extension>();
let already_in_list = ext.already_in_list;
ctx.set_extension(Extension { already_in_list: true });
if already_in_list {
let child = ctx.builder.child_widget_of_type(
ctx.span_node,
ctx.info.nesting_level,
Some(&super::Configuration::always(super::hierarchy::Config)),
);
root.replace_children(&[&child.root_object]);
} else if ctx.span_node.is_insertion_point() {
write!(self.default_value, "[{}]", cfg.item_default).unwrap();
self.configure_insertion_point(root, ctx)
} else {
self.default_value.push_str(&cfg.item_default);
self.configure_vector(root, cfg, ctx)
}
}
fn configure_insertion_point(&mut self, root: &object::Instance, ctx: super::ConfigContext) {
self.elements.clear();
let insertion_point = ctx.builder.child_widget_of_type(
ctx.span_node,
ctx.info.nesting_level,
Some(&super::Configuration::always(super::label::Config)),
);
root.replace_children(&[self.list.display_object(), &*insertion_point]);
}
fn configure_vector(
&mut self,
root: &object::Instance,
cfg: &Config,
ctx: super::ConfigContext,
) {
let no_nest = ctx.info.nesting_level;
let nest = ctx.info.nesting_level.next();
let child_config = cfg.item_widget.as_deref();
let mut build_child_widget = |i, nest, config, allow_margin: bool| {
let mut node = ctx.span_node.clone().child(i).expect("invalid index");
if !allow_margin {
node.sibling_offset = 0.into();
}
ctx.builder.child_widget_of_type(node, nest, config)
};
let mut open_bracket = None;
let mut close_bracket = None;
let mut last_insert_crumb = None;
let mut list_items = SmallVec::<[ListItem; 16]>::new();
self.insertion_indices.clear();
for (index, child) in ctx.span_node.node.children.iter().enumerate() {
let node = &child.node;
let start = child.parent_offset.value as usize;
let range = start..start + node.size.value as usize;
let expr = &self.expression[range.clone()];
match node.kind {
Kind::Token if expr == "[" && open_bracket.is_none() => {
let child = build_child_widget(index, no_nest, None, true);
open_bracket = Some(child.root_object);
}
None => {
list.remove(widgets_kept);
Kind::Token if expr == "]" && close_bracket.is_none() => {
let child = build_child_widget(index, no_nest, None, false);
close_bracket = Some(child.root_object);
}
Kind::InsertionPoint(_) => {
last_insert_crumb = Some(index);
}
Kind::Argument(_) if last_insert_crumb.is_some() => {
let insert_index = last_insert_crumb.take().unwrap();
let insert = build_child_widget(insert_index, no_nest, None, false);
let item = build_child_widget(index, nest, child_config, false);
let element = self.elements.entry(item.id).or_insert_with(|| {
self.received_drag.take().map_or_else(Element::new, |d| d.element)
});
element.alive = Some(());
element.item_crumb = index;
element.expr_range = range;
element.content.replace_children(&[&*insert, &*item]);
self.insertion_indices.push(insert_index);
list_items.push(ListItem {
element_id: Immutable(item.id),
display_object: element.display_object.clone(),
drag_data: self.drag_data_rc.clone(),
});
}
_ => {}
}
}
for code in codes {
let widget = Text::new(app);
widget.set_content(ImString::new(code));
app.display.default_scene.layers.label.add(&widget);
list.push(widget);
self.elements.retain(|_, child| child.alive.take().is_some());
self.insertion_indices.extend(last_insert_crumb);
let current_items = self.list.items();
list_diff(&current_items, &list_items, |op| match op {
DiffOp::Delete { at, old, present_later } =>
if present_later.is_some()
|| list_items.iter().any(|i| i.display_object == old.display_object)
{
self.list.take_item(at);
} else {
self.list.remove(at);
},
DiffOp::Insert { at, new } => {
self.list.insert_item(at, new.clone_ref());
}
DiffOp::Update { at, old, new } =>
if old.display_object() != new.display_object() {
self.list.replace_item(at, new.clone_ref());
},
});
let append_insert = last_insert_crumb
.map(|index| build_child_widget(index, no_nest, None, false).root_object);
let (open_bracket, close_bracket) = open_bracket.zip(close_bracket).unzip();
let mut children = SmallVec::<[&object::Instance; 4]>::new();
children.extend(&open_bracket);
children.push(self.list.display_object());
children.extend(&append_insert);
children.extend(&close_bracket);
root.replace_children(&children);
}
fn on_item_removed(&mut self, item: ListItem) -> Option<(span_tree::Crumbs, TransferRequest)> {
let element = self.elements.get(&item.element_id)?;
let crumbs = self.crumbs.sub(element.item_crumb);
let request = TransferRequest {
new_owner: self.self_id,
to_transfer: *item.element_id,
whole_subtree: true,
};
Some((crumbs, request))
}
fn receive_ownership_of_dragged_item(
&mut self,
req: TransferRequest,
owned_subtree: Vec<(WidgetIdentity, TreeNode)>,
) {
let element_id = req.to_transfer;
let element = self.elements.remove(&element_id);
if let Some(element) = element {
let expression = self.expression[element.expr_range.clone()].to_owned();
let drag_data = DragData { element_id, element, expression, owned_subtree };
self.drag_data_rc.replace(Some(drag_data));
} else {
error!("Grabbed item not found.");
}
}
fn push_new_element(app: &Application, list: &ListEditor<Text>) {
let widget = Text::new(app);
widget.set_content("_");
list.push(widget);
fn on_item_added(
&mut self,
item: ListItem,
at: usize,
) -> Option<(span_tree::Crumbs, Option<ImString>)> {
self.received_drag = item.take_drag_data();
let expression: ImString = mem::take(&mut self.received_drag.as_mut()?.expression).into();
match &self.insertion_indices[..] {
&[] => Some((self.crumbs.clone(), Some(expression))),
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
}
}
fn construct_code(list: &ListEditor<Text>) -> String {
let subwidgets = list.items().into_iter();
let mut subwidgets_codes = subwidgets.map(|sub| sub.content.value().to_string());
format!("[{}]", subwidgets_codes.join(","))
}
fn parse_array_code(code: &str) -> impl Iterator<Item = &str> {
let looks_like_array = code.starts_with('[') && code.ends_with(']');
let opt_iterator = looks_like_array.then(|| {
let without_braces = code.trim_start_matches([' ', '[']).trim_end_matches([' ', ']']);
let elements_with_trailing_spaces = without_braces.split(',');
elements_with_trailing_spaces.map(|s| s.trim())
});
opt_iterator.into_iter().flatten()
fn on_new_item(&mut self, at: usize) -> Option<(span_tree::Crumbs, Option<ImString>)> {
let expression: ImString = self.default_value.clone().into();
match &self.insertion_indices[..] {
&[] => Some((self.crumbs.clone(), Some(expression))),
ids => ids.get(at).map(|idx| (self.crumbs.sub(*idx), Some(expression))),
}
}
}
@ -208,22 +369,119 @@ pub struct Config {
impl super::SpanWidget for Widget {
type Config = Config;
fn root_object(&self) -> &display::object::Instance {
fn root_object(&self) -> &object::Instance {
&self.display_object
}
fn new(_: &Config, ctx: &super::ConfigContext) -> Self {
Self::new(ctx.app(), ctx.frp(), ctx.styles())
console_log!("NEW");
let display_object = object::Instance::new_named("widget::ListEditor");
let model = Model::new(ctx, &display_object);
let network = frp::Network::new("widget::ListEditor");
Self { display_object, network, model: Rc::new(RefCell::new(model)) }.init_list_updates(ctx)
}
fn configure(&mut self, _: &Config, ctx: super::ConfigContext) {
let current_value: Option<ImString> = Some(ctx.expression_at(ctx.span_node.span()).into());
self.config_frp.current_value(current_value);
self.config_frp.current_crumbs(ctx.span_node.crumbs.clone());
fn configure(&mut self, cfg: &Config, ctx: super::ConfigContext) {
console_log!("CONFIGURE");
let mut model = self.model.borrow_mut();
model.configure(&self.display_object, cfg, ctx);
}
let child_level = ctx.info.nesting_level.next_if(ctx.span_node.is_argument());
let label_meta = super::Configuration::always(super::label::Config);
let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&label_meta));
self.child_container.replace_children(&[child]);
fn receive_ownership(&mut self, req: TransferRequest, nodes: Vec<(WidgetIdentity, TreeNode)>) {
let mut model = self.model.borrow_mut();
model.receive_ownership_of_dragged_item(req, nodes);
}
}
#[derive(PartialEq)]
enum DiffOp<'old, 'new, T> {
Delete { at: usize, old: &'old T, present_later: Option<usize> },
Insert { at: usize, new: &'new T },
Update { at: usize, old: &'old T, new: &'new T },
}
fn list_diff<'old, 'new, T>(
old_elements: &'old [T],
new_elements: &'new [T],
mut f: impl FnMut(DiffOp<'old, 'new, T>),
) where
T: PartialEq,
{
// Indices for next elements to process in both lists.
let mut current_old = 0;
let mut current_new = 0;
// The current index of insertion or deletion in the list that is being processed. Effectively
// the number of insertions or equal pairs that have been processed so far.
let mut at = 0;
while current_old < old_elements.len() && current_new < new_elements.len() {
let old = &old_elements[current_old];
let new = &new_elements[current_new];
// Next pair of elements are equal, so we don't need to do anything.
if old == new {
f(DiffOp::Update { at, old, new });
current_old += 1;
current_new += 1;
at += 1;
continue;
}
let remaining_old = &old_elements[current_old + 1..];
let remaining_new = &new_elements[current_new + 1..];
let old_still_in_new_list = remaining_new.contains(old);
if !old_still_in_new_list {
f(DiffOp::Delete { at, old, present_later: None });
current_old += 1;
continue;
}
let index_in_remaining_old = remaining_old.iter().position(|x| x == new);
match index_in_remaining_old {
// Not present in old, thus it is an insertion.
None => {
f(DiffOp::Insert { at, new });
at += 1;
current_new += 1;
continue;
}
// Present in old. Delete all elements in between and insert the matching one.
Some(advance) => {
f(DiffOp::Delete { at, old, present_later: Some(current_old + advance + 1) });
for k in 0..advance {
let present_later = remaining_old[k + 1..].iter().position(|old| old == new);
f(DiffOp::Delete { at, old: &remaining_old[k], present_later });
}
current_old += advance + 1;
let old = &old_elements[current_old];
let new = &new_elements[current_new];
f(DiffOp::Update { at, old, new });
current_old += 1;
current_new += 1;
at += 1;
continue;
}
}
}
while current_old < old_elements.len() {
f(DiffOp::Delete { at, old: &old_elements[current_old], present_later: None });
current_old += 1;
}
while current_new < new_elements.len() {
f(DiffOp::Insert { at, new: &new_elements[current_new] });
current_new += 1;
at += 1;
}
}
// =================
// === Extension ===
// =================
#[derive(Clone, Copy, Debug, Default)]
struct Extension {
already_in_list: bool,
}

View File

@ -119,7 +119,7 @@ impl super::SpanWidget for Widget {
let layers = &app.display.default_scene.layers;
layers.label.add(&activation_shape);
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("widget::SingleChoice");
let content_wrapper = display_object.new_child();
content_wrapper.add_child(&activation_shape);
let label_wrapper = content_wrapper.new_child();
@ -182,7 +182,7 @@ impl super::SpanWidget for Widget {
};
let child_level = ctx.info.nesting_level;
let child = ctx.builder.child_widget_of_type(ctx.span_node, child_level, Some(&config));
self.label_wrapper.replace_children(&[child]);
self.label_wrapper.replace_children(&[child.root_object]);
}
}

View File

@ -171,7 +171,7 @@ impl Model {
/// Constructor.
#[profile(Debug)]
pub fn new(app: &Application, frp: &Frp) -> Self {
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("output");
let ports = display::object::Instance::new();
let app = app.clone_ref();
let label = app.new_view::<text::Text>();

View File

@ -619,8 +619,7 @@ ensogl::define_endpoints_2! {
set_detached_edge_sources (EdgeEndpoint),
set_edge_source ((EdgeId, EdgeEndpoint)),
set_edge_target ((EdgeId, EdgeEndpoint)),
unset_edge_source (EdgeId),
unset_edge_target (EdgeId),
replace_detached_edge_target ((EdgeId, span_tree::Crumbs)),
connect_nodes ((EdgeEndpoint,EdgeEndpoint)),
deselect_all_nodes (),
press_node_input (EdgeEndpoint),
@ -1830,7 +1829,7 @@ impl GraphEditorModel {
pub fn new(app: &Application, cursor: cursor::Cursor, frp: &Frp) -> Self {
let network = frp.network();
let scene = &app.display.default_scene;
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("GraphEditor");
let nodes = Nodes::new();
let edges = Edges::new();
let vis_registry = visualization::Registry::with_default_visualizations();
@ -2183,6 +2182,22 @@ impl GraphEditorModel {
}
}
fn replace_detached_edge_target(&self, edge_id: EdgeId, crumbs: &span_tree::Crumbs) {
if !self.edges.detached_source.contains(&edge_id) {
return;
}
if let Some(edge) = self.edges.get_cloned_ref(&edge_id) {
if let Some(target) = edge.take_target() {
self.set_input_connected(&target, None);
let port = crumbs.clone();
let new_target = EdgeEndpoint { port, ..target };
edge.set_target(new_target);
self.refresh_edge_position(edge_id);
}
}
}
fn take_edges_with_detached_targets(&self) -> HashSet<EdgeId> {
let edges = self.edges.detached_target.mem_take();
self.check_edge_attachment_status_and_emit_events();
@ -3647,6 +3662,7 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
eval out.on_edge_source_unset (((id,_)) model.remove_edge_source(*id));
eval out.on_edge_target_unset (((id,_)) model.remove_edge_target(*id));
eval inputs.replace_detached_edge_target (((id,tgt)) model.replace_detached_edge_target(*id,tgt));
is_only_tgt_not_set <-
out.on_edge_source_set.map(f!(((id,_)) model.with_edge_map_target(*id,|_|()).is_none()));
@ -3660,6 +3676,8 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
out.on_edge_only_source_not_set <+ out.on_edge_target_set_with_source_not_set._0();
out.on_edge_only_source_not_set <+ out.on_edge_source_unset._0();
eval inputs.replace_detached_edge_target ([model,neutral_color]((id, _))
model.refresh_edge_color(*id,neutral_color.value().into()));
eval out.on_edge_source_set ([model,neutral_color]((id, _))
model.refresh_edge_color(*id,neutral_color.value().into()));
eval out.on_edge_target_set ([model,neutral_color]((id, _))

View File

@ -116,7 +116,7 @@
"primary": false
},
"vectorEditor": {
"value": false,
"value": true,
"description": "Show Vector Editor widget on nodes.",
"primary": false
},

View File

@ -199,3 +199,27 @@ body {
background: #b96a50;
margin: 0.8em -1em;
}
#debug-root {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
margin: 0;
overflow: hidden;
pointer-events: none;
}
#debug-root > .debug-layer {
position: absolute;
top: 50vh;
left: 50vw;
width: 0px;
height: 0px;
}
#debug-root > .debug-layer * {
position: absolute;
background: rgba(0, 0, 0, 0.05);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}

View File

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

View File

@ -34,9 +34,9 @@ impl<T: display::Object> Item<T> {
pub fn new_from_placeholder(elem: T, placeholder: StrongPlaceholder) -> Self {
let frp = Frp::new();
placeholder.add_child(&elem);
let network = frp.network();
let elem_obj = elem.display_object();
placeholder.replace_children(&[&elem_obj]);
let margin_left = Animation::<f32>::new_with_init(network, 0.0);
let elem_offset = Animation::<Vector2>::new_with_init(network, elem.position().xy());
margin_left.simulator.update_spring(|s| s * crate::DEBUG_ANIMATION_SPRING_FACTOR);

View File

@ -73,6 +73,7 @@
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
use ensogl_core::display::shape::compound::rectangle::*;
use ensogl_core::display::world::*;
use ensogl_core::prelude::*;
@ -82,7 +83,6 @@ use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::object::Event;
use ensogl_core::display::object::ObjectOps;
use ensogl_core::display::shape::compound::rectangle::*;
use ensogl_core::gui::cursor;
use ensogl_core::gui::cursor::Cursor;
use ensogl_core::gui::cursor::Trash;
@ -93,7 +93,6 @@ use placeholder::StrongPlaceholder;
use placeholder::WeakPlaceholder;
// ==============
// === Export ===
// ==============
@ -153,6 +152,10 @@ impl<T> Response<T> {
pub fn gui(payload: T) -> Self {
Self::new(payload, true)
}
pub fn gui_interaction_payload(self) -> Option<T> {
self.gui_interaction.then_some(self.payload)
}
}
@ -203,6 +206,20 @@ impl<T: display::Object> ItemOrPlaceholder<T> {
}
}
/// Replace the item element with a new one. Returns old element if it was replaced.
pub fn replace_element(&mut self, element: T) -> Option<T> {
match self {
ItemOrPlaceholder::Item(t) =>
if t.elem.display_object() == element.display_object() {
Some(mem::replace(&mut t.elem, element))
} else {
let new_item = Item::new_from_placeholder(element, t.placeholder.clone_ref());
Some(mem::replace(t, new_item).elem)
},
_ => None,
}
}
/// Get the display object of this item or placeholder. In case the placeholder is weak and does
/// not exist anymore, it will return [`None`].
pub fn display_object(&self) -> Option<display::object::Instance> {
@ -344,9 +361,9 @@ impl<T> Model<T> {
pub fn new(cursor: &Cursor) -> Self {
let cursor = cursor.clone_ref();
let items = default();
let root = display::object::Instance::new();
let layout = display::object::Instance::new();
let layout_with_icons = display::object::Instance::new();
let root = display::object::Instance::new_named("ListEditor");
let layout = display::object::Instance::new_named("layout");
let layout_with_icons = display::object::Instance::new_named("layout_with_icons");
let gap = default();
layout_with_icons.use_auto_layout();
layout.use_auto_layout();
@ -354,7 +371,7 @@ impl<T> Model<T> {
root.add_child(&layout_with_icons);
let add_elem_icon = Rectangle().build(|t| {
t.set_corner_radius_max()
.set_size((24.0, 24.0))
.set_size((14.0, 14.0))
.set_color(color::Rgba::new(0.0, 0.0, 0.0, 0.2));
});
layout_with_icons.add_child(&add_elem_icon);
@ -390,7 +407,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let network = self.frp.network();
let model = &self.model;
let on_add_elem_icon_down = model.borrow().add_elem_icon.on_event::<mouse::Down>();
let on_add_elem_icon_up = model.borrow().add_elem_icon.on_event::<mouse::Up>();
let on_down = model.borrow().layout.on_event_capturing::<mouse::Down>();
let on_up_source = scene.on_event::<mouse::Up>();
let on_move = scene.on_event::<mouse::Move>();
@ -400,7 +417,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let drag_target = cursor::DragTarget::new();
frp::extend! { network
frp.private.output.request_new_item <+ on_add_elem_icon_down.map(f_!([model] {
frp.private.output.request_new_item <+ on_add_elem_icon_up.map(f_!([model] {
Response::gui(model.borrow().len())
}));
@ -455,13 +472,13 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
}
self.init_add_and_remove();
let (is_dragging, _drag_diff, no_drag) =
let no_drag =
self.init_dragging(cursor, &on_up, &on_up_cleaning_phase, &on_down, &target, &pos_diff);
frp::extend! { network
on_up_close <- on_up.gate(&is_close);
}
self.init_dropping(&on_up_close, &dragged_item_bbox_center, &is_close);
let insert_pointer_style = self.init_insertion_points(&on_up, &pos_on_move, &is_dragging);
let insert_pointer_style = self.init_insertion_points(cursor, &on_up, &pos_on_move);
frp::extend! { network
cursor.frp.set_style_override <+ insert_pointer_style;
@ -481,13 +498,12 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
fn init_insertion_points(
&self,
cursor: &Cursor,
on_up: &frp::Stream<Event<mouse::Up>>,
pos_on_move: &frp::Stream<Vector2>,
is_dragging: &frp::Stream<bool>,
) -> frp::Stream<Option<cursor::Style>> {
let on_up = on_up.clone_ref();
let pos_on_move = pos_on_move.clone_ref();
let is_dragging = is_dragging.clone_ref();
let frp = &self.frp;
let network = self.frp.network();
@ -496,6 +512,11 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
frp::extend! { network
gaps <- model_borrowed.layout.on_resized.map(f_!(model.gaps()));
// We are debouncing the `is_dragging` stream to avoid double-borrow of list editor, as
// this event is fired immediately after list-editor instructs cursor to stop dragging.
is_dragging <- cursor.frp.is_dragging.debounce();
opt_index <- all_with7(
&frp.gap,
&gaps,
@ -519,7 +540,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
).on_change();
index <= opt_index;
enabled <- opt_index.is_some();
pointer_style <- enabled.then_constant(cursor::Style::plus());
pointer_style <- enabled.then_constant(cursor::Style::plus()).on_change();
on_up_in_gap <- on_up.gate(&enabled);
insert_in_gap <- index.sample(&on_up_in_gap);
frp.private.output.request_new_item <+ insert_in_gap.map(|t| Response::gui(*t));
@ -544,7 +565,8 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
let on_item_removed = &frp.private.output.on_item_removed;
eval frp.remove([model, on_item_removed] (index) {
if let Some(item) = model.borrow_mut().trash_item_at(*index) {
let item = model.borrow_mut().trash_item_at(*index);
if let Some(item) = item {
on_item_removed.emit(Response::api((*index, Rc::new(RefCell::new(Some(item))))));
}
});
@ -560,7 +582,7 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
on_down: &frp::Stream<Event<mouse::Down>>,
target: &frp::Stream<display::object::Instance>,
pos_diff: &frp::Stream<Vector2>,
) -> (frp::Stream<bool>, frp::Stream<Vector2>, frp::Stream<bool>) {
) -> frp::Stream<bool> {
let model = &self.model;
let on_up = on_up.clone_ref();
let on_up_cleaning_phase = on_up_cleaning_phase.clone_ref();
@ -591,15 +613,15 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
start <- status.on_true();
target_on_start <- target.sample(&start);
let on_item_removed = &frp.private.output.on_item_removed;
eval target_on_start([model, on_item_removed, cursor] (t) {
let indexed_item = model.borrow_mut().start_item_drag(t);
if let Some((index, item)) = indexed_item {
eval target_on_start([model, cursor, on_item_removed] (t) {
let item = model.borrow_mut().start_item_drag(t);
if let Some((index, item)) = item {
cursor.start_drag(item.clone_ref());
on_item_removed.emit(Response::gui((index, Rc::new(RefCell::new(Some(item))))));
}
});
}
(status, drag_diff, no_drag)
no_drag
}
/// Implementation of dropping items logic, including showing empty placeholders when the item
@ -659,6 +681,35 @@ impl<T: display::Object + CloneRef + Debug> ListEditor<T> {
pub fn items(&self) -> Vec<T> {
self.model.borrow().items.iter().flat_map(|item| item.as_item().cloned()).collect()
}
pub fn insert_item(&self, index: Index, item: T) {
self.model.borrow_mut().insert(index, item);
}
pub fn replace_item(&self, index: Index, new_item: T) -> Option<T> {
let mut model = self.model.borrow_mut();
let index = model.index_to_item_or_placeholder_index(index)?;
let item = model.items.get_mut(index)?;
item.replace_element(new_item)
}
pub fn take_item(&self, index: Index) -> Option<T> {
let mut model = self.model.borrow_mut();
let index = model.index_to_item_or_placeholder_index(index)?;
match model.items.remove(index) {
ItemOrPlaceholder::Item(item) => {
model.item_count_changed();
Some(item.elem)
}
ItemOrPlaceholder::Placeholder(_) => unreachable!(),
}
}
pub fn item_at(&self, index: Index) -> Option<T> {
let model = self.model.borrow();
let index = model.index_to_item_or_placeholder_index(index)?;
model.items.get(index).and_then(|item| item.as_item().cloned())
}
}
impl<T: display::Object + CloneRef + 'static> SharedModel<T> {
@ -792,6 +843,7 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
self.push(item)
};
self.reposition_items();
self.item_count_changed();
index
}
@ -906,8 +958,8 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
/// See docs of [`Self::start_item_drag_at`] for more information.
fn start_item_drag(&mut self, target: &display::object::Instance) -> Option<(Index, T)> {
let objs = target.rev_parent_chain().reversed();
let tarrget_index = objs.into_iter().find_map(|t| self.item_index_of(&t));
if let Some((index, index_or_placeholder_index)) = tarrget_index {
let target_index = objs.into_iter().find_map(|t| self.item_index_of(&t));
if let Some((index, index_or_placeholder_index)) = target_index {
self.start_item_drag_at(index_or_placeholder_index).map(|item| (index, item))
} else {
warn!("Could not find the item to drag.");
@ -1005,24 +1057,28 @@ impl<T: display::Object + CloneRef + 'static> Model<T> {
/// Place the currently dragged element in the given index. The item will be enclosed in the
/// [`Item`] object, will handles its animation. See the documentation of
/// [`ItemOrPlaceholder`] to learn more.
fn place_dragged_item(&mut self, index: ItemOrPlaceholderIndex) -> Option<Index> {
fn place_dragged_item(&mut self, index: ItemOrPlaceholderIndex) -> Option<Index>
where T: Debug {
if let Some(item) = self.cursor.stop_drag_if_is::<T>() {
self.collapse_all_placeholders_no_margin_update();
if let Some((index, placeholder)) = self.get_indexed_merged_placeholder_at(index) {
let actual_index = if let Some((index, placeholder)) =
self.get_indexed_merged_placeholder_at(index)
{
placeholder.set_target_size(placeholder.computed_size().x);
item.update_xy(|t| t - placeholder.global_position().xy());
self.items[index] =
Item::new_from_placeholder(item.clone_ref(), placeholder).into();
self.items[index] = Item::new_from_placeholder(item, placeholder).into();
index
} else {
// This branch should never be reached, as when dragging an item we always create
// a placeholder for it (see the [`Self::add_insertion_point_if_type_match`]
// function). However, in case something breaks, we want it to still
// provide the user with the correct outcome.
self.items.insert(index, Item::new(item.clone_ref()).into());
self.items.insert(index, Item::new(item).into());
warn!("An element was inserted without a placeholder. This should not happen.");
}
index
};
self.reposition_items();
self.item_or_placeholder_index_to_index(index)
self.item_or_placeholder_index_to_index(actual_index)
} else {
warn!("Called function to insert dragged element, but no element is being dragged.");
None

View File

@ -3,6 +3,7 @@ use ensogl_core::prelude::*;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::world::with_context;
use ensogl_core::Animation;
@ -111,7 +112,7 @@ pub struct PlaceholderModel {
impl PlaceholderModel {
fn new() -> Self {
let frp = Frp::new();
let root = display::object::Instance::new();
let root = display::object::Instance::new_named("Placeholder");
let self_ref = default();
let collapsing = default();
let size = Animation::<f32>::new(frp.network());
@ -124,6 +125,9 @@ impl PlaceholderModel {
.set_border_color(color::Rgba::new(1.0, 0.0, 0.0, 1.0));
});
root.add_child(&viz);
with_context(|ctx| {
ctx.layers.above_nodes.add(&viz);
});
viz
});
Self { frp, root, self_ref, collapsing, size, _deubg_viz }

View File

@ -247,18 +247,18 @@ impl ScrollArea {
#[profile(Detail)]
pub fn new(app: &Application) -> ScrollArea {
let scene = &app.display.default_scene;
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("ScrollArea");
let masked_layer = layer::Masked::new();
let display_object = display::object::InstanceWithLayer::new(display_object, masked_layer);
let content_layer = display_object.layer.masked_layer.create_sublayer("content_layer");
let ui_layer = display_object.layer.masked_layer.create_sublayer("ui_layer");
let content = display::object::Instance::new();
let content = display::object::Instance::new_named("content");
display_object.add_child(&content);
content_layer.add(&content);
let scrollbars = display::object::Instance::new();
let scrollbars = display::object::Instance::new_named("scrollbars");
display_object.add_child(&scrollbars);
ui_layer.add(&scrollbars);

View File

@ -734,7 +734,7 @@ impl TextModel {
let app = app.clone_ref();
let scene = &app.display.default_scene;
let selection_map = default();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_named("Text");
let glyph_system = font::glyph::System::new(scene, font::DEFAULT_FONT_MONO);
frp.private.output.glyph_system.emit(Some(glyph_system.clone()));
let glyph_system = RefCell::new(glyph_system);

View File

@ -504,7 +504,7 @@ impl System {
#[profile(Debug)]
pub fn new_glyph(&self) -> Glyph {
let context = self.context.clone();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_no_debug();
let font = self.font.clone_ref();
let glyph_id = default();
let line_byte_offset = default();

View File

@ -205,7 +205,7 @@ impl Camera2dData {
let z_zoom_1 = 1.0;
let matrix = default();
let dirty = Dirty::new();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_no_debug();
let zoom_update_registry = default();
let screen_update_registry = default();
display_object.modify_position(|p| p.z = 1.0);

View File

@ -1119,6 +1119,25 @@ use unit2::Fraction;
// =================
// === CONSTANTS ===
// =================
/// Enable debugging of display objects hierarchy. When enabled, all display objects will be
/// represented in the DOM tree, which can be inspected using browser devtools inspector.
pub const ENABLE_DOM_DEBUG: bool = false;
/// Enable DOM debugging for all display objects as a default. When enabled, all display objects
/// created with `new` or `new_named` constructors will be represented in the debug DOM tree. When
/// disabled, only display objects created with explicitly enabled debugging (e.g. using `new_debug`
/// constructor) will have that behavior.
///
/// Has effect only when [`ENABLE_DOM_DEBUG`] is already enabled.
pub const ENABLE_DOM_DEBUG_ALL: bool = true;
/// The name of a display object when not specified. Object names are visible in the DOM inspector.
pub const DEFAULT_NAME: &str = "UnnamedDisplayObject";
// ==========
// === Id ===
// ==========
@ -1144,7 +1163,7 @@ pub struct ChildIndex(usize);
// =============
/// The main display object structure. Read the docs of [this module](self) to learn more.
#[derive(Clone, CloneRef, Default, Deref, From)]
#[derive(Clone, CloneRef, Deref, From)]
#[repr(transparent)]
pub struct Instance {
def: InstanceDef,
@ -1182,33 +1201,65 @@ pub struct Model {
hierarchy: HierarchyModel,
event: EventModel,
layout: LayoutModel,
debug_dom: Option<enso_web::HtmlDivElement>,
}
// === Contructors ===
impl Instance {
/// Constructor.
#[profile(Debug)]
/// Constructor with default name. Will have DOM debugging enabled if [`ENABLE_DOM_DEBUG_ALL`]
/// flag is enabled.
pub fn new() -> Self {
default()
Self::new_named_with_debug(DEFAULT_NAME, ENABLE_DOM_DEBUG_ALL)
}
/// Constructor of a named display object. The name is used for debugging purposes only.
/// Constructor with DOM debugging always disabled.
pub fn new_no_debug() -> Self {
Self::new_named_with_debug(DEFAULT_NAME, false)
}
/// Constructor with DOM debugging always enabled.
pub fn new_debug() -> Self {
Self::new_named_with_debug(DEFAULT_NAME, true)
}
/// Constructor with custom name. Will have DOM debugging enabled if [`ENABLE_DOM_DEBUG_ALL`]
/// flag is enabled.
pub fn new_named(name: &'static str) -> Self {
Self { def: InstanceDef::new_named(name) }
Self::new_named_with_debug(name, ENABLE_DOM_DEBUG_ALL)
}
/// Constructor with custom name and DOM debugging always disabled.
pub fn new_named_no_debug(name: &'static str) -> Self {
Self::new_named_with_debug(name, false)
}
/// Constructor with custom name and DOM debugging always enabled.
pub fn new_named_debug(name: &'static str) -> Self {
Self::new_named_with_debug(name, true)
}
/// Constructor with custom name and DOM debugging enabled with an argument.
pub fn new_named_with_debug(name: &'static str, enable_debug: bool) -> Self {
Self { def: InstanceDef::new(name, enable_debug) }
}
}
impl InstanceDef {
/// Constructor.
pub fn new() -> Self {
Self { rc: Rc::new(Model::new()) }.init_events_handling()
}
#[profile(Debug)]
fn new(name: &'static str, enable_debug: bool) -> Self {
let mut model = Model::new_named(name);
if ENABLE_DOM_DEBUG && enable_debug {
use enso_web::prelude::*;
if let Some(document) = enso_web::window.document() {
let dom = document.create_div_or_panic();
dom.set_attribute_or_warn("data-name", name);
model.debug_dom = Some(dom);
}
}
/// Constructor.
pub fn new_named(name: &'static str) -> Self {
Self { rc: Rc::new(Model::new_named(name)) }.init_events_handling()
Self { rc: Rc::new(model) }.init_events_handling().init_dom_debug(enable_debug)
}
/// ID getter of this display object.
@ -1229,14 +1280,24 @@ impl Model {
let hierarchy = HierarchyModel::new(&network);
let event = EventModel::new(&network);
let layout = LayoutModel::default();
Self { network, hierarchy, event, layout, name }
let debug_dom = None;
Self { network, hierarchy, event, layout, name, debug_dom }
}
}
impl Drop for Model {
fn drop(&mut self) {
if ENABLE_DOM_DEBUG {
if let Some(dom) = self.debug_dom.take() {
dom.remove();
}
}
}
}
// === Impls ===
impl Default for InstanceDef {
impl Default for Instance {
fn default() -> Self {
Self::new()
}
@ -1342,6 +1403,7 @@ impl Root {
/// Constructor of a named display object. The name is used for debugging purposes only.
pub fn new_named(name: &'static str) -> Self {
let def = Instance::new_named(name);
def.set_size((0.0, 0.0));
Self { def }.init()
}
@ -2273,7 +2335,7 @@ impl Model {
self.transformation.borrow().rotation()
}
/// Transformation matrix of the object in the parent coordinate space.
/// Transformation matrix of the object in the camera coordinate space.
fn transformation_matrix(&self) -> Matrix4<f32> {
self.transformation.borrow().matrix()
}
@ -2405,6 +2467,77 @@ impl InstanceDef {
self
}
fn init_dom_debug(self, enable_debug: bool) -> Self {
use enso_web::prelude::*;
use std::fmt::Write;
if !(ENABLE_DOM_DEBUG && enable_debug) {
return self;
}
let style_string = RefCell::new(String::new());
let display = Rc::new(Cell::new(""));
let last_parent_node = RefCell::new(None);
let weak = self.downgrade();
let network = &self.network;
frp::extend! { network
eval_ self.on_show (display.set(""));
eval_ self.on_hide (display.set("display:none;"));
eval_ self.on_transformed ([display] {
let Some(object) = weak.upgrade() else { return };
let Some(dom) = object.debug_dom.as_ref() else { return };
let mut parent_node = last_parent_node.borrow_mut();
let transform;
let new_parent;
let upgrade = |l: &LayerAssignment| l.layer.upgrade();
if let Some(layer) = object.assigned_layer.borrow().as_ref().and_then(upgrade) {
transform = object.transformation.borrow().matrix();
new_parent = layer.debug_dom.clone();
} else if let Some(parent) = object.parent().and_then(|p| p.debug_dom.clone()) {
transform = object.transformation.borrow().local_matrix();
new_parent = Some(parent);
} else if let Some(layer) = object.layer.borrow().as_ref().and_then(upgrade) {
transform = object.transformation.borrow().matrix();
new_parent = layer.debug_dom.clone();
} else {
transform = Matrix4::zero();
new_parent = None;
}
if *parent_node != new_parent {
if let Some(parent) = &new_parent {
parent.append_child(dom).unwrap();
} else {
dom.remove();
}
*parent_node = new_parent;
}
let size = object.computed_size();
let transform = transform.as_slice();
let mut style_string = style_string.borrow_mut();
let x = size.x();
let y = size.y();
let display = display.get();
let (first, rest) = transform.split_first().unwrap();
write!(style_string, "\
{display}\
width:{x}px;\
height:{y}px;\
transform:matrix3d({first:.4}"
).unwrap();
rest.iter().for_each(|f| write!(style_string, ",{f:.4}").unwrap());
style_string.write_str(");").unwrap();
dom.set_attribute_or_warn("style", &*style_string);
style_string.clear();
});
}
self
}
fn emit_event_impl(
event: &event::SomeEvent,
parent: Option<Instance>,

View File

@ -202,6 +202,10 @@ impl CachedTransformation {
// === Getters ===
impl CachedTransformation {
pub fn local_matrix(&self) -> Matrix4<f32> {
self.transform_matrix
}
pub fn matrix(&self) -> Matrix4<f32> {
self.matrix
}

View File

@ -602,6 +602,7 @@ pub struct HardcodedLayers {
pub viz: Layer,
pub below_main: Layer,
pub main: Layer,
pub widget: Layer,
pub port: Layer,
pub port_selection: Layer,
pub label: Layer,
@ -645,6 +646,7 @@ impl HardcodedLayers {
let viz = root.create_sublayer("viz");
let below_main = root.create_sublayer("below_main");
let main = root.create_sublayer("main");
let widget = root.create_sublayer("widget");
let port = root.create_sublayer("port");
let port_selection =
root.create_sublayer_with_camera("port_selection", &port_selection_cam);
@ -671,6 +673,7 @@ impl HardcodedLayers {
viz,
below_main,
main,
widget,
port,
port_selection,
label,
@ -970,6 +973,7 @@ impl SceneData {
// Updating all other cameras (the main camera was already updated, so it will be skipped).
self.layers.iter_sublayers_and_masks_nested(|layer| {
let dirty = layer.camera().update(scene);
layer.update_debug_view();
was_dirty = was_dirty || dirty;
});

View File

@ -172,7 +172,6 @@ impl DomScene {
dom.set_style_or_warn("position", "absolute");
dom.set_style_or_warn("top", "0px");
dom.set_style_or_warn("overflow", "hidden");
dom.set_style_or_warn("overflow", "hidden");
dom.set_style_or_warn("width", "100%");
dom.set_style_or_warn("height", "100%");
// We ignore pointer events to avoid stealing them from other DomScenes.

View File

@ -434,6 +434,9 @@ pub struct LayerModel {
scissor_box: RefCell<Option<ScissorBox>>,
mem_mark: Rc<()>,
pub flags: LayerFlags,
/// When [`display::object::ENABLE_DOM_DEBUG`] is enabled all display objects on this layer
/// will be represented by a DOM in this DOM layer.
pub debug_dom: Option<enso_web::HtmlDivElement>,
}
impl Debug for LayerModel {
@ -451,6 +454,9 @@ impl Debug for LayerModel {
impl Drop for LayerModel {
fn drop(&mut self) {
self.remove_from_parent();
if let Some(dom) = &mut self.debug_dom {
dom.remove();
}
}
}
@ -461,6 +467,27 @@ impl LayerModel {
let on_mut = on_depth_order_dirty(&parent);
let depth_order_dirty = dirty::SharedBool::new(on_mut);
let sublayers = Sublayers::new(&parent);
let mut debug_dom = default();
if display::object::ENABLE_DOM_DEBUG {
use enso_web::prelude::*;
if let Some(document) = enso_web::window.document() {
let root = document.get_html_element_by_id("debug-root").unwrap_or_else(|| {
let root = document.create_html_element_or_panic("div");
root.set_id("debug-root");
root.set_style_or_warn("z-index", "100");
document.body().unwrap().append_child(&root).unwrap();
root
});
let dom = document.create_div_or_panic();
dom.set_class_name("debug-layer");
dom.set_attribute_or_warn("data-layer-name", &name);
root.append_child(&dom).unwrap();
debug_dom = Some(dom);
}
}
Self {
name,
depth_order_dirty,
@ -479,6 +506,7 @@ impl LayerModel {
mask: default(),
scissor_box: default(),
mem_mark: default(),
debug_dom,
}
}
@ -677,6 +705,41 @@ impl LayerModel {
was_dirty
}
/// Update this layer's DOM debug object with current camera transform.
pub(crate) fn update_debug_view(&self) {
if !display::object::ENABLE_DOM_DEBUG {
return;
}
use display::camera::camera2d::Projection;
use enso_web::prelude::*;
use std::fmt::Write;
let Some(dom) = &self.debug_dom else { return };
let camera = self.camera.borrow();
let trans_cam = camera.transformation_matrix().try_inverse();
let mut trans_cam = trans_cam.expect("Camera's matrix is not invertible.");
let half_dim_y = camera.screen().height / 2.0;
let fovy_slope = camera.half_fovy_slope();
let near = half_dim_y / fovy_slope;
match camera.projection() {
Projection::Perspective { .. } => {
trans_cam.prepend_translation_mut(&Vector3(0.0, 0.0, near));
}
Projection::Orthographic => {}
}
trans_cam.append_nonuniform_scaling_mut(&Vector3(1.0, -1.0, 1.0));
let mut transform = String::with_capacity(100);
let (first, rest) = trans_cam.as_slice().split_first().unwrap();
write!(transform, "perspective({near}px) matrix3d({first:.4}").unwrap();
rest.iter().for_each(|f| write!(transform, ",{f:.4}").unwrap());
transform.write_str(")").unwrap();
dom.set_style_or_warn("transform", transform);
}
/// Compute a combined [`DependencyGraph`] for the layer taking into consideration the global
/// dependency graph (from root [`Layer`]), the local one (per layer), and individual shape
/// preferences (see the "Compile Time Shapes Ordering Relations" section in docs of [`Layer`]

View File

@ -137,6 +137,11 @@ pub trait Shape: 'static + Sized + AsRef<Self::InstanceParams> {
fn flavor(_data: &Self::ShapeData) -> ShapeSystemFlavor {
ShapeSystemFlavor { flavor: 0 }
}
fn enable_dom_debug() -> bool {
// By default, only shapes with alignment compatible with layout are enabled for DOM debug.
// Otherwise, due to different shape offsets, the debug view is very confusing.
Self::default_alignment() == alignment::Dim2::left_bottom()
}
}
/// An alias for [`Shape]` where [`Shape::ShapeData`] is [`Default`].
@ -323,7 +328,9 @@ impl<S: Shape> ShapeSystem<S> {
let instance_id = sprite.instance_id;
let global_id = sprite.global_instance_id;
let shape = S::new_instance_params(&self.gpu_params, instance_id);
let display_object = display::object::Instance::new_named("ShapeSystem");
let debug = S::enable_dom_debug();
let display_object =
display::object::Instance::new_named_with_debug(type_name::<S>(), debug);
display_object.add_child(&sprite);
// FIXME: workaround:
// display_object.use_auto_layout();

View File

@ -564,7 +564,7 @@ impl SymbolData {
let bindings = default();
let stats = SymbolStats::new(stats);
let context = default();
let display_object = display::object::Instance::new();
let display_object = display::object::Instance::new_no_debug();
let is_hidden = Rc::new(Cell::new(false));
let instance_scope = surface.instance_scope();

View File

@ -126,7 +126,7 @@ pub struct SizedObject {
impl SizedObject {
fn new(attr: Attribute<Vector2<f32>>, transform: &Attribute<Matrix4<f32>>) -> Self {
let size = Size::new(attr);
let display_object = display::object::Instance::new_named("Sprite");
let display_object = display::object::Instance::new_named_no_debug("Sprite");
let weak_display_object = display_object.downgrade();
let network = &display_object.network;
frp::extend! { network

View File

@ -152,7 +152,7 @@ impl<S: Shape> Drop for ShapeViewModel<S> {
impl<S: Shape> ShapeViewModel<S> {
/// Constructor.
pub fn new_with_data(data: S::ShapeData) -> Self {
fn new_with_data(data: S::ShapeData) -> Self {
let (shape, _) = world::with_context(|t| t.layers.DETACHED.instantiate(&data, default()));
let events_deprecated = PointerTarget_DEPRECATED::new();
let pointer_targets = default();

View File

@ -210,8 +210,9 @@ crate::define_endpoints_2! {
scene_position (Vector3),
/// Change between the current and the previous scene position.
scene_position_delta (Vector3),
start_drag (),
start_drag(),
stop_drag(),
is_dragging(bool),
}
}
@ -239,15 +240,15 @@ impl CursorModel {
/// Constructor.
pub fn new(scene: &Scene, frp: WeakFrp) -> Self {
let scene = scene.clone_ref();
let display_object = display::object::Instance::new();
let dragged_elem = display::object::Instance::new();
let display_object = display::object::Instance::new_no_debug();
let dragged_elem = display::object::Instance::new_named("dragged_elem");
let view = shape::View::new();
let port_selection = shape::View::new();
let style = default();
display_object.add_child(&view);
display_object.add_child(&port_selection);
view.add_child(&dragged_elem);
scene.add_child(&dragged_elem);
let tgt_layer = &scene.layers.cursor;
let port_selection_layer = &scene.layers.port_selection;
tgt_layer.add(&view);
@ -563,7 +564,7 @@ impl Cursor {
eval position ((t) model.display_object.set_position(*t));
eval front_color ((t) model.view.color.set(t.into()));
eval port_selection_color ((t) model.port_selection.color.set(t.into()));
eval_ screen_position (model.update_drag_position());
// === Outputs ===
@ -571,6 +572,7 @@ impl Cursor {
frp.private.output.screen_position <+ screen_position;
frp.private.output.scene_position <+ scene_position;
frp.private.output.scene_position_delta <+ scene_position_delta;
frp.private.output.is_dragging <+ bool(&frp.stop_drag, &frp.start_drag);
}
// Hide on init.
@ -592,21 +594,34 @@ impl CursorModel {
warn!("Can't start dragging an item because another item is already being dragged.");
} else {
let object = target.display_object().clone();
self.dragged_elem.add_child(&object);
let target_position = object.global_position().xy();
let cursor_position = self.frp.scene_position.value().xy();
object.set_xy(target_position - cursor_position);
if let Some(object_layer) = object.display_layer() {
object_layer.add(&self.dragged_elem);
}
let scene = scene();
let camera = scene.camera();
let zoom = camera.zoom();
self.dragged_elem.set_scale_xy((zoom, zoom));
let screen_pos = self.frp.screen_position.value().xy();
let offset = scene.screen_to_object_space(&object, screen_pos);
object.set_xy(-offset);
self.dragged_elem.add_child(&object);
*self.dragged_item.borrow_mut() = Some((Box::new(target), object));
self.frp.private.output.start_drag.emit(());
}
}
fn update_drag_position(&self) {
if self.dragged_item.borrow().is_some() {
let scene = scene();
let layer = self.dragged_elem.display_layer();
let camera = layer.map_or(scene.camera(), |l| l.camera());
let screen_pos = self.frp.screen_position.value().xy() / camera.zoom();
let pos_in_layer = camera.inversed_view_matrix() * screen_pos.push(0.0).push(1.0);
self.dragged_elem.set_xy(pos_in_layer.xy());
}
}
/// Remove the dragged item and return it as [`Any`]. If you want to retrieve the item if it is
/// of a particular type, use the [`Self::stop_drag_if_is`] method instead.
pub fn stop_drag(&self) -> Option<Box<dyn Any>> {

View File

@ -537,6 +537,7 @@ mock_data! { Document => EventTarget
// === Window ===
mock_data! { Window => EventTarget
fn document(&self) -> Option<Document>;
fn open_with_url_and_target(&self, url: &str, target: &str)
-> Result<Option<Window>, JsValue>;
fn request_animation_frame(&self, callback: &Function) -> Result<i32, JsValue>;
@ -687,6 +688,11 @@ impl From<HtmlDivElement> for EventTarget {
default()
}
}
impl PartialEq<HtmlDivElement> for HtmlDivElement {
fn eq(&self, _: &HtmlDivElement) -> bool {
true
}
}
// === HtmlDivElement ===

View File

@ -664,10 +664,8 @@ ops! { HtmlElementOps for HtmlElement
fn set_style_or_warn(&self, name: impl AsRef<str>, value: impl AsRef<str>) {
let name = name.as_ref();
let value = value.as_ref();
let values = format!("\"{name}\" = \"{value}\" on \"{self:?}\"");
let warn_msg: &str = &format!("Failed to set style {values}");
if self.style().set_property(name, value).is_err() {
warn!("{warn_msg}");
warn!("Failed to set style \"{name}\" = \"{value}\" on \"{self:?}\"");
}
}
}