mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 11:52:59 +03:00
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:
parent
d59714a29d
commit
7f8190e663
@ -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
1
Cargo.lock
generated
@ -2513,6 +2513,7 @@ dependencies = [
|
||||
"ensogl-grid-view",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-text-msdf-sys",
|
||||
"itertools 0.10.3",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
281
lib/rust/ensogl/component/grid-view/src/selectable.rs
Normal file
281
lib/rust/ensogl/component/grid-view/src/selectable.rs
Normal 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());
|
||||
}
|
||||
}
|
413
lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs
Normal file
413
lib/rust/ensogl/component/grid-view/src/selectable/highlight.rs
Normal 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(¬_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<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));
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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>;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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>>;
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
|
Loading…
Reference in New Issue
Block a user