From 7f8190e663caa6dbd3339f6ace4094e26a5ba503 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Mon, 1 Aug 2022 12:54:42 +0200 Subject: [PATCH] Grid view selection and hover (#3622) This PR adds a new variant of selection, where the mouse-hovered entry is highlighted and may be selected by clicking. In the video below, we have three grid views with slightly different settings: * In the left-top corner, both hover and selection highlight is just a shape under the label. Such a grid view does not require additional layers (when compared to non-selectable grid view). * In the left-bottom corner the hover is normal shape, but selection is a _masked layer_ which allows us to have different text color. This setting requires three more layers to render. * In the right-top corner, both hover and selection are displayed in the masked layer, creating 6 additional layers. https://user-images.githubusercontent.com/3919101/181514178-f243bfeb-f2dd-4507-adc3-5344ae0579b7.mp4 --- CHANGELOG.md | 3 +- Cargo.lock | 1 + build-config.yaml | 2 +- .../ensogl/component/grid-view/src/entry.rs | 131 +++++- .../ensogl/component/grid-view/src/lib.rs | 190 +++++++- .../component/grid-view/src/scrollable.rs | 65 ++- .../component/grid-view/src/selectable.rs | 281 ++++++++++++ .../grid-view/src/selectable/highlight.rs | 413 ++++++++++++++++++ .../src/selectable/highlight/layer.rs | 83 ++++ .../src/selectable/highlight/shape.rs | 154 +++++++ .../ensogl/component/grid-view/src/simple.rs | 150 ++++--- .../ensogl/component/scroll-area/src/lib.rs | 5 + .../ensogl/core/src/data/color/space/def.rs | 4 +- .../src/display/shape/primitive/def/var.rs | 58 +++ .../display/shape/primitive/glsl/math.glsl | 27 +- lib/rust/ensogl/example/grid-view/Cargo.toml | 1 + lib/rust/ensogl/example/grid-view/src/lib.rs | 96 +++- lib/rust/types/src/algebra.rs | 24 +- 18 files changed, 1551 insertions(+), 137 deletions(-) create mode 100644 lib/rust/ensogl/component/grid-view/src/selectable.rs create mode 100644 lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs create mode 100644 lib/rust/ensogl/component/grid-view/src/selectable/highlight/layer.rs create mode 100644 lib/rust/ensogl/component/grid-view/src/selectable/highlight/shape.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d18b1d1b8..cb17fc3645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,8 @@ - [Added a new component: Grid View.][3588] It's parametrized by Entry object, display them arranged in a Grid. It does not instantiate all entries, only those visible, and re-use created entries during scrolling thus achieving - great performance. + great performance. There are variants of grid view with selection and + highlight, scrollbars, and both. #### Enso Standard Library diff --git a/Cargo.lock b/Cargo.lock index 939289203b..d67b0d9f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2513,6 +2513,7 @@ dependencies = [ "ensogl-grid-view", "ensogl-hardcoded-theme", "ensogl-text-msdf-sys", + "itertools 0.10.3", "wasm-bindgen", ] diff --git a/build-config.yaml b/build-config.yaml index 4cf42f9a2c..7e44292f72 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 5.17 MiB +wasm-size-limit: 5.22 MiB required-versions: cargo-watch: ^8.1.1 diff --git a/lib/rust/ensogl/component/grid-view/src/entry.rs b/lib/rust/ensogl/component/grid-view/src/entry.rs index e49fc22f56..88fa2b6ee9 100644 --- a/lib/rust/ensogl/component/grid-view/src/entry.rs +++ b/lib/rust/ensogl/component/grid-view/src/entry.rs @@ -1,12 +1,32 @@ -//! A module with an [`Entry`] abstraction for [`crate::GridView`]. `GridView` can be parametrized -//! by any entry with the specified API. +//! Structures related to a single [`crate::GridView`] entry. use crate::prelude::*; +use crate::selectable::highlight; +use crate::Col; +use crate::Row; + use enso_frp as frp; use ensogl_core::application::Application; +use ensogl_core::data::color; use ensogl_core::display; +use ensogl_core::display::geometry::compound::sprite; use ensogl_core::display::scene::Layer; +use ensogl_core::display::Attribute; + + + +// =============== +// === Contour === +// =============== + +/// A structure describing entry contour. +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Contour { + pub size: Vector2, + pub corners_radius: f32, +} @@ -19,8 +39,26 @@ ensogl_core::define_endpoints_2! { = Frp<::Model, ::Params>; // ============= -// === Trait === +// === Entry === // ============= /// The abstraction of Entry for [`crate::GridView`]. @@ -47,8 +85,91 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static { type Params: Clone + Debug + Default; /// An Entry constructor. - fn new(app: &Application, text_layer: &Option) -> Self; + fn new(app: &Application, text_layer: Option<&Layer>) -> Self; /// FRP endpoints getter. fn frp(&self) -> &EntryFrp; } + + + +// ============== +// === Shapes === +// ============== + +// === ShapeWithEntryContour === + +/// The trait implemented by all shapes sharing the contour of an entry. +pub trait ShapeWithEntryContour { + /// Padding added to the shape to avoid antialiasing issues. + const PADDING_PX: f32 = 5.0; + + /// Get the size parameter. + fn size(&self) -> &DynamicParam; + + /// Get the corner radius parameter. + fn corner_radius(&self) -> &DynamicParam>; + + /// Update shape's contour. + fn set_contour(&self, contour: Contour) { + let padding = Vector2(Self::PADDING_PX, Self::PADDING_PX) * 2.0; + self.size().set(contour.size + padding); + self.corner_radius().set(contour.corners_radius); + } +} + +macro_rules! implement_shape_with_entry_contour { + () => { + impl ShapeWithEntryContour for View { + fn size(&self) -> &DynamicParam { + &self.size + } + + fn corner_radius(&self) -> &DynamicParam> { + &self.corner_radius + } + } + }; +} + + +// === overlay === + +/// The entry overlay used for catching mouse events over an entry. +pub mod overlay { + use super::*; + + ensogl_core::define_shape_system! { + (style:Style, corner_radius: f32) { + let shape_width : Var = "input_size.x".into(); + let shape_height : Var = "input_size.y".into(); + let width = shape_width - 2.0.px() * View::PADDING_PX; + let height = shape_height - 2.0.px() * View::PADDING_PX; + Rect((width, height)).corners_radius(corner_radius.px()).fill(HOVER_COLOR).into() + } + } + + implement_shape_with_entry_contour!(); +} + + +// === shape === + +/// The shape having an entry contour filled with color. It's a helper which may be used in +/// entries implementations - for example [crate::simple::Entry`]. +pub mod shape { + use super::*; + + ensogl_core::define_shape_system! { + below = [overlay, highlight::shape]; + (style:Style, corner_radius: f32, color: Vector4) { + let shape_width : Var = "input_size.x".into(); + let shape_height : Var = "input_size.y".into(); + let width = shape_width - 2.0.px() * View::PADDING_PX; + let height = shape_height - 2.0.px() * View::PADDING_PX; + Rect((width, height)).corners_radius(corner_radius.px()).fill(color).into() + } + } + + implement_shape_with_entry_contour!(); +} diff --git a/lib/rust/ensogl/component/grid-view/src/lib.rs b/lib/rust/ensogl/component/grid-view/src/lib.rs index 7d5ed1ac4b..994b944c0e 100644 --- a/lib/rust/ensogl/component/grid-view/src/lib.rs +++ b/lib/rust/ensogl/component/grid-view/src/lib.rs @@ -26,6 +26,7 @@ pub mod entry; pub mod scrollable; +pub mod selectable; pub mod simple; pub mod visible_area; @@ -35,7 +36,14 @@ pub use ensogl_scroll_area::Viewport; /// Commonly used types and functions. pub mod prelude { + pub use ensogl_core::display::shape::*; pub use ensogl_core::prelude::*; + + pub use crate::entry::ShapeWithEntryContour; + pub use crate::selectable::highlight::shape::AttrSetter as TRAIT_AttrSetter; + + pub use enso_frp as frp; + pub use ensogl_core::application::command::FrpNetworkProvider; } use crate::prelude::*; @@ -55,6 +63,13 @@ use crate::visible_area::visible_rows; pub use entry::Entry; +// ================= +// === Constants === +// ================= + +const MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX: f32 = 1.5; + + // =========== // === FRP === @@ -71,12 +86,19 @@ ensogl_core::define_endpoints_2! { /// Declare what area of the GridView is visible. The area position is relative to left-top /// corner of the Grid View. set_viewport(Viewport), - /// Reset entries, providing number of rows and columns. All currently displayed entries + /// Set new size of the grid. If the number of rows or columns is reduced, the entries are + /// removed from the view. If it is extended, new model for entries may be requested if + /// needed. + resize_grid(Row, Col), + /// Reset entries, providing new number of rows and columns. All currently displayed entries /// will be detached and their models re-requested. reset_entries(Row, Col), /// Provide model for specific entry. Should be called only after `model_for_entry_needed` /// event for given row and column. After that the entry will be visible. model_for_entry(Row, Col, EntryModel), + /// Emit `model_for_entry_needed` signal for each visible entry. In contrary to + /// [`reset_entries`], it does not detach any entry. + request_model_for_visible_entries(), /// Set the entries size. All entries have the same size. set_entries_size(Vector2), /// Set the entries parameters. @@ -84,17 +106,42 @@ ensogl_core::define_endpoints_2! { /// Set the layer for any texts rendered by entries. The layer will be passed to entries' /// constructors. **Performance note**: This will re-instantiate all entries. set_text_layer(Option), + select_entry(Option<(Row, Col)>), + hover_entry(Option<(Row, Col)>), + accept_entry(Row, Col), } Output { - row_count(Row), - column_count(Col), + grid_size(Row, Col), viewport(Viewport), entries_size(Vector2), entries_params(EntryParams), content_size(Vector2), /// Event emitted when the Grid View needs model for an uncovered entry. model_for_entry_needed(Row, Col), + entry_shown(Row, Col), + entry_hovered(Option<(Row, Col)>), + entry_selected(Option<(Row, Col)>), + entry_accepted(Row, Col), + } +} + + + +// ==================== +// === VisibleEntry === +// ==================== + +#[derive(Clone, CloneRef, Debug)] +#[clone_ref(bound = "Entry: CloneRef")] +struct VisibleEntry { + entry: Entry, + overlay: entry::overlay::View, +} + +impl display::Object for VisibleEntry { + fn display_object(&self) -> &display::object::Instance { + self.entry.display_object() } } @@ -114,29 +161,71 @@ struct EntryCreationCtx { network: frp::WeakNetwork, set_entry_size: frp::Stream, set_entry_params: frp::Stream, + entry_hovered: frp::Any>, + entry_selected: frp::Any>, + entry_accepted: frp::Any<(Row, Col)>, } -impl EntryCreationCtx { - fn create_entry>(&self, text_layer: &Option) -> E { +impl EntryCreationCtx +where EntryParams: frp::node::Data +{ + fn create_entry>( + &self, + text_layer: Option<&Layer>, + ) -> VisibleEntry { let entry = E::new(&self.app, text_layer); + let overlay = entry::overlay::View::new(Logger::new("EntryOverlay")); + entry.add_child(&overlay); if let Some(network) = self.network.upgrade_or_warn() { let entry_frp = entry.frp(); let entry_network = entry_frp.network(); + let mouse = &self.app.display.default_scene.mouse.frp; frp::new_bridge_network! { [network, entry_network] grid_view_entry_bridge init <- source_(); entry_frp.set_size <+ all(init, self.set_entry_size)._1(); entry_frp.set_params <+ all(init, self.set_entry_params)._1(); + contour <- all(init, entry_frp.contour)._1(); + eval contour ((c) overlay.set_contour(*c)); + + let events = &overlay.events; + let disabled = &entry_frp.disabled; + let location = entry_frp.set_location.clone_ref(); + + // We make a distinction between "hovered" state and "mouse_in" state, because + // we want to highlight entry as hovered only when mouse moves a bit. + hovering <- any(...); + hovering <+ events.mouse_out.constant(false); + mouse_in <- bool(&events.mouse_out, &events.mouse_over); + // We can receive `mouse_over` event a couple of frames after actual hovering. + // Therefore, we count our "mouse move" from a couple of frames before. + mouse_pos_some_time_ago <- mouse.prev_position.previous().previous().previous(); + mouse_over_movement_start <- mouse_pos_some_time_ago.sample(&events.mouse_over); + mouse_over_with_start_pos <- all(mouse.position, mouse_over_movement_start).gate(&mouse_in); + mouse_move_which_hovers <- mouse_over_with_start_pos.filter( + |(pos, start_pos)| (pos - start_pos).norm() > MOUSE_MOVEMENT_NEEDED_TO_HOVER_PX + ); + hovered <- mouse_move_which_hovers.gate_not(&hovering).gate_not(disabled); + hovering <+ hovered.constant(true); + selected <- events.mouse_down.gate_not(disabled); + accepted <- events.mouse_down_primary.gate_not(disabled); + self.entry_hovered <+ location.sample(&hovered).map(|l| Some(*l)); + self.entry_selected <+ location.sample(&selected).map(|l| Some(*l)); + self.entry_accepted <+ location.sample(&accepted); } init.emit(()); } - entry + VisibleEntry { entry, overlay } } } -fn set_entry_position(entry: &E, row: Row, col: Col, entry_size: Vector2) { +fn entry_position(row: Row, col: Col, entry_size: Vector2) -> Vector2 { let x = (col as f32 + 0.5) * entry_size.x; let y = (row as f32 + 0.5) * -entry_size.y; - entry.set_position_xy(Vector2(x, y)); + Vector2(x, y) +} + +fn set_entry_position(entry: &E, row: Row, col: Col, entry_size: Vector2) { + entry.set_position_xy(entry_position(row, col, entry_size)); } @@ -150,6 +239,12 @@ struct Properties { entries_size: Vector2, } +impl Properties { + fn all_visible_locations(&self) -> impl Iterator { + all_visible_locations(&self.viewport, self.entries_size, self.row_count, self.col_count) + } +} + // === Model === @@ -157,8 +252,8 @@ struct Properties { #[derive(Clone, Debug)] pub struct Model { display_object: display::object::Instance, - visible_entries: RefCell>, - free_entries: RefCell>, + visible_entries: RefCell>>, + free_entries: RefCell>>, entry_creation_ctx: EntryCreationCtx, } @@ -201,7 +296,6 @@ impl Model { } fn reset_entries(&self, properties: Properties) -> Vec<(Row, Col)> { - let Properties { viewport, entries_size, row_count, col_count } = properties; let mut visible_entries = self.visible_entries.borrow_mut(); let mut free_entries = self.free_entries.borrow_mut(); let detached = visible_entries.drain().map(|(_, entry)| { @@ -209,7 +303,7 @@ impl Model { entry }); free_entries.extend(detached); - all_visible_locations(&viewport, entries_size, row_count, col_count).collect_vec() + properties.all_visible_locations().collect_vec() } fn drop_all_entries(&self, properties: Properties) -> Vec<(Row, Col)> { @@ -233,18 +327,19 @@ impl Model { let mut free_entries = self.free_entries.borrow_mut(); let create_new_entry = || { let text_layer = text_layer.as_ref().and_then(|l| l.upgrade()); - self.entry_creation_ctx.create_entry(&text_layer) + self.entry_creation_ctx.create_entry(text_layer.as_ref()) }; let entry = match visible_entries.entry((row, col)) { Occupied(entry) => entry.into_mut(), Vacant(lack_of_entry) => { let new_entry = free_entries.pop().unwrap_or_else(create_new_entry); set_entry_position(&new_entry, row, col, entry_size); + new_entry.entry.frp().set_location((row, col)); self.display_object.add_child(&new_entry); lack_of_entry.insert(new_entry) } }; - entry.frp().set_model(model); + entry.entry.frp().set_model(model); } } @@ -302,6 +397,15 @@ pub struct GridViewTemplate< /// anymore, after adding connections to this FRP node in particular. Therefore, be sure, that you /// connect providing models logic before emitting any of [`Frp::set_entries_size`] or /// [`Frp::set_viewport`]. +/// +/// # Hovering, Selecting and Accepting Entries +/// +/// The support for hovering, selecting or accepting entries is limited in this component - it will +/// react for mouse events and emit appropriate event when an entry is hovered/selected or accepted. +/// It does not set `is_selected/is_hovered` flag on entry nor highlight any of those components. If +/// you want to have full support, use [`selectable::GridView`] instead. +/// +/// The entries are both selected accepted with LMB-click, and selected with any other mouse click. pub type GridView = GridViewTemplate::Model, ::Params>; impl GridView { @@ -320,26 +424,31 @@ impl GridView { network: network.downgrade(), set_entry_size: set_entry_size.into(), set_entry_params: set_entry_params.into(), + entry_hovered: out.entry_hovered.clone_ref(), + entry_selected: out.entry_selected.clone_ref(), + entry_accepted: out.entry_accepted.clone_ref(), }; let model = Rc::new(Model::new(entry_creation_ctx)); frp::extend! { network - out.row_count <+ input.reset_entries._0(); - out.column_count <+ input.reset_entries._1(); + out.grid_size <+ input.resize_grid; + out.grid_size <+ input.reset_entries; out.viewport <+ input.set_viewport; out.entries_size <+ input.set_entries_size; out.entries_params <+ input.set_entries_params; - prop <- all_with4( - &out.row_count, &out.column_count, &out.viewport, &out.entries_size, - |&row_count, &col_count, &viewport, &entries_size| { + prop <- all_with3( + &out.grid_size, &out.viewport, &out.entries_size, + |&(row_count, col_count), &viewport, &entries_size| { Properties { row_count, col_count, viewport, entries_size } } ); - content_size_params <- all(input.reset_entries, input.set_entries_size); + content_size_params <- all(out.grid_size, input.set_entries_size); out.content_size <+ content_size_params.map(|&((rows, cols), esz)| Self::content_size(rows, cols, esz)); request_models_after_vis_area_change <= input.set_viewport.map2(&prop, f!((_, p) model.update_entries_visibility(*p))); + request_model_after_grid_size_change <= + input.resize_grid.map2(&prop, f!((_, p) model.update_entries_visibility(*p))); request_models_after_entry_size_change <= input.set_entries_size.map2( &prop, f!((_, p) model.update_after_entries_size_change(*p)) @@ -348,17 +457,31 @@ impl GridView { input.reset_entries.map2(&prop, f!((_, p) model.reset_entries(*p))); request_models_after_text_layer_change <= input.set_text_layer.map2(&prop, f!((_, p) model.drop_all_entries(*p))); + request_models_for_request <= input.request_model_for_visible_entries.map2( + &prop, + |_, p| p.all_visible_locations().collect_vec() + ); out.model_for_entry_needed <+ request_models_after_vis_area_change; + out.model_for_entry_needed <+ request_model_after_grid_size_change; out.model_for_entry_needed <+ request_models_after_entry_size_change; out.model_for_entry_needed <+ request_models_after_reset; out.model_for_entry_needed <+ request_models_after_text_layer_change; + out.model_for_entry_needed <+ request_models_for_request; + out.entry_hovered <+ input.hover_entry; + out.entry_selected <+ input.select_entry; + out.entry_accepted <+ input.accept_entry; + + // The ordering here is important: we want to first call [`update_entry`] and only then + // inform everyone that the entry is visible. They may want to immediately get the entry + // with [`get_entry`] method. model_prop_and_layer <- input.model_for_entry.map3(&prop, &input.set_text_layer, |model, prop, layer| (model.clone(), *prop, layer.clone())); eval model_prop_and_layer ((((row, col, entry_model), prop, layer): &((Row, Col, E::Model), Properties, Option)) model.update_entry(*row, *col, entry_model.clone(), prop.entries_size, layer) ); + out.entry_shown <+ input.model_for_entry.map(|(row, col, _)| (*row, *col)); } let display_object = model.display_object.clone_ref(); let widget = Widget::new(app, frp, model, display_object); @@ -372,6 +495,27 @@ impl GridView { } } +impl GridViewTemplate +where + Entry: CloneRef, + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +{ + fn get_entry(&self, row: Row, column: Col) -> Option { + let entries = self.widget.model().visible_entries.borrow(); + let entry = entries.get(&(row, column)); + entry.map(|e| e.entry.clone_ref()) + } +} + +impl AsRef + for GridViewTemplate +{ + fn as_ref(&self) -> &Self { + self + } +} + impl display::Object for GridViewTemplate { @@ -407,7 +551,7 @@ mod tests { type Model = Immutable; type Params = TestEntryParams; - fn new(_app: &Application, _: &Option) -> Self { + fn new(_app: &Application, _: Option<&Layer>) -> Self { let frp = entry::EntryFrp::::new(); let network = frp.network(); let param_set = Rc::new(Cell::new(0)); @@ -459,8 +603,8 @@ mod tests { let created_entries = grid_view.model().visible_entries.borrow(); assert_eq!(created_entries.len(), 25); for ((row, col), entry) in created_entries.iter() { - assert_eq!(entry.model_set.get(), row * 200 + col); - assert_eq!(entry.param_set.get(), 13); + assert_eq!(entry.entry.model_set.get(), row * 200 + col); + assert_eq!(entry.entry.param_set.get(), 13); } } } diff --git a/lib/rust/ensogl/component/grid-view/src/scrollable.rs b/lib/rust/ensogl/component/grid-view/src/scrollable.rs index 849345536f..3c99c3625c 100644 --- a/lib/rust/ensogl/component/grid-view/src/scrollable.rs +++ b/lib/rust/ensogl/component/grid-view/src/scrollable.rs @@ -2,6 +2,7 @@ use crate::prelude::*; +use crate::selectable; use crate::Entry; use enso_frp as frp; @@ -17,14 +18,14 @@ use ensogl_scroll_area::ScrollArea; // === GridView === // ================ -/// A template for [`GridView`] structure, where entry parameters and model are separate generic -/// arguments, similar to [`crate::GridViewTemplate`] - see its docs for details. -#[derive(CloneRef, Debug, Deref, Derivative)] -#[derivative(Clone(bound = ""))] -pub struct GridViewTemplate { +/// A template for [`GridView`] and [`SelectableGridView`] structures, parametrized by the +/// exact GridView implementation inside scroll area. +#[derive(Clone, CloneRef, Debug, Deref)] +#[clone_ref(bound = "InnerGridView: CloneRef")] +pub struct GridViewTemplate { area: ScrollArea, #[deref] - grid: crate::GridViewTemplate, + inner_grid: InnerGridView, text_layer: Layer, } @@ -43,25 +44,37 @@ pub struct GridViewTemplate /// /// See [`crate::GridView`] docs for more info about entries instantiation and process of requesting /// for Models. -pub type GridView = GridViewTemplate::Model, ::Params>; +pub type GridView = GridViewTemplate>; -impl GridView { - /// Create new Scrollable Grid View component. - pub fn new(app: &Application) -> Self { +/// Scrollable and Selectable Grid View Component. +/// +/// This Component displays any kind of entry `E` in a grid, inside the Scroll area and allowing +/// displaying highlights for hovered and selected entries. +/// +/// Essentially, it's a [scrollable `GridView`](GridView) wrapping the [selectable `GridView`]. See +/// their respective documentations for usage information. +pub type SelectableGridView = GridViewTemplate>; + +impl GridViewTemplate { + /// Create new Scrollable Grid View component wrapping a created instance of `inner_grid`. + pub fn new_wrapping(app: &Application, inner_grid: InnerGridView) -> Self + where + E: Entry, + InnerGridView: AsRef> + display::Object, { let area = ScrollArea::new(app); - let grid = crate::GridView::::new(app); - area.content().add_child(&grid); - let network = grid.network(); + let base_grid = inner_grid.as_ref(); + area.content().add_child(&inner_grid); + let network = base_grid.network(); let text_layer = area.content_layer().create_sublayer(); - grid.set_text_layer(Some(text_layer.downgrade())); + base_grid.set_text_layer(Some(text_layer.downgrade())); frp::extend! { network - grid.set_viewport <+ area.viewport; - area.set_content_width <+ grid.content_size.map(|s| s.x); - area.set_content_height <+ grid.content_size.map(|s| s.y); + base_grid.set_viewport <+ area.viewport; + area.set_content_width <+ base_grid.content_size.map(|s| s.x); + area.set_content_height <+ base_grid.content_size.map(|s| s.y); } - Self { area, grid, text_layer } + Self { area, inner_grid, text_layer } } /// Resize the component. It's a wrapper for [`scroll_frp`]`().resize`. @@ -76,7 +89,21 @@ impl GridView { } } -impl display::Object for GridViewTemplate { +impl GridView { + /// Create new scrollable [`GridView`] component. + pub fn new(app: &Application) -> Self { + Self::new_wrapping(app, crate::GridView::new(app)) + } +} + +impl SelectableGridView { + /// Create new scrollable [`SelectableGridView`] component. + pub fn new(app: &Application) -> Self { + Self::new_wrapping(app, selectable::GridView::new(app)) + } +} + +impl display::Object for GridViewTemplate { fn display_object(&self) -> &display::object::Instance { self.area.display_object() } diff --git a/lib/rust/ensogl/component/grid-view/src/selectable.rs b/lib/rust/ensogl/component/grid-view/src/selectable.rs new file mode 100644 index 0000000000..05361054f6 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/selectable.rs @@ -0,0 +1,281 @@ +//! A module containing the selectable [`GridView`] component. + +use crate::prelude::*; + +use crate::Entry; + +use ensogl_core::application::Application; +use ensogl_core::display; + + +// ============== +// === Export === +// ============== + +pub mod highlight; + + + +// ================ +// === GridView === +// ================ + +/// A template for [`GridView`] structure, where entry parameters and model are separate generic +/// arguments, similar to [`crate::GridViewTemplate`] - see its docs for details. +#[derive(CloneRef, Debug, Deref, Derivative)] +#[derivative(Clone(bound = ""))] +pub struct GridViewTemplate< + Entry: 'static, + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +> { + #[deref] + grid: crate::GridViewTemplate, + highlights: highlight::shape::View, + selection_handler: highlight::Handler, + hover_handler: highlight::Handler, +} + +/// The Selectable Grid View. +/// +/// An extension of [the base `GridView`](crate::GridView), where hovered and selected entries +/// will be highlighted. It shares the base [FRP API][`crate::Frp`], also providing additional +/// FRP APIs for handling [selection](selection_highlight_frp) and [hover](hover_highlight_frp) +/// highlights. +/// +/// # Highlight modes +/// +/// **Basic**: By default, the highlight is displayed as `highlight::shape::View` instance. The +/// shapes used by entries should specify if they need to be above or below highlight (for example +/// by adding `above = [ensogl_grid_view::selectable::highlight::shape]` clause to +/// [`define_shape_system!`] macro. +/// +/// **Masked Layer**: The basic highlight, however does not work correctly with case where we want +/// selected entries having different style (e.g. different text color) when highlighted. Because +/// of the highlight's smooth transition between entries, there are frames where only part of +/// the entry is highlighted. +/// +/// Therefore, you may specify a special layer, which is displayed over base grid view. The Grid +/// View will then create needed sub-layers (main and for text) and their mask: the sub-layers will +/// have another [`GridView`] instance with identical content but different entry parameters. They +/// are masked with the highlight shape, so only the highlighted fragment of the another grid view +/// is actually displayed. +/// +/// The "Masked layer" mode may be set for highlight and selection independently, by calling +/// [`highlight::FRP::setup_masked_layer`] on the proper highlight API. +pub type GridView = GridViewTemplate::Model, ::Params>; + +impl GridView { + /// Create new Selectable Grid View instance. + pub fn new(app: &Application) -> Self { + let grid = crate::GridView::::new(app); + let highlights = highlight::shape::View::new(Logger::new("highlights")); + let selection_handler = highlight::Handler::new_for_selection_connected(app, &grid); + let hover_handler = highlight::Handler::new_for_hover_connected(app, &grid); + grid.add_child(&highlights); + selection_handler.connect_with_shape::(&highlights); + hover_handler.connect_with_shape::(&highlights); + + let network = grid.frp().network(); + frp::extend! { network + eval grid.viewport ([highlights](&vp) highlight::shape::set_viewport(&highlights, vp)); + } + + Self { grid, highlights, selection_handler, hover_handler } + } +} + +impl GridViewTemplate +where + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +{ + /// Access to the Selection Highlight FRP. + pub fn selection_highlight_frp(&self) -> &highlight::Frp { + &self.selection_handler.frp + } + + /// Access to the Hover Highlight FRP. + pub fn hover_highlight_frp(&self) -> &highlight::Frp { + &self.hover_handler.frp + } +} + +impl AsRef> + for GridViewTemplate +where + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +{ + fn as_ref(&self) -> &crate::GridViewTemplate { + &self.grid + } +} + +impl display::Object + for GridViewTemplate +where + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +{ + fn display_object(&self) -> &display::object::Instance { + self.grid.display_object() + } +} + + + +// ============ +// === Test === +// ============ + +#[cfg(test)] +mod tests { + use super::*; + use crate::entry; + use crate::entry_position; + use crate::Col; + use crate::EntryFrp; + use crate::Row; + use ensogl_core::application::frp::API; + use ensogl_core::data::color; + use ensogl_core::display::scene::Layer; + use ensogl_core::display::Scene; + use ensogl_scroll_area::Viewport; + use itertools::iproduct; + + const CONTOUR_VARIANTS: [entry::Contour; 3] = [ + entry::Contour { size: Vector2(10.0, 10.0), corners_radius: 0.0 }, + entry::Contour { size: Vector2(20.0, 20.0), corners_radius: 2.0 }, + entry::Contour { size: Vector2(15.0, 15.0), corners_radius: 3.0 }, + ]; + + const COLOR_VARIANTS: [color::Rgba; 2] = + [color::Rgba(1.0, 0.0, 0.0, 1.0), color::Rgba(0.0, 1.0, 0.0, 1.0)]; + + #[derive(Clone, Debug, Default)] + struct TestEntryModel { + selected: Cell, + hovered: Cell, + contour: entry::Contour, + color: color::Rgba, + } + + impl TestEntryModel { + fn new(contour_variant: usize, color_variant: usize) -> Self { + TestEntryModel { + selected: default(), + hovered: default(), + contour: CONTOUR_VARIANTS[contour_variant], + color: COLOR_VARIANTS[color_variant], + } + } + } + + #[derive(Clone, CloneRef, Debug)] + struct TestEntry { + display_object: display::object::Instance, + frp: EntryFrp, + } + + impl Entry for TestEntry { + type Model = Rc; + type Params = (); + + fn new(_: &Application, _: Option<&Layer>) -> Self { + let frp = EntryFrp::::new(); + let display_object = display::object::Instance::new(Logger::new("TestEntry")); + let network = frp.network(); + let input = &frp.private().input; + let out = &frp.private().output; + frp::extend! {network + model_with_flags <- all(input.set_model, input.set_selected, input.set_hovered); + eval model_with_flags ([]((model, selected, hovered)) { + model.selected.set(*selected); + model.hovered.set(*hovered); + }); + out.contour <+ input.set_model.map(|m| m.contour); + out.selection_highlight_color <+ input.set_model.map(|m| m.color); + out.hover_highlight_color <+ input.set_model.map(|m| m.color); + } + Self { frp, display_object } + } + + fn frp(&self) -> &EntryFrp { + &self.frp + } + } + + impl display::Object for TestEntry { + fn display_object(&self) -> &display::object::Instance { + &self.display_object + } + } + + #[test] + fn selecting_entries() { + let app = Application::new("root"); + let network = frp::Network::new("selecting_entries"); + let grid_view = GridView::::new(&app); + let highlight_frp = grid_view.selection_highlight_frp(); + let entries: HashMap<(Row, Col), Rc> = iproduct!(0..2, 0..2) + .map(|(i, j)| ((i, j), Rc::new(TestEntryModel::new(i, j)))) + .collect(); + let models = entries.clone(); + frp::extend! { network + grid_view.model_for_entry <+ grid_view.model_for_entry_needed.map(move |&(row, col)| (row, col, models[&(row, col)].clone_ref())); + } + grid_view.set_entries_size(Vector2(20.0, 20.0)); + grid_view.set_viewport(Viewport { left: 0.0, top: 0.0, right: 40.0, bottom: -40.0 }); + grid_view.reset_entries(2, 2); + + assert_eq!(highlight_frp.contour.value(), entry::Contour::default()); + + for (row, col) in iproduct!(0..2, 0..2) { + grid_view.select_entry(Some((row, col))); + let expected_pos = entry_position(row, col, Vector2(20.0, 20.0)); + assert_eq!(highlight_frp.position.value(), expected_pos); + assert_eq!(highlight_frp.contour.value(), CONTOUR_VARIANTS[row]); + assert_eq!(highlight_frp.color.value(), COLOR_VARIANTS[col]); + for (&loc, model) in &entries { + assert_eq!(model.selected.get(), loc == (row, col)) + } + } + grid_view.select_entry(None); + assert_eq!(highlight_frp.contour.value(), entry::Contour::default()); + } + + #[test] + fn covering_and_uncovering_selected_entry() { + let app = Application::new("root"); + let network = frp::Network::new("selecting_entries"); + let grid_view = GridView::::new(&app); + let entries = (0..3).map(|i| Rc::new(TestEntryModel::new(i, 0))).collect_vec(); + let models = entries.clone(); + frp::extend! { network + grid_view.model_for_entry <+ + grid_view.model_for_entry_needed.map(move |&(r, c)| (r, c, models[c].clone_ref())); + } + grid_view.set_entries_size(Vector2(20.0, 20.0)); + let viewport_showing_first_two = + Viewport { left: 1.0, top: 0.0, right: 21.0, bottom: -20.0 }; + let viewport_showing_last_two = + Viewport { left: 39.0, top: 0.0, right: 59.0, bottom: -20.0 }; + grid_view.set_viewport(viewport_showing_last_two); + grid_view.reset_entries(1, 3); + grid_view.select_entry(Some((0, 2))); + assert!(!entries[1].selected.get()); + assert!(entries[2].selected.get()); + + grid_view.set_viewport(viewport_showing_first_two); + // Make sure the set_selected flag was not kept in re-used entry. + assert!(!entries[0].selected.get()); + assert!(!entries[1].selected.get()); + // We clear the selected flag; this way we'll check if it will be set to true once the third + // (selected) entry will be uncovered. + entries[2].selected.set(false); + grid_view.set_viewport(viewport_showing_last_two); + assert!(!entries[1].selected.get()); + assert!(entries[2].selected.get()); + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs b/lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs new file mode 100644 index 0000000000..c0262ed0e8 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs @@ -0,0 +1,413 @@ +//! A module containing the single highlight (selection or hover) [`Handler`] used in +//! [`crate::selectable::GridView`]. + +use crate::prelude::*; + +use crate::entry; +use crate::entry_position; +use crate::selectable::highlight::shape::HoverAttrSetter; +use crate::selectable::highlight::shape::SelectionAttrSetter; +use crate::Col; +use crate::Entry; +use crate::Row; + +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display::scene::layer::WeakLayer; +use ensogl_core::Animation; + + +// ============== +// === Export === +// ============== + +pub mod layer; +pub mod shape; + + + +// =========== +// === FRP === +// =========== + +ensogl_core::define_endpoints_2! { + Input { + entry_highlighted(Option<(Row, Col)>), + setup_masked_layer(Option), + set_entries_params(EntryParams), + } + Output { + entries_params(EntryParams), + position(Vector2), + contour(entry::Contour), + color(color::Rgba), + is_masked_layer_set(bool), + } +} + +/// A subset of [`entry::FRP`] endpoints used by the specific highlight [`Handler`]. +#[derive(Clone, CloneRef, Debug)] +pub struct EntryEndpoints { + /// The "is_highlighted" input: may be `is_selected` or `is_hovered`. + flag: frp::Any, + location: frp::Stream<(Row, Col)>, + contour: frp::Stream, + color: frp::Stream, +} + +/// All highlight animations. +#[derive(Clone, CloneRef, Debug)] +pub struct Animations { + position: Animation, + size: Animation, + corners_radius: Animation, + color: Animation, +} + +impl Animations { + /// Set up the animations and connect their targets with handler's FRP. + fn new(frp: &Frp) -> Self { + let network = frp.network(); + let position = Animation::::new(network); + let size = Animation::::new(network); + let corners_radius = Animation::::new(network); + let color = Animation::::new(network); + + frp::extend! { network + init <- source_(); + position.target <+ frp.position; + size.target <+ frp.contour.map(|&c| c.size); + corners_radius.target <+ frp.contour.map(|&c| c.corners_radius); + color.target <+ frp.color; + + // Skip animations when the highlight is not visible. + size_val <- all(init, size.value)._1(); + not_visible <- size_val.map(|sz| sz.x < f32::EPSILON || sz.y < f32::EPSILON); + corners_radius.skip <+ frp.color.gate(¬_visible).constant(()); + position.skip <+ frp.position.gate(¬_visible).constant(()); + color.skip <+ frp.color.gate(¬_visible).constant(()); + } + init.emit(()); + Self { position, size, corners_radius, color } + } +} + + +// ====================== +// === HighlightGuard === +// ====================== + +/// A guard managing the connections between highlighted entry and the [`Handler`] FRP outputs. +/// +/// Until dropped, this structure keeps connected the entry endpoints declaring the highlight +/// appearance (`position`, `contour` and `color`) to the appropriate [`Handler`] endpoints. +/// Also, the entry's flag (`is_selected` or `is_hovered`) will be set to `true` on construction and +/// set back to `false` on drop. +#[derive(Debug)] +struct ConnectedEntryGuard { + network: frp::Network, + /// An event emitted when we should drop this guard, for example when the Entry instance is + /// re-used in another location. + should_be_dropped: frp::Stream, + dropped: frp::Source, +} + +impl ConnectedEntryGuard { + /// Create guard for entry FRP at given location. + fn new_for_entry( + entry_frp: EntryEndpoints, + row: Row, + col: Col, + frp: &api::private::Output, + ) -> Self { + let network = frp::Network::new("HighlightedEntryGuard"); + frp::extend! { network + init <- source_(); + dropped <- source_(); + contour <- all(init, entry_frp.contour)._1(); + color <- all(init, entry_frp.color)._1(); + entry_frp.flag <+ init.constant(true); + entry_frp.flag <+ dropped.constant(false); + frp.contour <+ contour; + frp.color <+ color; + location_change <- entry_frp.location.filter(move |loc| *loc != (row, col)); + should_be_dropped <- location_change.constant(()); + } + init.emit(()); + Self { network, dropped, should_be_dropped } + } +} + +impl Drop for ConnectedEntryGuard { + fn drop(&mut self) { + self.dropped.emit(()); + } +} + + + +// ============= +// === Model === +// ============= + +/// Function returning [`EntryEndpoints`] of given entry, used as parameter of highlight +/// [`Handler`]. +pub type EntryEndpointsGetter = fn(&Entry) -> EntryEndpoints; + +/// The inner data structure for [`Handler`]. +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Data { + app: Application, + grid: crate::GridViewTemplate, + connected_entry: Cell>, + guard: RefCell>, + output: api::private::Output, + animations: Animations, + #[derivative(Debug = "ignore")] + entry_endpoints_getter: EntryEndpointsGetter, + layers: RefCell>>, +} + +impl Data +where + EntryModel: frp::node::Data, + EntryParams: frp::node::Data, +{ + fn new( + app: &Application, + grid: &crate::GridViewTemplate, + output: &api::private::Output, + animations: Animations, + entry_endpoints_getter: EntryEndpointsGetter, + ) -> Self { + Self { + app: app.clone_ref(), + grid: grid.clone_ref(), + connected_entry: default(), + guard: default(), + output: output.clone_ref(), + animations, + entry_endpoints_getter, + layers: default(), + } + } + + fn drop_guard(&self) { + self.connected_entry.set(None); + self.guard.take(); + } +} + +impl Data { + /// Drop old [`ConnectedEntryGuard`] and create a new one for new highlighted entry. + fn connect_new_highlighted_entry(self: &Rc, location: Option<(Row, Col)>) { + if let Some((row, col)) = location { + let current_entry = self.connected_entry.get(); + let entry_changed = current_entry.map_or(true, |loc| loc != (row, col)); + if entry_changed { + self.guard.take(); + let entry = self.grid.get_entry(row, col); + *self.guard.borrow_mut() = entry.map(|e| { + let endpoints = (self.entry_endpoints_getter)(&e); + ConnectedEntryGuard::new_for_entry(endpoints, row, col, &self.output) + }); + self.connected_entry.set(Some((row, col))); + self.set_up_guard_dropping(); + } + } else { + self.drop_guard() + } + } + + fn set_up_guard_dropping(self: &Rc) { + if let Some(guard) = &*self.guard.borrow() { + let network = &guard.network; + let this = self; + frp::extend! { network + eval_ guard.should_be_dropped (this.drop_guard()); + } + } + } + + fn setup_masked_layer(&self, layer: Option<&WeakLayer>) -> bool { + self.layers.take(); + let new_layers = layer.and_then(|l| l.upgrade()).map(|layer| { + let layers = layer::Handler::new(&self.app, &layer, &self.grid); + let shape = &layers.shape; + let network = layers.grid.network(); + let highlight_grid_frp = layers.grid.frp(); + let frp = &self.output; + self.connect_with_shape::(network, shape, false, None); + frp::extend! { network + highlight_grid_frp.set_entries_params <+ frp.entries_params; + } + HoverAttrSetter::set_color(shape, color::Rgba::black()); + SelectionAttrSetter::set_size(shape, default()); + layers + }); + let is_layer_set = new_layers.is_some(); + *self.layers.borrow_mut() = new_layers; + is_layer_set + } + + /// Update the highlight shape using given [`shape::AttrSetter`]. + /// + /// This function is used both for updating highlight shape in case of _Basic_ highlight mode, + /// or updating the mask of the highlight layer in case of _Masked Layer_ mode (See + /// [`crate::selectable::GridView`] docs for more information about highlight modes). + fn connect_with_shape( + &self, + network: &frp::Network, + shape: &shape::View, + connect_color: bool, + hide: Option<&frp::Stream>, + ) { + let grid_frp = self.grid.frp(); + frp::extend! { network + init <- source_(); + position <- all(init, self.animations.position.value)._1(); + size <- all(init, self.animations.size.value)._1(); + corners_radius <- all(init, self.animations.corners_radius.value)._1(); + } + let size = if let Some(hide) = hide { + frp::extend! { network + size <- all_with(&size, hide, |sz, hidden| if *hidden { default() } else { *sz }); + } + size + } else { + size + }; + frp::extend! { network + pos_and_viewport <- all(position, grid_frp.viewport); + eval pos_and_viewport ([shape](&(pos, vp)) Setter::set_position(&shape, pos, vp)); + eval size ([shape](&size) Setter::set_size(&shape, size)); + eval corners_radius ([shape](&r) Setter::set_corners_radius(&shape, r)); + } + if connect_color { + frp::extend! { network + color <- all(init, self.animations.color.value)._1(); + eval color ([shape](&color) Setter::set_color(&shape, color)); + } + } + init.emit(()) + } +} + + + +// =============== +// === Handler === +// =============== + +/// The Highlight Handler. +/// +/// This is a helper structure for [`selectable::GridView`] which handles a single highlight: +/// selection or hover: +/// * It provides an FRP API for given highlight, where the exact position, contour and color may be +/// read, and the _Masked layer_ highlight mode may be configured (see the [grid +/// view](`selectable::GridView`) documentation for more info about modes. +/// * It can be connected to highlight shape, so it's position, contour and color will be updated. +/// * It sets/unsets the proper flag on [`Entry`] instance (`set_selected` or `set_hovered`). +#[allow(missing_docs)] +#[derive(CloneRef, Debug, Derivative, Deref)] +#[derivative(Clone(bound = ""))] +pub struct Handler { + #[deref] + pub frp: Frp, + model: Rc>, +} + +impl Handler { + /// Create new Handler. + /// + /// This is a generic constructor: the exact entry endpoints handled by it should be specified + /// by `entry_endpoints_getter`, and the `entry_highlighted` endpoint in returned handler is + /// not connected. + /// + /// Use [`new_for_selection_connected`] or [`new_for_hover_connected`] to create a fully working + /// handler for specific highlight. + pub fn new( + app: &Application, + grid: &crate::GridView, + entry_endpoints_getter: EntryEndpointsGetter, + ) -> Self { + let frp = Frp::new(); + let animations = Animations::new(&frp); + let out = &frp.private.output; + let model = Rc::new(Data::new(app, grid, out, animations, entry_endpoints_getter)); + let network = frp.network(); + let grid_frp = grid.frp(); + frp::extend! {network + shown_with_highlighted <- + grid_frp.entry_shown.map2(&frp.entry_highlighted, |a, b| (*a, *b)); + highlighted_is_shown <- + shown_with_highlighted.map(|(sh, hlt)| hlt.contains(sh).and_option(*hlt)); + should_reconnect <- any(frp.entry_highlighted, highlighted_is_shown); + eval should_reconnect ((loc) model.connect_new_highlighted_entry(*loc)); + + became_highlighted <- frp.entry_highlighted.filter_map(|l| *l); + out.position <+ became_highlighted.all_with( + &grid_frp.entries_size, + |&(row, col), &es| entry_position(row, col, es) + ); + none_highlightd <- frp.entry_highlighted.filter(|opt| opt.is_none()).constant(()); + out.contour <+ none_highlightd.constant(default()); + + out.entries_params <+ frp.set_entries_params; + out.is_masked_layer_set <+ + frp.setup_masked_layer.map(f!((layer) model.setup_masked_layer(layer.as_ref()))); + + } + + Self { frp, model } + } + + /// Create selection highlight handler. + /// + /// The returned handler will have `entry_highlighted` properly connected to the passed `grid`. + pub fn new_for_selection_connected(app: &Application, grid: &crate::GridView) -> Self { + let this = Self::new(app, grid, Self::selection_endpoints_getter); + this.entry_highlighted.attach(&grid.entry_selected); + this + } + + /// Create hover highlight handler. + /// + /// The returned handler will have `entry_highlighted` properly connected to the passed `grid`. + pub fn new_for_hover_connected(app: &Application, grid: &crate::GridView) -> Self { + let this = Self::new(app, grid, Self::hover_endpoints_getter); + this.entry_highlighted.attach(&grid.entry_hovered); + this + } + + fn selection_endpoints_getter(entry: &E) -> EntryEndpoints { + let frp = entry.frp(); + EntryEndpoints { + flag: frp.set_selected.clone_ref(), + location: frp.set_location.clone_ref().into(), + contour: frp.contour.clone_ref().into(), + color: frp.selection_highlight_color.clone_ref().into(), + } + } + + fn hover_endpoints_getter(entry: &E) -> EntryEndpoints { + let frp = entry.frp(); + EntryEndpoints { + flag: frp.set_hovered.clone_ref(), + location: frp.set_location.clone_ref().into(), + contour: frp.contour.clone_ref().into(), + color: frp.hover_highlight_color.clone_ref().into(), + } + } + + /// Connects shape with the handler. + /// + /// The shape will be updated (using the specified `shape::AttrSetter`) according to the + /// `position`, `contour` and `color` outputs. + pub fn connect_with_shape(&self, shape: &shape::View) { + let network = self.frp.network(); + let shape_hidden = (&self.frp.is_masked_layer_set).into(); + self.model.connect_with_shape::(network, shape, true, Some(&shape_hidden)); + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/selectable/highlight/layer.rs b/lib/rust/ensogl/component/grid-view/src/selectable/highlight/layer.rs new file mode 100644 index 0000000000..04d17444c7 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/selectable/highlight/layer.rs @@ -0,0 +1,83 @@ +//! A module containing the single _Masked layer_ [`Handler`] used in +//! [`crate::selectable::highlight::Handler`]. + +use crate::prelude::*; + +use crate::selectable::highlight::shape; +use crate::Entry; + +use ensogl_core::application::Application; +use ensogl_core::display::scene::Layer; + + + +// =============== +// === Handler === +// =============== + +/// The highlight _Masked layer_ handler. +/// +/// The handler can be created for some specific layer and _base_ grid view. It will add two +/// sub-layers: +/// * one with the new _inner_ [`crate::GridView`] component; +/// * second being set up as a text layer of aforementioned grid view. +/// The _inner_ Grid View will be fully synchronized with the _base_ one except the entries' +/// parameters. The layers will be masked with highlight [`shape`], so only the highlighted fragment +/// of the _inner_ grid view is actually displayed. +/// +/// See [`selection::GridView`] docs for usage example. +#[derive(CloneRef, Debug, Derivative)] +#[derivative(Clone(bound = ""))] +pub struct Handler { + entries: Layer, + text: Layer, + mask: Layer, + /// The _inner_ grid view. + pub grid: crate::GridViewTemplate, + /// The shape being a mask for the sub-layers. + pub shape: shape::View, +} + +impl Handler { + /// Create new handler for given layer and _base_ [`GridView`](crate::GridView). + pub fn new(app: &Application, parent_layer: &Layer, base_grid: &crate::GridView) -> Self { + let grid = crate::GridView::new(app); + let shape = shape::View::new(Logger::new("HighlightMask")); + let entries = parent_layer.create_sublayer(); + let text = parent_layer.create_sublayer(); + let mask = Layer::new_with_cam( + Logger::new("grid_view::HighlightLayers::mask"), + &parent_layer.camera(), + ); + entries.set_mask(&mask); + text.set_mask(&mask); + entries.add_exclusive(&grid); + grid.set_text_layer(Some(text.downgrade())); + mask.add_exclusive(&shape); + base_grid.add_child(&grid); + grid.add_child(&shape); + + // The order of instructions below is very important! We need to initialize the viewport + // and entries size first, because its required to receive `model_for_entry` events after + // resizing grid properly. + let network = grid.network(); + frp::extend! { network + init <- source_(); + viewport <- all(init, base_grid.viewport)._1(); + eval viewport ([shape](&vp) shape::set_viewport(&shape, vp)); + grid.set_viewport <+ viewport; + grid.set_entries_size <+ all(init, base_grid.entries_size)._1(); + grid.resize_grid <+ all(init, base_grid.grid_size)._1(); + grid.model_for_entry <+ base_grid.model_for_entry; + + different_entry_hovered <- grid.entry_hovered.map2(&base_grid.entry_hovered, |e1, e2| e1 != e2); + base_grid.hover_entry <+ grid.entry_hovered.gate(&different_entry_hovered); + base_grid.select_entry <+ grid.entry_selected; + base_grid.accept_entry <+ grid.entry_accepted; + } + init.emit(()); + base_grid.request_model_for_visible_entries(); + + Self { entries, text, mask, grid, shape } + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/selectable/highlight/shape.rs b/lib/rust/ensogl/component/grid-view/src/selectable/highlight/shape.rs new file mode 100644 index 0000000000..a39d15f4c6 --- /dev/null +++ b/lib/rust/ensogl/component/grid-view/src/selectable/highlight/shape.rs @@ -0,0 +1,154 @@ +//! A module containing the highlight shape. +//! +//! The Highlight shape handles both highlights at once (selection and hover), so we are sure +//! the selection highlight will always be displayed over hover highlight without making unnecessary +//! shape systems. +//! +//! The shape is clipped to the viewport "manually", because it is used as a mask for _Masked Layer_ +//! (see [`crate::selectable::highlight::layer::Handler`], and masks in EnsoGL cannot be further +//! masked. +//! +//! # Setting Parameters +//! +//! The positioning, size, and other highlight parameters are not easily mapped to shape parameter +//! due to above reasons, and the fact that the number of parameters of one shape is constrained. +//! Use the helper functions defined in this module instead: +//! * Keep up-to-date the info about the grid view's viewport using [`set_viewport`]. +//! * Set parameters of the specific highlight using one of [`AttrSetter`] instances: +//! [`HoverAttrSetter`] or [`SelectionAttrSetter`] + +use crate::prelude::*; + +use ensogl_core::data::color; +use ensogl_scroll_area::Viewport; + + + +// ======================== +// === Shape Definition === +// ======================== + +ensogl_core::define_shape_system! { + pointer_events = false; + ( + style: Style, + // Corners radii of viewport (x), hover highlight (y) and selection highlight (z). + corners_radii: Vector3, + // Positions of highlights: hover (xy) and selection (zw). + highlights_pos: Vector4, + // Sizes of highlights: hover (xy) and selection (zw). + highlights_sizes: Vector4, + hover_color: Vector4, + selection_color: Vector4, + ) { + let viewport_width = Var::::from("input_size.x"); + let viewport_height = Var::::from("input_size.y"); + let viewport = Rect((viewport_width, viewport_height)); + let viewport = viewport.corners_radius(corners_radii.x().px()); + let hover = Rect(highlights_sizes.xy().px()).corners_radius(corners_radii.y().px()); + let hover = hover.translate(highlights_pos.xy().px()); + let hover = (&hover * &viewport).fill(hover_color); + let selection = Rect(highlights_sizes.zw().px()).corners_radius(corners_radii.z().px()); + let selection = selection.translate(highlights_pos.zw().px()); + let selection = (&selection * &viewport).fill(selection_color); + let highlights = &hover + &selection; + highlights.into() + } +} + + + +// =========================== +// === Parameters' Setters === +// =========================== + +// === set_viewport === + +/// Updates the shape's viewport. The position and size of the sprite will be updated. See +/// [module's docs](mod@self) for more info. +pub fn set_viewport(shape: &View, viewport: Viewport) { + shape.size.set(viewport.size()); + shape.set_position_xy(viewport.center_point()); +} + + +// === AttrSetter === + +/// The trait with setters for all attributes of a single highlight. +#[allow(missing_docs)] +pub trait AttrSetter { + fn set_position(shape: &View, position: Vector2, viewport: Viewport); + fn set_size(shape: &View, size: Vector2); + fn set_corners_radius(shape: &View, radius: f32); + fn set_color(shape: &View, color: color::Rgba); +} + + +// === HoverAttrSetter === + +/// Struct with setters for all attributes for hover highlight. +#[derive(Copy, Clone, Debug)] +pub struct HoverAttrSetter; + +impl AttrSetter for HoverAttrSetter { + fn set_position(shape: &View, position: Vector2, viewport: Viewport) { + let viewport_position = viewport.center_point(); + let relative_pos = position - viewport_position; + let mut attr = shape.highlights_pos.get(); + attr.x = relative_pos.x; + attr.y = relative_pos.y; + shape.highlights_pos.set(attr); + } + + fn set_size(shape: &View, size: Vector2) { + let mut attr = shape.highlights_sizes.get(); + attr.x = size.x; + attr.y = size.y; + shape.highlights_sizes.set(attr) + } + + fn set_corners_radius(shape: &View, radius: f32) { + let mut old_radii = shape.corners_radii.get(); + old_radii.y = radius; + shape.corners_radii.set(old_radii); + } + + fn set_color(shape: &View, color: color::Rgba) { + shape.hover_color.set(color.into()) + } +} + + +// === SelectionAttrSetter === + +/// Struct with setters for all attributes for selection highlight. +#[derive(Copy, Clone, Debug)] +pub struct SelectionAttrSetter; + +impl AttrSetter for SelectionAttrSetter { + fn set_position(shape: &View, position: Vector2, viewport: Viewport) { + let viewport_position = viewport.center_point(); + let relative_pos = position - viewport_position; + let mut attr = shape.highlights_pos.get(); + attr.z = relative_pos.x; + attr.w = relative_pos.y; + shape.highlights_pos.set(attr); + } + + fn set_size(shape: &View, size: Vector2) { + let mut attr = shape.highlights_sizes.get(); + attr.z = size.x; + attr.w = size.y; + shape.highlights_sizes.set(attr) + } + + fn set_corners_radius(shape: &View, radius: f32) { + let mut old_radii = shape.corners_radii.get(); + old_radii.z = radius; + shape.corners_radii.set(old_radii); + } + + fn set_color(shape: &View, color: color::Rgba) { + shape.selection_color.set(color.into()) + } +} diff --git a/lib/rust/ensogl/component/grid-view/src/simple.rs b/lib/rust/ensogl/component/grid-view/src/simple.rs index 58af12ffe5..f2a2d4b99b 100644 --- a/lib/rust/ensogl/component/grid-view/src/simple.rs +++ b/lib/rust/ensogl/component/grid-view/src/simple.rs @@ -3,7 +3,9 @@ use crate::prelude::*; use ensogl_core::display::shape::*; +use crate::entry; use crate::scrollable; +use crate::selectable; use crate::EntryFrp; use ensogl_core::application::command::FrpNetworkProvider; @@ -16,31 +18,6 @@ use ensogl_text as text; -// ================== -// === Background === -// ================== - -/// The background of single Entry. The actually displayed rectangle is shrunk by [`PADDING_PX`] -/// from the shape size, to avoid antialiasing glitches. -pub mod entry_background { - use super::*; - - /// A padding added to the background rectangle to avoid antialiasing glitches. - pub const PADDING_PX: f32 = 5.0; - - ensogl_core::define_shape_system! { - (style:Style, color: Vector4) { - let shape_width : Var = "input_size.x".into(); - let shape_height : Var = "input_size.y".into(); - let width = shape_width - 2.0.px() * PADDING_PX; - let height = shape_height - 2.0.px() * PADDING_PX; - Rect((width, height)).fill(color).into() - } - } -} - - - // =================== // === EntryParams === // =================== @@ -49,29 +26,61 @@ pub mod entry_background { #[allow(missing_docs)] #[derive(Clone, Debug)] pub struct EntryParams { - pub bg_color: color::Rgba, - pub bg_margin: f32, - pub font: ImString, - pub text_offset: f32, - pub text_size: text::Size, - pub text_color: color::Rgba, + pub bg_color: color::Rgba, + pub bg_margin: f32, + pub hover_color: color::Rgba, + pub selection_color: color::Rgba, + pub font: ImString, + pub text_offset: f32, + pub text_size: text::Size, + pub text_color: color::Rgba, + pub disabled_text_color: color::Rgba, } impl Default for EntryParams { fn default() -> Self { Self { - bg_color: color::Rgba::transparent(), - bg_margin: 0.0, - font: text::typeface::font::DEFAULT_FONT.into(), - text_offset: 7.0, - text_size: text::Size(14.0), - text_color: default(), + bg_color: color::Rgba::transparent(), + bg_margin: 0.0, + hover_color: color::Rgba(0.9, 0.9, 0.9, 1.0), + selection_color: color::Rgba(0.8, 0.8, 0.8, 1.0), + font: text::typeface::font::DEFAULT_FONT.into(), + text_offset: 7.0, + text_size: text::Size(14.0), + text_color: color::Rgba(0.0, 0.0, 0.0, 1.0), + disabled_text_color: color::Rgba(0.7, 0.7, 0.7, 1.0), } } } +// ================== +// === EntryModel === +// ================== + +/// The model of [`SimpleGridView`]`s entries. +#[allow(missing_docs)] +#[derive(Clone, CloneRef, Debug, Default)] +pub struct EntryModel { + pub text: ImString, + pub disabled: Immutable, +} + +impl EntryModel { + fn new(text: impl Into) -> Self { + Self { text: text.into(), ..default() } + } +} + +impl> From for EntryModel { + fn from(text: T) -> Self { + Self::new(text) + } +} + + + // ============= // === Entry === // ============= @@ -84,15 +93,15 @@ impl Default for EntryParams { pub struct EntryData { display_object: display::object::Instance, pub label: text::Area, - pub background: entry_background::View, + pub background: entry::shape::View, } impl EntryData { - fn new(app: &Application, text_layer: &Option) -> Self { + fn new(app: &Application, text_layer: Option<&Layer>) -> Self { let logger = Logger::new("list_view::entry::Label"); let display_object = display::object::Instance::new(&logger); let label = app.new_view::(); - let background = entry_background::View::new(&logger); + let background = entry::shape::View::new(&logger); display_object.add_child(&label); display_object.add_child(&background); if let Some(layer) = text_layer { @@ -101,18 +110,10 @@ impl EntryData { Self { display_object, label, background } } - fn update_layout( - &self, - entry_size: Vector2, - bg_margin: f32, - text_size: text::Size, - text_offset: f32, - ) { - use entry_background::PADDING_PX; - let bg_size = entry_size - Vector2(bg_margin, bg_margin) * 2.0; - let bg_size_with_padding = bg_size + Vector2(PADDING_PX, PADDING_PX) * 2.0; - self.background.size.set(bg_size_with_padding); - self.label.set_position_xy(Vector2(text_offset - entry_size.x / 2.0, text_size.raw / 2.0)); + fn update_layout(&self, contour: entry::Contour, text_size: text::Size, text_offset: f32) { + self.background.set_contour(contour); + let size = contour.size; + self.label.set_position_xy(Vector2(text_offset - size.x / 2.0, text_size.raw / 2.0)); } } @@ -127,34 +128,52 @@ pub struct Entry { } impl crate::Entry for Entry { - type Model = ImString; + type Model = EntryModel; type Params = EntryParams; - fn new(app: &Application, text_layer: &Option) -> Self { + fn new(app: &Application, text_layer: Option<&Layer>) -> Self { let data = Rc::new(EntryData::new(app, text_layer)); let frp = EntryFrp::::new(); let input = &frp.private().input; + let out = &frp.private().output; let network = frp.network(); enso_frp::extend! { network + size <- input.set_size.on_change(); bg_color <- input.set_params.map(|p| p.bg_color).on_change(); bg_margin <- input.set_params.map(|p| p.bg_margin).on_change(); + hover_color <- input.set_params.map(|p| p.hover_color).on_change(); + selection_color <- input.set_params.map(|p| p.selection_color).on_change(); font <- input.set_params.map(|p| p.font.clone_ref()).on_change(); text_offset <- input.set_params.map(|p| p.text_offset).on_change(); text_color <- input.set_params.map(|p| p.text_color).on_change(); text_size <- input.set_params.map(|p| p.text_size).on_change(); + dis_text_color <- input.set_params.map(|p| p.disabled_text_color).on_change(); - layout <- all(input.set_size, bg_margin, text_size, text_offset); - eval layout ((&(es, m, ts, to)) data.update_layout(es, m, ts, to)); - + contour <- all_with(&size, &bg_margin, |size, margin| entry::Contour { + size: *size - Vector2(*margin, *margin) * 2.0, + corners_radius: 0.0, + }); + layout <- all(contour, text_size, text_offset); + eval layout ((&(c, ts, to)) data.update_layout(c, ts, to)); eval bg_color ((color) data.background.color.set(color.into())); - data.label.set_default_color <+ text_color.on_change(); - data.label.set_font <+ font.on_change().map(ToString::to_string); - data.label.set_default_text_size <+ text_size.on_change(); - - content <- input.set_model.map(|s| s.to_string()); + disabled <- input.set_model.map(|m| *m.disabled); + data.label.set_default_color <+ all_with3( + &text_color, + &dis_text_color, + &disabled, + |c, dc, d| if *d { *dc } else { *c } + ); + data.label.set_font <+ font.map(ToString::to_string); + data.label.set_default_text_size <+ text_size; + content <- input.set_model.map(|m| m.text.to_string()); max_width_px <- input.set_size.map(|size| size.x); data.label.set_content_truncated <+ all(&content, &max_width_px); + + out.contour <+ contour; + out.disabled <+ disabled; + out.hover_highlight_color <+ hover_color; + out.selection_highlight_color <+ selection_color; } Self { frp, data } } @@ -181,3 +200,10 @@ pub type SimpleGridView = crate::GridView; /// The Simple version of Scrollable Grid View, where each entry is just a label with background. pub type SimpleScrollableGridView = scrollable::GridView; + +/// The Simple version of Selectable Grid View, where each entry is just a label with background. +pub type SimpleSelectableGridView = selectable::GridView; + +/// The Simple version of scrollable and selectable Grid View, where each entry is just a label with +/// background. +pub type SimpleScrollableSelectableGridView = scrollable::SelectableGridView; diff --git a/lib/rust/ensogl/component/scroll-area/src/lib.rs b/lib/rust/ensogl/component/scroll-area/src/lib.rs index db88d73b9e..ee7ea2c87f 100644 --- a/lib/rust/ensogl/component/scroll-area/src/lib.rs +++ b/lib/rust/ensogl/component/scroll-area/src/lib.rs @@ -111,6 +111,11 @@ impl Viewport { pub fn size(&self) -> Vector2 { Vector2(self.right - self.left, self.top - self.bottom) } + + /// Return the central point of the viewport. + pub fn center_point(&self) -> Vector2 { + Vector2(self.left + self.right, self.top + self.bottom) / 2.0 + } } diff --git a/lib/rust/ensogl/core/src/data/color/space/def.rs b/lib/rust/ensogl/core/src/data/color/space/def.rs index 49aef4b790..3d9aaf772f 100644 --- a/lib/rust/ensogl/core/src/data/color/space/def.rs +++ b/lib/rust/ensogl/core/src/data/color/space/def.rs @@ -121,7 +121,7 @@ macro_rules! define_color_space { /// Constructor. #[allow(non_snake_case)] - pub fn $name($($comp:f32),*) -> $name { + pub const fn $name($($comp:f32),*) -> $name { $name::new($($comp),*) } @@ -142,7 +142,7 @@ macro_rules! define_color_space { /// Constructor. #[allow(non_snake_case)] - pub fn $a_name($($comp:f32),*,alpha:f32) -> $a_name { + pub const fn $a_name($($comp:f32),*,alpha:f32) -> $a_name { $a_name::new($($comp),*,alpha) } diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/def/var.rs b/lib/rust/ensogl/core/src/display/shape/primitive/def/var.rs index 1b1785f10e..dae1fc8861 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/def/var.rs +++ b/lib/rust/ensogl/core/src/display/shape/primitive/def/var.rs @@ -200,6 +200,10 @@ impl HasComponents for Var> { type Component = Var; } +impl HasComponents for Var> { + type Component = Var; +} + impl Dim1 for Var> { fn x(&self) -> Var { match self { @@ -245,6 +249,60 @@ impl Dim3 for Var> { } } +impl Dim1 for Var> { + fn x(&self) -> Var { + match self { + Self::Static(t) => Var::Static(t.x.clone()), + Self::Dynamic(t) => Var::Dynamic(format!("{}.x", t).into()), + } + } +} + +impl Dim2 for Var> { + fn y(&self) -> Var { + match self { + Self::Static(t) => Var::Static(t.y.clone()), + Self::Dynamic(t) => Var::Dynamic(format!("{}.y", t).into()), + } + } +} + +impl Dim3 for Var> { + fn z(&self) -> Var { + match self { + Self::Static(t) => Var::Static(t.z.clone()), + Self::Dynamic(t) => Var::Dynamic(format!("{}.z", t).into()), + } + } +} + +impl Dim4 for Var> { + fn w(&self) -> Var { + match self { + Self::Static(t) => Var::Static(t.z.clone()), + Self::Dynamic(t) => Var::Dynamic(format!("{}.z", t).into()), + } + } +} + +impl HasDim2Version for Var> { + type Dim2Version = Var>; + + fn xy(&self) -> Self::Dim2Version { + match self { + Self::Static(t) => Var::Static(t.xy()), + Self::Dynamic(t) => Var::Dynamic(format!("{}.xy", t).into()), + } + } + + fn zw(&self) -> Self::Dim2Version { + match self { + Self::Static(t) => Var::Static(Vector2(t[2].clone(), t[3].clone())), + Self::Dynamic(t) => Var::Dynamic(format!("{}.zw", t).into()), + } + } +} + impl PixelDistance for Var> { type Output = Var>; diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/glsl/math.glsl b/lib/rust/ensogl/core/src/display/shape/primitive/glsl/math.glsl index 1cad0aef95..25e6d30b4b 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/glsl/math.glsl +++ b/lib/rust/ensogl/core/src/display/shape/primitive/glsl/math.glsl @@ -183,8 +183,33 @@ float neg(float a) { return -a; } +vec2 add(vec2 a, vec2 b) { + return a + b; +} -// === Encode === +vec2 sub(vec2 a, vec2 b) { + return a - b; +} + +vec2 div(vec2 a, vec2 b) { + return a / b; +} + +vec2 mul(vec2 a, vec2 b) { + return a * b; +} + +vec2 rem(vec2 a, vec2 b) { + return mod(a,b); +} + +vec2 neg(vec2 a) { + return -a; +} + + + + // === Encode === /// Enables check for ID encoding. #define ID_ENCODING_OVERFLOW_CHECK diff --git a/lib/rust/ensogl/example/grid-view/Cargo.toml b/lib/rust/ensogl/example/grid-view/Cargo.toml index ddccff1f1c..8aa68a037b 100644 --- a/lib/rust/ensogl/example/grid-view/Cargo.toml +++ b/lib/rust/ensogl/example/grid-view/Cargo.toml @@ -15,3 +15,4 @@ ensogl-grid-view = { path = "../../component/grid-view" } ensogl-text-msdf-sys = { path = "../../component/text/msdf-sys" } enso-text = { path = "../../../text" } wasm-bindgen = { version = "0.2.78", features = ["nightly"] } +itertools = "0.10.3" diff --git a/lib/rust/ensogl/example/grid-view/src/lib.rs b/lib/rust/ensogl/example/grid-view/src/lib.rs index 4328d55de5..c6d5e073cb 100644 --- a/lib/rust/ensogl/example/grid-view/src/lib.rs +++ b/lib/rust/ensogl/example/grid-view/src/lib.rs @@ -24,6 +24,7 @@ use ensogl_core::prelude::*; use wasm_bindgen::prelude::*; use enso_frp as frp; +use enso_frp::web::forward_panic_hook_to_console; use ensogl_core::application::Application; use ensogl_core::data::color; use ensogl_core::display::navigation::navigator::Navigator; @@ -44,6 +45,7 @@ use ensogl_text_msdf_sys::run_once_initialized; pub fn main() { run_once_initialized(|| { init_tracing(TRACE); + forward_panic_hook_to_console(); let app = Application::new("root"); init(&app); mem::forget(app); @@ -51,6 +53,38 @@ pub fn main() { } +fn setup_grid_view(app: &Application) -> grid_view::simple::SimpleScrollableSelectableGridView { + let view = grid_view::simple::SimpleScrollableSelectableGridView::new(app); + frp::new_network! { network + requested_entry <- view.model_for_entry_needed.map(|(row, col)| { + let model = grid_view::simple::EntryModel { + text: format!("Entry ({row}, {col})").into(), + disabled: Immutable(row == col), + }; + (*row, *col, model) + }); + view.model_for_entry <+ requested_entry; + entry_hovered <- view.entry_hovered.filter_map(|l| *l); + entry_selected <- view.entry_selected.filter_map(|l| *l); + eval entry_hovered ([]((row, col)) tracing::debug!("Hovered entry ({row}, {col}).")); + eval entry_selected ([]((row, col)) tracing::debug!("Selected entry ({row}, {col}).")); + eval view.entry_accepted ([]((row, col)) tracing::debug!("ACCEPTED entry ({row}, {col}).")); + } + view.set_entries_size(Vector2(130.0, 28.0)); + let params = grid_view::simple::EntryParams { + bg_color: color::Rgba(0.8, 0.8, 0.9, 1.0), + bg_margin: 1.0, + ..default() + }; + view.set_entries_params(params); + view.scroll_frp().resize(Vector2(400.0, 300.0)); + view.reset_entries(1000, 1000); + std::mem::forget(network); + app.display.add_child(&view); + view +} + + // ======================== // === Init Application === @@ -61,32 +95,54 @@ fn init(app: &Application) { theme::builtin::light::register(&app); theme::builtin::light::enable(&app); - let grid_view = grid_view::simple::SimpleScrollableGridView::new(app); - grid_view.scroll_frp().resize(Vector2(400.0, 300.0)); - app.display.default_scene.layers.node_searcher.add_exclusive(&grid_view); - frp::new_network! { network - requested_entry <- grid_view.model_for_entry_needed.map(|(row, col)| { - (*row, *col, ImString::from(format!("Entry ({row}, {col})"))) - }); - grid_view.model_for_entry <+ requested_entry; - } - grid_view.set_entries_size(Vector2(130.0, 28.0)); - let params = grid_view::simple::EntryParams { - bg_color: color::Rgba(0.8, 0.8, 0.9, 1.0), - bg_margin: 1.0, - ..default() - }; - grid_view.set_entries_params(params); - grid_view.reset_entries(1000, 1000); + let main_layer = &app.display.default_scene.layers.node_searcher; + let grids_layer = main_layer.create_sublayer(); + let hover_layer = main_layer.create_sublayer(); + let selection_layer = main_layer.create_sublayer(); + + let grid_views = std::iter::repeat_with(|| setup_grid_view(app)).take(3).collect_vec(); + let with_hover_mask = [&grid_views[2]]; + let with_selection_mask = [&grid_views[1], &grid_views[2]]; + let positions = itertools::iproduct!([-450.0, 50.0], [350.0, -50.0]); + + for (view, (x, y)) in grid_views.iter().zip(positions) { + grids_layer.add_exclusive(view); + view.set_position_xy(Vector2(x, y)); + } + + for view in with_hover_mask { + view.hover_highlight_frp().setup_masked_layer(Some(hover_layer.downgrade())); + let params = grid_view::simple::EntryParams { + bg_color: color::Rgba(0.7, 0.7, 0.9, 1.0), + bg_margin: 0.0, + text_offset: 8.0, + text_color: color::Rgba(0.9, 0.9, 0.9, 1.0), + ..default() + }; + view.hover_highlight_frp().set_entries_params(params); + } + + for view in with_selection_mask { + view.selection_highlight_frp().setup_masked_layer(Some(selection_layer.downgrade())); + let params = grid_view::simple::EntryParams { + bg_color: color::Rgba(0.5, 0.5, 0.5, 1.0), + bg_margin: 0.0, + text_color: color::Rgba(1.0, 1.0, 1.0, 1.0), + text_offset: 8.0, + ..default() + }; + view.selection_highlight_frp().set_entries_params(params); + } - app.display.add_child(&grid_view); let navigator = Navigator::new( &app.display.default_scene, &app.display.default_scene.layers.node_searcher.camera(), ); navigator.disable_wheel_panning(); - std::mem::forget(grid_view); - std::mem::forget(network); + std::mem::forget(grid_views); + std::mem::forget(grids_layer); + std::mem::forget(hover_layer); + std::mem::forget(selection_layer); std::mem::forget(navigator); } diff --git a/lib/rust/types/src/algebra.rs b/lib/rust/types/src/algebra.rs index ed239e4358..0e08f526a3 100644 --- a/lib/rust/types/src/algebra.rs +++ b/lib/rust/types/src/algebra.rs @@ -121,9 +121,9 @@ pub trait HasComponents { } -// ============ -// === Dim1 === -// ============ +// ================== +// === Dimensions === +// ================== /// Describes types that have the first dimension component. pub trait Dim1: HasComponents { @@ -143,6 +143,24 @@ pub trait Dim3: Dim2 { fn z(&self) -> Self::Component; } +/// Describes types that have the fourth dimension component. +pub trait Dim4: Dim3 { + /// fourth value getter. + fn w(&self) -> Self::Component; +} + +/// Describes types with at least 4 dimension components, which has their "two-dimension" version, +/// for example [`Vector4`] whose "two-dimension" version is [`Vector2`]. +pub trait HasDim2Version: Dim4 { + /// The type being the "two-dimension" version of self. + type Dim2Version; + + /// Create "two-dimension" version constructed from first and second component. + fn xy(&self) -> Self::Dim2Version; + /// Create "two-dimension" version constructed third and fourth component. + fn zw(&self) -> Self::Dim2Version; +} + // ===========