Fix list editor panics during insertion (#6540)

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

37
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -40,6 +40,10 @@ pub struct Node<T> {
pub size: ByteDiff,
pub children: Vec<Child<T>>,
pub ast_id: Option<ast::Id>,
/// 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).
pub extended_ast_id: Option<ast::Id>,
pub parenthesized: bool,
pub payload: T,
}
@ -74,8 +78,9 @@ impl<T> Node<T> {
let size = self.size;
let children = self.children.into_iter().map(|t| t.map(f)).collect_vec();
let ast_id = self.ast_id;
let extended_ast_id = self.extended_ast_id;
let payload = f(self.payload);
Node { kind, parenthesized, size, children, ast_id, payload }
Node { kind, parenthesized, size, children, ast_id, extended_ast_id, payload }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -365,7 +365,6 @@ ensogl::define_endpoints! {
on_port_press (Crumbs),
on_port_hover (Switch<Crumbs>),
on_port_code_update (Crumbs,ImString),
on_background_press (),
view_mode (view::Mode),
/// A set of widgets attached to a method requests their definitions to be queried from an
/// external source. The tuple contains the ID of the call expression the widget is attached
@ -435,10 +434,11 @@ 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(&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;
model.widget_tree.set_edit_ready_mode <+ frp.set_edit_ready_mode;
refresh_edges <- model.widget_tree.connected_port_updated.debounce();
frp.output.source.input_edges_need_refresh <+ refresh_edges;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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