diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2bc8408a5..02f82bca0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,9 @@ - [Added a new component: Slider][3852]. It allows adjusting a numeric value with the mouse. The precision of these adjustments can be increased or decreased. +- [Slider component functionality improvements][3885]. The slider component now + supports multiple ways to handle out-of-range values. The slider's value can + be edited as text, and a new vertical slider layout is available. - [Added ProjectsGrid view for Cloud Dashboard][3857]. It provides the first steps towards migrating the Cloud Dashboard from the existing React (web-only) implementation towards a shared structure that can be used in both the Desktop @@ -397,6 +400,7 @@ [3874]: https://github.com/enso-org/enso/pull/3874 [3852]: https://github.com/enso-org/enso/pull/3852 [3841]: https://github.com/enso-org/enso/pull/3841 +[3885]: https://github.com/enso-org/enso/pull/3885 [3919]: https://github.com/enso-org/enso/pull/3919 [3923]: https://github.com/enso-org/enso/pull/3923 diff --git a/Cargo.lock b/Cargo.lock index d6a49538315..fa1a41b61c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2787,6 +2787,7 @@ dependencies = [ name = "ensogl-example-slider" version = "0.1.0" dependencies = [ + "enso-frp", "ensogl-core", "ensogl-hardcoded-theme", "ensogl-slider", @@ -2991,6 +2992,7 @@ dependencies = [ "ensogl-core", "ensogl-hardcoded-theme", "ensogl-text", + "ensogl-tooltip", ] [[package]] diff --git a/build-config.yaml b/build-config.yaml index a07ea213b03..54f6b44a526 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 15.11 MiB +wasm-size-limit: 15.15 MiB required-versions: cargo-watch: ^8.1.1 diff --git a/lib/rust/ensogl/component/slider/Cargo.toml b/lib/rust/ensogl/component/slider/Cargo.toml index 5334eecb010..1dbd672df76 100644 --- a/lib/rust/ensogl/component/slider/Cargo.toml +++ b/lib/rust/ensogl/component/slider/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] enso-frp = { path = "../../../frp" } ensogl-core = { path = "../../core" } +ensogl-tooltip = { path = "../../component/tooltip/" } ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" } ensogl-text = { path = "../text" } diff --git a/lib/rust/ensogl/component/slider/src/lib.rs b/lib/rust/ensogl/component/slider/src/lib.rs index 8c59ec5290a..4d5f95d2250 100644 --- a/lib/rust/ensogl/component/slider/src/lib.rs +++ b/lib/rust/ensogl/component/slider/src/lib.rs @@ -1,6 +1,6 @@ //! A slider UI component that allows adjusting a value through mouse interaction. -#![recursion_limit = "256"] +#![recursion_limit = "512"] // === Standard Linter Configuration === #![deny(non_ascii_idents)] #![warn(unsafe_code)] @@ -22,7 +22,10 @@ use crate::model::*; use ensogl_core::prelude::*; +use ensogl_core::animation::animation::delayed::DelayedAnimation; use ensogl_core::application; +use ensogl_core::application::shortcut; +use ensogl_core::application::tooltip; use ensogl_core::application::Application; use ensogl_core::data::color; use ensogl_core::display; @@ -44,7 +47,7 @@ pub mod model; /// Default slider precision when slider dragging is initiated. The precision indicates both how /// much the value is changed per pixel dragged and how many digits are displayed after the decimal. -const PRECISION_DEFAULT: f32 = 0.1; +const PRECISION_DEFAULT: f32 = 1.0; /// Default upper limit of the slider value. const MAX_VALUE_DEFAULT: f32 = 1.0; /// Default for the maximum number of digits after the decimal point that is displayed. @@ -63,12 +66,26 @@ const PRECISION_ADJUSTMENT_STEP_SIZE: f32 = 50.0; /// 10.0, 100.0, ...] when decreasing the precision and [0.1, 0.01, 0.001, ...] when increasing the /// precision. const PRECISION_ADJUSTMENT_STEP_BASE: f32 = 10.0; +/// Limit the number of precision steps to prevent overflow or rounding to zero of the precision. +const MAX_PRECISION_ADJUSTMENT_STEPS: usize = 8; +/// A pop-up is displayed whenever the slider's precision is changed. This is the duration for +/// which the pop-up is visible. +const PRECISION_ADJUSTMENT_POPUP_DURATION: f32 = 1000.0; +/// The delay before an information tooltip is displayed when hovering over a slider component. +const INFORMATION_TOOLTIP_DELAY: f32 = 1000.0; +/// The default size of the slider's thumb as a fraction of the slider's length. +const THUMB_SIZE_DEFAULT: f32 = 0.2; +/// The threshold for shrinking an extended slider limit as a fraction of the current range. If the +/// slider's value is adjusted below this threshold then the limit will be shrunk. This threshold is +/// lower than 1/2 to prevent rapid switching of limits as the extend and shrink thresholds would +/// otherwise coincide. +const ADAPTIVE_LIMIT_SHRINK_THRESHOLD: f32 = 0.4; -// =========================== -// === Label position enum === -// =========================== +// ====================== +// === Label position === +// ====================== /// Position of the slider label. #[derive(Clone, Copy, Debug, Default)] @@ -82,6 +99,117 @@ pub enum LabelPosition { +// ========================== +// === Slider orientation === +// ========================== + +/// The orientation of the slider component. +#[derive(Clone, Copy, Debug, Default)] +pub enum SliderOrientation { + #[default] + /// The slider value is changed by dragging the slider horizontally. + Horizontal, + /// The slider value is changed by dragging the slider vertically. + Vertical, +} + + + +// ================================= +// === Slider position indicator === +// ================================= + +/// The type of element that indicates the slider's value along its length. +#[derive(Clone, Copy, Debug, Default)] +pub enum ValueIndicator { + #[default] + /// A track is a bar that fills the slider as the value increases. The track is empty when the + /// slider's value is at the lower limit and filled when the value is at the upper limit. + Track, + /// A thumb is a small element that moves across the slider as the value changes. The thumb is + /// on the left/lower end of the slider when the slider's value is at the lower limit and on + /// the right/upper end of the slider when the value is at the upper limit. + Thumb, +} + + + +// ============================= +// === Slider limit behavior === +// ============================= + +/// The behavior of the slider when the value is adjusted beyond the slider's limits. This can be +/// set independently for the upper and the lower limits. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SliderLimit { + #[default] + /// The hard limit behavior clamps the value to always be within the slider's limits. + Hard, + /// The soft limit behavior allows the value to exceed the slider's limits. If the the slider + /// value is beyond either limit then an overflow indicator is displayed. In this case the + /// slider's track does not indicate the value adjustments. + Soft, + /// The adaptive limit behavior extends the slider's range when the originally set limit is + /// reached. Specifically, when a limit is exceeded the slider range is doubled in that + /// direction. This happens iteratively so that the slider's value is always contained within + /// the extended range. If the slider's range is extended and the value is able to fit in a + /// range that is half of the current range, then the slider's range will be shrunk by half. + /// This again happens iteratively until the slider is at its original range. + Adaptive, +} + +/// Adaptive upper limit adjustment. +fn adapt_upper_limit( + &(value, min, max, max_ext, upper_limit): &(f32, f32, f32, f32, SliderLimit), +) -> f32 { + if upper_limit == SliderLimit::Adaptive && value > max { + let range = max_ext - min; + let extend = value > max_ext; + let shrink = value < min + range * ADAPTIVE_LIMIT_SHRINK_THRESHOLD; + let max_ext = match (extend, shrink) { + (true, _) => adapt_upper_limit(&(value, min, max, min + range * 2.0, upper_limit)), + (_, true) => adapt_upper_limit(&(value, min, max, min + range * 0.5, upper_limit)), + _ => max_ext, + }; + max_ext.max(max) // Do no set extended limit below original `max`. + } else { + max + } +} + +/// Adaptive lower limit adjustment. +fn adapt_lower_limit( + &(value, min, max, min_ext, lower_limit): &(f32, f32, f32, f32, SliderLimit), +) -> f32 { + if lower_limit == SliderLimit::Adaptive && value < min { + let range = max - min_ext; + let extend = value < min_ext; + let shrink = value > max - range * ADAPTIVE_LIMIT_SHRINK_THRESHOLD; + let min_ext = match (extend, shrink) { + (true, _) => adapt_lower_limit(&(value, min, max, max - range * 2.0, lower_limit)), + (_, true) => adapt_lower_limit(&(value, min, max, max - range * 0.5, lower_limit)), + _ => min_ext, + }; + min_ext.min(min) // Do no set extended limit above original `min`. + } else { + min + } +} + +/// Clamp the slider's value according to the selected limits setting. +fn value_limit_clamp( + &(value, min, max, lower_limit, upper_limit): &(f32, f32, f32, SliderLimit, SliderLimit), +) -> f32 { + match (lower_limit, upper_limit) { + (SliderLimit::Hard, SliderLimit::Hard) => value.clamp(min, max), + (SliderLimit::Hard, _) => value.max(min), + (_, SliderLimit::Hard) => value.min(max), + _ => value, + } +} + + + // =========== // === FRP === // =========== @@ -92,8 +220,10 @@ ensogl_core::define_endpoints_2! { set_width(f32), /// Set the height of the slider component. set_height(f32), - /// Set the color of the slider's track. - set_slider_track_color(color::Lcha), + /// Set the type of the slider's value indicator. + set_value_indicator(ValueIndicator), + /// Set the color of the slider's value indicator. + set_value_indicator_color(color::Lcha), /// Set the color of the slider's background. set_background_color(color::Lcha), /// Set the slider value. @@ -108,6 +238,8 @@ ensogl_core::define_endpoints_2! { set_max_value(f32), /// Set the color of the text displaying the current value. set_value_text_color(color::Lcha), + /// Set whether the slider's value text is hidden. + set_value_text_hidden(bool), /// Set the default precision at which the slider operates. The slider's precision /// determines by what increment the value will be changed on mouse movement. It also /// affects the number of digits after the decimal point displayed. @@ -120,6 +252,11 @@ ensogl_core::define_endpoints_2! { /// The `adjustment_step_size` defines the distance the mouse must be moved to increase or /// decrease the precision by one step. set_precision_adjustment_step_size(f32), + /// Set the maximum number of precision steps to prevent overflow or rounding to zero of the + /// precision increments. + set_max_precision_adjustment_steps(usize), + /// Set whether the precision adjustment mechansim is disabled. + set_precision_adjustment_disabled(bool), /// Set the slider's label. The label will be displayed to the left of the slider's value /// display. set_label(ImString), @@ -129,18 +266,59 @@ ensogl_core::define_endpoints_2! { set_label_hidden(bool), /// Set the position of the slider's label. set_label_position(LabelPosition), + /// Set the orientation of the slider component. + set_orientation(SliderOrientation), + /// Set a tooltip that pops up when the mose hovers over the component. + set_tooltip(ImString), + /// Set the delay of the tooltip showing after the mouse hovers over the component. + set_tooltip_delay(f32), + /// A pop-up is displayed whenever the slider's precision is changed. This is the duration + /// for which the pop-up is visible. + set_precision_popup_duration(f32), /// Set whether the slider is disabled. When disabled, the slider's value cannot be changed /// and the slider is greyed out. set_slider_disabled(bool), /// The maximum number of digits after the decimal point to be displayed when showing the /// component's value. set_max_disp_decimal_places(usize), + /// Set the behavior of the slider when its value is adjusted to below the `min_value`. + set_lower_limit_type(SliderLimit), + /// Set the behavior of the slider when its value is adjusted to above the `max_value`. + set_upper_limit_type(SliderLimit), + /// Begin textual editing of the slider value. + start_value_editing(), + /// End textual editing of the slider value and apply the edited value to the slider. + finish_value_editing(), + /// End textual editing of the slider value and revert to the slider value before editing. + cancel_value_editing(), + /// Set the slider's thumb size as fraction of the slider's length. + set_thumb_size(f32), } Output { + /// The component's width. width(f32), + /// The component's height. height(f32), + /// The slider's value. value(f32), + /// The slider's precision. precision(f32), + /// The slider value's lower limit. This takes into account limit extension if an adaptive + /// slider limit is set. + min_value(f32), + /// The slider value's upper limit. This takes into account limit extension if an adaptive + /// slider limit is set. + max_value(f32), + /// Indicates whether the mouse is currently hovered over the component. + hovered(bool), + /// Indicates whether the slider is currently being dragged. + dragged(bool), + /// Indicates whether the slider is disabled. + disabled(bool), + /// Indicates whether the slider's value is being edited currently. + editing(bool), + /// The orientation of the slider, either horizontal or vertical. + orientation(SliderOrientation), } } @@ -156,7 +334,7 @@ ensogl_core::define_endpoints_2! { /// value within the specified range. Dragging the slider in a vertical direction adjusts the /// precision of the slider. The precision affects the increments by which the value changes when /// the mouse is moved. -#[derive(Debug, Deref)] +#[derive(Debug, Deref, Clone)] pub struct Slider { /// Public FRP api of the component. #[deref] @@ -171,18 +349,22 @@ pub struct Slider { impl Slider { /// Construct a new slider component. pub fn new(app: &Application) -> Self { - let model = Rc::new(Model::new(app)); - let app = app.clone_ref(); let frp = Frp::new(); + let model = Rc::new(Model::new(app, frp.network())); + let app = app.clone_ref(); Self { frp, model, app }.init() } fn init(self) -> Self { self.init_value_update(); + self.init_value_editing(); + self.init_limit_handling(); self.init_value_display(); + self.init_precision_popup(); + self.init_information_tooltip(); self.init_component_layout(); self.init_component_colors(); - self.init_precision_defaults(); + self.init_slider_defaults(); self } @@ -203,71 +385,208 @@ impl Slider { component_click <- component_events.mouse_down_primary .gate_not(&input.set_slider_disabled); - component_release <- component_events.mouse_release_primary - .gate_not(&input.set_slider_disabled); + component_click <- component_click.gate_not(&output.editing); + slider_disabled_is_true <- input.set_slider_disabled.on_true(); + slider_editing_is_true <- output.editing.on_true(); + component_release <- any3( + &component_events.mouse_release_primary, + &slider_disabled_is_true, + &slider_editing_is_true, + ); component_drag <- bool(&component_release, &component_click); component_drag <- component_drag.gate_not(&input.set_slider_disabled); + component_drag <- component_drag.gate_not(&keyboard.is_control_down); component_ctrl_click <- component_click.gate(&keyboard.is_control_down); drag_start_pos <- mouse.position.sample(&component_click); drag_end_pos <- mouse.position.gate(&component_drag); drag_end_pos <- any2(&drag_end_pos, &drag_start_pos); - drag_delta_x <- all2(&drag_end_pos, &drag_start_pos); - drag_delta_x <- drag_delta_x.map(|(end, start)| end.x - start.x).on_change(); + drag_delta <- all2(&drag_end_pos, &drag_start_pos).map(|(end, start)| end - start); + drag_delta_primary <- all2(&drag_delta, &input.set_orientation); + drag_delta_primary <- drag_delta_primary.map( |(delta, orientation)| + match orientation { + SliderOrientation::Horizontal => delta.x, + SliderOrientation::Vertical => delta.y, + } + ).on_change(); mouse_position_click <- mouse.position.sample(&component_click); mouse_position_drag <- mouse.position.gate(&component_drag); mouse_position_click_or_drag <- any2(&mouse_position_click, &mouse_position_drag); - mouse_y_local <- mouse_position_click_or_drag.map( - f!([scene, model] (pos) scene.screen_to_object_space(&model.background, *pos).y) + mouse_local <- mouse_position_click_or_drag.map( + f!([scene, model] (pos) scene.screen_to_object_space(&model.background, *pos)) ); - - - // === Precision calculation === - - precision_adjustment_margin <- all2( - &input.set_height, - &input.set_precision_adjustment_margin, - ).map(|(h, m)| h / 2.0 + m); - precision_offset_steps <- all3( - &mouse_y_local, - &precision_adjustment_margin, - &input.set_precision_adjustment_step_size, - ).map( - |(offset, margin, step_size)| { - let sign = offset.signum(); - // Calculate mouse y-position offset beyond margin, or 0 if within margin. - let offset = (offset.abs() - margin).max(0.0); - // Calculate number of steps and direction of the precision adjustment. - (offset / step_size).ceil() * sign + mouse_local_secondary <- all2(&mouse_local, &input.set_orientation); + mouse_local_secondary <- mouse_local_secondary.map( |(offset, orientation)| + match orientation { + SliderOrientation::Horizontal => offset.y, + SliderOrientation::Vertical => offset.x, } - ).on_change(); - precision <- all2(&input.set_default_precision, &precision_offset_steps); - precision <- precision.map( - // Adjust the precision by the number of offset steps. - |(precision, offset)| *precision * (PRECISION_ADJUSTMENT_STEP_BASE).pow(*offset) ); - output.precision <+ precision; + output.hovered <+ bool(&component_events.mouse_out, &component_events.mouse_over); + output.dragged <+ component_drag; - // === Value calculation === + // === Get slider value on drag start === value_reset <- input.set_default_value.sample(&component_ctrl_click); value_on_click <- output.value.sample(&component_click); value_on_click <- any2(&value_reset, &value_on_click); + + + // === Precision calculation === + + slider_length <- all3(&input.set_orientation, &input.set_width, &input.set_height); + slider_length <- slider_length.map( |(orientation, width, height)| + match orientation { + SliderOrientation::Horizontal => *width, + SliderOrientation::Vertical => *height, + } + ); + slider_length <- all3( + &slider_length, + &input.set_value_indicator, + &input.set_thumb_size + ); + slider_length <- slider_length.map(|(length, indicator, thumb_size)| + match indicator { + ValueIndicator::Thumb => length * (1.0 - thumb_size), + ValueIndicator::Track => *length, + } + ); + min_value_on_click <- output.min_value.sample(&component_click); + min_value_on_click <- any2(&min_value_on_click, &input.set_min_value); + max_value_on_click <- output.max_value.sample(&component_click); + max_value_on_click <- any2(&max_value_on_click, &input.set_max_value); + slider_range <- all2(&min_value_on_click, &max_value_on_click); + slider_range <- slider_range.map(|(min, max)| max - min); + prec_at_mouse_speed <- all2(&slider_length, &slider_range).map(|(l, r)| r / l); + + output.precision <+ prec_at_mouse_speed.sample(&component_click); + precision_adjustment_margin <- all4( + &input.set_width, + &input.set_height, + &input.set_precision_adjustment_margin, + &input.set_orientation, + ); + precision_adjustment_margin <- precision_adjustment_margin.map( + |(width, height, margin, orientation)| match orientation { + SliderOrientation::Horizontal => height / 2.0 + margin, + SliderOrientation::Vertical => width / 2.0 + margin, + } + ); + precision_offset_steps <- all3( + &mouse_local_secondary, + &precision_adjustment_margin, + &input.set_precision_adjustment_step_size, + ); + precision_offset_steps <- precision_offset_steps.map( + |(offset, margin, step_size)| { + let sign = offset.signum(); + // Calculate mouse y-position offset beyond margin. + let offset = offset.abs() - margin; + if offset < 0.0 { return None } // No adjustment if offset is within margin. + // Calculate number of steps and direction of the precision adjustment. + let steps = (offset / step_size).ceil() * sign; + match steps { + // Step 0 is over the component, which returns early. Make step 0 be the + // first adjustment step above the component (precision = 1.0). + steps if steps > 0.0 => Some(steps - 1.0), + steps => Some(steps), + } + } + ).on_change(); + precision_offset_steps <- all2( + &precision_offset_steps, + &input.set_max_precision_adjustment_steps, + ); + precision_offset_steps <- precision_offset_steps.map(|(step, max_step)| + step.map(|step| step.clamp(- (*max_step as f32), *max_step as f32)) + ); + precision <- all4( + &prec_at_mouse_speed, + &input.set_default_precision, + &precision_offset_steps, + &input.set_precision_adjustment_disabled, + ); + precision <- precision.map( + |(mouse_prec, step_prec, offset, disabled)| match (offset, disabled) { + // Adjust the precision by the number of offset steps. + (Some(offset), false) => + *step_prec * (PRECISION_ADJUSTMENT_STEP_BASE).pow(*offset), + // Set the precision for 1:1 track movement to mouse movement. + _ => *mouse_prec, + } + ); + + + // === Value calculation === + update_value <- bool(&component_release, &value_on_click); - value <- all3(&value_on_click, &precision, &drag_delta_x).gate(&update_value); + value <- all3(&value_on_click, &precision, &drag_delta_primary); + value <- value.gate(&update_value); value <- value.map(|(value, precision, delta)| value + delta * precision); value <- any2(&input.set_value, &value); // Snap the slider's value to the nearest precision increment. value <- all2(&value, &precision); value <- value.map(|(value, precision)| (value / precision).round() * precision); - // Clamp the slider's value to within the slider's min/max limits. - value <- all3(&value, &input.set_min_value, &input.set_max_value); - value <- value.map(|(value, min, max)| value.clamp(*min, *max)); + value <- all5( + &value, + &input.set_min_value, + &input.set_max_value, + &input.set_lower_limit_type, + &input.set_upper_limit_type, + ).map(value_limit_clamp); output.value <+ value; + output.precision <+ precision; + + model.value_animation.target <+ value; + small_value_step <- all2(&precision, &prec_at_mouse_speed); + small_value_step <- small_value_step.map(|(prec, threshold)| prec <= threshold); + value_adjust <- drag_delta_primary.map(|x| *x != 0.0); + prec_adjust <- precision.on_change(); + prec_adjust <- bool(&value_adjust, &prec_adjust); + skip_value_anim <- value.constant(()).gate(&small_value_step); + skip_value_anim <- skip_value_anim.gate(&value_adjust).gate_not(&prec_adjust); + model.value_animation.skip <+ skip_value_anim; }; } - /// Initialize the value display FRP network + /// Initialize the slider limit handling FRP network. + fn init_limit_handling(&self) { + let network = self.frp.network(); + let input = &self.frp.input; + let output = &self.frp.private.output; + let model = &self.model; + + frp::extend! { network + min_value <- all5( + &output.value, + &input.set_min_value, + &input.set_max_value, + &output.min_value, + &input.set_lower_limit_type, + ); + min_value <- min_value.map(adapt_lower_limit).on_change(); + output.min_value <+ min_value; + max_value<- all5( + &output.value, + &input.set_min_value, + &input.set_max_value, + &output.max_value, + &input.set_upper_limit_type, + ); + max_value <- max_value.map(adapt_upper_limit).on_change(); + output.max_value <+ max_value; + + overflow_lower <- all2(&output.value, &output.min_value).map(|(val, min)| val < min ); + overflow_upper <- all2(&output.value, &output.max_value).map(|(val, max)| val > max ); + overflow_lower <- overflow_lower.on_change(); + overflow_upper <- overflow_upper.on_change(); + eval overflow_lower((v) model.set_overflow_lower_visible(*v)); + eval overflow_upper((v) model.set_overflow_upper_visible(*v)); + }; + } + + /// Initialize the value display FRP network. fn init_value_display(&self) { let network = self.frp.network(); let input = &self.frp.input; @@ -275,9 +594,9 @@ impl Slider { let model = &self.model; frp::extend! { network - value <- output.value.on_change(); - precision <- output.precision.on_change(); - + eval input.set_value_text_hidden((v) model.set_value_text_hidden(*v)); + value <- output.value.gate_not(&input.set_value_text_hidden).on_change(); + precision <- output.precision.gate_not(&input.set_value_text_hidden).on_change(); value_is_default <- all2(&value, &input.set_default_value).map(|(val, def)| val==def); value_is_default_true <- value_is_default.on_true(); value_is_default_false <- value_is_default.on_false(); @@ -289,11 +608,77 @@ impl Slider { value_text_left <- value_text_left_right._0(); value_text_right <- value_text_left_right._1(); model.value_text_left.set_content <+ value_text_left; - value_text_right_is_visible <- value_text_right.map(|t| t.is_some()); + value_text_right_is_visible <- value_text_right.map(|t| t.is_some()).on_change(); value_text_right <- value_text_right.gate(&value_text_right_is_visible); model.value_text_right.set_content <+ value_text_right.unwrap(); - value_text_right_visibility_change <- value_text_right_is_visible.on_change(); - eval value_text_right_visibility_change((v) model.set_value_text_right_visible(*v)); + eval value_text_right_is_visible((v) model.set_value_text_right_visible(*v)); + }; + } + + /// Initialize the precision pop-up FRP network. + fn init_precision_popup(&self) { + let network = self.frp.network(); + let input = &self.frp.input; + let output = &self.frp.private.output; + let model = &self.model; + let component_events = &model.background.events; + let popup_anim = DelayedAnimation::new(network); + + frp::extend! { network + popup_anim.set_duration <+ input.set_precision_popup_duration; + component_drag <- bool( + &component_events.mouse_release_primary, + &component_events.mouse_down_primary + ); + precision <- output.precision.on_change().gate(&component_drag); + model.tooltip.frp.set_style <+ precision.map(|precision| { + let prec_text = format!( + "Precision: {:.digits$}", + precision, + digits=MAX_DISP_DECIMAL_PLACES_DEFAULT + ); + let prec_text = prec_text.trim_end_matches('0'); + let prec_text = prec_text.trim_end_matches('.'); + tooltip::Style::set_label(prec_text.into()) + }); + precision_changed <- precision.constant(()); + popup_anim.reset <+ precision_changed; + popup_anim.start <+ precision_changed; + popup_hide <- any2(&popup_anim.on_end, &component_events.mouse_release_primary); + model.tooltip.frp.set_style <+ popup_hide.map(|_| + tooltip::Style::unset_label() + ); + }; + } + + /// Initialize the information tooltip FRP network. + fn init_information_tooltip(&self) { + let network = self.frp.network(); + let input = &self.frp.input; + let output = &self.frp.private.output; + let model = &self.model; + let component_events = &model.background.events; + let tooltip_anim = DelayedAnimation::new(network); + + frp::extend! { network + tooltip_anim.set_delay <+ input.set_tooltip_delay; + tooltip_start <- any2(&component_events.mouse_over, &component_events.mouse_up_primary); + tooltip_start <- tooltip_start.gate_not(&output.dragged); + tooltip_start <- tooltip_start.gate_not(&output.editing); + tooltip_empty <- input.set_tooltip.sample(&tooltip_start).map(|s| s.trim().is_empty()); + tooltip_start <- tooltip_start.gate_not(&tooltip_empty); + tooltip_anim.start <+ tooltip_start; + tooltip_anim.reset <+ any2( + &component_events.mouse_out, + &component_events.mouse_down_primary + ); + tooltip_show <- input.set_tooltip.sample(&tooltip_anim.on_end); + model.tooltip.frp.set_style <+ tooltip_show.map(|tooltip| { + tooltip::Style::set_label(format!("{}", tooltip)) + }); + model.tooltip.frp.set_style <+ tooltip_anim.on_reset.map(|_| + tooltip::Style::unset_label() + ); }; } @@ -303,45 +688,64 @@ impl Slider { let input = &self.frp.input; let output = &self.frp.private.output; let model = &self.model; - let track_pos_anim = Animation::new_non_init(network); + let min_limit_anim = Animation::new_non_init(network); + let max_limit_anim = Animation::new_non_init(network); frp::extend! { network comp_size <- all2(&input.set_width, &input.set_height).map(|(w, h)| Vector2(*w,*h)); eval comp_size((size) model.set_size(*size)); + eval input.set_value_indicator((i) model.set_value_indicator(i)); output.width <+ input.set_width; output.height <+ input.set_height; + min_limit_anim.target <+ output.min_value; + max_limit_anim.target <+ output.max_value; + indicator_pos <- all3(&model.value_animation.value, &min_limit_anim.value, &max_limit_anim.value); + indicator_pos <- indicator_pos.map(|(value, min, max)| (value - min) / (max - min)); + indicator_pos <- all3(&indicator_pos, &input.set_thumb_size, &input.set_orientation); + eval indicator_pos((v) model.set_indicator_position(v)); - track_pos <- all3(&output.value, &input.set_min_value, &input.set_max_value); - track_pos_anim.target <+ track_pos.map(|(value, min, max)| (value - min) / (max - min)); - eval track_pos_anim.value((v) model.track.slider_fraction_filled.set(*v)); - - value_text_left_pos_x <- all2( + value_text_left_pos_x <- all3( &model.value_text_left.width, &model.value_text_dot.width, + &output.precision, + ); + value_text_left_pos_x <- value_text_left_pos_x.map( + // Center text if precision higher than 1.0 (integer display), else align to dot. + |(left, dot, prec)| if *prec >= 1.0 {- *left / 2.0} else {- *left - *dot / 2.0} ); - value_text_left_pos_x <- value_text_left_pos_x.map(|(left, dot)| -*left - *dot / 2.0); eval value_text_left_pos_x((x) model.value_text_left.set_x(*x)); eval model.value_text_left.height((h) model.value_text_left.set_y(*h / 2.0)); - eval model.value_text_dot.width((w) model.value_text_dot.set_x(-*w / 2.0)); + eval model.value_text_dot.width((w) { + model.value_text_dot.set_x(-*w / 2.0); + model.value_text_right.set_x(*w / 2.0); + }); eval model.value_text_dot.height((h) model.value_text_dot.set_y(*h / 2.0)); - eval model.value_text_dot.width((w) model.value_text_right.set_x(*w / 2.0)); eval model.value_text_right.height((h) model.value_text_right.set_y(*h / 2.0)); + eval model.value_text_edit.width((w) model.value_text_edit.set_x(-*w / 2.0)); + eval model.value_text_edit.height((h) model.value_text_edit.set_y(*h / 2.0)); + + overflow_marker_position <- all3( + &input.set_width, + &input.set_height, + &input.set_orientation, + ); + eval overflow_marker_position((p) model.set_overflow_marker_position(p)); + overflow_marker_shape <- all2(&model.value_text_left.height, &input.set_orientation); + eval overflow_marker_shape((s) model.set_overflow_marker_shape(s)); - model.label.set_content <+ input.set_label; eval input.set_label_hidden((v) model.set_label_hidden(*v)); - eval model.label.height((h) model.label.set_y(*h / 2.0)); - label_pos_x <- all4( + model.label.set_content <+ input.set_label; + label_position <- all6( &input.set_width, &input.set_height, &model.label.width, + &model.label.height, &input.set_label_position, - ).map( - |(comp_width, comp_height, lab_width, position)| match *position { - LabelPosition::Inside => -comp_width / 2.0 + comp_height / 2.0, - LabelPosition::Outside => -comp_width / 2.0 - comp_height / 2.0 - lab_width, - } + &input.set_orientation, ); - eval label_pos_x((x) model.label.set_x(*x)); + eval label_position((p) model.set_label_position(p)); + + output.orientation <+ input.set_orientation; }; } @@ -349,9 +753,10 @@ impl Slider { fn init_component_colors(&self) { let network = self.frp.network(); let input = &self.frp.input; + let output = &self.frp.private.output; let model = &self.model; let background_color_anim = color::Animation::new(network); - let track_color_anim = color::Animation::new(network); + let indicator_color_anim = color::Animation::new(network); let value_text_color_anim = color::Animation::new(network); let label_color_anim = color::Animation::new(network); @@ -359,26 +764,79 @@ impl Slider { background_color <- all2(&input.set_background_color, &input.set_slider_disabled); background_color_anim.target <+ background_color.map(desaturate_color); eval background_color_anim.value((color) model.set_background_color(color)); - track_color <- all2(&input.set_slider_track_color, &input.set_slider_disabled); - track_color_anim.target <+ track_color.map(desaturate_color); - eval track_color_anim.value((color) model.set_track_color(color)); + indicator_color <- all2(&input.set_value_indicator_color, &input.set_slider_disabled); + indicator_color_anim.target <+ indicator_color.map(desaturate_color); + eval indicator_color_anim.value((color) model.set_indicator_color(color)); value_text_color <- all2(&input.set_value_text_color, &input.set_slider_disabled); value_text_color_anim.target <+ value_text_color.map(desaturate_color); eval value_text_color_anim.value((color) model.set_value_text_property(color)); label_color <- all2(&input.set_label_color, &input.set_slider_disabled); label_color_anim.target <+ label_color.map(desaturate_color); eval label_color_anim.value((color) model.label.set_property_default(color)); + + output.disabled <+ input.set_slider_disabled; }; } - /// Initialize the precision adjustment areas above/below the slider and the default precision - /// value. - fn init_precision_defaults(&self) { + /// Initialize the textual value editing FRP network. + fn init_value_editing(&self) { + let network = self.frp.network(); + let input = &self.frp.input; + let output = &self.frp.private.output; + let model = &self.model; + + frp::extend! { network + start_editing <- input.start_value_editing.gate_not(&output.disabled); + start_editing <- start_editing.gate_not(&input.set_value_text_hidden); + value_on_edit <- output.value.sample(&start_editing); + prec_on_edit <- output.precision.sample(&start_editing); + max_places_on_edit <- + input.set_max_disp_decimal_places.sample(&start_editing); + value_text_on_edit <- all3(&value_on_edit, &prec_on_edit, &max_places_on_edit); + value_text_on_edit <- value_text_on_edit.map(|t| value_text_truncate(t).to_im_string()); + model.value_text_edit.set_content <+ value_text_on_edit; + stop_editing <- any2(&input.finish_value_editing, &input.cancel_value_editing); + editing <- bool(&stop_editing, &start_editing); + + value_text_after_edit <- + model.value_text_edit.content.sample(&input.finish_value_editing); + value_text_after_edit <- value_text_after_edit.map(|s| String::from(s).to_im_string()); + value_after_edit <- value_text_after_edit.map(|s| f32::from_str(s).ok()); + edit_success <- value_after_edit.map(|v| v.is_some()); + value_after_edit <- value_after_edit.map(|v| v.unwrap_or_default()); + prec_after_edit <- value_text_after_edit.map(|s| get_value_text_precision(s)); + prec_after_edit <- all2(&prec_after_edit, &input.set_default_precision); + prec_after_edit <- prec_after_edit.map(|(prec, default_prec)| prec.min(*default_prec)); + value_after_edit <- all5( + &value_after_edit, + &input.set_min_value, + &input.set_max_value, + &input.set_lower_limit_type, + &input.set_upper_limit_type, + ).map(value_limit_clamp); + + output.editing <+ editing; + output.precision <+ prec_after_edit.gate(&edit_success); + value_after_edit <- value_after_edit.gate(&edit_success); + output.value <+ value_after_edit; + model.value_animation.target <+ value_after_edit; + editing_event <- any2(&start_editing, &stop_editing); + editing <- all2(&editing, &output.precision).sample(&editing_event); + eval editing((t) model.set_edit_mode(t)); + }; + } + + /// Initialize the compinent with default values. + fn init_slider_defaults(&self) { self.frp.set_default_precision(PRECISION_DEFAULT); self.frp.set_precision_adjustment_margin(PRECISION_ADJUSTMENT_MARGIN); self.frp.set_precision_adjustment_step_size(PRECISION_ADJUSTMENT_STEP_SIZE); + self.frp.set_max_precision_adjustment_steps(MAX_PRECISION_ADJUSTMENT_STEPS); self.frp.set_max_value(MAX_VALUE_DEFAULT); self.frp.set_max_disp_decimal_places(MAX_DISP_DECIMAL_PLACES_DEFAULT); + self.frp.set_tooltip_delay(INFORMATION_TOOLTIP_DELAY); + self.frp.set_precision_popup_duration(PRECISION_ADJUSTMENT_POPUP_DURATION); + self.frp.set_thumb_size(THUMB_SIZE_DEFAULT); } } @@ -406,6 +864,27 @@ impl application::View for Slider { fn app(&self) -> &Application { &self.app } + + fn default_shortcuts() -> Vec { + use shortcut::ActionType::DoublePress; + use shortcut::ActionType::Press; + vec![ + Self::self_shortcut_when( + DoublePress, + "left-mouse-button", + "start_value_editing", + "hovered & !editing", + ), + Self::self_shortcut_when( + DoublePress, + "left-mouse-button", + "cancel_value_editing", + "!hovered & editing", + ), + Self::self_shortcut_when(Press, "enter", "finish_value_editing", "editing"), + Self::self_shortcut_when(Press, "escape", "cancel_value_editing", "editing"), + ] + } } @@ -414,23 +893,36 @@ impl application::View for Slider { // === Value text formatting === // ============================= +/// Rounds and truncates a floating point value to a specified precision. +fn value_text_truncate((value, precision, max_digits): &(f32, f32, usize)) -> String { + if *precision < 1.0 || *max_digits == 0 { + let digits = (-precision.log10()).ceil() as usize; + let digits = digits.min(*max_digits); + format!("{:.prec$}", value, prec = digits) + } else { + format!("{:.0}", value) + } +} + /// Rounds a floating point value to a specified precision and provides two strings: one with the /// digits left of the decimal point, and one optional with the digits right of the decimal point. fn value_text_truncate_split( (value, precision, max_digits): &(f32, f32, usize), ) -> (ImString, Option) { - if *precision < 1.0 || *max_digits == 0 { - let digits = (-precision.log10()).ceil() as usize; - let digits = digits.min(*max_digits); - let text = format!("{:.prec$}", value, prec = digits); - let mut text_iter = text.split('.'); - let text_left = text_iter.next().map(|s| s.to_im_string()); - let text_left = text_left.unwrap_or_default(); - let text_right = text_iter.next().map(|s| s.to_im_string()); - (text_left, text_right) - } else { - let text_left = format!("{:.0}", value.trunc()).to_im_string(); - (text_left, None) + let text = value_text_truncate(&(*value, *precision, *max_digits)); + let mut text_iter = text.split('.'); + let text_left = text_iter.next().map(|s| s.to_im_string()).unwrap_or_default(); + let text_right = text_iter.next().map(|s| s.to_im_string()); + (text_left, text_right) +} + +/// Get the precision of a string containing a decimal value. +fn get_value_text_precision(text: &str) -> f32 { + let mut text_iter = text.split('.').skip(1); + let text_right_len = text_iter.next().map(|t| t.len()); + match text_right_len { + None => 1.0, + Some(n) => 10f32.powi(-(n as i32)), } } diff --git a/lib/rust/ensogl/component/slider/src/model.rs b/lib/rust/ensogl/component/slider/src/model.rs index 8e65ef073d9..cc578d892e4 100644 --- a/lib/rust/ensogl/component/slider/src/model.rs +++ b/lib/rust/ensogl/component/slider/src/model.rs @@ -3,12 +3,18 @@ use ensogl_core::display::shape::*; use ensogl_core::prelude::*; +use crate::LabelPosition; +use crate::SliderOrientation; +use crate::ValueIndicator; + use ensogl_core::application::Application; use ensogl_core::data::color; use ensogl_core::display; -use ensogl_hardcoded_theme as theme; +use ensogl_core::Animation; +use ensogl_hardcoded_theme::component::slider as theme; use ensogl_text as text; use ensogl_text::formatting::ResolvedProperty; +use ensogl_tooltip::Tooltip; @@ -22,6 +28,8 @@ const COMPONENT_MARGIN: f32 = 4.0; const COMPONENT_WIDTH_DEFAULT: f32 = 200.0; /// Default component height on initialization. const COMPONENT_HEIGHT_DEFAULT: f32 = 50.0; +/// Overflow marker size as fraction of the text height. +const OVERFLOW_MARKER_SIZE: f32 = 0.75; @@ -68,16 +76,65 @@ mod track { ensogl_core::shape! { above = [background]; pointer_events = false; - (style:Style, slider_fraction_filled:f32, color:Vector4) { + (style:Style, slider_fraction_horizontal:f32, slider_fraction_vertical:f32, color:Vector4) { let Background{width,height,shape: background} = Background::new(); - let track = Rect((&width * &slider_fraction_filled,&height)); - let track = track.translate_x(&width * (&slider_fraction_filled - 1.0) * 0.5); + let track = Rect(( + &width * &slider_fraction_horizontal, + &height * &slider_fraction_vertical, + )); + let track = track.translate_x(&width * (&slider_fraction_horizontal - 1.0) * 0.5); + let track = track.translate_y(&height * (&slider_fraction_vertical - 1.0) * 0.5); let track = track.intersection(background).fill(color); track.into() } } } +/// Thumb shape that moves along the slider proportional to the slider value. +mod thumb { + use super::*; + + ensogl_core::shape! { + above = [background]; + pointer_events = false; + (style:Style, slider_fraction:f32, thumb_width:f32, thumb_height:f32, color:Vector4) { + let Background{width,height,shape: background} = Background::new(); + let thumb_width = &width * &thumb_width; + let thumb_height = &height * &thumb_height; + let thumb = Rect((&thumb_width, &thumb_height)); + let thumb = thumb.corners_radius(&thumb_height / 2.0); + let range_x = &width - &thumb_width; + let range_y = &height - &thumb_height; + let thumb = thumb.translate_x(-&range_x * 0.5 + &range_x * &slider_fraction); + let thumb = thumb.translate_y(-&range_y * 0.5 + &range_y * &slider_fraction); + let thumb = thumb.intersection(background).fill(color); + thumb.into() + } + } +} + +/// Triangle shape used as an overflow indicator on either side of the range. +mod overflow { + use super::*; + + ensogl_core::shape! { + above = [background, track, thumb]; + pointer_events = false; + (style:Style, color:Vector4) { + let width: Var = "input_size.x".into(); + let height: Var = "input_size.y".into(); + let width = width - COMPONENT_MARGIN.px() * 2.0; + let height = height - COMPONENT_MARGIN.px() * 2.0; + + let color = style.get_color(theme::overflow::color); + let triangle = Triangle(width, height); + let triangle = triangle.fill(color); + + triangle.into() + } + } +} + // =============================== @@ -91,6 +148,12 @@ pub struct Model { pub background: background::View, /// Slider track element that fills the slider proportional to the slider value. pub track: track::View, + /// Slider thumb element that moves across the slider proportional to the slider value. + pub thumb: thumb::View, + /// Indicator for overflow when the value is below the lower limit. + pub overflow_lower: overflow::View, + /// Indicator for overflow when the value is above the upper limit. + pub overflow_upper: overflow::View, /// Slider label that is shown next to the slider. pub label: text::Text, /// Textual representation of the slider value, only part left of the decimal point. @@ -99,20 +162,32 @@ pub struct Model { pub value_text_dot: text::Text, /// Textual representation of the slider value, only part right of the decimal point. pub value_text_right: text::Text, + /// Textual representation of the slider value used when editing the value as text input. + pub value_text_edit: text::Text, + /// Tooltip component showing either a tooltip message or slider precision changes. + pub tooltip: Tooltip, + /// Animation component that smoothly adjusts the slider value on large jumps. + pub value_animation: Animation, /// Root of the display object. pub root: display::object::Instance, } impl Model { /// Create a new slider model. - pub fn new(app: &Application) -> Self { + pub fn new(app: &Application, frp_network: &frp::Network) -> Self { let root = display::object::Instance::new(); let label = app.new_view::(); let value_text_left = app.new_view::(); let value_text_dot = app.new_view::(); let value_text_right = app.new_view::(); + let value_text_edit = app.new_view::(); + let tooltip = Tooltip::new(app); + let value_animation = Animation::new_non_init(frp_network); let background = background::View::new(); let track = track::View::new(); + let thumb = thumb::View::new(); + let overflow_lower = overflow::View::new(); + let overflow_upper = overflow::View::new(); let scene = &app.display.default_scene; let style = StyleWatch::new(&app.display.default_scene.style_sheet); @@ -122,19 +197,27 @@ impl Model { root.add_child(&value_text_left); root.add_child(&value_text_dot); root.add_child(&value_text_right); + app.display.default_scene.add_child(&tooltip); value_text_left.add_to_scene_layer(&scene.layers.label); value_text_dot.add_to_scene_layer(&scene.layers.label); value_text_right.add_to_scene_layer(&scene.layers.label); + value_text_edit.add_to_scene_layer(&scene.layers.label); label.add_to_scene_layer(&scene.layers.label); let model = Self { background, track, + thumb, + overflow_lower, + overflow_upper, label, value_text_left, value_text_dot, value_text_right, + value_text_edit, + tooltip, + value_animation, root, }; model.init(style) @@ -142,10 +225,16 @@ impl Model { /// Initialise slider model. pub fn init(self, style: StyleWatch) -> Self { - let background_color = style.get_color(theme::component::slider::background::color); - let track_color = style.get_color(theme::component::slider::track::color); + let background_color = style.get_color(theme::background::color); + let track_color = style.get_color(theme::track::color); + self.value_text_left.set_font(text::font::DEFAULT_FONT); + self.value_text_dot.set_font(text::font::DEFAULT_FONT); + self.value_text_right.set_font(text::font::DEFAULT_FONT); + self.value_text_edit.set_font(text::font::DEFAULT_FONT); + self.label.set_font(text::font::DEFAULT_FONT); self.background.color.set(background_color.into()); self.track.color.set(track_color.into()); + self.thumb.color.set(track_color.into()); self.set_size(Vector2(COMPONENT_WIDTH_DEFAULT, COMPONENT_HEIGHT_DEFAULT)); self.value_text_dot.set_content("."); self @@ -156,11 +245,13 @@ impl Model { let margin = Vector2(COMPONENT_MARGIN * 2.0, COMPONENT_MARGIN * 2.0); self.background.size.set(size + margin); self.track.size.set(size + margin); + self.thumb.size.set(size + margin); } - /// Set the color of the slider track. - pub fn set_track_color(&self, color: &color::Lcha) { + /// Set the color of the slider track or thumb. + pub fn set_indicator_color(&self, color: &color::Lcha) { self.track.color.set(color::Rgba::from(color).into()); + self.thumb.color.set(color::Rgba::from(color).into()); } /// Set the color of the slider background. @@ -168,6 +259,143 @@ impl Model { self.background.color.set(color::Rgba::from(color).into()); } + /// Set whether the lower overfow marker is visible. + pub fn set_value_indicator(&self, indicator: &ValueIndicator) { + match indicator { + ValueIndicator::Track => { + self.root.add_child(&self.track); + self.root.remove_child(&self.thumb); + } + ValueIndicator::Thumb => { + self.root.add_child(&self.thumb); + self.root.remove_child(&self.track); + } + } + } + + /// Set the position of the value indicator. + pub fn set_indicator_position( + &self, + (fraction, size, orientation): &(f32, f32, SliderOrientation), + ) { + self.thumb.slider_fraction.set(*fraction); + match orientation { + SliderOrientation::Horizontal => { + self.track.slider_fraction_horizontal.set(fraction.clamp(0.0, 1.0)); + self.track.slider_fraction_vertical.set(1.0); + self.thumb.thumb_width.set(*size); + self.thumb.thumb_height.set(1.0); + } + SliderOrientation::Vertical => { + self.track.slider_fraction_horizontal.set(1.0); + self.track.slider_fraction_vertical.set(fraction.clamp(0.0, 1.0)); + self.thumb.thumb_width.set(1.0); + self.thumb.thumb_height.set(*size); + } + } + } + + /// Set the size and orientation of the overflow markers. + pub fn set_overflow_marker_shape(&self, (size, orientation): &(f32, SliderOrientation)) { + let margin = Vector2(COMPONENT_MARGIN * 2.0, COMPONENT_MARGIN * 2.0); + let size = Vector2(*size, *size) * OVERFLOW_MARKER_SIZE + margin; + self.overflow_lower.size.set(size); + self.overflow_upper.size.set(size); + match orientation { + SliderOrientation::Horizontal => { + self.overflow_lower.set_rotation_z(std::f32::consts::FRAC_PI_2); + self.overflow_upper.set_rotation_z(-std::f32::consts::FRAC_PI_2); + } + SliderOrientation::Vertical => { + self.overflow_lower.set_rotation_z(std::f32::consts::PI); + self.overflow_upper.set_rotation_z(0.0); + } + } + } + + /// Set the position of the overflow markers. + pub fn set_overflow_marker_position( + &self, + (comp_width, comp_height, orientation): &(f32, f32, SliderOrientation), + ) { + match orientation { + SliderOrientation::Horizontal => { + let pos_x = comp_width / 2.0 - comp_height / 4.0; + self.overflow_lower.set_x(-pos_x); + self.overflow_lower.set_y(0.0); + self.overflow_upper.set_x(pos_x); + self.overflow_upper.set_y(0.0); + } + SliderOrientation::Vertical => { + let pos_y = comp_height / 2.0 - comp_width / 4.0; + self.overflow_lower.set_x(0.0); + self.overflow_lower.set_y(-pos_y); + self.overflow_upper.set_x(0.0); + self.overflow_upper.set_y(pos_y); + } + } + } + + /// Set whether the lower overfow marker is visible. + pub fn set_overflow_lower_visible(&self, visible: bool) { + if visible { + self.root.add_child(&self.overflow_lower); + } else { + self.root.remove_child(&self.overflow_lower); + } + } + + /// Set whether the upper overfow marker is visible. + pub fn set_overflow_upper_visible(&self, visible: bool) { + if visible { + self.root.add_child(&self.overflow_upper); + } else { + self.root.remove_child(&self.overflow_upper); + } + } + + /// Set the position of the slider's label. + pub fn set_label_position( + &self, + (comp_width, comp_height, lab_width, lab_height, position, orientation): &( + f32, + f32, + f32, + f32, + LabelPosition, + SliderOrientation, + ), + ) { + let label_position_x = match orientation { + SliderOrientation::Horizontal => match position { + LabelPosition::Inside => -comp_width / 2.0 + comp_height / 2.0, + LabelPosition::Outside => -comp_width / 2.0 - comp_height / 2.0 - lab_width, + }, + SliderOrientation::Vertical => -lab_width / 2.0, + }; + let label_position_y = match orientation { + SliderOrientation::Horizontal => lab_height / 2.0, + SliderOrientation::Vertical => match position { + LabelPosition::Inside => comp_height / 2.0 - comp_width / 2.0, + LabelPosition::Outside => comp_height / 2.0 + comp_width / 2.0 + lab_height, + }, + }; + self.label.set_xy(Vector2(label_position_x, label_position_y)); + } + + /// Set whether the slider value text is hidden. + pub fn set_value_text_hidden(&self, hidden: bool) { + if hidden { + self.root.remove_child(&self.value_text_left); + self.root.remove_child(&self.value_text_dot); + self.root.remove_child(&self.value_text_right); + } else { + self.root.add_child(&self.value_text_left); + self.root.add_child(&self.value_text_dot); + self.root.add_child(&self.value_text_right); + } + } + /// Set whether the slider label is hidden. pub fn set_label_hidden(&self, hidden: bool) { if hidden { @@ -177,6 +405,29 @@ impl Model { } } + /// Set whether the value is being edited. This hides the value display and shows a text editor + /// field to enter a new value. + pub fn set_edit_mode(&self, (editing, precision): &(bool, f32)) { + if *editing { + self.root.remove_child(&self.value_text_left); + self.root.remove_child(&self.value_text_dot); + self.root.remove_child(&self.value_text_right); + self.root.add_child(&self.value_text_edit); + self.value_text_edit.deprecated_focus(); + self.value_text_edit.add_cursor_at_front(); + self.value_text_edit.cursor_select_to_text_end(); + } else { + self.root.add_child(&self.value_text_left); + if *precision < 1.0 { + self.root.add_child(&self.value_text_dot); + self.root.add_child(&self.value_text_right); + } + self.root.remove_child(&self.value_text_edit); + self.value_text_edit.deprecated_defocus(); + self.value_text_edit.remove_all_cursors(); + } + } + /// Set whether the value display decimal point and the text right of it are visible. pub fn set_value_text_right_visible(&self, enabled: bool) { if enabled { diff --git a/lib/rust/ensogl/example/slider/Cargo.toml b/lib/rust/ensogl/example/slider/Cargo.toml index 6a3e4b8703b..1b47f852c63 100644 --- a/lib/rust/ensogl/example/slider/Cargo.toml +++ b/lib/rust/ensogl/example/slider/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] +enso-frp = { path = "../../../frp" } ensogl-core = { path = "../../core" } ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" } ensogl-text-msdf = { path = "../../component/text/src/font/msdf" } diff --git a/lib/rust/ensogl/example/slider/src/lib.rs b/lib/rust/ensogl/example/slider/src/lib.rs index e9d848d643d..8484f3aff81 100644 --- a/lib/rust/ensogl/example/slider/src/lib.rs +++ b/lib/rust/ensogl/example/slider/src/lib.rs @@ -24,20 +24,291 @@ use ensogl_core::prelude::*; use wasm_bindgen::prelude::*; +use enso_frp as frp; +use ensogl_core::application::shortcut; use ensogl_core::application::Application; +use ensogl_core::application::View; use ensogl_core::data::color; -use ensogl_core::display::object::ObjectOps; +use ensogl_core::display; use ensogl_hardcoded_theme as theme; use ensogl_slider as slider; use ensogl_text_msdf::run_once_initialized; +// =================================== +// === Basic slider initialization === +// =================================== + +/// Create a basic slider. +fn make_slider(app: &Application) -> slider::Slider { + let slider = app.new_view::(); + slider.frp.set_background_color(color::Lcha(0.8, 0.0, 0.0, 1.0)); + slider.frp.set_max_value(5.0); + slider.frp.set_default_value(1.0); + slider.frp.set_value(1.0); + slider +} + + + +// ======================== +// === Model definition === +// ======================== + +/// The slider collection model holds a set of sliders that can be instantiated and dropped. +#[derive(Debug, Clone, CloneRef)] +pub struct Model { + /// Vector that holds example sliders until they are dropped. + sliders: Rc>>, + app: Application, + root: display::object::Instance, +} + +impl Model { + fn new(app: &Application) -> Self { + let app = app.clone_ref(); + let sliders = Rc::new(RefCell::new(Vec::new())); + let root = display::object::Instance::new(); + let model = Self { app, sliders, root }; + model.init_sliders(); + model + } + + /// Add example sliders to scene. + fn init_sliders(&self) { + let slider1 = make_slider(&self.app); + slider1.frp.set_width(400.0); + slider1.frp.set_height(50.0); + slider1.set_y(-120.0); + slider1.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider1.frp.set_label("Soft limits + tooltip"); + slider1.frp.set_lower_limit_type(slider::SliderLimit::Soft); + slider1.frp.set_upper_limit_type(slider::SliderLimit::Soft); + slider1.frp.set_tooltip("Slider information tooltip."); + self.root.add_child(&slider1); + self.sliders.borrow_mut().push(slider1); + + let slider2 = make_slider(&self.app); + slider2.frp.set_width(400.0); + slider2.frp.set_height(50.0); + slider2.set_y(-60.0); + slider2.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider2.frp.set_slider_disabled(true); + slider2.frp.set_label("Disabled"); + self.root.add_child(&slider2); + self.sliders.borrow_mut().push(slider2); + + let slider3 = make_slider(&self.app); + slider3.frp.set_width(400.0); + slider3.frp.set_height(50.0); + slider3.set_y(0.0); + slider3.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider3.frp.set_default_value(100.0); + slider3.frp.set_value(100.0); + slider3.frp.set_max_value(500.0); + slider3.frp.set_label("Adaptive lower limit"); + slider3.frp.set_lower_limit_type(slider::SliderLimit::Adaptive); + self.root.add_child(&slider3); + self.sliders.borrow_mut().push(slider3); + + let slider4 = make_slider(&self.app); + slider4.frp.set_width(400.0); + slider4.frp.set_height(50.0); + slider4.set_y(60.0); + slider4.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider4.frp.set_label("Adaptive upper limit"); + slider4.frp.set_label_position(slider::LabelPosition::Inside); + slider4.frp.set_upper_limit_type(slider::SliderLimit::Adaptive); + self.root.add_child(&slider4); + self.sliders.borrow_mut().push(slider4); + + let slider5 = make_slider(&self.app); + slider5.frp.set_width(75.0); + slider5.frp.set_height(230.0); + slider5.set_y(-35.0); + slider5.set_x(275.0); + slider5.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider5.frp.set_label("Hard limits"); + slider5.frp.set_orientation(slider::SliderOrientation::Vertical); + slider5.frp.set_max_disp_decimal_places(4); + self.root.add_child(&slider5); + self.sliders.borrow_mut().push(slider5); + + let slider6 = make_slider(&self.app); + slider6.frp.set_width(75.0); + slider6.frp.set_height(230.0); + slider6.set_y(-35.0); + slider6.set_x(375.0); + slider6.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider6.frp.set_label("Soft\nlimits"); + slider6.frp.set_label_position(slider::LabelPosition::Inside); + slider6.frp.set_lower_limit_type(slider::SliderLimit::Soft); + slider6.frp.set_upper_limit_type(slider::SliderLimit::Soft); + slider6.frp.set_orientation(slider::SliderOrientation::Vertical); + slider6.frp.set_max_disp_decimal_places(4); + self.root.add_child(&slider6); + self.sliders.borrow_mut().push(slider6); + + let slider7 = make_slider(&self.app); + slider7.frp.set_width(400.0); + slider7.frp.set_height(10.0); + slider7.set_y(-160.0); + slider7.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider7.frp.set_value_text_hidden(true); + slider7.frp.set_precision_adjustment_disabled(true); + slider7.frp.set_value_indicator(slider::ValueIndicator::Thumb); + slider7.frp.set_thumb_size(0.1); + self.root.add_child(&slider7); + self.sliders.borrow_mut().push(slider7); + + let slider8 = make_slider(&self.app); + slider8.frp.set_width(400.0); + slider8.frp.set_height(10.0); + slider8.set_y(-180.0); + slider8.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider8.frp.set_value_text_hidden(true); + slider8.frp.set_precision_adjustment_disabled(true); + slider8.frp.set_value_indicator(slider::ValueIndicator::Thumb); + slider8.frp.set_thumb_size(0.25); + self.root.add_child(&slider8); + self.sliders.borrow_mut().push(slider8); + + let slider9 = make_slider(&self.app); + slider9.frp.set_width(400.0); + slider9.frp.set_height(10.0); + slider9.set_y(-200.0); + slider9.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider9.frp.set_value_text_hidden(true); + slider9.frp.set_precision_adjustment_disabled(true); + slider9.frp.set_value_indicator(slider::ValueIndicator::Thumb); + slider9.frp.set_thumb_size(0.5); + self.root.add_child(&slider9); + self.sliders.borrow_mut().push(slider9); + + let slider10 = make_slider(&self.app); + slider10.frp.set_width(10.0); + slider10.frp.set_height(230.0); + slider10.set_y(-35.0); + slider10.set_x(430.0); + slider10.frp.set_value_indicator_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); + slider10.frp.set_value_text_hidden(true); + slider10.frp.set_precision_adjustment_disabled(true); + slider10.frp.set_value_indicator(slider::ValueIndicator::Thumb); + slider10.frp.set_orientation(slider::SliderOrientation::Vertical); + self.root.add_child(&slider10); + self.sliders.borrow_mut().push(slider10); + } + + /// Drop all sliders from scene. + fn drop_sliders(&self) { + for slider in self.sliders.borrow_mut().drain(0..) { + self.root.remove_child(&slider); + } + } +} + +impl display::Object for Model { + fn display_object(&self) -> &display::object::Instance { + &self.root + } +} + + + +// =================== +// === FRP network === +// =================== + +ensogl_core::define_endpoints! { + Input { + /// Add example sliders to scene. + init_sliders(), + /// Drop all sliders from scene. + drop_sliders(), + } + Output { + } +} + +impl FrpNetworkProvider for SliderCollection { + fn network(&self) -> &frp::Network { + self.frp.network() + } +} + + + +// ========================== +// === Slider collection === +// ========================== + +/// A component that stores an array of slider components. It receives shortcuts to either +/// instantiate a new set of sliders or to drop the existing ones. +#[derive(Clone, Debug, Deref)] +struct SliderCollection { + #[deref] + frp: Frp, + app: Application, + model: Model, +} + +impl SliderCollection { + fn new(app: &Application) -> Self { + let frp = Frp::new(); + let app = app.clone_ref(); + let model = Model::new(&app); + Self { frp, app, model }.init() + } + + fn init(self) -> Self { + let network = self.frp.network(); + let input = &self.frp.input; + let model = &self.model; + + frp::extend! { network + eval_ input.init_sliders( model.init_sliders() ); + eval_ input.drop_sliders( model.drop_sliders() ); + } + self + } +} + +impl display::Object for SliderCollection { + fn display_object(&self) -> &display::object::Instance { + self.model.display_object() + } +} + +impl View for SliderCollection { + fn label() -> &'static str { + "Slider Collection" + } + + fn new(app: &Application) -> Self { + Self::new(app) + } + + fn app(&self) -> &Application { + &self.app + } + + fn default_shortcuts() -> Vec { + use shortcut::ActionType::Press; + vec![ + Self::self_shortcut(Press, "ctrl a", "init_sliders"), + Self::self_shortcut(Press, "ctrl d", "drop_sliders"), + ] + } +} + + + // =================== // === Entry Point === // =================== -/// An entry point. +/// Entry point for the example scene. #[entry_point] #[allow(dead_code)] pub fn main() { @@ -48,59 +319,19 @@ pub fn main() { }); } -fn make_slider(app: &Application) -> Leak { - let slider = app.new_view::(); - slider.frp.set_background_color(color::Lcha(0.8, 0.0, 0.0, 1.0)); - slider.frp.set_max_value(5.0); - slider.frp.set_default_value(1.0); - slider.frp.set_value(1.0); - app.display.add_child(&slider); - Leak::new(slider) -} - // ======================== // === Init Application === // ======================== +/// Initialize a `SliderCollection` and do not drop it. fn init(app: &Application) { theme::builtin::dark::register(app); theme::builtin::light::register(app); theme::builtin::light::enable(app); - let slider1 = make_slider(app); - slider1.inner().frp.set_width(400.0); - slider1.inner().frp.set_height(50.0); - slider1.inner().frp.set_slider_track_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); - slider1.inner().frp.set_value_text_color(color::Lcha(0.2, 0.7, 0.2, 1.0)); - slider1.inner().frp.set_label_color(color::Lcha(0.2, 0.7, 0.2, 1.0)); - slider1.inner().frp.set_label("Color label"); - - let slider2 = make_slider(app); - slider2.inner().frp.set_width(400.0); - slider2.inner().frp.set_height(50.0); - slider2.inner().set_y(60.0); - slider2.inner().frp.set_slider_track_color(color::Lcha(0.4, 0.7, 0.2, 1.0)); - slider2.inner().frp.set_value_text_color(color::Lcha(0.2, 0.7, 0.7, 1.0)); - slider2.inner().frp.set_slider_disabled(true); - slider2.inner().frp.set_label("Disabled slider"); - - let slider3 = make_slider(app); - slider3.inner().frp.set_width(400.0); - slider3.inner().frp.set_height(50.0); - slider3.inner().set_y(120.0); - slider3.inner().frp.set_slider_track_color(color::Lcha(0.4, 0.7, 0.7, 1.0)); - slider3.inner().frp.set_value_text_color(color::Lcha(0.2, 0.7, 0.2, 1.0)); - slider3.inner().frp.set_label("Inner label"); - slider3.inner().frp.set_label_position(slider::LabelPosition::Inside); - - let slider4 = make_slider(app); - slider4.inner().frp.set_width(400.0); - slider4.inner().frp.set_height(50.0); - slider4.inner().set_y(180.0); - slider4.inner().frp.set_slider_track_color(color::Lcha(0.4, 0.7, 0.2, 1.0)); - slider4.inner().frp.set_value_text_color(color::Lcha(0.2, 0.7, 0.7, 1.0)); - slider4.inner().frp.set_label("Disabled label"); - slider4.inner().frp.set_label_hidden(true); + let slider_collection = app.new_view::(); + app.display.add_child(&slider_collection); + Leak::new(slider_collection); }