Decouple node edit mode from ports (#5983)

Implements #5919

Apart from some fixed glitches, no visual differences are present. This is mostly a refactor.

- Decoupled node edit mode code from existing port implementation, so ports can easily be replaced in the near future without affecting edit functionality.
- Connected ports and widgets are now always hidden in edit mode. Previously in some situations the colored shapes were incorrectly displayed at wrong positions during editing.
- When entering edit mode, the text cursor is placed at the correct location corresponding to clicked code, compensating for shift introduced by argument placeholders.

# Important Notes
There is a remaining known issue with incoming edges being placed at incorrect places during edit mode, sometimes even outside of the node. This issue is also present in develop. It doesn't make sense to resolve it now, as we are planning to rewrite the ports tree very soon. It will be fixed with that rewrite.
This commit is contained in:
Paweł Grabarz 2023-03-29 13:16:31 +02:00 committed by GitHub
parent a9dbebf3f3
commit 99a6f8f2f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 239 additions and 142 deletions

View File

@ -528,6 +528,7 @@ fn generate_node_for_prefix_chain<T: Payload>(
context: &impl Context,
) -> FallibleResult<Node<T>> {
let app_base = ApplicationBase::from_prefix_chain(this);
let fallback_call_id = app_base.call_id;
let mut application = app_base.resolve(context);
// When using method notation, expand the infix access chain manually to maintain correct method
@ -585,6 +586,8 @@ fn generate_node_for_prefix_chain<T: Payload>(
if let Some((index, info)) = info {
arg_kind.set_argument_info(info);
arg_kind.set_definition_index(index);
} else {
arg_kind.set_call_id(fallback_call_id);
}
if !resolved {

View File

@ -229,6 +229,25 @@ impl Kind {
}
}
/// Call ID setter. Returns bool indicating whether the operation was possible.
pub fn set_call_id(&mut self, call_id: Option<ast::Id>) -> bool {
match self {
Self::Chained(t) => {
t.call_id = call_id;
true
}
Self::Argument(t) => {
t.call_id = call_id;
true
}
Self::InsertionPoint(t) => {
t.call_id = call_id;
true
}
_ => false,
}
}
/// Short string representation. Skips the inner fields and returns only the variant name.
pub fn variant_name(&self) -> &str {
match self {

View File

@ -143,12 +143,6 @@ impl Model {
);
}
/// Node expression was edited in the view. Should be called whenever the user changes the
/// contents of a node during editing.
fn node_expression_set(&self, id: ViewNodeId, expression: ImString) {
self.state.update_from_view().set_node_expression(id, expression);
}
/// Update a part of node expression under specific span tree crumbs. Preserves identity of
/// unaffected parts of the expression.
fn node_expression_span_set(
@ -709,7 +703,6 @@ impl Graph {
eval view.on_edge_endpoint_unset(((edge_id,_)) model.connection_removed(*edge_id));
eval view.nodes_collapsed(((nodes, _)) model.nodes_collapsed(nodes));
eval view.enabled_visualization_path(((node_id, path)) model.node_visualization_changed(*node_id, path.clone()));
eval view.node_expression_set(((node_id, expression)) model.node_expression_set(*node_id, expression.clone_ref()));
eval view.node_expression_span_set(((node_id, crumbs, expression)) model.node_expression_span_set(*node_id, crumbs, expression.clone_ref()));
eval view.node_action_skip(((node_id, enabled)) model.node_action_skip(*node_id, *enabled));
eval view.node_action_freeze(((node_id, enabled)) model.node_action_freeze(*node_id, *enabled));

View File

@ -328,12 +328,15 @@ ensogl::define_endpoints_2! {
/// Press event. Emitted when user clicks on non-active part of the node, like its
/// background. In edit mode, the whole node area is considered non-active.
background_press (),
/// Emitted when node expression is modified as a whole. Does not include partial changes on
/// individual spans, which are emitted via `expression_span` output.
expression (ImString),
/// Emitted when node expression is edited in context of specific span. Does not include
/// changes to the expression as a whole, which are emitted via `expression` output.
expression_span (span_tree::Crumbs, ImString),
/// This event occurs when the user modifies an expression, either by typing directly or
/// using a widget. It includes information about the specific part of the expression that
/// was changed and where it fits within the larger expression.
///
/// Note: Node component is not able to perform the actual modification of the expression,
/// as that requires rebuilding the span-tree, which in turn requires access to the
/// execution context. It is the responsibility of the parent component to apply the changes
/// and update the node with new expression tree using `set_expression`.
on_expression_modified (span_tree::Crumbs, ImString),
comment (Comment),
skip (bool),
freeze (bool),
@ -752,10 +755,9 @@ impl Node {
eval filtered_usage_type (((a,b)) model.set_expression_usage_type(a,b));
eval input.set_expression ((a) model.set_expression(a));
model.input.edit_expression <+ input.edit_expression;
out.expression <+ model.input.frp.expression;
out.expression_span <+ model.input.frp.on_port_code_update;
out.requested_widgets <+ model.input.frp.requested_widgets;
out.request_import <+ model.input.frp.request_import;
out.on_expression_modified <+ model.input.frp.on_port_code_update;
out.requested_widgets <+ model.input.frp.requested_widgets;
out.request_import <+ model.input.frp.request_import;
model.input.set_connected <+ input.set_input_connected;
model.input.set_disabled <+ input.set_disabled;

View File

@ -32,6 +32,24 @@ impl Expression {
let whole_expression_id = default();
Self { pattern, code, whole_expression_id, input_span_tree, output_span_tree }
}
/// Get the expression code with given span replaced with provided string. Does not modify the
/// existing expression.
pub fn code_with_replaced_span(
&self,
crumbs: &span_tree::Crumbs,
replacement: &str,
) -> ImString {
if let Ok(span_ref) = self.input_span_tree.get_node(crumbs) {
let span = span_ref.span();
let byte_range = span.start.value..span.end.value;
let mut code = self.code.to_string();
code.replace_range(byte_range, replacement);
code.into()
} else {
self.code.clone()
}
}
}
impl Display for Expression {

View File

@ -204,16 +204,21 @@ impl From<node::Expression> for Expression {
/// Internal model of the port area.
#[derive(Debug)]
pub struct Model {
app: Application,
display_object: display::object::Instance,
ports: display::object::Instance,
header: display::object::Instance,
label: text::Text,
expression: RefCell<Expression>,
id_crumbs_map: RefCell<HashMap<ast::Id, Crumbs>>,
widgets_map: RefCell<HashMap<WidgetBind, Crumbs>>,
styles: StyleWatch,
styles_frp: StyleWatchFrp,
app: Application,
display_object: display::object::Instance,
ports: display::object::Instance,
header: display::object::Instance,
/// Text label used for displaying the ports. Contains both expression text and inserted
/// argument placeholders. The style is adjusted based on port types.
ports_label: text::Text,
/// Text label used during edit mode. Contains only the expression text without any
/// modifications. Handles user input in edit mode.
edit_mode_label: text::Text,
expression: RefCell<Expression>,
id_crumbs_map: RefCell<HashMap<ast::Id, Crumbs>>,
widgets_map: RefCell<HashMap<WidgetBind, Crumbs>>,
styles: StyleWatch,
styles_frp: StyleWatchFrp,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
@ -230,13 +235,13 @@ impl Model {
let ports = display::object::Instance::new();
let header = display::object::Instance::new();
let app = app.clone_ref();
let label = app.new_view::<text::Text>();
let edit_mode_label = app.new_view::<text::Text>();
let ports_label = app.new_view::<text::Text>();
let id_crumbs_map = default();
let expression = default();
let styles = StyleWatch::new(&app.display.default_scene.style_sheet);
let styles_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let widgets_map = default();
display_object.add_child(&label);
display_object.add_child(&ports);
ports.add_child(&header);
Self {
@ -244,7 +249,8 @@ impl Model {
display_object,
ports,
header,
label,
edit_mode_label,
ports_label,
expression,
id_crumbs_map,
widgets_map,
@ -254,28 +260,84 @@ impl Model {
.init()
}
/// React to edit mode change. Shows and hides appropriate child views according to current
/// mode. Sets cursor position when entering edit mode.
pub fn set_edit_mode(&self, edit_mode_active: bool) {
if edit_mode_active {
// When transitioning to edit mode, we need to find the code location that corresponds
// to the code at mouse position. First we search for the port at that position, then
// find the right character index within that port.
let expression = self.expression.borrow();
let clicked_label_location = self.ports_label.location_at_mouse_position();
let clicked_char_index =
expression.viz_code.char_indices().nth(clicked_label_location.offset.into());
let location_to_set = clicked_char_index.and_then(|char_index| {
let loc_offset = char_index.0.byte().to_diff();
let clicked_port = expression.span_tree.root_ref().leaf_iter().find(|node| {
let range = node.payload.range();
range.contains(&loc_offset)
})?;
let byte_offset_within_port = loc_offset - clicked_port.payload.index;
let byte_offset_within_port = byte_offset_within_port.min(clicked_port.size);
let final_code_byte_offset = clicked_port.span_offset + byte_offset_within_port;
let final_code_column: Column =
expression.code[..final_code_byte_offset.into()].chars().count().into();
let final_code_location = clicked_label_location.with_offset(final_code_column);
Some(final_code_location)
});
self.edit_mode_label.set_content(expression.code.clone());
self.display_object.remove_child(&self.ports);
self.display_object.remove_child(&self.ports_label);
self.display_object.add_child(&self.edit_mode_label);
if let Some(location) = location_to_set {
self.edit_mode_label.set_cursor(location);
} else {
// If we were unable to find a port under current mouse position, set the edit label
// cursor at the mouse position immediately after setting its content to the raw
// expression code.
self.edit_mode_label.set_cursor_at_mouse_position();
}
} else {
self.display_object.remove_child(&self.edit_mode_label);
self.display_object.add_child(&self.ports);
self.display_object.add_child(&self.ports_label);
// When we exit the edit mode, clear the label. That way we don't have any extra glyphs
// to process during rendering in non-edit mode.
self.edit_mode_label.set_content("");
}
self.edit_mode_label.deprecated_set_focus(edit_mode_active);
}
#[profile(Debug)]
fn init(self) -> Self {
// TODO: Depth sorting of labels to in front of the mouse pointer. Temporary solution.
// It needs to be more flexible once we have proper depth management.
// See https://www.pivotaltracker.com/story/show/183567632.
let scene = &self.app.display.default_scene;
scene.layers.main.remove(&self.label);
self.label.add_to_scene_layer(&scene.layers.label);
self.set_label_layer(&scene.layers.label);
let text_color = self.styles.get_color(theme::graph_editor::node::text);
self.label.set_single_line_mode(true);
self.label.disable_command("cursor_move_up");
self.label.disable_command("cursor_move_down");
self.label.disable_command("add_cursor_at_mouse_position");
self.label.set_property_default(text_color);
self.label.set_property_default(text::Size(TEXT_SIZE));
self.label.remove_all_cursors();
self.ports_label.set_property_default(text_color);
self.ports_label.set_property_default(text::Size(TEXT_SIZE));
let origin = Vector2(TEXT_OFFSET, 0.0);
self.ports.set_xy(origin);
self.label.set_xy(origin);
self.label.modify_position(|t| t.y += TEXT_SIZE / 2.0);
self.edit_mode_label.set_single_line_mode(true);
self.edit_mode_label.disable_command("cursor_move_up");
self.edit_mode_label.disable_command("cursor_move_down");
self.edit_mode_label.disable_command("add_cursor_at_mouse_position");
self.edit_mode_label.set_property_default(text_color);
self.edit_mode_label.set_property_default(text::Size(TEXT_SIZE));
self.edit_mode_label.remove_all_cursors();
let ports_origin = Vector2(TEXT_OFFSET, 0.0);
let label_origin = Vector2(TEXT_OFFSET, TEXT_SIZE / 2.0);
self.ports.set_xy(ports_origin);
self.ports_label.set_xy(label_origin);
self.edit_mode_label.set_xy(label_origin);
self.set_edit_mode(false);
self
}
@ -290,7 +352,8 @@ impl Model {
fn set_label_layer(&self, layer: &display::scene::Layer) {
self.label.add_to_scene_layer(layer);
self.edit_mode_label.add_to_scene_layer(layer);
self.ports_label.add_to_scene_layer(layer);
}
/// Run the provided function on the target port if exists.
@ -337,26 +400,9 @@ impl Model {
}
}
/// Set the visibility of all widgets in this input area. This is only a visual change, and does
/// not affect the widget's state. Widget updates are still processed when the widget is hidden.
fn set_widgets_visibility(&self, visible: bool) {
let expression = self.expression.borrow();
let widgets_map = self.widgets_map.borrow();
for (id, crumbs) in widgets_map.iter() {
let root = expression.span_tree.root_ref();
let port = root.get_descendant(crumbs).ok();
let widget = port.and_then(|port| port.payload.widget.clone_ref());
if let Some(widget) = widget {
widget.set_visible(visible);
} else {
error!("Widget {id:?} not found for crumbs {crumbs:?}.");
}
}
}
#[profile(Debug)]
fn set_label_on_new_expression(&self, expression: &Expression) {
self.label.set_content(expression.viz_code.clone());
self.ports_label.set_content(expression.viz_code.clone());
}
#[profile(Debug)]
@ -519,7 +565,11 @@ impl Model {
pointer_style_over <- pointer_style_over.sample(&mouse_over);
pointer_style_hover <- any(pointer_style_over,pointer_style_out);
pointer_styles <- all[pointer_style_hover,self.label.pointer_style];
pointer_styles <- all[
pointer_style_hover,
self.ports_label.pointer_style,
self.edit_mode_label.pointer_style
];
pointer_style <- pointer_styles.fold();
area_frp.source.pointer_style <+ pointer_style;
}
@ -538,6 +588,7 @@ impl Model {
} else {
widget.set_current_value(None);
}
widget.set_visible(true);
let port_network = &port.network;
frp::extend! { port_network
@ -726,7 +777,7 @@ impl Model {
let index = node.payload.index;
let length = node.payload.length;
let label = model.label.clone_ref();
let label = model.ports_label.clone_ref();
frp::extend! { port_network
eval label_color ([label](color) {
let range = enso_text::Range::new(index, index + length);
@ -814,12 +865,7 @@ impl Model {
/// Set a displayed expression, updating the input ports. `is_editing` indicates whether the
/// expression is being edited by the user.
#[profile(Debug)]
fn set_expression(
&self,
new_expression: impl Into<node::Expression>,
is_editing: bool,
area_frp: &FrpEndpoints,
) -> Expression {
fn set_expression(&self, new_expression: impl Into<node::Expression>, area_frp: &FrpEndpoints) {
let mut new_expression = Expression::from(new_expression.into());
if DEBUG {
debug!("set expression: \n{:?}", new_expression.tree_pretty_printer());
@ -830,11 +876,6 @@ impl Model {
self.build_port_shapes_on_new_expression(&mut new_expression, area_frp, &call_info);
self.init_port_frp_on_new_expression(&mut new_expression, area_frp);
self.init_new_expression(new_expression.clone(), area_frp, &call_info);
if is_editing {
self.label.set_cursor_at_text_end();
}
self.set_widgets_visibility(!is_editing);
new_expression
}
}
@ -900,7 +941,6 @@ ensogl::define_endpoints! {
Output {
pointer_style (cursor::Style),
width (f32),
expression (ImString),
expression_edit (ImString, Vec<Selection<Byte>>),
editing (bool),
@ -915,8 +955,8 @@ ensogl::define_endpoints! {
/// A set of widgets attached to a method requires metadata to be queried. The tuple
/// contains the ID of the call expression the widget is attached to, and the ID of that
/// call's target expression (`self` or first argument).
requested_widgets (ast::Id, ast::Id),
request_import (ImString),
requested_widgets (ast::Id, ast::Id),
request_import (ImString),
}
}
@ -955,6 +995,8 @@ impl Area {
let selection_color = Animation::new(network);
frp::extend! { network
init <- source::<()>();
set_editing <- all(frp.set_editing, init)._0();
// === Body Hover ===
// This is meant to be on top of FRP network. Read more about `Node` docs to
@ -966,42 +1008,41 @@ impl Area {
// === Cursor setup ===
let edit_mode = frp.input.set_editing.clone_ref();
let on_background_press = frp.output.on_background_press.clone_ref();
model.label.set_cursor_at_mouse_position <+ on_background_press.gate(&edit_mode);
eval edit_mode([model](edit_mode) {
model.label.deprecated_set_focus(edit_mode);
model.set_widgets_visibility(!edit_mode);
if *edit_mode {
// Reset the code to hide non-connected port names.
model.label.set_content(model.expression.borrow().code.clone());
model.label.set_cursor_at_mouse_position();
} else {
model.label.remove_all_cursors();
}
eval set_editing((is_editing) model.set_edit_mode(*is_editing));
// Prevent text selection from being created right after entering edit mode. Otherwise,
// a selection would be created between the current mouse position (the position at
// which we clicked) and initial cursor position within edit mode label (the code
// position corresponding to clicked port).
start_editing <- set_editing.on_true();
stop_editing <- set_editing.on_false();
start_editing_delayed <- start_editing.debounce();
reenable_selection_update <- any(&start_editing_delayed, &stop_editing);
selection_update_enabled <- bool(&start_editing, &reenable_selection_update);
eval selection_update_enabled([model] (enabled) {
let cmd_start = "start_newest_selection_end_follow_mouse";
let cmd_stop = "stop_newest_selection_end_follow_mouse";
model.edit_mode_label.set_command_enabled(cmd_start, *enabled);
model.edit_mode_label.set_command_enabled(cmd_stop, *enabled);
});
// === Show / Hide Phantom Ports ===
edit_ready_mode <- all_with3
( &frp.input.set_editing
, &frp.input.set_edit_ready_mode
, &frp.input.set_ports_active
, |editing, edit_ready_mode, (set_ports_active, _)|
(*editing || *edit_ready_mode) && !set_ports_active
);
port_vis <- all_with(&frp.input.set_ports_active,&edit_ready_mode,|(a,_),b|*a&&(!b));
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);
frp.output.source.ports_visible <+ port_vis;
frp.output.source.editing <+ frp.set_editing;
frp.output.source.editing <+ set_editing;
// === Label Hover ===
label_hovered <- edit_ready_mode && frp.output.body_hover;
eval label_hovered ((t) model.label.set_hover(t));
label_hovered <- reacts_to_hover && frp.output.body_hover;
not_editing <- set_editing.not();
model.ports_label.set_hover <+ label_hovered && not_editing;
model.edit_mode_label.set_hover <+ label_hovered && set_editing;
// === Port Hover ===
@ -1015,33 +1056,34 @@ impl Area {
// === Properties ===
width <- model.label.width.map(|t| t + 2.0 * TEXT_OFFSET);
frp.output.source.width <+ width;
label_width <- set_editing.switch(
&model.ports_label.width,
&model.edit_mode_label.width
);
frp.output.source.width <+ label_width.map(|t| t + 2.0 * TEXT_OFFSET);
// === Expression ===
let frp_endpoints = &frp.output;
expression <- frp.input.set_expression.map2(
&frp.input.set_editing, f!([frp_endpoints, model](expr, is_editing)
model.set_expression(expr, *is_editing, &frp_endpoints)
)
);
legit_edit <- frp.input.edit_expression.gate(&frp.input.set_editing);
model.label.select <+ legit_edit.map(|(range, _)| (range.start.into(), range.end.into()));
model.label.insert <+ legit_edit._1();
frp.output.source.expression <+ expression.map(|e| e.code.clone_ref());
expression_changed_by_user <- model.label.content.gate(&frp.input.set_editing);
frp.output.source.expression <+ expression_changed_by_user.ref_into();
frp.output.source.expression_edit <+ model.label.selections.map2(
eval frp.set_expression([frp_endpoints, model](expr) model.set_expression(expr, &frp_endpoints));
legit_edit <- frp.input.edit_expression.gate(&set_editing);
model.edit_mode_label.select <+ legit_edit.map(|(range, _)| (range.start.into(), range.end.into()));
model.edit_mode_label.insert <+ legit_edit._1();
expression_changed_by_user <- model.edit_mode_label.content.gate(&set_editing);
frp.output.source.expression_edit <+ model.edit_mode_label.selections.map2(
&expression_changed_by_user,
f!([model](selection, full_content) {
let full_content = full_content.into();
let to_byte = |loc| text::Byte::from_in_context_snapped(&model.label, loc);
let to_byte = |loc| text::Byte::from_in_context_snapped(&model.edit_mode_label, loc);
let selections = selection.iter().map(|sel| sel.map(to_byte)).collect_vec();
(full_content, selections)
})
);
frp.output.source.on_port_code_update <+ expression_changed_by_user.map(|e| {
// Treat edit mode update as a code modification at the span tree root.
(default(), e.into())
});
// === Expression Type ===
@ -1069,16 +1111,16 @@ impl Area {
selection_color_rgba <- profiled.switch(&std_selection_color,&profiled_selection_color);
selection_color.target <+ selection_color_rgba.map(|c| color::Lcha::from(c));
model.label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c));
model.ports_label.set_selection_color <+ selection_color.value.map(|c| color::Lch::from(c));
init_colors <- source::<()>();
std_base_color <- all(std_base_color,init_colors)._0();
profiled_base_color <- all(profiled_base_color,init_colors)._0();
std_base_color <- all(std_base_color,init)._0();
profiled_base_color <- all(profiled_base_color,init)._0();
base_color <- profiled.switch(&std_base_color,&profiled_base_color);
eval base_color ((color) model.label.set_property_default(color));
init_colors.emit(());
eval base_color ((color) model.ports_label.set_property_default(color));
}
init.emit(());
Self { frp, model }
}

View File

@ -1591,9 +1591,18 @@ impl GraphEditorModelWithNetwork {
)
));
eval node.expression((t) model.frp.private.output.node_expression_set.emit((node_id,t.into())));
let is_editing = &node_model.input.frp.editing;
expression_change_temporary <- node.on_expression_modified.gate(is_editing);
expression_change_permanent <- node.on_expression_modified.gate_not(is_editing);
eval node.expression_span([model]((crumbs,code)) {
temporary_expression <- expression_change_temporary.map2(
&node_model.input.set_expression,
move |(crumbs, code), expr| expr.code_with_replaced_span(crumbs, code)
);
eval temporary_expression([model] (code) {
model.frp.private.output.node_expression_set.emit((node_id, code));
});
eval expression_change_permanent([model]((crumbs,code)) {
let args = (node_id, crumbs.clone(), code.clone());
model.frp.private.output.node_expression_span_set.emit(args)
});

View File

@ -386,6 +386,8 @@ impl View {
let shape = scene.shape().clone_ref();
frp::extend! { network
init <- source::<()>();
shape <- all(shape, init)._0();
eval shape ((shape) model.on_dom_shape_changed(shape));
eval_ frp.show_graph_editor(model.show_graph_editor());
@ -488,7 +490,7 @@ impl View {
(searcher.as_ref()?.input == *node_id).then(input_change)
}
);
searcher_input_change <- searcher_input_change_opt.filter_map(|change| change.clone());
searcher_input_change <- searcher_input_change_opt.unwrap();
input_change_delay.restart <+ searcher_input_change.constant(INPUT_CHANGE_DELAY_MS);
update_searcher_input_on_commit <- frp.output.editing_committed.constant(());
input_change_delay.cancel <+ update_searcher_input_on_commit;
@ -593,8 +595,6 @@ impl View {
}
});
init <- source::<()>();
// === Disabling Navigation ===
let documentation = &model.searcher.model().documentation;

View File

@ -7,6 +7,7 @@ use enso_gui::integration_test::prelude::*;
use approx::assert_abs_diff_eq;
use enso_frp::future::FutureEvent;
use enso_frp::io::mouse::PrimaryButton;
use enso_frp::stream::ValueProvider;
use enso_gui::view::graph_editor::component::node as node_view;
use enso_gui::view::graph_editor::component::node::test_utils::NodeModelExt;
use enso_gui::view::graph_editor::GraphEditor;
@ -38,7 +39,7 @@ async fn create_new_project_and_add_nodes() {
let added_node =
graph_editor.nodes().get_cloned_ref(&added_node_id).expect("Added node is not added");
assert_eq!(added_node.view.expression.value().to_string(), "");
assert_eq!(added_node.view.set_expression.value().code, "");
}
#[wasm_bindgen_test]

View File

@ -479,6 +479,15 @@ impl Text {
}
}
/// Get current text location under the mouse cursor within this text area.
pub fn location_at_mouse_position(&self) -> Location {
let m = &self.data;
let scene = &m.app.display.default_scene;
let mouse = &scene.mouse.frp_deprecated;
let position = mouse.position.value();
m.screen_to_text_location(position)
}
fn init_selections(&self) {
let m = &self.data;
let scene = &m.app.display.default_scene;

View File

@ -62,13 +62,19 @@ pub trait View: FrpNetworkProvider + DerefToCommandApi {
/// Disable the command in this component instance.
fn disable_command(&self, name: impl AsRef<str>)
where Self: Sized {
self.app().commands.disable_command(self, name)
self.set_command_enabled(name, false)
}
/// Enable the command in this component instance.
fn enable_command(&self, name: impl AsRef<str>)
where Self: Sized {
self.app().commands.enable_command(self, name)
self.set_command_enabled(name, true)
}
/// Set the command enable status in this component instance.
fn set_command_enabled(&self, name: impl AsRef<str>, enabled: bool)
where Self: Sized {
self.app().commands.set_command_enabled(self, name, enabled)
}
}
@ -225,13 +231,8 @@ impl Registry {
}
}
/// Disables the command for the provided component instance.
fn disable_command<T: View>(&self, instance: &T, name: impl AsRef<str>) {
self.with_command_mut(instance, name, |command| command.enabled = false)
}
/// Enables the command for the provided component instance.
fn enable_command<T: View>(&self, instance: &T, name: impl AsRef<str>) {
self.with_command_mut(instance, name, |command| command.enabled = true)
/// Sets the command enable status for the provided component instance.
fn set_command_enabled<T: View>(&self, instance: &T, name: impl AsRef<str>, enabled: bool) {
self.with_command_mut(instance, name, |command| command.enabled = enabled)
}
}

View File

@ -1793,7 +1793,7 @@ impl<T: EventOutput> OwnedTrace<T> {
impl<T: EventOutput> stream::EventConsumer<Output<T>> for OwnedTrace<T> {
fn on_event(&self, stack: CallStack, event: &Output<T>) {
warn!("[FRP] {}: {:?}", self.label(), event);
debug!("[FRP] {}: {:?}", self.label(), event);
debug!("[FRP] {}", stack);
self.emit_event(stack, event);
}