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
This commit is contained in:
Adam Obuchowicz 2022-08-01 12:54:42 +02:00 committed by GitHub
parent d59714a29d
commit 7f8190e663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1551 additions and 137 deletions

View File

@ -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

1
Cargo.lock generated
View File

@ -2513,6 +2513,7 @@ dependencies = [
"ensogl-grid-view",
"ensogl-hardcoded-theme",
"ensogl-text-msdf-sys",
"itertools 0.10.3",
"wasm-bindgen",
]

View File

@ -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

View File

@ -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! { <Model: (frp::node::Data), Params: (frp::node
set_model(Model),
set_size(Vector2),
set_params(Params),
set_location((Row, Col)),
/// True if the entry is currently selected.
///
/// This flag is set only in [selectable](crate::selectable) grid views.
set_selected(bool),
/// True is the entry is currently hovered by mouse.
///
/// This flag is set only in [selectable](crate::selectable) grid views.
set_hovered(bool),
}
Output {
/// Disabled entries does not react for mouse events, and cannot be selected.
disabled(bool),
/// Entry's contour. Defines what part of the entry will react for mouse events, and also
/// defines the shape of the selection/hover highlight (in case of
/// [selectable](crate::selectable) grid views.).
contour(Contour),
selection_highlight_color(color::Rgba),
hover_highlight_color(color::Rgba)
}
Output {}
}
/// FRP Api of a specific Entry.
@ -29,7 +67,7 @@ pub type EntryFrp<E> = Frp<<E as Entry>::Model, <E as Entry>::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<Layer>) -> Self;
fn new(app: &Application, text_layer: Option<&Layer>) -> Self;
/// FRP endpoints getter.
fn frp(&self) -> &EntryFrp<Self>;
}
// ==============
// === 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<sprite::Size>;
/// Get the corner radius parameter.
fn corner_radius(&self) -> &DynamicParam<Attribute<f32>>;
/// 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<sprite::Size> {
&self.size
}
fn corner_radius(&self) -> &DynamicParam<Attribute<f32>> {
&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<Pixels> = "input_size.x".into();
let shape_height : Var<Pixels> = "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<Pixels> = "input_size.x".into();
let shape_height : Var<Pixels> = "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!();
}

View File

@ -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<WeakLayer>),
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: Entry,
overlay: entry::overlay::View,
}
impl<E: display::Object> display::Object for VisibleEntry<E> {
fn display_object(&self) -> &display::object::Instance {
self.entry.display_object()
}
}
@ -114,29 +161,71 @@ struct EntryCreationCtx<EntryParams> {
network: frp::WeakNetwork,
set_entry_size: frp::Stream<Vector2>,
set_entry_params: frp::Stream<EntryParams>,
entry_hovered: frp::Any<Option<(Row, Col)>>,
entry_selected: frp::Any<Option<(Row, Col)>>,
entry_accepted: frp::Any<(Row, Col)>,
}
impl<EntryParams: frp::node::Data> EntryCreationCtx<EntryParams> {
fn create_entry<E: Entry<Params = EntryParams>>(&self, text_layer: &Option<Layer>) -> E {
impl<EntryParams> EntryCreationCtx<EntryParams>
where EntryParams: frp::node::Data
{
fn create_entry<E: Entry<Params = EntryParams>>(
&self,
text_layer: Option<&Layer>,
) -> VisibleEntry<E> {
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<E: display::Object>(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<E: display::Object>(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<Item = (Row, Col)> {
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<Entry, EntryParams> {
display_object: display::object::Instance,
visible_entries: RefCell<HashMap<(Row, Col), Entry>>,
free_entries: RefCell<Vec<Entry>>,
visible_entries: RefCell<HashMap<(Row, Col), VisibleEntry<Entry>>>,
free_entries: RefCell<Vec<VisibleEntry<Entry>>>,
entry_creation_ctx: EntryCreationCtx<EntryParams>,
}
@ -201,7 +296,6 @@ impl<Entry: display::Object, EntryParams> Model<Entry, EntryParams> {
}
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<Entry: display::Object, EntryParams> Model<Entry, EntryParams> {
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<E: Entry> Model<E, E::Params> {
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<E> = GridViewTemplate<E, <E as Entry>::Model, <E as Entry>::Params>;
impl<E: Entry> GridView<E> {
@ -320,26 +424,31 @@ impl<E: Entry> GridView<E> {
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<E: Entry> GridView<E> {
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<WeakLayer>))
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<E: Entry> GridView<E> {
}
}
impl<Entry, EntryModel, EntryParams> GridViewTemplate<Entry, EntryModel, EntryParams>
where
Entry: CloneRef,
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
{
fn get_entry(&self, row: Row, column: Col) -> Option<Entry> {
let entries = self.widget.model().visible_entries.borrow();
let entry = entries.get(&(row, column));
entry.map(|e| e.entry.clone_ref())
}
}
impl<Entry, EntryModel: frp::node::Data, EntryParams: frp::node::Data> AsRef<Self>
for GridViewTemplate<Entry, EntryModel, EntryParams>
{
fn as_ref(&self) -> &Self {
self
}
}
impl<Entry, EntryModel: frp::node::Data, EntryParams: frp::node::Data> display::Object
for GridViewTemplate<Entry, EntryModel, EntryParams>
{
@ -407,7 +551,7 @@ mod tests {
type Model = Immutable<usize>;
type Params = TestEntryParams;
fn new(_app: &Application, _: &Option<Layer>) -> Self {
fn new(_app: &Application, _: Option<&Layer>) -> Self {
let frp = entry::EntryFrp::<Self>::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);
}
}
}

View File

@ -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<E: 'static, M: frp::node::Data, P: frp::node::Data> {
/// 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<InnerGridView> {
area: ScrollArea,
#[deref]
grid: crate::GridViewTemplate<E, M, P>,
inner_grid: InnerGridView,
text_layer: Layer,
}
@ -43,25 +44,37 @@ pub struct GridViewTemplate<E: 'static, M: frp::node::Data, P: frp::node::Data>
///
/// See [`crate::GridView`] docs for more info about entries instantiation and process of requesting
/// for Models.
pub type GridView<E> = GridViewTemplate<E, <E as Entry>::Model, <E as Entry>::Params>;
pub type GridView<E> = GridViewTemplate<crate::GridView<E>>;
impl<E: Entry> GridView<E> {
/// 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<E> = GridViewTemplate<selectable::GridView<E>>;
impl<InnerGridView> GridViewTemplate<InnerGridView> {
/// Create new Scrollable Grid View component wrapping a created instance of `inner_grid`.
pub fn new_wrapping<E>(app: &Application, inner_grid: InnerGridView) -> Self
where
E: Entry,
InnerGridView: AsRef<crate::GridView<E>> + display::Object, {
let area = ScrollArea::new(app);
let grid = crate::GridView::<E>::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<E: Entry> GridView<E> {
}
}
impl<E, M: frp::node::Data, P: frp::node::Data> display::Object for GridViewTemplate<E, M, P> {
impl<E: Entry> GridView<E> {
/// Create new scrollable [`GridView`] component.
pub fn new(app: &Application) -> Self {
Self::new_wrapping(app, crate::GridView::new(app))
}
}
impl<E: Entry> SelectableGridView<E> {
/// Create new scrollable [`SelectableGridView`] component.
pub fn new(app: &Application) -> Self {
Self::new_wrapping(app, selectable::GridView::new(app))
}
}
impl<InnerGridView> display::Object for GridViewTemplate<InnerGridView> {
fn display_object(&self) -> &display::object::Instance {
self.area.display_object()
}

View File

@ -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<Entry, EntryModel, EntryParams>,
highlights: highlight::shape::View,
selection_handler: highlight::Handler<Entry, EntryModel, EntryParams>,
hover_handler: highlight::Handler<Entry, EntryModel, EntryParams>,
}
/// 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<E> = GridViewTemplate<E, <E as Entry>::Model, <E as Entry>::Params>;
impl<E: Entry> GridView<E> {
/// Create new Selectable Grid View instance.
pub fn new(app: &Application) -> Self {
let grid = crate::GridView::<E>::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::<highlight::shape::SelectionAttrSetter>(&highlights);
hover_handler.connect_with_shape::<highlight::shape::HoverAttrSetter>(&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<Entry, EntryModel, EntryParams> GridViewTemplate<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
{
/// Access to the Selection Highlight FRP.
pub fn selection_highlight_frp(&self) -> &highlight::Frp<EntryParams> {
&self.selection_handler.frp
}
/// Access to the Hover Highlight FRP.
pub fn hover_highlight_frp(&self) -> &highlight::Frp<EntryParams> {
&self.hover_handler.frp
}
}
impl<Entry, EntryModel, EntryParams> AsRef<crate::GridViewTemplate<Entry, EntryModel, EntryParams>>
for GridViewTemplate<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
{
fn as_ref(&self) -> &crate::GridViewTemplate<Entry, EntryModel, EntryParams> {
&self.grid
}
}
impl<Entry, EntryModel, EntryParams> display::Object
for GridViewTemplate<Entry, EntryModel, EntryParams>
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<bool>,
hovered: Cell<bool>,
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<Self>,
}
impl Entry for TestEntry {
type Model = Rc<TestEntryModel>;
type Params = ();
fn new(_: &Application, _: Option<&Layer>) -> Self {
let frp = EntryFrp::<Self>::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> {
&self.frp
}
}
impl display::Object for TestEntry {
fn display_object(&self) -> &display::object::Instance<Scene> {
&self.display_object
}
}
#[test]
fn selecting_entries() {
let app = Application::new("root");
let network = frp::Network::new("selecting_entries");
let grid_view = GridView::<TestEntry>::new(&app);
let highlight_frp = grid_view.selection_highlight_frp();
let entries: HashMap<(Row, Col), Rc<TestEntryModel>> = 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::<TestEntry>::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());
}
}

View File

@ -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! { <EntryParams: (frp::node::Data)>
Input {
entry_highlighted(Option<(Row, Col)>),
setup_masked_layer(Option<WeakLayer>),
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<bool>,
location: frp::Stream<(Row, Col)>,
contour: frp::Stream<entry::Contour>,
color: frp::Stream<color::Rgba>,
}
/// All highlight animations.
#[derive(Clone, CloneRef, Debug)]
pub struct Animations {
position: Animation<Vector2>,
size: Animation<Vector2>,
corners_radius: Animation<f32>,
color: Animation<color::Rgba>,
}
impl Animations {
/// Set up the animations and connect their targets with handler's FRP.
fn new<EntryParams: frp::node::Data>(frp: &Frp<EntryParams>) -> Self {
let network = frp.network();
let position = Animation::<Vector2>::new(network);
let size = Animation::<Vector2>::new(network);
let corners_radius = Animation::<f32>::new(network);
let color = Animation::<color::Rgba>::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(&not_visible).constant(());
position.skip <+ frp.position.gate(&not_visible).constant(());
color.skip <+ frp.color.gate(&not_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<EntryParams: frp::node::Data>(
entry_frp: EntryEndpoints,
row: Row,
col: Col,
frp: &api::private::Output<EntryParams>,
) -> 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<Entry> = fn(&Entry) -> EntryEndpoints;
/// The inner data structure for [`Handler`].
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Data<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
app: Application,
grid: crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
connected_entry: Cell<Option<(Row, Col)>>,
guard: RefCell<Option<ConnectedEntryGuard>>,
output: api::private::Output<EntryParams>,
animations: Animations,
#[derivative(Debug = "ignore")]
entry_endpoints_getter: EntryEndpointsGetter<Entry>,
layers: RefCell<Option<layer::Handler<Entry, EntryModel, EntryParams>>>,
}
impl<Entry, EntryModel, EntryParams> Data<Entry, EntryModel, EntryParams>
where
EntryModel: frp::node::Data,
EntryParams: frp::node::Data,
{
fn new(
app: &Application,
grid: &crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
output: &api::private::Output<EntryParams>,
animations: Animations,
entry_endpoints_getter: EntryEndpointsGetter<Entry>,
) -> 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<E: Entry> Data<E, E::Model, E::Params> {
/// Drop old [`ConnectedEntryGuard`] and create a new one for new highlighted entry.
fn connect_new_highlighted_entry(self: &Rc<Self>, 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<Self>) {
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::<HoverAttrSetter>(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<Setter: shape::AttrSetter>(
&self,
network: &frp::Network,
shape: &shape::View,
connect_color: bool,
hide: Option<&frp::Stream<bool>>,
) {
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<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
#[deref]
pub frp: Frp<EntryParams>,
model: Rc<Data<Entry, EntryModel, EntryParams>>,
}
impl<E: Entry> Handler<E, E::Model, E::Params> {
/// 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<E>,
entry_endpoints_getter: EntryEndpointsGetter<E>,
) -> 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<E>) -> 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<E>) -> 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<Setter: shape::AttrSetter>(&self, shape: &shape::View) {
let network = self.frp.network();
let shape_hidden = (&self.frp.is_masked_layer_set).into();
self.model.connect_with_shape::<Setter>(network, shape, true, Some(&shape_hidden));
}
}

View File

@ -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<Entry: 'static, EntryModel: frp::node::Data, EntryParams: frp::node::Data> {
entries: Layer,
text: Layer,
mask: Layer,
/// The _inner_ grid view.
pub grid: crate::GridViewTemplate<Entry, EntryModel, EntryParams>,
/// The shape being a mask for the sub-layers.
pub shape: shape::View,
}
impl<E: Entry> Handler<E, E::Model, E::Params> {
/// Create new handler for given layer and _base_ [`GridView`](crate::GridView).
pub fn new(app: &Application, parent_layer: &Layer, base_grid: &crate::GridView<E>) -> 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 }
}
}

View File

@ -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::<Pixels>::from("input_size.x");
let viewport_height = Var::<Pixels>::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())
}
}

View File

@ -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<Pixels> = "input_size.x".into();
let shape_height : Var<Pixels> = "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<bool>,
}
impl EntryModel {
fn new(text: impl Into<ImString>) -> Self {
Self { text: text.into(), ..default() }
}
}
impl<T: Into<ImString>> From<T> 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<Layer>) -> 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::<ensogl_text::Area>();
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<Layer>) -> Self {
fn new(app: &Application, text_layer: Option<&Layer>) -> Self {
let data = Rc::new(EntryData::new(app, text_layer));
let frp = EntryFrp::<Self>::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<Entry>;
/// The Simple version of Scrollable Grid View, where each entry is just a label with background.
pub type SimpleScrollableGridView = scrollable::GridView<Entry>;
/// The Simple version of Selectable Grid View, where each entry is just a label with background.
pub type SimpleSelectableGridView = selectable::GridView<Entry>;
/// The Simple version of scrollable and selectable Grid View, where each entry is just a label with
/// background.
pub type SimpleScrollableSelectableGridView = scrollable::SelectableGridView<Entry>;

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -200,6 +200,10 @@ impl<T: Scalar> HasComponents for Var<Vector3<T>> {
type Component = Var<T>;
}
impl<T: Scalar> HasComponents for Var<Vector4<T>> {
type Component = Var<T>;
}
impl<T: Scalar> Dim1 for Var<Vector2<T>> {
fn x(&self) -> Var<T> {
match self {
@ -245,6 +249,60 @@ impl<T: Scalar> Dim3 for Var<Vector3<T>> {
}
}
impl<T: Scalar> Dim1 for Var<Vector4<T>> {
fn x(&self) -> Var<T> {
match self {
Self::Static(t) => Var::Static(t.x.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.x", t).into()),
}
}
}
impl<T: Scalar> Dim2 for Var<Vector4<T>> {
fn y(&self) -> Var<T> {
match self {
Self::Static(t) => Var::Static(t.y.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.y", t).into()),
}
}
}
impl<T: Scalar> Dim3 for Var<Vector4<T>> {
fn z(&self) -> Var<T> {
match self {
Self::Static(t) => Var::Static(t.z.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.z", t).into()),
}
}
}
impl<T: Scalar> Dim4 for Var<Vector4<T>> {
fn w(&self) -> Var<T> {
match self {
Self::Static(t) => Var::Static(t.z.clone()),
Self::Dynamic(t) => Var::Dynamic(format!("{}.z", t).into()),
}
}
}
impl<T: Scalar> HasDim2Version for Var<Vector4<T>> {
type Dim2Version = Var<Vector2<T>>;
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<Vector2<f32>> {
type Output = Var<Vector2<Pixels>>;

View File

@ -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

View File

@ -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"

View File

@ -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);
}

View File

@ -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;
}
// ===========