From 3869e80aad9b988633c44bb112dccbacd0751620 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Fri, 21 May 2021 11:41:03 +0200 Subject: [PATCH] Implement number and range pickers. (https://github.com/enso-org/ide/pull/1524) Original commit: https://github.com/enso-org/ide/commit/b361fdb5296f8e4f247bfa174ac11a89404fe68d --- ide/CHANGELOG.md | 12 +- ide/src/rust/Cargo.lock | 7 + ide/src/rust/ensogl/example/src/leak.rs | 43 ++ ide/src/rust/ensogl/example/src/lib.rs | 5 +- ide/src/rust/ensogl/example/src/slider.rs | 78 ++++ ide/src/rust/ensogl/lib/components/Cargo.toml | 1 + .../ensogl/lib/components/src/component.rs | 92 +++++ ide/src/rust/ensogl/lib/components/src/lib.rs | 6 +- .../ensogl/lib/components/src/selector.rs | 68 ++++ .../lib/components/src/selector/bounds.rs | 372 ++++++++++++++++++ .../src/selector/decimal_aligned.rs | 107 +++++ .../ensogl/lib/components/src/selector/frp.rs | 126 ++++++ .../lib/components/src/selector/model.rs | 244 ++++++++++++ .../lib/components/src/selector/number.rs | 137 +++++++ .../lib/components/src/selector/range.rs | 135 +++++++ .../lib/components/src/selector/shape.rs | 297 ++++++++++++++ .../rust/ensogl/lib/components/src/shadow.rs | 43 +- .../ensogl/lib/core/src/application/frp.rs | 4 + .../lib/core/src/data/color/space/def.rs | 10 + .../display/shape/primitive/style_watch.rs | 12 + ide/src/rust/ensogl/lib/theme/src/lib.rs | 13 + ide/src/rust/lib/frp/src/nodes.rs | 60 +++ 22 files changed, 1859 insertions(+), 13 deletions(-) create mode 100644 ide/src/rust/ensogl/example/src/leak.rs create mode 100644 ide/src/rust/ensogl/example/src/slider.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/component.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/bounds.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/decimal_aligned.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/frp.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/model.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/number.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/range.rs create mode 100644 ide/src/rust/ensogl/lib/components/src/selector/shape.rs diff --git a/ide/CHANGELOG.md b/ide/CHANGELOG.md index b7f8ffdd972d..fabb2107bf6b 100644 --- a/ide/CHANGELOG.md +++ b/ide/CHANGELOG.md @@ -1,9 +1,13 @@ -# Enso 2.0.0-alpha.5 (2021-05-14) +# Next Release
![New Features](/docs/assets/tags/new_features.svg) #### Visual Environment +- [Components for picking numbers and ranges.][1524]. We now have some internal + re-usable UI components for selecting numbers or a range. Stay tuned for them + appearing in the IDE. + #### EnsoGL (rendering engine)
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) @@ -29,6 +33,7 @@ If you're interested in the enhancements and fixes made to the Enso compiler, you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). +[1524]: https://github.com/enso-org/ide/pull/1524 [1541]: https://github.com/enso-org/ide/pull/1511 [1538]: https://github.com/enso-org/ide/pull/1538 [1561]: https://github.com/enso-org/ide/pull/1561 @@ -52,10 +57,6 @@ you can find their release notes
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) -- [Fix some internal settings not being applied correctly in the IDE][1539]. - Some arguments were not passed correctly to the IDE leading to erroneous - behaviour in the electron app. This is now fixed. - #### Visual Environment - [Some command line arguments were not applied correctly in the IDE][1536]. @@ -71,7 +72,6 @@ you can find their release notes [1511]: https://github.com/enso-org/ide/pull/1511 [1536]: https://github.com/enso-org/ide/pull/1536 [1531]: https://github.com/enso-org/ide/pull/1531 -[1531]: https://github.com/enso-org/ide/pull/1539
diff --git a/ide/src/rust/Cargo.lock b/ide/src/rust/Cargo.lock index 17a7447b69ff..b791c647d2f0 100644 --- a/ide/src/rust/Cargo.lock +++ b/ide/src/rust/Cargo.lock @@ -881,6 +881,7 @@ dependencies = [ "ensogl-core", "ensogl-text", "ensogl-theme", + "float_eq", "wasm-bindgen-test", ] @@ -1038,6 +1039,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "float_eq" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb23b6902f3cdc0544f9916b4c092f46f4ff984e219d5a0c538b6b3539885af3" + [[package]] name = "fnv" version = "1.0.7" diff --git a/ide/src/rust/ensogl/example/src/leak.rs b/ide/src/rust/ensogl/example/src/leak.rs new file mode 100644 index 000000000000..73e7b3986d50 --- /dev/null +++ b/ide/src/rust/ensogl/example/src/leak.rs @@ -0,0 +1,43 @@ +//! `Leak` is a utility struct that prevents the wrapped value from being dropped when `Leak` is +//! being dropped. This is achieved by passing the contained value to `std::mem::forget` in the +//! drop implementation of `Leak`. Can bue used for examples to keep components alive for the whole +//! lifetime of the application. + + + +// ============ +// === Leak === +// ============ + +/// Wrapper that will prevent the wrapped value from being dropped. Instead, the value will be +/// leaked when the `Leak` is dropped. +#[derive(Debug)] +pub struct Leak { + value: Option +} + +impl Leak { + /// Constructor. The passed value will be prevented from being dropped. This will cause memory + /// leaks. + pub fn new(value:T) -> Self { + Self{value:Some(value)} + } + + /// Return a reference to the wrapped value. + pub fn inner(&self) -> &T { + // Guaranteed to never panic as this is always initialised with `Some` in the constructor. + self.value.as_ref().unwrap() + } + + /// Return a mutable reference to the wrapped value. + pub fn inner_mut(&mut self) -> &mut T { + // Guaranteed to never panic as this is always initialised with `Some` in the constructor. + self.value.as_mut().unwrap() + } +} + +impl Drop for Leak { + fn drop(&mut self) { + std::mem::forget(self.value.take()); + } +} diff --git a/ide/src/rust/ensogl/example/src/lib.rs b/ide/src/rust/ensogl/example/src/lib.rs index bcbfaffb3f66..5cf8e5ee5bd6 100644 --- a/ide/src/rust/ensogl/example/src/lib.rs +++ b/ide/src/rust/ensogl/example/src/lib.rs @@ -25,14 +25,16 @@ #[allow(clippy::option_map_unit_fn)] +mod leak; pub mod animation; +pub mod complex_shape_system; pub mod dom_symbols; pub mod easing_animator; pub mod glyph_system; pub mod list_view; pub mod mouse_events; pub mod shape_system; -pub mod complex_shape_system; +pub mod slider; pub mod sprite_system; pub mod sprite_system_benchmark; pub mod text_area; @@ -40,4 +42,5 @@ pub mod text_area; /// Common types that should be visible across the whole crate. pub mod prelude { pub use ensogl_core::prelude::*; + pub use super::leak::*; } diff --git a/ide/src/rust/ensogl/example/src/slider.rs b/ide/src/rust/ensogl/example/src/slider.rs new file mode 100644 index 000000000000..72eac2af8835 --- /dev/null +++ b/ide/src/rust/ensogl/example/src/slider.rs @@ -0,0 +1,78 @@ +//! A debug scene which shows the number and range selector. + +use crate::prelude::*; +use wasm_bindgen::prelude::*; + +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display::object::ObjectOps; +use ensogl_core::system::web; +use ensogl_gui_components::selector::Bounds; +use ensogl_gui_components::selector; +use ensogl_text_msdf_sys::run_once_initialized; +use ensogl_theme as theme; + + + +// =================== +// === Entry Point === +// =================== + +/// An entry point. +#[wasm_bindgen] +#[allow(dead_code)] +pub fn entry_point_slider() { + web::forward_panic_hook_to_console(); + web::set_stack_trace_limit(); + run_once_initialized(|| { + let app = Application::new(&web::get_html_element_by_id("root").unwrap()); + init(&app); + mem::forget(app); + }); +} + +fn make_number_picker(app:&Application) -> Leak { + let slider = app.new_view::(); + slider.frp.resize(Vector2(200.0,50.0)); + app.display.add_child(&slider); + Leak::new(slider) +} + +fn make_range_picker(app:&Application) -> Leak { + let slider = app.new_view::(); + slider.frp.resize(Vector2(400.0,50.0)); + app.display.add_child(&slider); + Leak::new(slider) +} + + + +// ======================== +// === Init Application === +// ======================== + +fn init(app:&Application) { + theme::builtin::dark::register(&app); + theme::builtin::light::register(&app); + theme::builtin::light::enable(&app); + + let slider1 = make_number_picker(app); + slider1.inner().frp.allow_click_selection(true); + + let slider2 = make_number_picker(app); + slider2.inner().frp.resize(Vector2(400.0,50.0)); + slider2.inner().frp.set_bounds.emit(Bounds::new(-100.0,100.0)); + slider2.inner().set_position_y(50.0); + slider2.inner().frp.use_overflow_bounds(Bounds::new(-150.0,200.0)); + slider2.inner().frp.set_caption(Some("Value:".to_string())); + + let slider3 = make_range_picker(app); + slider3.inner().set_position_y(-100.0); + slider3.inner().set_track_color(color::Rgba::new(0.0,0.80,0.80,1.0)); + + let slider4 = make_range_picker(app); + slider4.inner().set_position_y(-200.0); + slider4.inner().frp.use_overflow_bounds(Bounds::new(-2.0,3.0)); + slider4.inner().frp.set_caption(Some("Caption".to_string())); + slider4.inner().set_track_color(color::Rgba::new(0.5,0.70,0.70,1.0)); +} diff --git a/ide/src/rust/ensogl/lib/components/Cargo.toml b/ide/src/rust/ensogl/lib/components/Cargo.toml index 65889f69329d..0ff0c548a427 100644 --- a/ide/src/rust/ensogl/lib/components/Cargo.toml +++ b/ide/src/rust/ensogl/lib/components/Cargo.toml @@ -19,3 +19,4 @@ ensogl-theme = { path = "../theme" } [dev-dependencies] wasm-bindgen-test = { version = "0.3.8" } +float_eq = "0.5" diff --git a/ide/src/rust/ensogl/lib/components/src/component.rs b/ide/src/rust/ensogl/lib/components/src/component.rs new file mode 100644 index 000000000000..f67509b8554c --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/component.rs @@ -0,0 +1,92 @@ +//! UI component consisting of an FRP and a Model. +//! +//! Enforces correct ownership of components: Model must not own Frp. Both need to be owned via +//! `Rc` by the parent struct, which itself acts as a smart-pointer to the FRP. +//! +//! Requires both the Frp component, and the Model to implement a trait each, which provide +//! functionality for constructing / initialising the components. + +use crate::prelude::*; + +use enso_frp as frp; +use ensogl_core::application::Application; +use ensogl_core::application::command::CommandApi; +use ensogl_core::application; +use ensogl_core::display::shape::*; +use ensogl_core::display; + + + +// ============= +// === Model === +// ============= + +/// Model that can be used in a Component. Requires a constructor that takes an application and +/// returns `Self`. The model will be created with this constructor when constructing the +/// `Component`. +pub trait Model { + /// Constructor. + fn new(app:&Application) -> Self; +} + + + +// =========== +// === FRP === +// =========== + +/// Frp that can be used in a Component. The FRP requires an initializer that will be called during +/// the construction of the component. `Default` + `CommandApi` are usually implemented when using +/// the `ensogl_core::define_endpoints!` macro to create an FRP API. +pub trait Frp : Default + CommandApi { + /// Frp initializer. + fn init(&self, app:&Application, model:&Model, style:&StyleWatchFrp); +} + + + +// ================= +// === Component === +// ================= + +/// Base struct for UI components in EnsoGL. Contains the Data/Shape model and the FPR exposing its +/// behaviour. +#[derive(Clone,CloneRef,Debug)] +pub struct Component { + /// Public FRP api of the Component. + pub frp : Rc, + model : Rc, + /// Reference to the application the Component belongs to. Generally required for implementing + /// `application::View` and initialising the `Mode`l and `Frp` and thus provided by the + /// `Component`. + pub app : Application, +} + +impl> Component { + /// Constructor. + pub fn new(app:&Application) -> Self { + let app = app.clone_ref(); + let model = Rc::new(M::new(&app)); + let frp = F::default(); + let style = StyleWatchFrp::new(&app.display.scene().style_sheet); + frp.init(&app,&model,&style); + let frp = Rc::new(frp); + Self{frp,model,app} + } +} + +impl display::Object for Component { + fn display_object(&self) -> &display::object::Instance { + &self.model.display_object() + } +} + +impl> Deref for Component { + type Target = F; + fn deref(&self) -> &Self::Target { &self.frp } +} + +impl application::command::FrpNetworkProvider +for Component { + fn network(&self) -> &frp::Network { self.frp.network() } +} diff --git a/ide/src/rust/ensogl/lib/components/src/lib.rs b/ide/src/rust/ensogl/lib/components/src/lib.rs index 8cd16939077c..e60830365599 100644 --- a/ide/src/rust/ensogl/lib/components/src/lib.rs +++ b/ide/src/rust/ensogl/lib/components/src/lib.rs @@ -16,11 +16,13 @@ #![recursion_limit="512"] +pub mod component; pub mod drop_down_menu; -pub mod list_view; pub mod label; -pub mod toggle_button; +pub mod list_view; pub mod shadow; +pub mod selector; +pub mod toggle_button; /// Commonly used types and functions. pub mod prelude { diff --git a/ide/src/rust/ensogl/lib/components/src/selector.rs b/ide/src/rust/ensogl/lib/components/src/selector.rs new file mode 100644 index 000000000000..05e9cdccf29d --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector.rs @@ -0,0 +1,68 @@ +//! UI components that allows picking a number or range through mouse interaction. We have a number +//! picker that allows to pick a number in a range and a range picker that allows to pick a range. +//! +//! Both share the same model (i.e., arrangement of shapes), but they have different logic about +//! using the features of the shapes making up the model and handling interactions. The base model +//! is implemented in `model.rs` and the logic for the number and range picker are placed in +//! `number.rs` and `range.rs` respectively. The rest of the sub-modules contain utilities. +//! +//! The only things exposed form this module are the `NumberPicker`, `NumberRangePicker`, their +//! FRPs and the `Bounds` struct. + +mod bounds; +mod decimal_aligned; +mod frp; +mod model; +mod number; +mod range; +mod shape; + +use ensogl_core::application; +use ensogl_core::application::Application; + +pub use bounds::Bounds; +use model::*; +use frp::*; + + + +// ===================== +// === Number Picker === +// ===================== + +/// UI component for selecting a number. Looks like a rounded node that has the selected value as +/// text representation in the center. The background shows the selected value visually in the +/// range that the value can be picked from (e.g., 0..255) by by filling in the proportion of the +/// background that corresponds to the value relative in the range, for example, 0.0 would be not +/// filled in, 128.0 would be about halfway filled in, and 128.0 would be completely filled in. +/// The value can be changed by clicking and dragging on the shape. +pub type NumberPicker = crate::component::Component; + +impl application::View for NumberPicker { + fn label() -> &'static str { "NumberPicker" } + fn new(app:&Application) -> Self { NumberPicker::new(app) } + fn app(&self) -> &Application { &self.app } +} + + + +// =========================== +// === Number Range Picker === +// =========================== + +/// UI component for selecting a ranger. Looks like a rounded node that shows a textual +/// representation of the range bounds at the left and right end of the shape. The background shows +/// the selected value visually in the by displaying a track within the range of permissible values +/// that the range bounds can be selected from. For example, if the allowed range is 0.0..1.0 +/// a selected range of 0..0.5 would show the track covering the left half of the background, a +/// selected range of 0.25..0.75 would show the track centered on the background, and 0.5..1.0 +/// would show the track covering the right half of the background. The selected range can be +/// changed by clicking and dragging the track, which changes the whole range, but preserves the +/// width, or the individual edges of the track which changes just the respective end of the range. +pub type NumberRangePicker = crate::component::Component; + +impl application::View for NumberRangePicker { + fn label() -> &'static str { "RangePicker" } + fn new(app:&Application) -> Self { NumberRangePicker::new(app) } + fn app(&self) -> &Application { &self.app } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/bounds.rs b/ide/src/rust/ensogl/lib/components/src/selector/bounds.rs new file mode 100644 index 000000000000..03ad3bcd2862 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/bounds.rs @@ -0,0 +1,372 @@ +//! Bounds represent an interval with inclusive ends. They are used to indicates the lowest and +//! highest value that can be selected in a selection component. +//! +//! Note: this is used instead of `Range`, as `Range` cannot easily be used in our FRP because it +//! does not implement `Default`. + +use crate::prelude::*; + +use core::convert::From; +use core::option::Option; +use core::option::Option::Some; + + + +// ============== +// === Bounds === +// ============== + +/// Bounds of a selection. This indicates the lowest and highest value that can be selected in a +/// selection component. +#[derive(Clone,Copy,Debug,Default)] +pub struct Bounds { + /// Start of the bounds interval (inclusive). + pub start : f32, + /// End of the bounds interval (inclusive). + pub end : f32, +} + +impl Bounds { + /// Constructor. + pub fn new(start:f32,end:f32) -> Self { + Bounds{start,end} + } + + /// Return the `Bound` with the lower bound as `start` and the upper bound as `end`. + pub fn sorted(self) -> Self { + if self.start > self.end { + Bounds{start:self.end,end:self.start} + } else { + self + } + } + + /// Return the distance between start and end point. + pub fn width(self) -> f32 { + self.end - self.start + } +} + +impl From<(f32,f32)> for Bounds { + fn from((start,end): (f32,f32)) -> Self { + Bounds{start,end} + } +} + +/// Frp utility method to normalise the given value to the given Bounds. +/// +/// Example usage: +/// ```ignore +/// normalised <- all2(&value,&bounds).map(normalise_value); +/// ```` +pub fn normalise_value((value,bounds):&(f32,Bounds)) -> f32 { + let width = bounds.width(); + if width == 0.0 { return 0.0 } + (value - bounds.start) / width +} + +/// Frp utility method to compute the absolute value from a normalised value. +/// Inverse of `normalise_value`. +/// +/// Example usage: +/// ```ignore +/// value <- all(&bounds,&normalised).map(absolute_value); +/// ```` +pub fn absolute_value((bounds,normalised_value):&(Bounds,f32)) -> f32 { + (normalised_value * bounds.width()) + bounds.start +} + +/// Returns the normalised value that correspond to the click position on the shape. +/// Note that the shape is centered on (0,0), thus half the width extends into the negative values. +/// For use in FRP `map` method, thus taking references. +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn position_to_normalised_value(pos:&Vector2,width:&f32) -> f32 { + if *width == 0.0 { return 0.0 } + ((pos.x / (width / 2.0)) + 1.0) / 2.0 +} + +/// Check whether the given value is within the given bounds. +fn value_in_bounds(value:f32, bounds:Bounds) -> bool { + let bounds_sorted = bounds.sorted(); + value >= bounds_sorted.start && value <= bounds_sorted.end +} + +/// Check whether the given bounds are completely contained in the second bounds. +pub fn bounds_in_bounds(bounds_inner:Bounds, bounds_outer:Bounds) -> bool { + value_in_bounds(bounds_inner.start,bounds_outer) + && value_in_bounds(bounds_inner.end,bounds_outer) +} + +/// Clamp `value` to the `overflow_bounds`, or to [0, 1] if no bounds are given. +/// For use in FRP `map` method, thus taking references. +/// +/// Example usage: +/// ```ignore +/// clamped <- value_update.map2(&normalised_overflow_bounds,clamp_with_overflow); +/// ``` +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn clamp_with_overflow(value:&f32, overflow_bounds:&Option) -> f32 { + if let Some(overflow_bounds) = overflow_bounds{ + value.clamp(overflow_bounds.start,overflow_bounds.end) + } else { + value.clamp(0.0,1.0) + } +} + +/// Indicates whether the `bounds` would be clamped when given to `clamp_with_overflow`. +/// For use in FRP `map` method, thus taking references. +/// +/// Example usage: +/// ```ignore +/// is_in_bounds <- bounds_update.map2(&normalised_overflow_bounds,should_clamp_with_overflow); +/// ``` +#[allow(clippy::trivially_copy_pass_by_ref)] +pub fn should_clamp_with_overflow(bounds:&Bounds, overflow_bounds:&Option) -> bool { + if let Some(overflow_bounds) = overflow_bounds { + bounds_in_bounds(*bounds,*overflow_bounds) + } else { + bounds_in_bounds(*bounds,(0.0,1.0).into()) + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + use float_eq::assert_float_eq; + use std::f32::NAN; + + #[test] + fn test_normalise_value() { + let test = |start,end,value,expected| { + let bounds = Bounds::new(start,end); + let normalised = normalise_value(&(value,bounds)); + assert_float_eq!(normalised,expected,ulps<=7) + }; + + test(0.0,1.0,0.0,0.0); + test(0.0,1.0,0.1,0.1); + test(0.0,1.0,0.2,0.2); + test(0.0,1.0,0.3,0.3); + test(0.0,1.0,0.4,0.4); + test(0.0,1.0,0.5,0.5); + test(0.0,1.0,0.6,0.6); + test(0.0,1.0,0.7,0.7); + test(0.0,1.0,0.8,0.8); + test(0.0,1.0,0.9,0.9); + test(0.0,1.0,1.0,1.0); + + test(0.0,1.0,-2.0,-2.0); + test(0.0,1.0,-1.0,-1.0); + test(0.0,1.0,2.0,2.0); + test(0.0,1.0,3.0,3.0); + + test(-1.0,1.0,-1.0,0.0); + test(-1.0,1.0,-0.5,0.25); + test(-1.0,1.0,0.0,0.5); + test(-1.0,1.0,0.5,0.75); + test(-1.0,1.0,1.0,1.0); + + test(1.0,-1.0,-1.0,1.0); + test(1.0,-1.0,-0.5,0.75); + test(1.0,-1.0,0.0,0.5); + test(1.0,-1.0,0.5,0.25); + test(1.0,-1.0,1.0,0.0); + + test(-10.0,20.0,-10.0,0.0); + test(-10.0,20.0,20.0,1.0); + test(-10.0,20.0,0.0,0.33333333); + + test(-999999999.0,999999999.0,-999999999.0,0.0); + test(-999999999.0,999999999.0,0.0,0.5); + test(-999999999.0,999999999.0,999999999.0,1.0); + + test(0.0,0.0,1.0,0.0); + test(0.0,0.0,0.0,0.0); + test(0.0,0.0,-1.0,0.0); + } + + #[test] + fn test_absolute_value() { + let test = |start,end,value,expected| { + let bounds = Bounds::new(start,end); + let normalised = absolute_value(&(bounds,value)); + assert_float_eq!(normalised,expected,ulps<=7) + }; + + test(0.0,1.0,0.0,0.0); + test(0.0,1.0,0.1,0.1); + test(0.0,1.0,0.2,0.2); + test(0.0,1.0,0.3,0.3); + test(0.0,1.0,0.4,0.4); + test(0.0,1.0,0.5,0.5); + test(0.0,1.0,0.6,0.6); + test(0.0,1.0,0.7,0.7); + test(0.0,1.0,0.8,0.8); + test(0.0,1.0,0.9,0.9); + test(0.0,1.0,1.0,1.0); + + test(0.0,1.0,-2.0,-2.0); + test(0.0,1.0,-1.0,-1.0); + test(0.0,1.0,2.0,2.0); + test(0.0,1.0,3.0,3.0); + + test(-1.0,1.0,0.0,-1.0); + test(-1.0,1.0,0.25,-0.5); + test(-1.0,1.0,0.5,0.0); + test(-1.0,1.0,0.75,0.5); + test(-1.0,1.0,1.0,1.0); + + test(1.0,-1.0,1.0,-1.0); + test(1.0,-1.0,0.75,-0.5); + test(1.0,-1.0,0.5,0.0); + test(1.0,-1.0,0.25,0.5); + test(1.0,-1.0,0.0,1.0); + + test(-10.0,20.0,0.0,-10.0); + test(-10.0,20.0,1.0,20.0); + test(-10.0,20.0,0.33333333,0.0); + + test(-999999999.0,999999999.0,0.0,-999999999.0); + test(-999999999.0,999999999.0,0.5,0.0); + test(-999999999.0,999999999.0,1.0,999999999.0); + + test(0.0,0.0,1.0,0.0); + test(1.0,1.0,1.0,1.0); + test(1.0,1.0,2.0,1.0); + test(1.0,1.0,-2.0,1.0); + } + + + #[test] + fn test_position_to_normalised_value() { + let test = |pos,width,expected| { + let result = position_to_normalised_value(&pos,&width); + assert_float_eq!(result,expected,ulps<=7) + }; + + for &y in &[-100.0, 0.0, 100.0, NAN] { + test(Vector2::new(50.0,y),100.0,1.0); + test(Vector2::new(0.0,y),100.0,0.5); + test(Vector2::new(-50.0,y),100.0,0.0); + + test(Vector2::new(100.0,y),100.0,1.5); + test(Vector2::new(-100.0,y),100.0,-0.5); + test(Vector2::new(150.0,y),100.0,2.0); + test(Vector2::new(-150.0,y),100.0,-1.0); + test(Vector2::new(200.0,y),100.0,2.5); + test(Vector2::new(-200.0,y),100.0,-1.5); + + test(Vector2::new(-200.0,y),0.0,0.0); + } + } + + #[test] + fn test_value_in_bounds() { + let test = |start,end,value,expected| { + let result = value_in_bounds(value,Bounds::new(start,end)); + assert_eq!(result,expected, "Testing whether {} in ]{},{}[", value,start,end) + }; + + test(0.0,1.0,0.0,true); + test(0.0,1.0,0.5,true); + test(0.0,1.0,1.0,true); + test(0.0,1.0,1.00001,false); + test(0.0,1.0,-0.00001,false); + + test(0.0,10.0,10.0,true); + test(0.0,10.0,9.999999,true); + test(0.0,10.0,11.0,false); + + test(-100.0,10.0,11.0,false); + test(-101.0,10.0,-100.0,true); + test(-101.0,10.0,-101.0,true); + test(-101.0,10.0,-101.1,false); + + test(0.0,0.0,0.0,true); + test(0.0,0.0,1.0,false); + test(0.0,0.0,-1.0,false); + } + + #[test] + fn test_bounds_in_bounds() { + let test = |start1,end1,start2,end2,expected| { + let result = bounds_in_bounds(Bounds::new(start1,start2),Bounds::new(start2,end2)); + assert_eq!(result,expected, + "Testing whether ]{},{}[ in ]{},{}[", start1,end1,start2,end2); + }; + + test(0.0,1.0,0.0,1.0,true); + test(0.0,1.0,1.0,2.0,false); + test(0.0,1.0,0.5,2.0,false); + test(0.0,1.0,-100.0,100.0,true); + test(0.0,1.0,-100.0,-99.0,false); + test(0.0,1.0,0.1,0.9,false); + test(-100.0,200.0,50.0,75.0,false); + test(-100.0,200.0,-50.0,75.0,false); + test(-100.0,200.0,-50.0,-75.0,false); + test(-100.0,200.0,-50.0,99999.0,false); + test(-100.0,200.0,-99999.0,0.0,true); + test(-100.0,200.0,-99999.0,99999.0,true); + + test(0.0,0.0,0.0,0.0,true); + test(0.0,0.0,-1.0,2.0,true); + test(0.0,0.0,1.0,2.0,false); + } + + #[test] + fn test_clamp_with_overflow() { + let test = |value,bounds,expected| { + let result = clamp_with_overflow(&value,&bounds); + assert_float_eq!(result,expected,ulps<=7) + }; + + test(0.0,Some(Bounds::new(0.0,1.0)), 0.0); + test(-1.0,Some(Bounds::new(0.0,1.0)), 0.0); + test(2.0,Some(Bounds::new(0.0,1.0)), 1.0); + + test(-1.0,None, 0.0); + test(2.0,None,1.0); + + test(-999.0,Some(Bounds::new(-1.0,100.0)), -1.0); + test(999.0,Some(Bounds::new(-1.0,100.0)), 100.0); + test(-1.0,Some(Bounds::new(-1.0,100.0)), -1.0); + test(0.0,Some(Bounds::new(-1.0,100.0)), 0.0); + test(99.0,Some(Bounds::new(-1.0,100.0)), 99.0); + test(100.0,Some(Bounds::new(-1.0,100.0)), 100.0); + test(100.01,Some(Bounds::new(-1.0,100.0)), 100.0); + } + + #[test] + fn test_should_clamp_with_overflow() { + let test = |inner,outer,expected| { + let result = should_clamp_with_overflow(&inner,&outer); + assert_eq!(result,expected); + }; + + test(Bounds::new(0.0,1.0),Some(Bounds::new(0.0,1.0)),true); + test(Bounds::new(0.0,1.0),Some(Bounds::new(1.0,2.0)),false); + test(Bounds::new(0.0,1.0),Some(Bounds::new(0.5,2.0)),false); + test(Bounds::new(0.0,1.0),Some(Bounds::new(-100.0,100.0)),true); + test(Bounds::new(0.0,1.0),Some(Bounds::new(-100.0,-99.0)),false); + test(Bounds::new(0.0,1.0),Some(Bounds::new(0.1,0.9)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(50.0,75.0)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(-50.0,75.0)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(-50.0,-75.0)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(-50.0,99999.0)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(-99999.0,0.0)),false); + test(Bounds::new(-100.0,200.0),Some(Bounds::new(-99999.0,99999.0)),true); + test(Bounds::new(-100.0,0.0),None,false); + test(Bounds::new(0.1,1.1),None,false); + test(Bounds::new(-9.1,2.1),None,false); + test(Bounds::new(0.25,0.7),None,true); + + test(Bounds::new(0.0,0.0),None,true); + } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/decimal_aligned.rs b/ide/src/rust/ensogl/lib/components/src/selector/decimal_aligned.rs new file mode 100644 index 000000000000..1b06f3e29522 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/decimal_aligned.rs @@ -0,0 +1,107 @@ +//! Utility wrapper for a text field representing a float. Centers the string representation of the +//! float on the decimal separator. This is a very bare bones implementation, thus not exposed as +//! a public utility. + +use crate::prelude::*; + +use crate::component; + +use enso_frp as frp; +use ensogl_core::application::Application; +use ensogl_core::application; +use ensogl_core::display::object::ObjectOps; +use ensogl_core::display::shape::StyleWatchFrp; +use ensogl_core::display; +use ensogl_text as text; + + + +// ================= +// === Constants === +// ================= + +const LABEL_OFFSET : f32 = 13.0; + + + +// ============ +// === FRP === +// ============ + +ensogl_core::define_endpoints! { + Input { + set_content(f32), + } + Output {} +} + + + +// ============== +// === Model === +// ============== + +#[derive(Clone,CloneRef,Debug)] +pub struct Model { + /// Root object. Required as the rendered text label will have an offset relative to the + /// base position of the root, depending on the position of the decimal separator. + root : display::object::Instance, + /// Label containing the text to display. This is the label that will be shown. + label_full : text::Area, + /// This label contains the text to the left of the decimal. This is here, so we can get + /// information about the text width of this portion of the label. This label will + /// not appear in the UI. + label_left : text::Area, +} + +impl component::Model for Model { + fn new(app:&Application) -> Self { + let logger = Logger::new("DecimalAlignedLabel"); + let root = display::object::Instance::new(&logger); + let label_full = app.new_view::(); + let label_left = app.new_view::(); + + label_full.remove_from_scene_layer_DEPRECATED(&app.display.scene().layers.main); + label_full.add_to_scene_layer_DEPRECATED(&app.display.scene().layers.label); + + root.add_child(&label_full); + root.add_child(&label_left); + + Self{root,label_full,label_left} + } +} + +impl component::Frp for Frp { + fn init(&self, _app:&Application, model:&Model, _style:&StyleWatchFrp) { + let frp = &self; + let network = &frp.network; + + frp::extend! { network + formatted <- frp.set_content.map(|value| format!("{:.2}", value)); + // FIXME: the next line is locale dependent as it is meant to split on the decimal + // separator, which might be different from "." in some locales. We need a way to get + // the current locale dependent decimal separator for this. + // See https://github.com/enso-org/ide/issues/1542 for progress on this. + left <- formatted.map(|s| s.split('.').next().map(|s| s.to_string())).unwrap(); + + model.label_left.set_content <+ left; + model.label_full.set_content <+ formatted; + + eval model.label_left.width((offset) + model.label_full.set_position_x(-offset-LABEL_OFFSET)); + } + } +} + +impl display::Object for Model { + fn display_object(&self) -> &display::object::Instance { self.root.display_object() } +} + +/// Decimal aligned text label that shows the text representation of a floating point number. +pub type FloatLabel = component::Component; + +impl application::View for FloatLabel { + fn label() -> &'static str { "FloatLabel" } + fn new(app:&Application) -> Self { FloatLabel::new(app) } + fn app(&self) -> &Application { &self.app } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/frp.rs b/ide/src/rust/ensogl/lib/components/src/selector/frp.rs new file mode 100644 index 000000000000..ce779d47762a --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/frp.rs @@ -0,0 +1,126 @@ +//! Frp logic common to both Number and Range selector. + +use crate::prelude::*; + +use enso_frp as frp; +use enso_frp::io::Mouse; +use enso_frp::Network; +use ensogl_core::display::object::ObjectOps; +use ensogl_core::display::shape::StyleWatchFrp; +use ensogl_theme as theme; + +use crate::shadow; + +use super::model::Model; +use super::shape::shape_is_dragged; + + + +// =========================== +// === Frp Utility Methods === +// =========================== + +/// Compute the slider width from the given shape size. For use in FRP, thus taking a reference. +#[allow(clippy::trivially_copy_pass_by_ref)] +fn slider_area_width(size:&Vector2) -> f32 { + // Radius of the rounded corners of the background shape. + let rounded_width = size.y / 2.0; + size.x - rounded_width +} + + + +// =============== +// === Frp === +// =============== + +/// Frp endpoints provided for general information about mouse interactions and shape properties +/// of the `common::Model`. +pub struct Frp { + /// Current maximum extent of the track in scene coordinate space. + pub track_max_width : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the left overflow shape. + pub is_dragging_left_overflow : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the right overflow shape. + pub is_dragging_right_overflow : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the track shape. + pub is_dragging_track : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the background shape. + pub is_dragging_background : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the invisible shape that covers + /// the left end of the track. + pub is_dragging_left_handle : frp::Stream, + /// Indicates whether there is an ongoing dragging action from the invisible shape that covers + /// the right end of the track. + pub is_dragging_right_handle : frp::Stream, + /// Indicates whether there is an ongoing dragging action on any of the component shapes. + pub is_dragging_any : frp::Stream, +} + +impl Frp { + pub fn new + (model:&Model, style:&StyleWatchFrp, network:&Network, size:frp::Stream, mouse:&Mouse) + -> Frp { + let shadow = shadow::frp_from_style(style,theme::shadow); + let text_size = style.get_number(theme::text::size); + + let is_dragging_left_overflow = shape_is_dragged( + network,&model.left_overflow.events,mouse); + let is_dragging_right_overflow = shape_is_dragged( + network,&model.right_overflow.events,mouse); + let is_dragging_track = shape_is_dragged( + network,&model.track.events, mouse); + let is_dragging_background = shape_is_dragged( + network,&model.background.events,mouse); + let is_dragging_left_handle = shape_is_dragged( + network,&model.track_handle_left.events,mouse); + let is_dragging_right_handle = shape_is_dragged( + network,&model.track_handle_right.events,mouse); + + // Initialisation of components. Required for correct layout on startup. + model.label_right.set_position_y(text_size.value()/2.0); + model.label_left.set_position_y(text_size.value()/2.0); + model.label.set_position_y(text_size.value()/2.0); + model.caption_left.set_position_y(text_size.value()/2.0); + model.caption_center.set_position_y(text_size.value()/2.0); + + frp::extend! { network + + // Style updates. + shadow_padding <- shadow.size.map(|&v| Vector2(v,v)); + eval text_size ((size) { + model.label.set_position_y(size / 2.0); + model.label_right.set_position_y(size / 2.0); + model.label_left.set_position_y(size / 2.0); + }); + + // Caption updates. + update_caption_position <- all(&size,&text_size); + eval update_caption_position((args) model.update_caption_position(args)); + eval model.caption_center.frp.width((width) + model.caption_center.set_position_x(-width/2.0) + ); + + // Size updates + track_max_width <- size.map(slider_area_width); + size_update <- all(size,shadow_padding); + eval size_update(((size, shadow_padding)) { + model.set_size(*size,*shadow_padding) + }); + + // Mouse IO + is_dragging_overflow <- any(&is_dragging_left_overflow,&is_dragging_right_overflow); + is_dragging_handle <- any(&is_dragging_left_handle,&is_dragging_right_handle); + is_dragging_any <- any4( + &is_dragging_track, + &is_dragging_background, + &is_dragging_overflow, + &is_dragging_handle, + ); + } + + Frp {track_max_width,is_dragging_left_overflow,is_dragging_right_overflow, + is_dragging_track,is_dragging_background,is_dragging_left_handle, + is_dragging_right_handle,is_dragging_any} + } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/model.rs b/ide/src/rust/ensogl/lib/components/src/selector/model.rs new file mode 100644 index 000000000000..55aa35da15b2 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/model.rs @@ -0,0 +1,244 @@ +//! Base model for the number and range selector components. Contains all functionality needed by +//! both selectors and can be configured to suit the needs of both. + +use crate::prelude::*; + +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display; +use ensogl_core::display::shape::*; +use ensogl_text as text; + +use crate::component; + +use super::Bounds; +use super::shape::*; +use super::decimal_aligned::FloatLabel; + + + +// ================= +// === Constants === +// ================= + +const LABEL_OFFSET : f32 = 13.0; + + + +// ============= +// === Model === +// ============= + +#[derive(Clone,CloneRef,Debug)] +pub struct Model { + /// Background shape that the other UI elements are placed on. + pub background : background::View, + /// Visual element that indicates where in the available range the selected number or track is + /// located. Looks like a colored in bit of the background. + pub track : track::View, + /// Invisible UI element that enables mouse interaction with the left end of the track. Will + /// always be placed on the left edge of the track. + pub track_handle_left : io_rect::View, + /// Invisible UI element that enables mouse interaction with the right end of the track. Will + /// always be placed on the right edge of the track. + pub track_handle_right : io_rect::View, + /// Icon that can be used out of range values. The left overflow is placed on the left side + /// of the shape and looks like an arrow/triangle pointing left. + pub left_overflow : left_overflow::View, + /// Icon that can be used out of range values. The left overflow is placed on the right side + /// of the shape and looks like an arrow/triangle pointing right. + pub right_overflow : right_overflow::View, + /// A label that is centered on the background and can be set to show a floating point value + /// that is centered on the decimal label. + pub label : FloatLabel, + /// A label that is aligned to the left edge of the background + pub label_left : text::Area, + /// A label that is aligned to the right edge of the background + pub label_right : text::Area, + /// A label that is left aligned on the background. Meant to contain a caption describing the + /// value that is selected. For example "Alpha", "Red", or "Size". + pub caption_left : text::Area, + /// A label that is centered on the background. Meant to contain a caption describing the + /// range that is selected. For example "Allowed Size", or "Valid Price". + pub caption_center : text::Area, + /// Shape root that all other elements are parented to. Should be used to place the shapes as + /// a group. + pub root : display::object::Instance, +} + +impl component::Model for Model { + fn new(app: &Application) -> Self { + let logger = Logger::new("selector::common::Model"); + let root = display::object::Instance::new(&logger); + let label = app.new_view::(); + let label_left = app.new_view::(); + let label_right = app.new_view::(); + let caption_center = app.new_view::(); + let caption_left = app.new_view::(); + let background = background::View::new(&logger); + let track = track::View::new(&logger); + let track_handle_left = io_rect::View::new(&logger); + let track_handle_right = io_rect::View::new(&logger); + let left_overflow = left_overflow::View::new(&logger); + let right_overflow = right_overflow::View::new(&logger); + + let app = app.clone_ref(); + let scene = app.display.scene(); + scene.layers.add_shapes_order_dependency::(); + scene.layers.add_shapes_order_dependency::(); + scene.layers.add_shapes_order_dependency::(); + scene.layers.add_shapes_order_dependency::(); + + root.add_child(&label); + root.add_child(&label_left); + root.add_child(&label_right); + root.add_child(&caption_left); + root.add_child(&caption_center); + root.add_child(&background); + root.add_child(&track); + root.add_child(&right_overflow); + + label_left.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + label_left.add_to_scene_layer_DEPRECATED(&scene.layers.label); + label_right.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + label_right.add_to_scene_layer_DEPRECATED(&scene.layers.label); + caption_left.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + caption_left.add_to_scene_layer_DEPRECATED(&scene.layers.label); + caption_center.remove_from_scene_layer_DEPRECATED(&scene.layers.main); + caption_center.add_to_scene_layer_DEPRECATED(&scene.layers.label); + + Self{background,track,track_handle_left,track_handle_right,left_overflow,right_overflow, + label,label_left,label_right,caption_left,caption_center,root} + } +} + +impl Model { + /// Set the size of the overall shape, taking into account the extra padding required to + /// render the shadow. + pub fn set_size(&self, size:Vector2, shadow_padding:Vector2) { + let padded_size = size + shadow_padding; + self.background.size.set(padded_size); + self.track.size.set(padded_size); + self.left_overflow.size.set(padded_size); + self.right_overflow.size.set(padded_size); + + let left_padding = LABEL_OFFSET; + let overflow_icon_size = size.y; + let label_offset = size.x / 2.0 - overflow_icon_size + left_padding; + + self.label_left.set_position_x(-label_offset); + self.label_right.set_position_x(label_offset-self.label_right.width.value()); + + let overflow_icon_offset = size.x / 2.0 - overflow_icon_size / 2.0; + self.left_overflow.set_position_x(-overflow_icon_offset); + self.right_overflow.set_position_x(overflow_icon_offset); + + let track_handle_size = Vector2::new(size.y/2.0,size.y); + self.track_handle_left.size.set(track_handle_size); + self.track_handle_right.size.set(track_handle_size); + } + + /// Update the position of the captions based on the size of the shape and the text size. Takes + ///arguments in the way they are provided in the FRP network (as reference to a tuple) to make + ///usage more ergonomic on the call-site. + pub fn update_caption_position(&self, (size,text_size):&(Vector2,f32)) { + let left_padding = LABEL_OFFSET; + let overflow_icon_size = size.y / 2.0; + let caption_offset = size.x / 2.0 - overflow_icon_size - left_padding; + self.caption_left.set_position_x(-caption_offset); + self.caption_left.set_position_y(text_size / 2.0); + self.caption_center.set_position_y(text_size / 2.0); + } + + /// Set whether to allow interactions with the edges of the track shape. If this is set to + /// `false`, both `track_handle_left` and `track_handle_right` are removed from the component. + pub fn use_track_handles(&self, value:bool) { + if value { + self.track.add_child(&self.track_handle_left); + self.track.add_child(&self.track_handle_right); + } else { + self.track.remove_child(&self.track_handle_left); + self.track.remove_child(&self.track_handle_right); + } + } + + /// Set the track to cover the area from zero up to the given value. The value indicates the + /// width of the background that should be covered in the range 0..1. + pub fn set_background_value(&self, value:f32) { + self.track.left.set(0.0); + self.track.right.set(value); + } + + /// Set the track to cover the area indicated by the `value` Bounds that are passed. The value + /// indicates location aon the background in the range 0..1 (were 0 is the left edge and 1 is + /// the right edge). To do the proper layout of the track handles this method also needs to be + /// passed the size of the shape. + pub fn set_background_range(&self, value:Bounds, size:Vector2) { + self.track.left.set(value.start); + self.track.right.set(value.end); + + self.track_handle_left.set_position_x(value.start * size.x - size.x / 2.0); + self.track_handle_right.set_position_x(value.end * size.x - size.x / 2.0); + } + + /// Set the label in the center of the background to show the given numeric value. + pub fn set_center_label_content(&self, value:f32) { + self.label.frp.set_content.emit(value) + } + + /// Set the label at the left edge of the background to show the given numeric value. + pub fn set_left_label_content(&self, value:f32) { + self.label_left.frp.set_content.emit(format!("{:.2}", value)) + } + + /// Set the label at the right edge of the background to show the given numeric value. + pub fn set_right_label_content(&self, value:f32) { + self.label_right.frp.set_content.emit(format!("{:.2}", value)) + } + + pub fn set_caption_left(&self, caption:Option) { + let caption = caption.unwrap_or_default(); + self.caption_left.frp.set_content.emit(caption); + } + + pub fn set_caption_center(&self, caption:Option) { + let caption = caption.unwrap_or_default(); + self.caption_center.frp.set_content.emit(caption); + } + + pub fn show_left_overflow(&self, value:bool) { + if value { + self.root.add_child(&self.left_overflow); + } else { + self.root.remove_child(&self.left_overflow); + } + } + + pub fn show_right_overflow(&self, value:bool) { + if value { + self.root.add_child(&self.right_overflow); + } else { + self.root.remove_child(&self.right_overflow); + } + } + + pub fn left_corner_round(&self,value:bool) { + let corner_roundness = if value { 1.0 } else { 0.0 }; + self.background.corner_left.set(corner_roundness); + self.track.corner_left.set(corner_roundness) + } + + pub fn right_corner_round(&self,value:bool) { + let corner_roundness = if value { 1.0 } else { 0.0 }; + self.background.corner_right.set(corner_roundness); + self.track.corner_right.set(corner_roundness) + } + + pub fn set_track_color(&self, color:color::Rgba) { + self.track.track_color.set(color.into()); + } +} + +impl display::Object for Model { + fn display_object(&self) -> &display::object::Instance { &self.root } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/number.rs b/ide/src/rust/ensogl/lib/components/src/selector/number.rs new file mode 100644 index 000000000000..352014bd06e3 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/number.rs @@ -0,0 +1,137 @@ +///! Frp of the number selector. +use crate::prelude::*; + +use enso_frp as frp; +use ensogl_core::data::color; +use ensogl_core::application::Application; +use ensogl_core::display::shape::*; +use ensogl_core::display::shape::StyleWatchFrp; +use ensogl_theme as theme; + +use crate::component; +use crate::selector::shape::*; +use super::Bounds; +use super::bounds::absolute_value; +use super::bounds::clamp_with_overflow; +use super::bounds::normalise_value; +use super::bounds::position_to_normalised_value; +use super::shape::relative_shape_click_position; +use super::model::Model; + + + +// =========== +// === FRP === +// =========== + +ensogl_core::define_endpoints! { + Input { + set_value(f32), + resize(Vector2), + set_bounds(Bounds), + use_overflow_bounds(Option), + allow_click_selection(bool), + set_caption(Option), + set_left_corner_round(bool), + set_right_corner_round(bool), + set_track_color(color::Rgba), + } + Output { + value(f32), + bounds(Bounds), + } +} + +impl component::Frp for Frp { + fn init(&self, app:&Application, model:&Model, style:&StyleWatchFrp){ + let frp = &self; + let network = &frp.network; + let scene = app.display.scene(); + let mouse = &scene.mouse.frp; + + let base_frp = super::Frp::new(model,style,network,frp.resize.clone().into(),mouse); + + let track_shape_system = scene.shapes.shape_system(PhantomData::); + track_shape_system.shape_system.set_pointer_events(false); + + let madel_fn = model.clone_ref(); + let base_position = move || madel_fn.position().xy(); + let background_click = relative_shape_click_position( + base_position,network,&model.background.events,mouse); + + let madel_fn = model.clone_ref(); + let base_position = move || madel_fn.position().xy(); + let track_click = relative_shape_click_position( + base_position,network,&model.track.events,mouse); + + let style_track_color = style.get_color(theme::component::slider::track::color); + + frp::extend! { network + + // Rebind + frp.source.bounds <+ frp.set_bounds; + frp.source.value <+ frp.set_value; + + // Simple Inputs + eval frp.set_caption((caption) model.set_caption_left(caption.clone())); + eval frp.set_left_corner_round ((value) model.left_corner_round(*value)); + eval frp.set_right_corner_round((value) model.right_corner_round(*value)); + eval frp.set_track_color((value) model.set_track_color(*value)); + + // Internal + norm_value <- all2(&frp.value,&frp.bounds).map(normalise_value); + normalised_overflow_bounds <- all(&frp.use_overflow_bounds,&frp.bounds).map( + |(overflow_bounds,bounds)| + overflow_bounds.map(|Bounds{start,end}| + Bounds::new( + normalise_value(&(start,*bounds)),normalise_value(&(end,*bounds))) + ) + ); + has_underflow <- norm_value.map(|value| *value < 0.0); + has_overflow <- norm_value.map(|value| *value > 1.0); + + // Value Updates + eval norm_value((value) model.set_background_value(*value)); + eval has_underflow ((underflow) model.show_left_overflow(*underflow)); + eval has_overflow ((overflow) model.show_right_overflow(*overflow)); + eval frp.value((value) model.set_center_label_content(*value)); + + // Mouse IO + click <- any(&track_click,&background_click).gate(&frp.allow_click_selection); + click_value_update <- click.map2( + &base_frp.track_max_width, + position_to_normalised_value + ); + + is_dragging <- any + ( base_frp.is_dragging_track + , base_frp.is_dragging_background + , base_frp.is_dragging_left_overflow + , base_frp.is_dragging_right_overflow + ); + + drag_movement <- mouse.translation.gate(&is_dragging); + delta_value <- drag_movement.map2( + &base_frp.track_max_width, + |delta,width| (delta.x + delta.y) / width + ); + + drag_value_update <- delta_value.map2(&norm_value,|delta,value|*delta+*value); + value_update <- any(&click_value_update,&drag_value_update); + clamped_update <- value_update.map2( + &normalised_overflow_bounds, + clamp_with_overflow + ); + frp.source.value <+ all(&frp.set_bounds,&clamped_update).map(absolute_value); + } + + // Init defaults. + frp.set_bounds(Bounds::new(0.0,1.0)); + frp.allow_click_selection(false); + frp.use_overflow_bounds(None); + frp.set_value(0.5); + frp.set_left_corner_round(true); + frp.set_right_corner_round(true); + frp.set_track_color(style_track_color.value()); + } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/range.rs b/ide/src/rust/ensogl/lib/components/src/selector/range.rs new file mode 100644 index 000000000000..f2ab0f4d1ca3 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/range.rs @@ -0,0 +1,135 @@ +///! Frp of the range selector. +use crate::prelude::*; + +use enso_frp as frp; +use ensogl_core::data::color; +use ensogl_core::application::Application; +use ensogl_core::display::shape::*; +use ensogl_core::display::shape::StyleWatchFrp; +use ensogl_theme as theme; + +use crate::component; + +use super::Model; +use super::Bounds; +use super::bounds::absolute_value; +use super::bounds::normalise_value; +use super::bounds::should_clamp_with_overflow; + + + +// =========== +// === Frp === +// =========== + +ensogl_core::define_endpoints! { + Input { + set_value(Bounds), + resize(Vector2), + set_bounds(Bounds), + use_overflow_bounds(Option), + set_caption(Option), + set_left_corner_round(bool), + set_right_corner_round(bool), + set_track_color(color::Rgba), + } + Output { + value(Bounds), + bounds(Bounds), + } +} + +impl component::Frp for Frp { + fn init(&self, app:&Application, model:&Model, style:&StyleWatchFrp){ + let frp = &self; + let network = &frp.network; + let scene = app.display.scene(); + let mouse = &scene.mouse.frp; + + let base_frp = super::Frp::new(model, style, network, frp.resize.clone().into(), mouse); + + model.use_track_handles(true); + + let style_track_color = style.get_color(theme::component::slider::track::color); + + frp::extend! { network + + // Rebind + frp.source.bounds <+ frp.set_bounds; + frp.source.value <+ frp.set_value; + + // Simple Inputs + eval frp.set_caption((caption) model.set_caption_center(caption.clone())); + eval frp.set_left_corner_round ((value) model.left_corner_round(*value)); + eval frp.set_right_corner_round((value) model.right_corner_round(*value)); + eval frp.set_track_color((value) model.set_track_color(*value)); + + // Internal + norm_value <- all2(&frp.value,&frp.bounds).map(|(Bounds{start,end},bounds)|{ + Bounds::new(normalise_value(&(*start,*bounds)),normalise_value(&(*end,*bounds))) + }); + normalised_overflow_bounds <- all(&frp.use_overflow_bounds,&frp.bounds).map( + |(overflow_bounds,bounds)| + overflow_bounds.map(|Bounds{start,end}| + Bounds::new(normalise_value(&(start,*bounds)),normalise_value(&(end,*bounds))) + ) + ); + has_underflow <- norm_value.map(|value| (value.start.min(value.end)) < 0.0); + has_overflow <- norm_value.map(|value| (value.start.max(value.end)) > 1.0); + + // Slider Updates + update_slider <- all(&norm_value,&frp.resize); + eval update_slider(((value,size)) model.set_background_range(*value,*size)); + eval has_underflow ((underflow) model.show_left_overflow(*underflow)); + eval has_overflow ((overflow) model.show_right_overflow(*overflow)); + eval frp.value((value) { + model.set_left_label_content(value.start); + model.set_right_label_content(value.end); + }); + + // Mouse IO + let change_left_and_right_value = base_frp.is_dragging_track.clone_ref(); + change_left_value <- base_frp.is_dragging_left_handle + || base_frp.is_dragging_left_overflow; + change_right_value <- base_frp.is_dragging_right_handle + || base_frp.is_dragging_right_overflow; + + drag_movement <- mouse.translation.gate(&base_frp.is_dragging_any); + drag_delta <- drag_movement.map2(&base_frp.track_max_width, |delta,width| + (delta.x + delta.y) / width + ); + + drag_center_delta <- drag_delta.gate(&change_left_and_right_value); + center_update <- drag_center_delta.map2(&norm_value,|delta,Bounds{start,end}| + Bounds::new(start+delta,end+delta) + ); + + drag_left_delta <- drag_delta.gate(&change_left_value); + left_update <- drag_left_delta.map2(&norm_value,|delta,Bounds{start,end}| + Bounds::new(start+delta,*end) + ); + + drag_right_delta <- drag_delta.gate(&change_right_value); + right_update <- drag_right_delta.map2(&norm_value,|delta,Bounds{start,end}| + Bounds::new(*start,end+delta) + ); + + any_update <- any3(¢er_update,&left_update,&right_update); + is_in_bounds <- any_update.map2(&normalised_overflow_bounds,should_clamp_with_overflow); + new_value_absolute <- all(&frp.set_bounds,&any_update).map(|(bounds,Bounds{start,end})| + Bounds::new( + absolute_value(&(*bounds,*start)),absolute_value(&(*bounds,*end))).sorted() + ); + frp.source.value <+ new_value_absolute.gate(&is_in_bounds); + } + + // Init defaults + frp.set_bounds(Bounds::new(0.0,1.0)); + frp.use_overflow_bounds(None); + frp.set_value(Bounds::new(0.25,0.75)); + frp.set_left_corner_round(true); + frp.set_right_corner_round(true); + frp.set_track_color(style_track_color.value()); + + } +} diff --git a/ide/src/rust/ensogl/lib/components/src/selector/shape.rs b/ide/src/rust/ensogl/lib/components/src/selector/shape.rs new file mode 100644 index 000000000000..d2a5c4c67820 --- /dev/null +++ b/ide/src/rust/ensogl/lib/components/src/selector/shape.rs @@ -0,0 +1,297 @@ +//! This module contains the shapes and shape related functionality required. +use crate::prelude::*; + +use ensogl_core::data::color; +use ensogl_core::display::shape::*; +use ensogl_theme as theme; + +use crate::shadow; + + + +// ================== +// === Background === +// ================== + +/// Utility struct that contains the background shape for the selector components, as well as some +/// meta information about it. This information can be used to align other shapes with the +/// background. +struct Background { + pub width : Var, + pub height : Var, + pub corner_radius : Var, + pub shape : AnyShape, +} + +impl Background { + fn new(corner_left:&Var, corner_right:&Var, style:&StyleWatch) -> Background { + let sprite_width : Var = "input_size.x".into(); + let sprite_height : Var = "input_size.y".into(); + + let width = &sprite_width - shadow::size(style).px(); + let height = &sprite_height - shadow::size(style).px(); + let corner_radius = &height/2.0; + let rect_left = Rect((&width/2.0,&height)).corners_radius(&corner_radius*corner_left); + let rect_left = rect_left.translate_x(-&width/4.0); + let rect_right = Rect((&width/2.0,&height)).corners_radius(&corner_radius*corner_right); + let rect_right = rect_right.translate_x(&width/4.0); + let rect_center = Rect((&corner_radius*2.0,&height)); + + let shape = (rect_left+rect_right+rect_center).into(); + + Background{width,height,corner_radius,shape} + } +} + +/// Background shape. Appears as a rect with rounded corners. The roundness of each corner can be +/// toggled by passing `0.0` or `1.0` to either `corner_left` and `corner_right` where `0.0` means +/// "not rounded" and `1.0` means "rounded". +pub mod background { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style,corner_left:f32,corner_right:f32) { + let background = Background::new(&corner_left,&corner_right,style); + let color = style.get_color(theme::component::slider::background); + let shadow = shadow::from_shape(background.shape.clone(),style); + let background = background.shape.fill(color); + (shadow + background).into() + } + } +} + + + +// =============== +// === IO Rect === +// =============== + +/// Utility shape that is invisible but provides mouse input. Fills the whole sprite. +pub mod io_rect { + use super::*; + + ensogl_core::define_shape_system! { + () { + let sprite_width : Var = "input_size.x".into(); + let sprite_height : Var = "input_size.y".into(); + + let rect = Rect((&sprite_width,&sprite_height)); + let shape = rect.fill(HOVER_COLOR); + + shape.into() + } + } +} + + + +// ============= +// === Track === +// ============= + +/// Track of the selector. Appears as filled area of the background. Has a definable start and +/// end-point (`left`, `right`) which are passed as normalised values relative to the maximum +/// width. For consistency with the background shape, also has the property to round either side +/// of the track, when required to fit the background shape. (See `Background` above.). +pub mod track { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style,left:f32,right:f32,corner_left:f32,corner_right:f32,track_color:Vector4) { + let background = Background::new(&corner_left,&corner_right,style); + let width = background.width; + let height = background.height; + + let track_width = (&right - &left) * &width; + let track_start = left * &width; + let track = Rect((&track_width,&height)); + let track = track.translate_x(&track_start + (track_width / 2.0) ); + let track = track.translate_x(-width/2.0); + let track = track.intersection(&background.shape); + + let track_color = Var::::from(track_color); + let track = track.fill(track_color); + track.into() + } + } +} + + + +// ================ +// === Overflow === +// ================ + +/// Struct that contains the shape used to indicate an overflow (a triangle), and some metadata +/// that can be used to place and align it. +struct OverflowShape { + pub width : Var, + pub height : Var, + pub shape : AnyShape +} + +impl OverflowShape { + fn new(style:&StyleWatch) -> Self { + let sprite_width : Var = "input_size.x".into(); + let sprite_height : Var = "input_size.y".into(); + + let width = &sprite_width - shadow::size(style).px(); + let height = &sprite_height - shadow::size(style).px(); + let overflow_color = style.get_color(theme::component::slider::overflow::color); + let shape = Triangle(&sprite_height/6.0,&sprite_height/6.0); + let shape = shape.fill(&overflow_color); + + let hover_area = Circle(&height); + let hover_area = hover_area.fill(HOVER_COLOR); + + let shape = (shape + hover_area).into(); + OverflowShape{width,height,shape} + } +} + +/// Overflow shape that indicates a value can not be shown. Appears as a triangle/arrow pointing +/// left. +pub mod left_overflow { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style) { + let overflow_shape = OverflowShape::new(style); + let shape = overflow_shape.shape.rotate((-90.0_f32).to_radians().radians()); + shape.into() + } + } +} + +/// Overflow shape that indicates a value can not be shown. Appears as a triangle/arrow pointing +/// right. +pub mod right_overflow { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style) { + let overflow_shape = OverflowShape::new(style); + let shape = overflow_shape.shape.rotate(90.0_f32.to_radians().radians()); + shape.into() + } + } +} + + + +// ======================= +// === Shape Utilities === +// ======================= + +use enso_frp; +use enso_frp::Network; +use ensogl_core::frp::io::Mouse; +use ensogl_core::gui::component::ShapeViewEvents; + +pub use super::frp::*; +pub use super::model::*; + +/// Return whether a dragging action has been started from the shape passed to this function. A +/// dragging action is started by a mouse down on the shape, followed by a movement of the mouse. +/// Dragging is ended by a mouse up. +pub fn shape_is_dragged +(network:&Network, shape:&ShapeViewEvents, mouse:&Mouse) -> enso_frp::Stream { + enso_frp::extend! { network + mouse_up <- mouse.up.constant(()); + mouse_down <- mouse.down.constant(()); + over_shape <- bool(&shape.mouse_out,&shape.mouse_over); + mouse_down_over_shape <- mouse_down.gate(&over_shape); + is_dragging_shape <- bool(&mouse_up,&mouse_down_over_shape); + } + is_dragging_shape +} + +/// Returns the position of a mouse down on a shape. The position is given relative to the origin +/// of the shape position. +pub fn relative_shape_click_position( + base_position:impl Fn() -> Vector2 + 'static, + network:&Network, + shape:&ShapeViewEvents, + mouse:&Mouse) -> enso_frp::Stream { + enso_frp::extend! { network + mouse_down <- mouse.down.constant(()); + over_shape <- bool(&shape.mouse_out,&shape.mouse_over); + mouse_down_over_shape <- mouse_down.gate(&over_shape); + background_click_positon <- mouse.position.sample(&mouse_down_over_shape); + background_click_positon <- background_click_positon.map(move |pos| + pos - base_position() + ); + } + background_click_positon +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + use enso_frp::io::mouse::Button; + use enso_frp::stream::EventEmitter; + use enso_frp::stream::ValueProvider; + use float_eq::assert_float_eq; + + #[test] + fn test_shape_is_dragged() { + let network = enso_frp::Network::new("TestNetwork"); + let mouse = enso_frp::io::Mouse::default(); + let shape = ShapeViewEvents::default(); + + let is_dragged = shape_is_dragged(&network,&shape,&mouse); + let _watch = is_dragged.register_watch(); + + + // Default is false. + assert_eq!(is_dragged.value(),false); + + // Mouse down over shape activates dragging. + shape.mouse_over.emit(()); + mouse.down.emit(Button::from_code(0)); + assert_eq!(is_dragged.value(),true); + + // Release mouse stops dragging. + mouse.up.emit(Button::from_code(0)); + assert_eq!(is_dragged.value(),false); + + // Mouse down while not over shape does not activate dragging. + shape.mouse_out.emit(()); + mouse.down.emit(Button::from_code(0)); + assert_eq!(is_dragged.value(),false); + } + + #[test] + fn test_relative_shape_click_position() { + let network = enso_frp::Network::new("TestNetwork"); + let mouse = enso_frp::io::Mouse::default(); + let shape = ShapeViewEvents::default(); + + let base_position = || Vector2::new(-10.0,200.0); + let click_position = relative_shape_click_position(base_position, &network,&shape,&mouse); + let _watch = click_position.register_watch(); + + shape.mouse_over.emit(()); + mouse.position.emit(Vector2::new(-10.0,200.0)); + mouse.down.emit(Button::from_code(0)); + assert_float_eq!(click_position.value().x,0.0,ulps<=7); + assert_float_eq!(click_position.value().y,0.0,ulps<=7); + + mouse.position.emit(Vector2::new(0.0,0.0)); + mouse.down.emit(Button::from_code(0)); + assert_float_eq!(click_position.value().x,10.0,ulps<=7); + assert_float_eq!(click_position.value().y,-200.0,ulps<=7); + + mouse.position.emit(Vector2::new(400.0,0.5)); + mouse.down.emit(Button::from_code(0)); + assert_float_eq!(click_position.value().x,410.0,ulps<=7); + assert_float_eq!(click_position.value().y,-199.5,ulps<=7); + } +} diff --git a/ide/src/rust/ensogl/lib/components/src/shadow.rs b/ide/src/rust/ensogl/lib/components/src/shadow.rs index 653bbb296fae..082f0473c950 100644 --- a/ide/src/rust/ensogl/lib/components/src/shadow.rs +++ b/ide/src/rust/ensogl/lib/components/src/shadow.rs @@ -3,14 +3,15 @@ use crate::prelude::*; use ensogl_core::data::color; use ensogl_core::display::DomSymbol; -use ensogl_core::display::style; use ensogl_core::display::shape::*; use ensogl_core::display::shape::AnyShape; +use ensogl_core::display::style; +use ensogl_core::frp; use ensogl_core::system::web::StyleSetter; use ensogl_theme as theme; -/// Defines the apearance of a shadow +/// Defines the appearance of a shadow #[derive(Debug,Copy,Clone)] pub struct Parameters { base_color : color::Rgba, @@ -37,13 +38,19 @@ pub fn parameters_from_style_path(style:&StyleWatch,path:impl Into) } } -/// Return a shadow for the given shape. Exact appearance will depends on the theme parameters. +/// Utility method to retrieve the size of the shadow. can be used to determine shape padding etc. +pub fn size(style:&StyleWatch) -> f32 { + let parameters = parameters_from_style_path(style,theme::shadow); + parameters.size +} + +/// Return a shadow for the given shape. Exact appearance will depend on the theme parameters. pub fn from_shape(base_shape:AnyShape, style:&StyleWatch) -> AnyShape { let alpha = Var::::from(1.0); from_shape_with_alpha(base_shape,&alpha,style) } -/// Return a shadow for the given shape. Exact appearance will depends on the theme parameters. +/// Return a shadow for the given shape. Exact appearance will depend on the theme parameters. /// The color will be multiplied with the given alpha value, which is useful for fade-in/out /// animations. pub fn from_shape_with_alpha(base_shape:AnyShape,alpha:&Var,style:&StyleWatch) -> AnyShape { @@ -90,3 +97,31 @@ pub fn add_to_dom_element(element:&DomSymbol, style:&StyleWatch,logger:&Logger) let shadow = format!("{}px {}px {}px {}px rgba(0,0,0,{})",off_x,off_y,blur,spread,alpha); element.dom().set_style_or_warn("box-shadow",shadow,&logger); } + + +/// Provides FRP endpoints for the parameters that define the appearance of a shadow +#[derive(Debug,Clone,CloneRef)] +#[allow(missing_docs)] +pub struct ParametersFrp { + pub base_color : frp::Sampler, + pub fading : frp::Sampler, + pub size : frp::Sampler, + pub spread : frp::Sampler, + pub exponent : frp::Sampler, + pub offset_x : frp::Sampler, + pub offset_y : frp::Sampler +} + +/// Return FRP endpoints for the parameters that define a shadow. +pub fn frp_from_style(style:&StyleWatchFrp,path:impl Into) -> ParametersFrp { + let path: style::Path = path.into(); + ParametersFrp{ + base_color : style.get_color(&path), + fading : style.get_color(&path.sub("fading")), + size : style.get_number(&path.sub("size")), + spread : style.get_number(&path.sub("spread")), + exponent : style.get_number(&path.sub("exponent")), + offset_x : style.get_number(&path.sub("offset_x")), + offset_y : style.get_number(&path.sub("offset_y")) + } +} diff --git a/ide/src/rust/ensogl/lib/core/src/application/frp.rs b/ide/src/rust/ensogl/lib/core/src/application/frp.rs index 4e156af98b8f..3e4f5181df19 100644 --- a/ide/src/rust/ensogl/lib/core/src/application/frp.rs +++ b/ide/src/rust/ensogl/lib/core/src/application/frp.rs @@ -379,6 +379,10 @@ macro_rules! define_endpoints { self.status_map.clone() } } + + impl $crate::application::command::FrpNetworkProvider for Frp { + fn network(&self) -> &$crate::frp::Network { &self.network } + } }; } diff --git a/ide/src/rust/ensogl/lib/core/src/data/color/space/def.rs b/ide/src/rust/ensogl/lib/core/src/data/color/space/def.rs index 3f7204ef76cd..b057da06b9f8 100644 --- a/ide/src/rust/ensogl/lib/core/src/data/color/space/def.rs +++ b/ide/src/rust/ensogl/lib/core/src/data/color/space/def.rs @@ -397,6 +397,16 @@ impl Rgb { } impl Rgba { + /// Constructor. + pub fn black() -> Self { + Self::new(0.0,0.0,0.0,1.0) + } + + /// Constructor. + pub fn white() -> Self { + Self::new(1.0,1.0,1.0,1.0) + } + /// Constructor. pub fn red() -> Self { Self::new(1.0,0.0,0.0,1.0) diff --git a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/style_watch.rs b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/style_watch.rs index 3f680e4a7693..1135c976456f 100644 --- a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/style_watch.rs +++ b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/style_watch.rs @@ -82,6 +82,18 @@ impl StyleWatchFrp { sampler } + /// Queries style sheet value for a number. Returns 0.0 if not found. + pub fn get_number(&self, path:impl Into) -> frp::Sampler { + let network = &self.network; + let (source,current) = self.get_internal(path); + frp::extend! { network + value <- source.map(|t| t.number().unwrap_or(0.0)); + sampler <- value.sampler(); + } + source.emit(current); + sampler + } + /// Queries style sheet color, if not found fallbacks to [`FALLBACK_COLOR`]. pub fn get_color>(&self, path:T) -> frp::Sampler { let network = &self.network; diff --git a/ide/src/rust/ensogl/lib/theme/src/lib.rs b/ide/src/rust/ensogl/lib/theme/src/lib.rs index 18d309235756..e1adc3cdd7bc 100644 --- a/ide/src/rust/ensogl/lib/theme/src/lib.rs +++ b/ide/src/rust/ensogl/lib/theme/src/lib.rs @@ -395,6 +395,19 @@ define_themes! { [light:0, dark:1] padding_inner_y = 2.0, 2.0; height = 30.0, 30.0; } + slider { + background = graph_editor::node::background , graph_editor::node::background; + handle { + color = Lcha(0.3,0.0,0.0,1.0), Lcha(0.7,0.0,0.0,1.0); + } + track { + color = Lcha(0.9,0.0,0.0,0.5), Lcha(0.1,0.0,0.0,0.5); + } + overflow { + color = Lcha(0.0,0.0,0.0,1.0), Lcha(1.0,0.0,0.0,1.0); + scale = 1.0, 1.0; + } + } } diff --git a/ide/src/rust/lib/frp/src/nodes.rs b/ide/src/rust/lib/frp/src/nodes.rs index 3ee22f1bb1a5..cc6a04866626 100644 --- a/ide/src/rust/lib/frp/src/nodes.rs +++ b/ide/src/rust/lib/frp/src/nodes.rs @@ -241,6 +241,17 @@ impl Network { self.register(OwnedAny::new4(label,t1,t2,t3,t4)) } + /// Specialized version of `any`. + pub fn any5 + (&self, label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> Stream + where T1:EventOutput, + T2:EventOutput, + T3:EventOutput, + T4:EventOutput, + T5:EventOutput { + self.register(OwnedAny::new5(label,t1,t2,t3,t4,t5)) + } + // === Any_ === @@ -274,6 +285,13 @@ impl Network { self.register(OwnedAny_::new4(label,t1,t2,t3,t4)) } + /// Specialized version of `any_`. + pub fn any5_ + (&self, label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> Stream<()> + where T1:EventOutput, T2:EventOutput, T3:EventOutput, T4:EventOutput, T5:EventOutput { + self.register(OwnedAny_::new5(label,t1,t2,t3,t4,t5)) + } + // === All === @@ -624,6 +642,16 @@ impl DynamicNetwork { OwnedAny::new4(label,t1,t2,t3,t4).into() } + pub fn any5 + (self, label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> OwnedStream + where T1:EventOutput, + T2:EventOutput, + T3:EventOutput, + T4:EventOutput, + T5:EventOutput { + OwnedAny::new5(label,t1,t2,t3,t4,t5).into() + } + // === Any_ === @@ -652,6 +680,12 @@ impl DynamicNetwork { OwnedAny_::new4(label,t1,t2,t3,t4).into() } + pub fn any5_ + (self, label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> OwnedStream<()> + where T1:EventOutput, T2:EventOutput, T3:EventOutput, T4:EventOutput, T5:EventOutput { + OwnedAny_::new5(label,t1,t2,t3,t4,t5).into() + } + // === All === @@ -1257,6 +1291,16 @@ impl OwnedAny { Self::new(label).with(t1).with(t2).with(t3).with(t4) } + /// Constructor for 5 input streams. + pub fn new5(label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> Self + where T1:EventOutput, + T2:EventOutput, + T3:EventOutput, + T4:EventOutput, + T5:EventOutput { + Self::new(label).with(t1).with(t2).with(t3).with(t4).with(t5) + } + /// Emit new event. It's questionable if this node type should expose the `emit` functionality, /// but the current usage patterns proven it is a very handy utility. This node is used to /// define sources of frp output streams. Sources allow multiple streams to be attached and @@ -1351,6 +1395,12 @@ impl OwnedAny_ { where T1:EventOutput, T2:EventOutput, T3:EventOutput, T4:EventOutput { Self::new(label).with(t1).with(t2).with(t3).with(t4) } + + /// Constructor for 5 input streams. + pub fn new5(label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> Self + where T1:EventOutput, T2:EventOutput, T3:EventOutput, T4:EventOutput, T5:EventOutput { + Self::new(label).with(t1).with(t2).with(t3).with(t4).with(t5) + } } impl Any_ { @@ -1645,6 +1695,16 @@ impl OwnedAllMut { T4:EventOutput { Self::new(label).with(t1).with(t2).with(t3).with(t4) } + + /// Constructor for 5 input streams. + pub fn new5(label:Label, t1:&T1, t2:&T2, t3:&T3, t4:&T4, t5:&T5) -> Self + where T1:EventOutput, + T2:EventOutput, + T3:EventOutput, + T4:EventOutput, + T5:EventOutput { + Self::new(label).with(t1).with(t2).with(t3).with(t4).with(t5) + } } impl AllMut {