From a587976c4ed07121266c3742d7614a6a01fab1bb Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 9 Mar 2021 22:04:37 +0100 Subject: [PATCH 01/11] feat: Implement tooltips that show the type of an output port on hover. --- CHANGELOG.md | 3 + src/rust/ensogl/lib/components/src/lib.rs | 1 + .../ensogl/lib/components/src/text_label.rs | 198 ++++++++++++++++ .../lib/core/src/animation/frp/animation.rs | 2 + .../src/animation/frp/animation/hysteretic.rs | 116 ++++++++++ .../ensogl/lib/core/src/data/color/data.rs | 8 + .../lib/core/src/data/color/gradient.rs | 4 +- .../src/display/shape/primitive/def/var.rs | 35 +++ src/rust/ensogl/lib/core/src/gui.rs | 1 + src/rust/ensogl/lib/core/src/gui/cursor.rs | 97 +------- src/rust/ensogl/lib/core/src/gui/style.rs | 106 +++++++++ .../ensogl/lib/text/src/component/area.rs | 8 +- .../ide/view/graph-editor/src/component.rs | 3 +- .../view/graph-editor/src/component/node.rs | 6 +- .../src/component/node/output/area.rs | 52 ++--- .../src/component/node/output/port.rs | 7 + .../graph-editor/src/component/tooltip.rs | 219 ++++++++++++++++++ src/rust/ide/view/graph-editor/src/lib.rs | 47 +++- 18 files changed, 761 insertions(+), 152 deletions(-) create mode 100644 src/rust/ensogl/lib/components/src/text_label.rs create mode 100644 src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs create mode 100644 src/rust/ensogl/lib/core/src/gui/style.rs create mode 100644 src/rust/ide/view/graph-editor/src/component/tooltip.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e71f80e5..483274f2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ unnecessary library imports when selecting hints from node searcher. This makes the generated textual code easier to read and reduces likelihood of accidental name collision. +- [Hovering over an output port shows a pop-up with the result type of a node] + [1312]. - [Visualizations can define context for preprocessor evaluation][1291]. Users can now decide what module's context should be used for visualization preprocessor. This allows providing visualization with standard library @@ -187,6 +189,7 @@ you can find their release notes https://www.youtube.com/watch?v=BYUAL4ksEgY&ab_channel=Enso [podcast-future-of-enso]: https://www.youtube.com/watch?v=rF8DuJPOfTs&t=1863s&ab_channel=Enso +[1312]: https://github.com/enso-org/ide/pull/1312
diff --git a/src/rust/ensogl/lib/components/src/lib.rs b/src/rust/ensogl/lib/components/src/lib.rs index ddca4ec4d4..4c572d82e2 100644 --- a/src/rust/ensogl/lib/components/src/lib.rs +++ b/src/rust/ensogl/lib/components/src/lib.rs @@ -18,6 +18,7 @@ pub mod drop_down_menu; pub mod list_view; +pub mod text_label; pub mod toggle_button; /// Commonly used types and functions. diff --git a/src/rust/ensogl/lib/components/src/text_label.rs b/src/rust/ensogl/lib/components/src/text_label.rs new file mode 100644 index 0000000000..6ee1265d96 --- /dev/null +++ b/src/rust/ensogl/lib/components/src/text_label.rs @@ -0,0 +1,198 @@ +//! Text label component. Appears as text with background. + +use crate::prelude::*; + +use enso_frp as frp; +use enso_frp; +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display::shape::*; +use ensogl_core::display::traits::*; +use ensogl_core::display; +use ensogl_text as text; + + +// ========================== +// === Shapes Definitions === +// ========================== + + +// === Constants === + +const TEXT_OFFSET : f32 = 10.0; +const TEXT_SIZE : f32 = 12.0; +const HEIGHT : f32 = TEXT_SIZE * 3.0; +const PADDING : f32 = 15.0; +const SHADOW_SIZE : f32 = 10.0; + + +// === Background === + +mod background { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style,bg_color:Vector4) { + use ensogl_theme::graph_editor::node as node_theme; + + let width = Var::::from("input_size.x"); + let height = Var::::from("input_size.y"); + let width = width - PADDING.px() * 2.0; + let height = height - PADDING.px() * 2.0; + let radius = &height / 2.0; + let shape = Rect((&width,&height)).corners_radius(&radius); + let shape = shape.fill(Var::::from(bg_color.clone())); + + + // === Shadow === + let alpha = Var::::from(format!("({0}.w)",bg_color)); + let border_size_f = 16.0; + let shadow_size = SHADOW_SIZE.px(); + let shadow_width = &width + &shadow_size * 2.0; + let shadow_height = &height + &shadow_size * 2.0; + let shadow_radius = &shadow_height / 2.0; + let shadow = Rect((shadow_width,shadow_height)).corners_radius(shadow_radius); + let base_color = color::Rgba::from(style.get_color(node_theme::shadow)); + let base_color = Var::::from(base_color); + let base_color = base_color.multiply_alpha(&alpha); + let fading_color = color::Rgba::from(style.get_color(node_theme::shadow::fading)); + let fading_color = Var::::from(fading_color); + let fading_color = fading_color.multiply_alpha(&alpha); + let exponent = style.get_number_or(node_theme::shadow::exponent,2.0); + let shadow_color = color::LinearGradient::new() + .add(0.0,fading_color.into_linear()) + .add(1.0,base_color.into_linear()); + let shadow_color = color::SdfSampler::new(shadow_color) + .max_distance(border_size_f) + .slope(color::Slope::Exponent(exponent)); + let shadow = shadow.fill(shadow_color); + + (shadow+shape).into() + } + } +} + + + +// =========== +// === FRP === +// =========== + +ensogl_core::define_endpoints! { + Input { + set_content(String), + set_opacity(f32) + } + Output { + size (Vector2) + } +} + + + +// ============= +// === Model === +// ============= + +#[derive(Clone,Debug)] +struct Model { + background : background::View, + label : text::Area, + display_object : display::object::Instance, + app : Application +} + +impl Model { + fn new(app: Application) -> Self { + let app = app.clone_ref(); + let scene = app.display.scene(); + let logger = Logger::new("TextLabel"); + let display_object = display::object::Instance::new(&logger); + let label = app.new_view::(); + let background = background::View::new(&logger); + display_object.add_child(&background); + display_object.add_child(&label); + + // Depth sorting of labels to in front of the background. + label.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + label.add_to_scene_layer_DEPRECATED(&scene.layers.label); + + Model { label, display_object, background, app } + } + + pub fn height(&self) -> f32 { + HEIGHT + } + + fn set_width(&self, width:f32) -> Vector2 { + let height = self.height(); + let size = Vector2(width*1.25,height); + let padded_size = size + Vector2(PADDING,PADDING) * 2.0; + self.background.size.set(padded_size); + let text_origin = Vector2(PADDING / 2.0 + TEXT_OFFSET - size.x/2.0,TEXT_SIZE/2.0); + self.label.set_position_xy(text_origin); + padded_size + } + + fn set_content(&self, t:&str) -> Vector2 { + self.label.set_content(t); + self.set_width(self.label.width.value()) + } + + fn set_opacity(&self, value:f32) { + let style = StyleWatch::new(&self.app.display.scene().style_sheet); + let text_color_path = ensogl_theme::graph_editor::node::text; + let text_color = style.get_color(text_color_path).multiply_alpha(value); + let text_color = color::Rgba::from(text_color); + self.label.frp.set_color_all.emit(text_color); + self.label.frp.set_default_color.emit(text_color); + + let bg_color_path = ensogl_theme::graph_editor::node::background; + let bg_color = style.get_color(bg_color_path).multiply_alpha(value); + let bg_color = color::Rgba::from(bg_color); + self.background.bg_color.set(bg_color.into()) + } +} + + + +// ============================ +// === Text Label Component === +// ============================ + +#[allow(missing_docs)] +#[derive(Clone,CloneRef,Debug)] +pub struct TextLabel { + model : Rc, + pub frp : Rc, +} + + +impl TextLabel { + /// Constructor. + pub fn new(app:Application) -> Self { + let frp = Rc::new(Frp::new()); + let model = Rc::new(Model::new(app.clone_ref())); + TextLabel {frp,model}.init() + } + + fn init(self) -> Self { + let frp = &self.frp; + let network = &frp.network; + let model = &self.model; + + frp::extend! { network + frp.source.size <+ frp.set_content.map(f!((t) + model.set_content(t) + )); + + eval frp.set_opacity((value) model.set_opacity(*value)); + } + + self + } +} + +impl display::Object for TextLabel { + fn display_object(&self) -> &display::object::Instance { &self.model.display_object } +} diff --git a/src/rust/ensogl/lib/core/src/animation/frp/animation.rs b/src/rust/ensogl/lib/core/src/animation/frp/animation.rs index c6afec2393..34177f5d95 100644 --- a/src/rust/ensogl/lib/core/src/animation/frp/animation.rs +++ b/src/rust/ensogl/lib/core/src/animation/frp/animation.rs @@ -1,5 +1,7 @@ //! FRP bindings to the animation engine. +pub mod hysteretic; + use crate::prelude::*; use crate::animation::physics::inertia; diff --git a/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs b/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs new file mode 100644 index 0000000000..5a9298f600 --- /dev/null +++ b/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs @@ -0,0 +1,116 @@ +//! Animation that has a delayed onset.\ + +use crate::prelude::*; + +use crate::Animation; +use crate::Easing; + +use enso_frp as frp; + + + +// =========== +// === Frp === +// =========== + +crate::define_endpoints! { + Input { + /// Trigger start of animation towards start state (0.0). Will be delayed by onset time. + to_start(), + /// Trigger start of animation towards end state (1.0). Will be delayed by onset time. + to_end(), + } + Output { + /// Represents the numeric state of the animation in the range 0..1. + value(f32), + /// Triggered when the state reaches 1.0. + on_end(), + /// Triggered when the state reaches 0.0. + on_start(), + } +} + + + +// =========================== +// === HystereticAnimation === +// =========================== + +/// Animation that has a delayed onset. +/// +/// The basic idea behind this animation is, that it changes between two states: of (0.0) and on +/// (1.0), but only after a delay (`st`/`et` start delay, end delay). If the animation gets +/// canceled before the delay has passed, nothing happens, and the delay resets. +/// +/// +/// This can be used to hide state changes that are only happening for a short duration. For example +/// consider a UI element that is shown when hovering ofer an icon. When only moving the mouse +/// cursor past the icon we want to avoid the pop-up appearing and disappearing right away. The +/// `HystereticAnimation` allows this by setting a `start_delay_duration` long enough to avoid +/// triggering during the time it takes to move past the icon. Thus, when the cursor moves past the +/// icon nothing is visible to the user, but if the mosque cursor stays on the icon longer than +/// `start_delay_duration`, the animation starts, and the pop-up becomes visible. In reverse, when +/// moving the cursor between multiple icons, the pop-up should not permanently start and disappear +/// and re-appear. Thus setting a `end_delay_duration` will avoid the pop-up from disappearing, if +/// the time the cursor is between icons is less than the `end_delay_duration`. Instead, the hiding +/// will only start iof the cursos has left any icon triggering the pop-up for longer than the +/// `end_delay_duration`. +/// +#[derive(CloneRef,Debug,Shrinkwrap)] +pub struct HystereticAnimation { + /// Public FRP Api. + pub frp : FrpEndpoints, +} + +impl HystereticAnimation { + /// Constructor. + pub fn new(network:&frp::Network, start_delay_duration:f32, end_delay_duration:f32) -> Self { + let frp = Frp::extend(network); + + let start_delay = Easing::new(network); + let end_delay = Easing::new(network); + start_delay.set_duration(start_delay_duration); + end_delay.set_duration(end_delay_duration); + + let transition = Animation::::new(network); + + frp::extend! { network + + during_transition <- any_mut(); + + on_end <- frp.to_end.constant(()); + on_end_while_active <- on_end.gate(&during_transition); + on_end_while_inactive <- on_end.gate_not(&during_transition); + + on_start <- frp.to_start.constant(()); + on_start_while_active <- on_start.gate(&during_transition); + on_start_while_inactive <- on_start.gate_not(&during_transition); + + start_delay.target <+ on_start_while_inactive.constant(1.0); + end_delay.target <+ on_end_while_inactive.constant(1.0); + + start_delay.stop_and_rewind <+ on_end.constant(0.0); + end_delay.stop_and_rewind <+ on_start.constant(0.0); + + offset_start <- end_delay.on_end.map(|t| t.is_normal()).on_true(); + onset_start <- start_delay.on_end.map(|t| t.is_normal()).on_true(); + + onset_end <- transition.value.map(|t| (t - 1.0).abs() < std::f32::EPSILON).on_true(); + offset_end <- transition.value.map(|t| t.abs() < std::f32::EPSILON).on_true(); + + transition.target <+ onset_start.constant(1.0); + transition.target <+ offset_start.constant(0.0); + transition.target <+ on_end_while_active.constant(0.0); + transition.target <+ on_start_while_active.constant(1.0); + + during_transition <+ bool(&onset_end,&onset_start); + during_transition <+ bool(&offset_end,&offset_start); + + frp.source.value <+ transition.value; + frp.source.on_end <+ onset_end; + frp.source.on_start <+ offset_end; + } + + HystereticAnimation{frp} + } +} diff --git a/src/rust/ensogl/lib/core/src/data/color/data.rs b/src/rust/ensogl/lib/core/src/data/color/data.rs index 766f78688f..3fd20c967e 100644 --- a/src/rust/ensogl/lib/core/src/data/color/data.rs +++ b/src/rust/ensogl/lib/core/src/data/color/data.rs @@ -291,3 +291,11 @@ impl Default for Alpha { Self {alpha,opaque} } } + +impl Alpha { + /// Return the color with a multiplied alpha channel. + pub fn multiply_alpha(self, alpha:f32) -> Color { + let alpha = self.alpha * alpha; + Color(Alpha{alpha, opaque: self.opaque }) + } +} diff --git a/src/rust/ensogl/lib/core/src/data/color/gradient.rs b/src/rust/ensogl/lib/core/src/data/color/gradient.rs index 28237f0bc1..a688e76ec2 100644 --- a/src/rust/ensogl/lib/core/src/data/color/gradient.rs +++ b/src/rust/ensogl/lib/core/src/data/color/gradient.rs @@ -56,7 +56,7 @@ impl LinearGradient { } impls! { [Color] From<&LinearGradient> for Glsl -where [Color:Copy + RefInto] { +where [Color:RefInto] { |t| { let args = t.control_points.iter().map(|control_point| { let offset = control_point.offset.glsl(); @@ -82,7 +82,7 @@ pub const DEFAULT_DISTANCE_GRADIENT_MAX_DISTANCE : f32 = 10.0; /// A gradient which transforms a linear gradient to a gradient along the signed distance field. /// The slope parameter modifies how fast the gradient values are changed, allowing for nice, /// smooth transitions. -#[derive(Copy,Clone,Debug)] +#[derive(Clone,Debug)] pub struct SdfSampler { /// The distance from the shape border at which the gradient should start. pub min_distance : f32, diff --git a/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs b/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs index ac370a5179..cd5543127b 100644 --- a/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs +++ b/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs @@ -640,3 +640,38 @@ impl From>> for Var { } } } + +impl Var { + /// Return the color with the given alpha value. + pub fn with_alpha(self, alpha:&Var) -> Self { + match (self, alpha) { + (Var::Static (t), Var::Static(alpha)) => { + Var::Static(color::Rgba::new(t.data.red,t.data.green,t.data.blue,*alpha)) + }, + (t, alpha) => { + Var::Dynamic(format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{1})",t.glsl(),alpha.glsl()).into()) + }, + } + } + + /// Return the color with the alpha value replaced by multiplying the colors' alpha value with + /// the given alpha value. + pub fn multiply_alpha(self, alpha:&Var) -> Self { + match (self, alpha) { + (Var::Static (t), Var::Static(alpha)) => { + Var::Static(color::Rgba::new(t.data.red,t.data.green,t.data.blue,*alpha*t.data.alpha)) + }, + (t, alpha) => { + Var::Dynamic(format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{0}.raw.w*{1})",t.glsl(),alpha.glsl()).into()) + }, + } + } + + /// Transform to LinearRgba. + pub fn into_linear(self) -> Var { + match self { + Var::Static(c) => Var::Static(c.into()), + Var::Dynamic(c) => Var::Dynamic(format!("rgba({0})",c.glsl()).into()) + } + } +} diff --git a/src/rust/ensogl/lib/core/src/gui.rs b/src/rust/ensogl/lib/core/src/gui.rs index 0e4a325817..1894a034a3 100644 --- a/src/rust/ensogl/lib/core/src/gui.rs +++ b/src/rust/ensogl/lib/core/src/gui.rs @@ -2,3 +2,4 @@ pub mod component; pub mod cursor; +#[macro_use] pub mod style; diff --git a/src/rust/ensogl/lib/core/src/gui/cursor.rs b/src/rust/ensogl/lib/core/src/gui/cursor.rs index 76b0deb2a5..05eb5d0cfc 100644 --- a/src/rust/ensogl/lib/core/src/gui/cursor.rs +++ b/src/rust/ensogl/lib/core/src/gui/cursor.rs @@ -6,10 +6,12 @@ use crate::Animation; use crate::DEPRECATED_Animation; use crate::DEPRECATED_Tween; use crate::data::color; +use crate::define_style; use crate::display::scene::Scene; use crate::display::shape::*; use crate::display; use crate::frp; +use crate::gui::style::*; @@ -30,105 +32,10 @@ fn DEFAULT_SIZE() -> Vector2 { Vector2(16.0,16.0) } -// ================== -// === StyleValue === -// ================== - -/// Defines a value of the cursor style. -#[derive(Debug,Clone,Eq,PartialEq)] -#[allow(missing_docs)] -pub struct StyleValue { - /// Defines the value of the style. In case it is set to `None`, the default value will be used. - /// Please note that setting it to `None` has a different effect than not providing the value - /// in the `Style` at all. If the value is provided it can override the existing values when - /// used in a semigroup operation. - pub value : Option, - - /// Defines if the state transition should be used. Sometimes disabling animation is required. - /// A good example is the implementation of a selection box. When drawing selection box with the - /// mouse, the user wants to see it in real-time, without it growing over time. - pub animate : bool, -} - -impl Default for StyleValue { - fn default() -> Self { - let value = default(); - let animate = true; - Self {value,animate} - } -} - -impl StyleValue { - /// Constructor. - pub fn new(value:T) -> Self { - let value = Some(value); - let animate = true; - Self {value,animate} - } - - /// Constructor for a default value setter. Please note that this is not made a `Default` impl - /// on purpose. This method creates a non-empty value setter which sets the target to its - /// default value. Read `Style` docs to learn more. - pub fn new_default() -> Self { - let value = None; - let animate = true; - Self {value,animate} - } - - /// Constructor with disabled animation. - pub fn new_no_animation(value:T) -> Self { - let value = Some(value); - let animate = false; - Self {value,animate} - } -} - - - // ============= // === Style === // ============= -macro_rules! define_style {( $( $(#$meta:tt)* $field:ident : $field_type:ty),* $(,)? ) => { - /// Set of cursor style parameters. You can construct this object in FRP network, merge it using - /// its `Semigroup` instance, and finally pass to the cursor to apply the style. Please note - /// that cursor does not implement any complex style management (like pushing or popping a style - /// from a style stack) on purpose, as it is stateful, while it is straightforward to implement - /// it in FRP. - #[derive(Debug,Clone,Default,PartialEq)] - pub struct Style { - $($(#$meta)? $field : Option>),* - } - - impl Style { - /// Create a new style with all fields set to default value. Please note that it is - /// different than empty style, as this one overrides fields with default values when - /// used in a semigroup operation. - pub fn new_with_all_fields_default() -> Self { - $(let $field = Some(StyleValue::new_default());)* - Self {$($field),*} - } - - /// Check whether the style is a default, empty one. - pub fn is_default(&self) -> bool { - $(self.$field.is_none())&&* - } - } - - impl PartialSemigroup<&Style> for Style { - #[allow(clippy::clone_on_copy)] - fn concat_mut(&mut self, other:&Self) { - $(if self.$field . is_none() { self.$field = other.$field . clone() })* - } - } - - impl PartialSemigroup