mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 22:21:40 +03:00
Numeric slider component enhancement (#3885)
This is an enhancement of the `Slider` component implemented in #3852. It adds the following features: * Tooltips and precision change hints * Selectable slider limit behaviors * Textual slider value editing * Vertical slider layout #### Tooltips An information tooltip can now be added to a slider, it is shown when the mouse hovers over the component. Additionally, a pop-up indicating the slider's precision appears when the slider's precision has been adjusted. https://user-images.githubusercontent.com/117099775/206148098-3b4dc059-18aa-4200-9ee0-5d4382363810.mp4 #### Slider limits The previous slider implementation clamped the adjusted value to the slider's minimum/maximum limits. Now the following behaviors are available: * Hard limits: Clamp the value to a range within the slider's limits. * Soft limits: The value can extend beyond the slider's limits. When this occurs, an overflow indicator will be displayed on the side of the limit that is exceeded. * Adaptive limits: The value can extend beyond the slider's limits. When this occurs, the exceeded limit will temporarily be adjusted to double the slider's range. This will be performed iteratively until the value falls within the extended limits. When a limit is extended and the value is adjusted to fit a smaller range, the extended limit will be iteratively halved until only the necessary range is covered. The slider's extended limits will never shrink to a range smaller than the original range. These behaviors can be set to the lower and upper limits of a slider independently. https://user-images.githubusercontent.com/117099775/206148139-6149c91d-ef49-4e2d-97f6-71084f52591c.mp4 #### Textual editing The slider's value can now be entered through a text input field. Double-click to edit the slider's current value. To confirm the edit press `enter`, or press `escape` to cancel the edit. If an invalid value is entered on confirmation the slider will revert to its value before the edit. The slider's precision will be adjusted based on the number of decimal places of the value entered. https://user-images.githubusercontent.com/117099775/206148170-d3fa4c82-6e73-4b1c-9be9-cb99979f7b70.mp4 #### Vertical layout The slider component now supports a vertical layout. In this case value adjustment is performed by a vertical mouse movement, and a horizontal movement adjusts the slider's precision. The slider's track now fills the component in a vertical direction, and the slider's label is displayed near the top end of the component. https://user-images.githubusercontent.com/117099775/206148211-0f176aaf-bc1b-45e2-afd7-0d28391aafcb.mp4 #### Scroll bar mode The slider component supports two indicator modes: * `Track`: The component is filled with a colored bar from the lower limit (empty) to the upper limit (full) dependent on the slider's value. * `Thumb`: The component contains a rounded indicator that moves along the slider from one end to the other, indicating the slider's value proportionally to the slider's limits. The width of the indicator is configurable. In addition, the value text, text entry, and precision adjustment can be turned off to provide a scroll bar appearance when used with the `Thumb` indicator. https://user-images.githubusercontent.com/117099775/206148261-ae291073-85e9-4082-9f91-39b65fecdc0f.mp4 #### Example scene shortcuts The example scene contains two shortcuts in order to evaluate the dynamic addition and removal of the slider components: * `CTRL+D` drops all the slider components that are added to the scene. * `CTRL+A` adds a new set of example slider components to the scene.
This commit is contained in:
parent
3d93fc24c0
commit
b2b60612d7
@ -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
|
||||
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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]]
|
||||
|
@ -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
|
||||
|
@ -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" }
|
||||
|
@ -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<shortcut::Shortcut> {
|
||||
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<ImString>) {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Pixels> = "input_size.x".into();
|
||||
let height: Var<Pixels> = "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<f32>,
|
||||
/// 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::<text::Text>();
|
||||
let value_text_left = app.new_view::<text::Text>();
|
||||
let value_text_dot = app.new_view::<text::Text>();
|
||||
let value_text_right = app.new_view::<text::Text>();
|
||||
let value_text_edit = app.new_view::<text::Text>();
|
||||
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 {
|
||||
|
@ -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" }
|
||||
|
@ -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::Slider>();
|
||||
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<RefCell<Vec<slider::Slider>>>,
|
||||
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<shortcut::Shortcut> {
|
||||
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<slider::Slider> {
|
||||
let slider = app.new_view::<slider::Slider>();
|
||||
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::<SliderCollection>();
|
||||
app.display.add_child(&slider_collection);
|
||||
Leak::new(slider_collection);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user