mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 20:53:51 +03:00
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:
parent
2af970fe52
commit
c602404b1a
@ -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 ===
|
||||
|
@ -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,
|
||||
|
@ -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.");
|
||||
};
|
||||
}
|
||||
|
||||
|
247
app/gui/view/component-browser/component-group/src/set.rs
Normal file
247
app/gui/view/component-browser/component-group/src/set.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
// which is not just in testing builds.
|
||||
|
||||
use super::*;
|
||||
|
||||
use enso_frp::future::EventOutputExt;
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user