Multi Component Group Wrapper (#3473)

[ci no changelog needed]

This PR implements a new helper for the future Component Browser - `component_group::multi::Wrapper`. It propagates FRP events from multiple component groups and ensures that only a single component group is focused at all times.

See the updated component group demo scene (console logs shows propagated FRP events from all component groups):


https://user-images.githubusercontent.com/6566674/172359141-8ea6f1ba-e357-4c1b-852a-adb4d5207e03.mp4


- Fixed a `define_endpoints_2!` macro. FRP endpoints for `focus` events weren't connected properly.
- List View now uses an overlay shape to catch mouse events, it allows much easier implementation of `is_header_selected` in the component group.
This commit is contained in:
Ilya Bogdanov 2022-06-08 14:06:36 +03:00 committed by GitHub
parent 2af970fe52
commit c602404b1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 480 additions and 58 deletions

View File

@ -3,8 +3,6 @@
//! Responsible for owning any remote connection clients, and providing controllers for specific
//! files and modules. Expected to live as long as the project remains open in the IDE.
pub mod synchronized;
use crate::prelude::*;
use crate::model::module::ProjectMetadata;
@ -20,6 +18,13 @@ use parser::Parser;
use uuid::Uuid;
// ==============
// === Export ===
// ==============
pub mod synchronized;
// =============
// === Model ===

View File

@ -119,6 +119,7 @@ struct CurrentIcon {
/// A visual representation of a [`Model`].
#[derive(Clone, CloneRef, Debug)]
pub struct View {
network: frp::Network,
logger: Logger,
display_object: display::object::Instance,
icon: Rc<RefCell<CurrentIcon>>,
@ -133,13 +134,13 @@ impl list_view::Entry for View {
type Params = Params;
fn new(app: &Application, style_prefix: &style::Path, Params { colors }: &Params) -> Self {
let logger = Logger::new("component-group::Entry");
let logger = Logger::new("component_group::Entry");
let display_object = display::object::Instance::new(&logger);
let icon: Rc<RefCell<CurrentIcon>> = default();
let label = GlyphHighlightedLabel::new(app, style_prefix, &());
display_object.add_child(&label);
let network = &label.inner.network;
let network = frp::Network::new("component_group::Entry");
let style = &label.inner.style_watch;
let icon_text_gap = style.get_number(theme::icon_text_gap);
frp::extend! { network
@ -168,6 +169,7 @@ impl list_view::Entry for View {
init.emit(());
Self {
logger,
network,
display_object,
icon,
max_width_px,

View File

@ -43,6 +43,7 @@ use ensogl::data::color;
use ensogl::data::text;
use ensogl::display;
use ensogl::display::camera::Camera2d;
use ensogl::Animation;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::application::component_browser::component_group as theme;
use ensogl_list_view as list_view;
@ -55,6 +56,7 @@ use ensogl_shadow as shadow;
pub mod entry;
pub mod icon;
pub mod set;
pub mod wide;
pub use entry::View as Entry;
@ -115,6 +117,7 @@ pub mod header_background {
ensogl::define_shape_system! {
above = [background, list_view::background];
pointer_events = false;
(style:Style, color:Vector4, height: f32, shadow_height_multiplier: f32) {
let color = Var::<color::Rgba>::from(color);
let width: Var<Pixels> = "input_size.x".into();
@ -234,19 +237,20 @@ impl Colors {
let dimmed_intensity = style.get_number(theme::dimmed_color_intensity);
let icon_weak_intensity = style.get_number(theme::entry_list::icon::weak_color_intensity);
let entry_text_ = style.get_color(theme::entry_list::text::color);
let intensity = Animation::new(network);
frp::extend! { network
init <- source_();
one <- init.constant(1.0);
let is_dimmed = is_dimmed.clone_ref();
intensity <- is_dimmed.switch(&one, &dimmed_intensity);
intensity.target <+ is_dimmed.switch(&one, &dimmed_intensity);
app_bg <- all(&app_bg, &init)._0();
app_bg_and_input <- all(&app_bg, color);
main <- app_bg_and_input.all_with(&intensity, mix);
main <- app_bg_and_input.all_with(&intensity.value, mix);
app_bg_and_main <- all(&app_bg, &main);
header_text <- app_bg_and_main.all_with(&header_intensity, mix).sampler();
bg <- app_bg_and_main.all_with(&bg_intensity, mix).sampler();
app_bg_and_entry_text <- all(&app_bg, &entry_text_);
entry_text <- app_bg_and_entry_text.all_with(&intensity, mix).sampler();
entry_text <- app_bg_and_entry_text.all_with(&intensity.value, mix).sampler();
icon_weak <- app_bg_and_main.all_with(&icon_weak_intensity, mix).sampler();
icon_strong <- main.sampler();
}
@ -280,6 +284,7 @@ ensogl::define_endpoints_2! {
set_header_pos(f32),
}
Output {
is_mouse_over(bool),
selected_entry(Option<entry::Id>),
suggestion_accepted(entry::Id),
expression_accepted(entry::Id),
@ -366,26 +371,25 @@ impl component::Frp<Model> for Frp {
}
// === Entries ===
frp::extend! { network
model.entries.set_entries <+ input.set_entries;
out.selected_entry <+ model.entries.selected_entry;
}
// === Selection ===
let overlay_events = &model.header_overlay.events;
frp::extend! { network
model.entries.focus <+ input.focus;
model.entries.defocus <+ input.defocus;
model.entries.set_focus <+ input.set_focus;
let moved_out_above = model.entries.tried_to_move_out_above.clone_ref();
is_mouse_over <- bool(&overlay_events.mouse_out, &overlay_events.mouse_over);
is_mouse_over_header <- bool(&overlay_events.mouse_out, &overlay_events.mouse_over);
mouse_moved <- mouse_position.on_change().constant(());
mouse_moved_over_header <- mouse_moved.gate(&is_mouse_over);
is_entry_selected <- model.entries.selected_entry.on_change().map(|e| e.is_some());
some_entry_selected <- is_entry_selected.on_true();
mouse_moved_over_header <- mouse_moved.gate(&is_mouse_over_header);
mouse_moved_beyond_header <- mouse_moved.gate_not(&is_mouse_over_header);
select_header <- any(moved_out_above, mouse_moved_over_header, out.header_accepted);
deselect_header <- model.entries.selected_entry.filter_map(|entry| *entry);
out.is_header_selected <+ bool(&deselect_header, &select_header);
deselect_header <- any(&some_entry_selected, &mouse_moved_beyond_header);
out.is_header_selected <+ bool(&deselect_header, &select_header).on_change();
model.entries.select_entry <+ select_header.constant(None);
out.selection_position_target <+ all_with3(
@ -396,6 +400,22 @@ impl component::Frp<Model> for Frp {
);
}
// === Mouse hovering ===
frp::extend! { network
out.is_mouse_over <+ model.entries.is_mouse_over.or(&is_mouse_over_header);
}
// === Entries ===
frp::extend! { network
model.entries.set_entries <+ input.set_entries;
out.selected_entry <+ model.entries.selected_entry;
out.selected_entry <+ out.is_header_selected.on_true().constant(None);
}
init.emit(());
}
@ -630,16 +650,21 @@ mod tests {
use ensogl_list_view::entry::AnyModelProvider;
macro_rules! expect_entry_selected {
($cgv:ident, $id:expr$(, $argv:tt)?) => {
assert_eq!($cgv.selected_entry.value(), Some($id)$(, $argv)?);
assert!(!$cgv.is_header_selected.value()$(, $argv)?);
($cgv:ident, $id:expr) => {
assert_eq!(
$cgv.selected_entry.value(),
Some($id),
"Selected entry is not Some({}).",
$id
);
assert!(!$cgv.is_header_selected.value(), "Header is selected.");
};
}
macro_rules! expect_header_selected {
($cgv:ident$(, $argv:tt)?) => {
assert_eq!($cgv.selected_entry.value(), None$(, $argv)?);
assert!($cgv.is_header_selected.value()$(, $argv)?);
($cgv:ident) => {
assert_eq!($cgv.selected_entry.value(), None, "Selected entry is not None.");
assert!($cgv.is_header_selected.value(), "Header is not selected.");
};
}

View File

@ -0,0 +1,247 @@
//! Provides a multi component group wrapper, an abstraction used to propagate FRP events from
//! multiple component groups.
//!
//! The wrapper can work with all types of component groups using [`Group`] enum. Currently we
//! support one-column component groups and wide (multi-column) component groups.
//!
//! See [`Wrapper`] docs.
use ensogl::prelude::*;
use crate::entry;
use crate::wide;
use crate::View;
use enso_frp as frp;
use ensogl::data::OptVec;
// =============
// === Group ===
// =============
/// One of the possible component group types.
#[derive(Debug, Clone, CloneRef, From)]
#[allow(missing_docs)]
pub enum Group {
/// A one-column component group with a header. See [`View`] docs.
OneColumn(View),
/// A multi-column component group without a header. See [`wide::View`] docs.
Wide(wide::View),
}
impl Group {
fn focus(&self) {
match self {
Group::OneColumn(group) => group.focus(),
Group::Wide(group) => group.focus(),
}
}
fn defocus(&self) {
match self {
Group::OneColumn(group) => group.defocus(),
Group::Wide(group) => group.defocus(),
}
}
fn is_mouse_over(&self) -> &frp::Sampler<bool> {
match self {
Group::OneColumn(group) => &group.is_mouse_over,
Group::Wide(group) => &group.is_mouse_over,
}
}
}
/// ========================
/// === PropagatedEvents ===
/// ========================
/// Transform
/// ```ignore
/// propagate_frp!{ network, group, self,
/// (suggestion_accepted, move |e| (id, *e)),
/// (expression_accepted, move |e| (id, *e)),
/// }
/// ```
/// into
/// ```ignore
/// frp::extend! { network
/// self.suggestion_accepted <+ group.suggestion_accepted.gate(&group.focused).map(move |e| (id, *e));
/// self.expression_accepted <+ group.expression_accepted.gate(&group.focused).map(move |e| (id, *e));
/// }
/// ```
macro_rules! propagate_frp {
($network:ident, $group:ident, $events:ident, $(($endpoint:ident, $expr:expr)),*) => {
frp::extend! { $network
$($events.$endpoint <+ $group.$endpoint.gate(&$group.focused).map($expr);)*
}
}
}
/// Generate a struct with a set of FRP endpoints and provide a helper `attach` method that allows
/// to connect two such structs. See [`PropagatedEvents`] docs.
macro_rules! propagated_events {
(
$(#$meta:tt)* struct $ident:ident {
$($endpoint:ident : $endpoint_type:ty),* $(,)?
}
) => {
$(#$meta)*
pub struct $ident {
network: frp::Network,
$(pub $endpoint : frp::Any<$endpoint_type>),*
}
impl $ident {
/// Constructor.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let network = frp::Network::new(stringify!($ident));
frp::extend! { network
$($endpoint <- any_mut();)*
}
Self {
network,
$($endpoint),*
}
}
/// Attach all endpoints of `other` to corresponding endpoints of `self`.
pub fn attach(&self, other: &$ident) {
$(self.$endpoint.attach(&other.$endpoint);)*
}
}
}
}
propagated_events! {
/// A set of FRP endpoints that we want to propagate from component groups. Each field
/// corresponds to a certain FRP output of the component group. It's output type is modified by
/// adding a [`GroupId`] so that we can understand which group the event came from.
#[derive(Debug, Clone, CloneRef)]
#[allow(missing_docs)]
struct PropagatedEvents {
mouse_in_group: GroupId,
selected_entry: Option<(GroupId, entry::Id)>,
suggestion_accepted: (GroupId, entry::Id),
expression_accepted: (GroupId, entry::Id),
is_header_selected: (GroupId, bool),
header_accepted: GroupId,
selection_position_target: (GroupId, Vector2<f32>),
focused: (GroupId, bool),
}
}
// ===============
// === Wrapper ===
// ===============
newtype_prim! {
/// An index of the component group.
GroupId(usize);
}
/// The storage for the groups inside Wrapper. We store both a [`Group`] itself to manage its focus
/// and [`PropagatedEvents`] to propagate its FRP outputs.
type Groups = Rc<RefCell<OptVec<(Group, PropagatedEvents), GroupId>>>;
/// A wrapper around the FRP outputs of the several component groups.
///
/// This wrapper does two things:
/// 1. Propagates FRP events from multiple component groups adding the information which group
/// emitted the event. ([`GroupId`]) Only focused group events are propagated, events from
/// non-focused component groups are silently ignored.
/// 2. Ensures that only a single component group remains focused. This is done by emitting
/// `defocus` events for every other component group. The group that is hovered by mouse
/// (`mouse_in_group` endpoint) becomes focused.
///
/// A wrapper stores a list of "managed" component groups internally. Only groups in this list are
/// propagating their events and change their focus automatically. Use [`Wrapper::add`] and
/// [`Wrapper::remove`] methods to add/remove groups to/from this list.
#[allow(missing_docs)]
#[derive(Debug, Clone, CloneRef, Deref)]
pub struct Wrapper {
groups: Groups,
#[deref]
events: PropagatedEvents,
}
impl Default for Wrapper {
fn default() -> Self {
Self::new()
}
}
impl Wrapper {
/// Constructor.
pub fn new() -> Self {
let groups: Groups = default();
let events = PropagatedEvents::new();
let network = &events.network;
frp::extend! { network
eval events.mouse_in_group((group_id) {
groups.borrow().iter_enumerate().for_each(|(idx, (g, _))| {
if idx != *group_id { g.defocus(); }
});
if let Some((g, _)) = groups.borrow().safe_index(*group_id) { g.focus(); }
});
}
Self { groups, events }
}
/// Start managing a new group. Returned [`GroupId`] is non-unique, it might be reused by new
/// groups if this one removed by calling [`Self::remove`].
pub fn add(&self, group: Group) -> GroupId {
let events = PropagatedEvents::new();
self.events.attach(&events);
let id = self.groups.borrow_mut().insert((group.clone_ref(), events.clone_ref()));
self.setup_frp_propagation(&group, id, events);
id
}
/// Stop managing of a group. A freed [`GroupId`] might be reused by new groups later.
pub fn remove(&self, group_id: GroupId) {
self.groups.borrow_mut().remove(group_id);
}
fn setup_frp_propagation(&self, group: &Group, id: GroupId, events: PropagatedEvents) {
let network = &self.events.network;
frp::extend! { network
let mouse_in = group.is_mouse_over().clone_ref();
events.mouse_in_group <+ mouse_in.on_change().on_true().map(move |_| id);
}
match group {
Group::OneColumn(group) => {
frp::extend! { network
events.focused <+ group.focused.map(move |f| (id, *f));
}
propagate_frp! { network, group, events,
(selected_entry, move |e| e.map(|e| (id, e))),
(suggestion_accepted, move |e| (id, *e)),
(expression_accepted, move |e| (id, *e)),
(selection_position_target, move |p| (id, *p)),
(is_header_selected, move |h| (id, *h)),
(header_accepted, move |_| id)
}
}
Group::Wide(group) => {
frp::extend! { network
events.focused <+ group.focused.map(move |f| (id, *f));
}
propagate_frp! { network, group, events,
(selected_entry, move |e| e.map(|e| (id, e))),
(suggestion_accepted, move |e| (id, *e)),
(expression_accepted, move |e| (id, *e)),
(selection_position_target, move |p| (id, *p))
}
}
}
}
}

View File

@ -130,6 +130,7 @@ ensogl::define_endpoints_2! {
set_no_items_label_text(String),
}
Output {
is_mouse_over(bool),
selected_entry(Option<entry::Id>),
suggestion_accepted(entry::Id),
expression_accepted(entry::Id),
@ -158,6 +159,7 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
init <- source_();
entry_count <- input.set_entries.map(|p| p.entry_count());
out.entry_count <+ entry_count;
is_mouse_over <- init.constant(false);
selected_column_and_entry <- any(...);
update_selected_entry <- selected_column_and_entry.sample(&out.size);
@ -193,9 +195,22 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
}
init.emit(());
let mut is_mouse_over = is_mouse_over.clone_ref();
for column in model.columns.iter() {
let col_id = column.id.clone_ref();
frp::extend! { network
// === Focus propagation ===
eval_ input.defocus(model.defocus_columns());
// === Mouse hovering ===
// We connect `is_mouse_over` events from all columns into a single event stream
// using `or` combinator.
new_is_mouse_over <- is_mouse_over.or(&column.is_mouse_over);
is_mouse_over = new_is_mouse_over;
// === Accepting suggestions ===
accepted_entry <- column.selected_entry.sample(&input.accept_suggestion).filter_map(|e| *e);
@ -208,26 +223,37 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
));
// === Selected entry ===
is_column_selected <- column.selected_entry.map(|e| e.is_some());
selected_entry <- column.selected_entry.gate(&is_column_selected);
out.selected_entry <+ selected_entry.map2(&entry_count, move |entry, total| {
entry.map(|e| local_idx_to_global::<COLUMNS>(col_id, e, *total))
});
// === Selection position ===
entry_selected <- column.selected_entry.filter_map(|e| *e);
selected_column_and_entry <+ entry_selected.map(f!([column](e) (col_id, column.reverse_index(*e))));
on_column_selected <- column.selected_entry.map(|e| e.is_some()).on_true();
eval_ on_column_selected(model.on_column_selected(col_id));
eval_ on_column_selected(column.focus());
selection_pos <- column.selection_position_target.sample(&on_column_selected);
out.selection_position_target <+ selection_pos.map(f!((pos) column.selection_position(*pos)));
// === set_entries ===
out.selected_entry <+ column.selected_entry.map2(&entry_count, move |entry, total| {
entry.map(|e| local_idx_to_global::<COLUMNS>(col_id, e, *total))
});
entries <- input.set_entries.map(move |p| ModelProvider::<COLUMNS>::wrap(p, col_id));
background_height <+ entries.map(f_!(model.background_height()));
eval entries((e) column.set_entries(e));
_eval <- all_with(&entries, &out.size, f!((_, size) column.resize_and_place(*size)));
}
frp::extend! { network
out.is_mouse_over <+ is_mouse_over;
}
let params = entry::Params { colors: colors.clone_ref() };
column.list_view.set_entry_params_and_recreate_entries(params);
}
@ -379,12 +405,20 @@ impl<const COLUMNS: usize> Model<COLUMNS> {
columns_to_the_right.find(|col| col.len() > 0)
}
/// Send `defocus()` event to each column.
fn defocus_columns(&self) {
for column in self.columns.iter() {
column.defocus();
}
}
/// Deselect entries in all columns except the one with provided `column_index`. We ensure that
/// at all times only a single entry across all columns is selected.
fn on_column_selected(&self, column_id: ColumnId) {
let other_columns = self.columns.iter().enumerate().filter(|(i, _)| *i != *column_id);
for (_, column) in other_columns {
column.deselect_entries();
column.defocus();
}
}

View File

@ -35,6 +35,14 @@ use list_view::entry::AnyModelProvider;
// =================
// === Constants ===
// =================
const COMPONENT_GROUP_COLOR: color::Rgba = color::Rgba::new(0.527, 0.554, 0.18, 1.0);
// ===================
// === Entry Point ===
// ===================
@ -184,6 +192,12 @@ fn create_component_group(
component_group
}
fn create_wide_component_group(app: &Application) -> component_group::wide::View {
let component_group = app.new_view::<component_group::wide::View>();
component_group.set_width(450.0);
component_group.set_position_x(-200.0);
component_group
}
fn color_component_slider(app: &Application, caption: &str) -> selector::NumberPicker {
let slider = app.new_view::<selector::NumberPicker>();
@ -220,7 +234,7 @@ fn init(app: &Application) {
let network = frp::Network::new("Component Group Debug Scene");
let scroll_area = ScrollArea::new(app);
scroll_area.set_position_xy(Vector2(0.0, 100.0));
scroll_area.set_position_xy(Vector2(150.0, 100.0));
scroll_area.resize(Vector2(170.0, 400.0));
scroll_area.set_content_width(150.0);
scroll_area.set_content_height(2000.0);
@ -234,10 +248,11 @@ fn init(app: &Application) {
let first_component_group = create_component_group(app, group_name, &layers);
let group_name = "Second component group";
let second_component_group = create_component_group(app, group_name, &layers);
second_component_group.set_dimmed(true);
let wide_component_group = create_wide_component_group(app);
scroll_area.content().add_child(&first_component_group);
scroll_area.content().add_child(&second_component_group);
app.display.add_child(&wide_component_group);
// FIXME(#182193824): This is a workaround for a bug. See the docs of the
// [`transparent_circle`].
@ -251,12 +266,6 @@ fn init(app: &Application) {
// === Regular Component Group ===
frp::extend! { network
eval first_component_group.suggestion_accepted ([](id) DEBUG!("Accepted Suggestion {id}"));
eval first_component_group.expression_accepted ([](id) DEBUG!("Accepted Expression {id}"));
eval_ first_component_group.header_accepted ([] DEBUG!("Accepted Header"));
}
ComponentGroupController::init(
&[first_component_group.clone_ref(), second_component_group.clone_ref()],
&network,
@ -266,7 +275,8 @@ fn init(app: &Application) {
let mock_entries = MockEntries::new(15);
let model_provider = AnyModelProvider::from(mock_entries.clone_ref());
first_component_group.set_entries(model_provider.clone_ref());
second_component_group.set_entries(model_provider);
second_component_group.set_entries(model_provider.clone_ref());
wide_component_group.set_entries(model_provider.clone_ref());
// === Color sliders ===
@ -285,7 +295,7 @@ fn init(app: &Application) {
blue_slider.set_track_color(color::Rgba::new(0.6, 0.6, 1.0, 1.0));
let blue_slider_frp = &blue_slider.frp;
let default_color = color::Rgba(0.527, 0.554, 0.18, 1.0);
let default_color = COMPONENT_GROUP_COLOR;
frp::extend! { network
init <- source_();
red_slider_frp.set_value <+ init.constant(default_color.red);
@ -298,10 +308,41 @@ fn init(app: &Application) {
|r,g,b| color::Rgba(*r, *g, *b, 1.0));
first_component_group.set_color <+ color;
second_component_group.set_color <+ color;
wide_component_group.set_color <+ color;
}
init.emit(());
// === Components groups set ===
let groups: Rc<Vec<component_group::set::Group>> = Rc::new(vec![
first_component_group.clone_ref().into(),
second_component_group.clone_ref().into(),
wide_component_group.clone_ref().into(),
]);
let multiview = component_group::set::Wrapper::new();
for group in groups.iter() {
multiview.add(group.clone_ref());
}
frp::extend! { network
selected_entry <- multiview.selected_entry.on_change();
eval selected_entry([](e) if let Some(e) = e { DEBUG!("Entry {e.1} from group {e.0} selected") });
eval multiview.suggestion_accepted([]((g, s)) DEBUG!("Suggestion {s} accepted in group {g}"));
eval multiview.expression_accepted([]((g, s)) DEBUG!("Expression {s} accepted in group {g}"));
header_selected <- multiview.is_header_selected.filter_map(|(g, h)| if *h { Some(*g) } else { None });
eval header_selected([](g) DEBUG!("Header selected in group {g}"));
eval multiview.header_accepted([](g) DEBUG!("Header accepted in group {g}"));
eval multiview.focused([groups]((g, f)) {
match &groups[usize::from(g)] {
component_group::set::Group::OneColumn(group) => group.set_dimmed(!f),
component_group::set::Group::Wide(group) => group.set_dimmed(!f),
}
});
}
// === Forget ===
std::mem::forget(red_slider);
@ -309,7 +350,9 @@ fn init(app: &Application) {
std::mem::forget(blue_slider);
std::mem::forget(scroll_area);
std::mem::forget(network);
std::mem::forget(multiview);
std::mem::forget(first_component_group);
std::mem::forget(second_component_group);
std::mem::forget(wide_component_group);
std::mem::forget(layers);
}

View File

@ -4,6 +4,7 @@
// which is not just in testing builds.
use super::*;
use enso_frp::future::EventOutputExt;

View File

@ -122,6 +122,7 @@ impl Model {
let logger = Logger::new("SearcherView");
let display_object = display::object::Instance::new(&logger);
let list = app.new_view::<ListView<Entry>>();
list.focus();
let documentation = documentation::View::new(scene);
let doc_provider = default();
scene.layers.node_searcher.add_exclusive(&list);

View File

@ -187,6 +187,14 @@ impl<T, I: Index> OptVec<T, I> {
self.items.iter().filter_map(Option::as_ref)
}
/// Iterator with indexes.
pub fn iter_enumerate(&self) -> impl Iterator<Item = (I, &T)> {
self.items
.iter()
.enumerate()
.filter_map(|(idx, item)| item.as_ref().map(|i| (idx.into(), i)))
}
/// Mutable iterator.
pub fn iter_mut(&mut self) -> IterMut<T> {
self.items.iter_mut().filter_map(Option::as_mut)
@ -269,6 +277,28 @@ mod tests {
}
}
#[test]
fn test_iter_enumerate() {
let mut v = OptVec::<usize>::new();
let ix1 = v.insert(0);
let _ix2 = v.insert(1);
let _ix3 = v.insert(2);
assert_eq!(v.len(), 3);
for (i, (index, value)) in v.iter_enumerate().enumerate() {
assert_eq!(i, *value);
assert_eq!(i, index);
}
v.remove(ix1);
assert_eq!(v.len(), 2);
for (i, (index, value)) in v.iter_enumerate().enumerate() {
assert_eq!(i + 1, *value);
assert_eq!(i + 1, index);
}
}
#[test]
fn test_iter_mut() {
let mut v = OptVec::<usize>::new();

View File

@ -14,8 +14,9 @@
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
use derivative::Derivative;
use futures::prelude::*;
use derivative::Derivative;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;

View File

@ -72,6 +72,19 @@ pub const SHADOW_PX: f32 = 10.0;
pub const SHAPE_MARGIN: f32 = 5.0;
// === Helpers ===
/// Calculate background shape size by subtracting [`SHADOW_PX`] and `SHAPE_MARGIN`.
fn background_size(
sprite_width: Var<Pixels>,
sprite_height: Var<Pixels>,
) -> (Var<Pixels>, Var<Pixels>) {
let width = sprite_width - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
let height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
(width, height)
}
// === Selection ===
/// The selection rectangle shape.
@ -82,6 +95,7 @@ pub mod selection {
pub const CORNER_RADIUS_PX: f32 = 12.0;
ensogl_core::define_shape_system! {
pointer_events = false;
(style: Style, color: Vector4, corner_radius: f32) {
let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into();
@ -110,8 +124,7 @@ pub mod background {
(style: Style, shadow_alpha: f32, corners_radius_px: f32, color: Vector4) {
let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into();
let width = sprite_width - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
let height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
let (width, height) = background_size(sprite_width, sprite_height);
let color = Var::<color::Rgba>::from(color);
let rect = Rect((&width,&height)).corners_radius(corners_radius_px);
let shape = rect.fill(color);
@ -124,6 +137,25 @@ pub mod background {
}
// === Overlay ===
/// A list view overlay used to catch mouse events.
pub mod overlay {
use super::*;
ensogl_core::define_shape_system! {
above = [background];
below = [selection];
(style: Style, corners_radius_px: f32) {
let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into();
let (width, height) = background_size(sprite_width, sprite_height);
Rect((&width,&height)).corners_radius(corners_radius_px).fill(HOVER_COLOR).into()
}
}
}
// =============
// === Model ===
@ -159,6 +191,7 @@ struct Model<E: Entry> {
entries: entry::List<E>,
selection: selection::View,
background: background::View,
overlay: overlay::View,
scrolled_area: display::object::Instance,
display_object: display::object::Instance,
}
@ -171,12 +204,14 @@ impl<E: Entry> Model<E> {
let scrolled_area = display::object::Instance::new(&logger);
let entries = entry::List::new(&logger, &app);
let background = background::View::new(&logger);
let overlay = overlay::View::new(&logger);
let selection = selection::View::new(&logger);
display_object.add_child(&background);
display_object.add_child(&overlay);
display_object.add_child(&scrolled_area);
scrolled_area.add_child(&entries);
scrolled_area.add_child(&selection);
Model { app, entries, selection, background, scrolled_area, display_object }
Model { app, entries, selection, background, overlay, scrolled_area, display_object }
}
fn show_background_shadow(&self, value: bool) {
@ -200,6 +235,7 @@ impl<E: Entry> Model<E> {
let entry_width = view.size.x - 2.0 * entry_padding;
self.entries.set_position_x(-view.size.x / 2.0 + entry_padding);
self.background.size.set(view.size + padding + shadow + margin);
self.overlay.size.set(view.size + padding + shadow + margin);
self.scrolled_area.set_position_y(view.size.y / 2.0 - view.position_y);
self.entries.update_entries(visible_entries, entry_width, style_prefix);
}
@ -232,15 +268,6 @@ impl<E: Entry> Model<E> {
}
}
/// Check if the `point` is inside component assuming that it have given `size`.
fn is_inside(&self, point: Vector2<f32>, size: Vector2<f32>) -> bool {
let pos_obj_space =
self.app.display.default_scene.screen_to_object_space(&self.background, point);
let x_range = (-size.x / 2.0)..=(size.x / 2.0);
let y_range = (-size.y / 2.0)..=(size.y / 2.0);
x_range.contains(&pos_obj_space.x) && y_range.contains(&pos_obj_space.y)
}
fn jump_target(&self, current_entry: Option<entry::Id>, jump: isize) -> JumpTarget {
if jump < 0 {
match current_entry.and_then(|entry| entry.checked_sub(-jump as usize)) {
@ -314,6 +341,7 @@ ensogl_core::define_endpoints! {
}
Output {
is_mouse_over(bool),
selected_entry(Option<entry::Id>),
chosen_entry(Option<entry::Id>),
size(Vector2<f32>),
@ -466,9 +494,9 @@ where E::Model: Default
// === Mouse Position ===
mouse_in <- all_with(&mouse.position,&frp.size,f!((pos,size)
model.is_inside(*pos,*size)
));
let overlay_events = &model.overlay.events;
mouse_in <- bool(&overlay_events.mouse_out, &overlay_events.mouse_over);
frp.source.is_mouse_over <+ mouse_in;
mouse_moved <- mouse.distance.map(|dist| *dist > MOUSE_MOVE_THRESHOLD );
mouse_y_in_scroll <- mouse.position.map(f!([model,scene](pos) {
scene.screen_to_object_space(&model.scrolled_area,*pos).y
@ -480,6 +508,8 @@ where E::Model: Default
// === Selected Entry ===
eval frp.source.focused([frp](f) if !f { frp.deselect_entries.emit(()) } );
frp.source.selected_entry <+ frp.select_entry;
frp.source.selected_entry <+ frp.output.chosen_entry;
@ -688,7 +718,7 @@ impl<E: Entry> application::View for ListView<E> {
(Press, "enter", "chose_selected_entry"),
])
.iter()
.map(|(a, b, c)| Self::self_shortcut(*a, *b, *c))
.map(|(a, b, c)| Self::self_shortcut_when(*a, *b, *c, "focused"))
.collect()
}
}

View File

@ -1111,6 +1111,9 @@ macro_rules! define_endpoints_2_normalized_public {
$crate::frp::extend! { $output_opts network
$( $out_field <- private_output.$out_field.profile().sampler(); )*
focus_events <- bool(&public_input.defocus,&public_input.focus);
focused <- any(&public_input.set_focus,&focus_events);
private_output.focused <+ focused;
}
let mut status_map : HashMap<String,$crate::frp::Sampler<bool>> = default();
let mut command_map : HashMap<String,Command> = default();

View File

@ -16,6 +16,8 @@
//! does not report compilation errors when the context is not available.
//!
//! # `Compiler` and `Controller`
use crate::control::callback::traits::*;
///
/// In order to handle WebGL context loss, we divide the responsibilities of compiler
/// management between two objects: a `Compiler`, and a `Controller`.
@ -32,7 +34,6 @@ use crate::system::web::traits::*;
use crate::animation;
use crate::control::callback;
use crate::control::callback::traits::*;
use crate::display::ToGlEnum;
use crate::system::gpu::context::extension::KhrParallelShaderCompile;
use crate::system::gpu::context::native;

View File

@ -96,6 +96,7 @@ fn init(app: &Application) {
let provider = list_view::entry::AnyModelProvider::new(MockEntries::new(1000));
list_view.frp.resize(Vector2(100.0, 160.0));
list_view.frp.set_entries(provider);
list_view.focus();
app.display.add_child(&list_view);
// FIXME[WD]: This should not be needed after text gets proper depth-handling.
app.display.default_scene.layers.below_main.add_exclusive(&list_view);

View File

@ -23,13 +23,11 @@
#![warn(unused_import_braces)]
use enso_profiler_data as profiler_data;
use profiler_data::Class;
use profiler_data::MeasurementId;
use profiler_data::OpaqueMetadata;
use profiler_data::Profile;
use profiler_data::Timestamp;
use std::collections::HashMap;
use std::default::Default;
use std::path::Path;