diff --git a/ide/CHANGELOG.md b/ide/CHANGELOG.md index b881b6b2ede1..3ceba992cb2c 100644 --- a/ide/CHANGELOG.md +++ b/ide/CHANGELOG.md @@ -20,6 +20,9 @@ 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]. This allows discovering the result type of a node which can help with + debugging and development. - [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 @@ -195,6 +198,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/ide/src/rust/ensogl/lib/components/src/label.rs b/ide/src/rust/ensogl/lib/components/src/label.rs new file mode 100644 index 000000000000..9bd76e6c9d6f --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/label.rs @@ -0,0 +1,198 @@ +//! 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; + +use ensogl_theme::component::label as theme; + + + +// ========================== +// === Shapes Definitions === +// ========================== + +mod background { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style,bg_color:Vector4) { + + let width = Var::::from("input_size.x"); + let height = Var::::from("input_size.y"); + let padding = style.get_number_or(theme::padding, 0.0); + 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 shaow_size = style.get_number_or(theme::shadow::size,0.0); + let shadow_size = shaow_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(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(theme::shadow::fading)); + let fading_color = Var::::from(fading_color); + let fading_color = fading_color.multiply_alpha(&alpha); + let exponent = style.get_number_or(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, + style : StyleWatch, +} + +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); + + // FIXME[MM/WD]: Depth sorting of labels to in front of everything else in the scene. + // Temporary solution. The depth management needs to allow defining relative position of + // the text and background and let the whole component to be set to am an arbitrary layer. + label.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + label.add_to_scene_layer_DEPRECATED(&scene.layers.tooltip_text); + scene.layers.tooltip_background.add_exclusive(&background); + + display_object.add_child(&background); + display_object.add_child(&label); + + let style = StyleWatch::new(&app.display.scene().style_sheet); + + Model { label, display_object, background, app, style } + } + + pub fn height(&self) -> f32 { + self.style.get_number_or(theme::height, 0.0) + } + + fn set_width(&self, width:f32) -> Vector2 { + let padding = self.style.get_number_or(theme::padding,0.0); + let text_size = self.style.get_number_or(theme::text::size,0.0); + let text_offset = self.style.get_number_or(theme::text::offset,0.0); + 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 text_color_path = theme::text; + let text_color = self.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 = theme::background; + let bg_color = self.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()) + } +} + + + +// ======================= +// === Label Component === +// ======================= + +#[allow(missing_docs)] +#[derive(Clone,CloneRef,Debug)] +pub struct Label { + model : Rc, + pub frp : Rc, +} + +impl Label { + /// Constructor. + pub fn new(app:Application) -> Self { + let frp = Rc::new(Frp::new()); + let model = Rc::new(Model::new(app.clone_ref())); + Label {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 Label { + fn display_object(&self) -> &display::object::Instance { &self.model.display_object } +} diff --git a/ide/src/rust/ensogl/lib/components/src/lib.rs b/ide/src/rust/ensogl/lib/components/src/lib.rs index ddca4ec4d427..884be691141d 100644 --- a/ide/src/rust/ensogl/lib/components/src/lib.rs +++ b/ide/src/rust/ensogl/lib/components/src/lib.rs @@ -18,6 +18,7 @@ pub mod drop_down_menu; pub mod list_view; +pub mod label; pub mod toggle_button; /// Commonly used types and functions. diff --git a/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs index c6afec23931d..34177f5d95cd 100644 --- a/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs +++ b/ide/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/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs new file mode 100644 index 000000000000..cc88180e28af --- /dev/null +++ b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs @@ -0,0 +1,113 @@ +//! Animation that has a delayed onset and offset. +//! +//! 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`. +//! +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 and offset. +#[derive(CloneRef,Debug,Shrinkwrap)] +pub struct HystereticAnimation { + #[allow(missing_docs)] + pub frp : FrpEndpoints, +} + +impl HystereticAnimation { + #[allow(missing_docs)] + 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/ide/src/rust/ensogl/lib/core/src/data/color/data.rs b/ide/src/rust/ensogl/lib/core/src/data/color/data.rs index 766f78688f6b..adb797703ffe 100644 --- a/ide/src/rust/ensogl/lib/core/src/data/color/data.rs +++ b/ide/src/rust/ensogl/lib/core/src/data/color/data.rs @@ -291,3 +291,16 @@ 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 }) + } + + /// Modify the color's alpha channel. + pub fn mod_alpha(&mut self, f:F) { + f(&mut self.alpha) + } +} diff --git a/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs b/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs index 28237f0bc1c9..a688e76ec284 100644 --- a/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs +++ b/ide/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/ide/src/rust/ensogl/lib/core/src/display/scene.rs b/ide/src/rust/ensogl/lib/core/src/display/scene.rs index 359ad3f9cae1..d91413a9e02b 100644 --- a/ide/src/rust/ensogl/lib/core/src/display/scene.rs +++ b/ide/src/rust/ensogl/lib/core/src/display/scene.rs @@ -603,11 +603,15 @@ impl Renderer { /// should be abstracted away in the future. #[derive(Clone,CloneRef,Debug)] pub struct HardcodedLayers { - pub viz : Layer, - pub below_main : Layer, + pub viz : Layer, + pub below_main : Layer, // main <- here is the 'main` layer inserted. - pub cursor : Layer, - pub label : Layer, + pub cursor : Layer, + pub label : Layer, + + pub tooltip_background : Layer, + pub tooltip_text : Layer, + pub viz_fullscreen : Layer, pub breadcrumbs : Layer, layers : Layers, @@ -622,23 +626,30 @@ impl Deref for HardcodedLayers { impl HardcodedLayers { pub fn new(logger:impl AnyLogger) -> Self { - let layers = Layers::new(logger); - let viz = layers.new_layer(); - let cursor = layers.new_layer(); - let label = layers.new_layer(); - let viz_fullscreen = layers.new_layer(); - let below_main = layers.new_layer(); - let breadcrumbs = layers.new_layer(); + let layers = Layers::new(logger); + let viz = layers.new_layer(); + let cursor = layers.new_layer(); + let label = layers.new_layer(); + let tooltip_background = layers.new_layer(); + let tooltip_text = layers.new_layer(); + let viz_fullscreen = layers.new_layer(); + let below_main = layers.new_layer(); + let breadcrumbs = layers.new_layer(); viz.set_camera(layers.main.camera()); label.set_camera(layers.main.camera()); + tooltip_background.set_camera(layers.main.camera()); + tooltip_text.set_camera(layers.main.camera()); below_main.set_camera(layers.main.camera()); layers.add_layers_order_dependency(&viz,&below_main); layers.add_layers_order_dependency(&below_main,&layers.main); layers.add_layers_order_dependency(&layers.main,&cursor); layers.add_layers_order_dependency(&cursor,&label); - layers.add_layers_order_dependency(&label,&viz_fullscreen); + layers.add_layers_order_dependency(&label,&tooltip_background); + layers.add_layers_order_dependency(&tooltip_background,&tooltip_text); + layers.add_layers_order_dependency(&tooltip_text,&viz_fullscreen); layers.add_layers_order_dependency(&viz_fullscreen,&breadcrumbs); - Self {layers,viz,cursor,label,viz_fullscreen,below_main,breadcrumbs} + Self {layers,viz,cursor,label,viz_fullscreen,below_main,breadcrumbs,tooltip_background, + tooltip_text} } } diff --git a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs index ac370a517932..237b31f0bff2 100644 --- a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs +++ b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs @@ -640,3 +640,45 @@ 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) => { + let t = t.glsl(); + let alpha = alpha.glsl(); + let var = format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{1})",t,alpha); + Var::Dynamic(var.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)) => { + let var = color::Rgba::new(t.data.red,t.data.green,t.data.blue,*alpha*t.data.alpha); + Var::Static(var) + }, + (t, alpha) => { + let t = t.glsl(); + let alpha = alpha.glsl(); + let var = format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{0}.raw.w*{1})",t,alpha); + Var::Dynamic(var.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/ide/src/rust/ensogl/lib/core/src/gui.rs b/ide/src/rust/ensogl/lib/core/src/gui.rs index 0e4a32581733..acc35f569da9 100644 --- a/ide/src/rust/ensogl/lib/core/src/gui.rs +++ b/ide/src/rust/ensogl/lib/core/src/gui.rs @@ -2,3 +2,4 @@ pub mod component; pub mod cursor; +pub mod style; diff --git a/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs b/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs index 76b0deb2a558..05eb5d0cfc2c 100644 --- a/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs +++ b/ide/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