From e1d4369e6a7f393d1cc47d64161d8f060b7e3aad Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Wed, 15 Jun 2022 17:21:30 +0300 Subject: [PATCH] New selection box implementation for component groups (#3520) [Task link](https://www.pivotaltracker.com/story/show/182194574). [ci no changelog needed] This PR implements a new selection box that will replace an old (not really working) one in the component browser. The old selection box wasn't working well with the headers of the component groups, so we were forced to make a much harder implementation. The new implementation duplicates some visual components and places them in a separate layer. Then, a rectangular mask cuts off everything that is not "selected". This way: - We have more control over what the selected entries should look like. - We can easily support the multi-layer structure of the component groups with headers. - We avoid problems with nested masks that our renderer doesn't support at the moment. To be more precise, we duplicate the following: - Background of the component group becomes the "fill" of the selection. - Entries text and icons - we can alter them easily. - Header background and header text. By placing them in separate scene layers we ensure correct rendering order. https://user-images.githubusercontent.com/6566674/173657899-1067f538-4329-44f9-9dc2-78c8a4708b5a.mp4 # Important Notes - This PR implements the base of our future selection mechanism, selecting entries with a mouse and keyboard still has several issues that will be fixed in the future tasks. - The scrolling behavior will also be improved in future tasks. Right we only restrict the selection box position so that it never leaves the borders of the component group. - I added a new function to `add` shapes to new layers in a non-exclusive way (we had only `add_exclusive`) before. I have no idea how we didn't use this feature before even though we mention it a lot in the docs. - The demo scene restricts the position of the selection box for one-column component groups but does not for the wide component group. --- .../component-group/src/entry.rs | 175 +++- .../component-group/src/lib.rs | 247 +++++- .../component-group/src/set.rs | 19 +- .../component-group/src/wide.rs | 74 +- .../debug_scene/component-group/src/lib.rs | 792 ++++++++++-------- build-config.yaml | 2 +- .../ensogl/app/theme/hardcoded/src/lib.rs | 7 + .../component/list-view/src/entry/list.rs | 5 + .../ensogl/component/list-view/src/lib.rs | 15 +- .../core/src/animation/frp/animation.rs | 21 +- .../ensogl/core/src/display/scene/layer.rs | 5 + .../core/src/system/gpu/shader/compiler.rs | 20 +- 12 files changed, 879 insertions(+), 503 deletions(-) diff --git a/app/gui/view/component-browser/component-group/src/entry.rs b/app/gui/view/component-browser/component-group/src/entry.rs index ff1e8fa7544e..eb8621883630 100644 --- a/app/gui/view/component-browser/component-group/src/entry.rs +++ b/app/gui/view/component-browser/component-group/src/entry.rs @@ -6,12 +6,14 @@ use crate::prelude::*; use crate::icon; use crate::Colors; +use crate::SelectedColors; use enso_frp as frp; use ensogl::application::Application; use ensogl::data::color; use ensogl::display; -use ensogl::display::scene::Layer; +use ensogl::display::scene::layer::Layer; +use ensogl::display::scene::layer::WeakLayer; use ensogl::display::style; use ensogl::display::Scene; use ensogl_hardcoded_theme::application::component_browser::component_group::entry_list as theme; @@ -77,22 +79,32 @@ impl From<&str> for Model { #[allow(missing_docs)] #[derive(Clone, CloneRef, Debug)] pub struct Params { - pub colors: Colors, + pub colors: Colors, + pub selection_layer: Rc>, } impl Default for Params { fn default() -> Self { let network = frp::Network::new("component_browser::entry::Params::default"); frp::extend! { network - icon_strong <- source::().sampler(); - icon_weak <- source::().sampler(); - header_text <- source::().sampler(); - entry_text <- source::().sampler(); - background <- source::().sampler(); + default_color <- source::().sampler(); } - - let colors = Colors { icon_strong, icon_weak, header_text, entry_text, background }; - Self { colors } + let selected = SelectedColors { + background: default_color.clone_ref(), + header_text: default_color.clone_ref(), + entry_text: default_color.clone_ref(), + icon_strong: default_color.clone_ref(), + icon_weak: default_color.clone_ref(), + }; + let colors = Colors { + icon_strong: default_color.clone_ref(), + icon_weak: default_color.clone_ref(), + header_text: default_color.clone_ref(), + entry_text: default_color.clone_ref(), + background: default_color.clone_ref(), + selected, + }; + Self { colors, selection_layer: default() } } } @@ -105,7 +117,7 @@ impl Default for Params { // === CurrentIcon === -/// The structure keeping a currently displayed incon in Component Group Entry [`View`]. Remembering +/// The structure keeping a currently displayed icon in Component Group Entry [`View`]. Remembering /// id allows us to skip icon generation when not changed. #[derive(Debug, Default)] struct CurrentIcon { @@ -113,33 +125,92 @@ struct CurrentIcon { id: Option, } +impl CurrentIcon { + fn set_strong_color(&self, color: color::Rgba) { + if let Some(shape) = &self.shape { + shape.strong_color.set(color.into()); + } + } + + fn set_weak_color(&self, color: color::Rgba) { + if let Some(shape) = &self.shape { + shape.weak_color.set(color.into()); + } + } +} + + // === View === /// 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>, - max_width_px: frp::Source, - icon_strong_color: frp::Sampler, - icon_weak_color: frp::Sampler, - label: GlyphHighlightedLabel, + network: frp::Network, + logger: Logger, + display_object: display::object::Instance, + icon: Rc>, + selected_icon: Rc>, + max_width_px: frp::Source, + label: GlyphHighlightedLabel, + selected_label: GlyphHighlightedLabel, + selection_layer: Rc>, + colors: Colors, +} + +impl View { + /// Update an icon shape (create it if necessary), update its color, and add it to the + /// [`layer`] if supplied. + fn update_icon( + &self, + model: &Model, + icon: &RefCell, + layer: Option, + strong_color: color::Rgba, + weak_color: color::Rgba, + ) { + let mut icon = icon.borrow_mut(); + if !icon.id.contains(&model.icon) { + icon.id = Some(model.icon); + let shape = model.icon.create_shape(&self.logger, Vector2(icon::SIZE, icon::SIZE)); + shape.strong_color.set(strong_color.into()); + shape.weak_color.set(weak_color.into()); + shape.set_position_x(icon::SIZE / 2.0); + self.display_object.add_child(&shape); + if let Some(layer) = layer { + layer.add_exclusive(&shape); + } + icon.shape = Some(shape); + } + } } impl list_view::Entry for View { type Model = Model; type Params = Params; - fn new(app: &Application, style_prefix: &style::Path, Params { colors }: &Params) -> Self { + fn new( + app: &Application, + style_prefix: &style::Path, + Params { colors, selection_layer }: &Params, + ) -> Self { let logger = Logger::new("component_group::Entry"); let display_object = display::object::Instance::new(&logger); let icon: Rc> = default(); + let selected_icon: Rc> = default(); let label = GlyphHighlightedLabel::new(app, style_prefix, &()); + let selected_label = GlyphHighlightedLabel::new(app, style_prefix, &()); display_object.add_child(&label); + if let Some(selection_layer) = &**selection_layer { + if let Some(layer) = selection_layer.upgrade() { + selected_label.set_label_layer(&layer); + display_object.add_child(&selected_label); + } else { + error!(logger, "Selection layer is dropped."); + } + } + 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); @@ -150,46 +221,60 @@ impl list_view::Entry for View { label_x_position <- icon_text_gap.map(|gap| icon::SIZE + gap); label_max_width <- all_with(&max_width_px, &icon_text_gap, |width,gap| width - icon::SIZE- gap); - eval label_x_position ((x) label.set_position_x(*x)); - eval label_max_width ((width) label.set_max_width(*width)); + eval label_x_position ([label, selected_label](x) { + label.set_position_x(*x); + selected_label.set_position_x(*x); + }); + eval label_max_width ([label, selected_label](width) { + label.set_max_width(*width); + selected_label.set_max_width(*width); + }); label.inner.label.set_default_color <+ all(&colors.entry_text, &init)._0(); - eval colors.icon_strong ([icon](color) - if let Some(shape) = &icon.borrow().shape { - shape.strong_color.set(color.into()); - } - ); - eval colors.icon_weak ([icon](color) - if let Some(shape) = &icon.borrow().shape { - shape.weak_color.set(color.into()); - } - ); - icon_strong_color <- colors.icon_strong.sampler(); - icon_weak_color <- colors.icon_weak.sampler(); + selected_label.inner.label.set_default_color <+ all(&colors.selected.entry_text,&init)._0(); + eval colors.icon_strong ((&c) icon.borrow().set_strong_color(c)); + eval colors.selected.icon_strong((&c) selected_icon.borrow().set_strong_color(c)); + eval colors.icon_weak ((&c) icon.borrow().set_weak_color(c)); + eval colors.selected.icon_weak((&c) selected_icon.borrow().set_weak_color(c)); } init.emit(()); + let selection_layer = selection_layer.clone_ref(); + let colors = colors.clone_ref(); Self { logger, network, display_object, icon, + selected_icon, max_width_px, - icon_strong_color, - icon_weak_color, + colors, label, + selected_label, + selection_layer, } } fn update(&self, model: &Self::Model) { self.label.update(&model.highlighted_text); - let mut icon = self.icon.borrow_mut(); - if !icon.id.contains(&model.icon) { - icon.id = Some(model.icon); - let shape = model.icon.create_shape(&self.logger, Vector2(icon::SIZE, icon::SIZE)); - shape.strong_color.set(self.icon_strong_color.value().into()); - shape.weak_color.set(self.icon_weak_color.value().into()); - shape.set_position_x(icon::SIZE / 2.0); - self.display_object.add_child(&shape); - icon.shape = Some(shape); + self.selected_label.update(&model.highlighted_text); + self.update_icon( + model, + &self.icon, + None, + self.colors.icon_strong.value(), + self.colors.icon_weak.value(), + ); + if let Some(weak_layer) = &*self.selection_layer { + if let Some(layer) = weak_layer.upgrade() { + self.update_icon( + model, + &self.selected_icon, + Some(layer), + self.colors.selected.icon_strong.value(), + self.colors.selected.icon_weak.value(), + ); + } else { + error!(self.logger, "Cannot add icon shape to a dropped scene layer."); + } } } diff --git a/app/gui/view/component-browser/component-group/src/lib.rs b/app/gui/view/component-browser/component-group/src/lib.rs index a9ac6081f005..8a3aa959be0a 100644 --- a/app/gui/view/component-browser/component-group/src/lib.rs +++ b/app/gui/view/component-browser/component-group/src/lib.rs @@ -10,11 +10,35 @@ //! //! To simulate scrolling of the component group entries we move the header of the component group //! down while moving the whole component group up (see [`Frp::set_header_pos`]). When the header -//! is moved down the shadow appears below it. The shadow changes its intensity smoothly before +//! is pushed down the shadow appears below it. The shadow changes its intensity smoothly before //! the header reaches the [`HEADER_SHADOW_PEAK`] distance from the top of the component group. //! After that the shadow is unchanged. When the header approaches the bottom of the component group //! we gradually reduce the size of the shadow so that it will never be rendered outside the -//! component group boundaries. See `Header Backgound` section in the [`Model::resize`] method. +//! component group boundaries. See `Header Background` section in the [`Model::resize`] method. +//! +//! # Selection +//! +//! The selection box used to highlight "selected" entries is implemented in a pretty tricky way. +//! We want to render the selection box above the background of the component group, but below +//! any text - so that text color isn't blending with the selection box's color. However, a +//! component group uses four different [scene layers][Layer] to render its header correctly, and we +//! want the selection to be above the header background and below the text entries at the same +//! time, which is not possible. +//! +//! So instead we duplicate component group entries in a special `selection` scene layer (or +//! rather, in a multiple scene layers to ensure render ordering) and use [layers masking][mask] +//! to cut off everything except the selection shape. We duplicate the following: +//! - Entry text and icon (see [entry][] module). +//! - A background of the component group [background][]. +//! - (for component groups with header) A background of the header [selection_header_background][]. +//! - (for component groups with header) Header text. +//! +//! This implementation allows tweaking the appearance of the selected text and icons easily. +//! When the selection box moves, the transition between "normal" and "selected" appearances also +//! looks natural without any additional tricks. So you can see a "half-selected" entry if the +//! selection box is only covering part of it. +//! +//! [mask]: ensogl::display::scene::layer::Layer#masking-layers-with-arbitrary-shapes #![recursion_limit = "512"] // === Features === @@ -35,15 +59,13 @@ use crate::prelude::*; use ensogl::application::traits::*; -use crate::display::scene::layer; - use enso_frp as frp; use ensogl::application::shortcut::Shortcut; use ensogl::application::Application; use ensogl::data::color; use ensogl::data::text; use ensogl::display; -use ensogl::display::camera::Camera2d; +use ensogl::display::scene::layer::Layer; use ensogl::Animation; use ensogl_gui_component::component; use ensogl_hardcoded_theme::application::component_browser::component_group as theme; @@ -92,6 +114,27 @@ const HEADER_SHADOW_PEAK: f32 = list_view::entry::HEIGHT / 2.0; // === Shapes Definitions === // ========================== +// === Selection === + +/// A shape of a selection box. It is used as a mask to show only specific parts of the selection +/// layers. See module-level documentation to learn more. +pub mod selection_box { + use super::*; + + ensogl::define_shape_system! { + pointer_events = false; + (style:Style) { + let width: Var = "input_size.x".into(); + let height: Var = "input_size.y".into(); + let corners_radius = style.get_number(theme::selection::corners_radius); + let padding_y = style.get_number(theme::selection::vertical_padding); + let padding_x = style.get_number(theme::selection::horizontal_padding); + let shape = Rect((width - padding_x.px(), height - padding_y.px())); + shape.corners_radius(corners_radius.px()).into() + } + } +} + // === Background === @@ -101,6 +144,7 @@ pub mod background { ensogl::define_shape_system! { below = [list_view::background]; + pointer_events = false; (style:Style, color:Vector4) { let color = Var::::from(color); Plane().fill(color).into() @@ -140,6 +184,21 @@ pub mod header_background { } } +/// A background of the "selected" header. See module-level documentation. +pub mod selection_header_background { + use super::*; + + ensogl::define_shape_system! { + pointer_events = false; + (color:Vector4, height: f32) { + let color = Var::::from(color); + let width: Var = "input_size.x".into(); + let height: Var = height.into(); + Rect((width, height)).fill(color).into() + } + } +} + // === Header Overlay === @@ -206,6 +265,9 @@ impl HeaderGeometry { /// [`ide_component_group::wide::View`] can be created from single "main color" input. Each of /// these colors will be computed by mixing "main color" with application background - for details, /// see [`Colors::from_main_color`]. +/// +/// `icon_strong` and `icon_weak` parameters represent the more/less contrasting parts of the +/// [icon](crate::icon::Any), they do not represent highlighted state of the icon. #[allow(missing_docs)] #[derive(Clone, CloneRef, Debug)] pub struct Colors { @@ -218,10 +280,22 @@ pub struct Colors { pub header_text: frp::Sampler, pub entry_text: frp::Sampler, pub background: frp::Sampler, + pub selected: SelectedColors, +} + +/// Helper struct with colors of the selected entries. Part of [`Colors`]. +#[allow(missing_docs)] +#[derive(Clone, CloneRef, Debug)] +pub struct SelectedColors { + pub background: frp::Sampler, + pub entry_text: frp::Sampler, + pub header_text: frp::Sampler, + pub icon_strong: frp::Sampler, + pub icon_weak: frp::Sampler, } impl Colors { - /// Constructs [`Colors`] structure, where each variant is based on the "main" `color` + /// Constructs [`Colors`] structure, where each variant is based on the "main" [`color`] /// parameter. pub fn from_main_color( network: &frp::Network, @@ -235,9 +309,11 @@ impl Colors { let app_bg = style.get_color(ensogl_hardcoded_theme::application::background); let header_intensity = style.get_number(theme::header::text::color_intensity); let bg_intensity = style.get_number(theme::background_color_intensity); + let selection_intensity = style.get_number(theme::selection_color_intensity); 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 selected = style.get_color(theme::entry_list::selected_color); let intensity = Animation::new(network); frp::extend! { network init <- source_(); @@ -254,9 +330,19 @@ impl Colors { 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(); + selected_bg <- app_bg_and_main.all_with(&selection_intensity, mix).sampler(); + main_and_selected <- all(&main, &selected); + selected_icon_weak <- main_and_selected.all_with(&icon_weak_intensity, mix).sampler(); } init.emit(()); - Self { icon_weak, icon_strong, header_text, entry_text, background: bg } + let selected = SelectedColors { + background: selected_bg, + header_text: selected.clone_ref(), + entry_text: selected.clone_ref(), + icon_weak: selected_icon_weak, + icon_strong: selected.clone_ref(), + }; + Self { icon_weak, icon_strong, header_text, entry_text, background: bg, selected } } } @@ -335,7 +421,8 @@ impl component::Frp for Frp { // === Colors === let colors = Colors::from_main_color(network, style, &input.set_color, &input.set_dimmed); - let params = entry::Params { colors: colors.clone_ref() }; + let selection_layer = default(); + let params = entry::Params { colors: colors.clone_ref(), selection_layer }; model.entries.set_entry_params_and_recreate_entries(params); @@ -345,8 +432,10 @@ impl component::Frp for Frp { init <- source_(); header_text_font <- all(&header_text_font, &init)._0(); model.header.set_font <+ header_text_font; + model.selected_header.set_font <+ header_text_font; header_text_size <- all(&header_text_size, &init)._0(); model.header.set_default_text_size <+ header_text_size.map(|v| text::Size(*v)); + model.selected_header.set_default_text_size <+ header_text_size.map(|v| text::Size(*v)); _set_header <- input.set_header.map2(&size_and_header_geometry, f!( (text, (size, hdr_geom, _)) { model.header_text.replace(text.clone()); @@ -354,8 +443,13 @@ impl component::Frp for Frp { }) ); model.header.set_default_color <+ colors.header_text; + model.selected_header.set_default_color <+ all(&colors.selected.header_text,&init)._0(); eval colors.background((c) model.background.color.set(c.into())); eval colors.background((c) model.header_background.color.set(c.into())); + eval colors.selected.background((c) model.selection_background.color.set(c.into())); + eval colors.selected.background( + (c) model.selection_header_background.color.set(c.into()) + ); } @@ -393,11 +487,21 @@ impl component::Frp for Frp { 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( + out.selection_size <+ all_with3( + &header_geometry, &out.is_header_selected, + &out.focused, + f!((geom, h_sel, _) model.selection_size(geom.height, *h_sel)) + ); + out.selection_position_target <+ all_with5( + &out.is_header_selected, + &header_geometry, &out.size, &model.entries.selection_position_target, - f!((h_sel, size, esp) model.selection_position(*h_sel, *size, *esp)) + &input.set_header_pos, + f!((h_sel, h_geom, size, esp, h_pos) + model.selection_position(*h_sel, *h_geom, *size, *esp, *h_pos) + ) ); } @@ -437,40 +541,60 @@ impl component::Frp for Frp { /// A set of scene layers shared by every component group. /// -/// A component group consists of a several shapes with a strict rendering order. The order of the -/// fields of this struct represents the rendering order of layers, with `background` being the -/// bottom-most and `header_text` being the top-most. +/// Layers are duplicated into two sets ([`LayersInner`]). `normal` layers are used by the +/// component group itself, `selection` layers are used to implement the selection box. See +/// module-level documentation to learn more. +/// +/// A component group consists of several shapes with a strict rendering order. The order of the +/// fields in [`LayersInner`] struct represent the rendering order of layers, with `background` +/// being the bottom-most and `header_text` being the top-most. #[derive(Debug, Clone, CloneRef)] -#[allow(missing_docs)] pub struct Layers { - pub background: layer::Layer, - pub text: layer::Layer, - pub header: layer::Layer, - pub header_text: layer::Layer, + normal: LayersInner, + selection: LayersInner, } impl Layers { /// Constructor. /// - /// A `camera` will be used to render all layers. Layers will be attached to a `parent_layer` as - /// sublayers. - pub fn new(logger: &Logger, camera: &Camera2d, parent_layer: &layer::Layer) -> Self { - let background = layer::Layer::new_with_cam(logger.clone_ref(), camera); - let text = layer::Layer::new_with_cam(logger.clone_ref(), camera); - let header = layer::Layer::new_with_cam(logger.clone_ref(), camera); - let header_text = layer::Layer::new_with_cam(logger.clone_ref(), camera); + /// `normal` layers are assigned as sublayers of the `normal_parent`, while `selection` + /// layers are assigned to the `selected_parent`. + pub fn new(logger: &Logger, normal_parent: &Layer, selected_parent: &Layer) -> Self { + let normal = LayersInner::new(logger, normal_parent); + let selection = LayersInner::new(logger, selected_parent); + Self { normal, selection } + } +} +/// A set of scene layers shared by every component group. A part of [`Layers`]. +#[derive(Debug, Clone, CloneRef)] +struct LayersInner { + background: Layer, + text: Layer, + header: Layer, + header_text: Layer, +} + +impl LayersInner { + /// Constructor. + /// + /// Layers will be attached to a `parent_layer` as sublayers. + pub fn new(logger: &Logger, parent_layer: &Layer) -> Self { + let camera = parent_layer.camera(); + let background = Layer::new_with_cam(logger.clone_ref(), &camera); + let text = Layer::new_with_cam(logger.clone_ref(), &camera); + let header = Layer::new_with_cam(logger.clone_ref(), &camera); + let header_text = Layer::new_with_cam(logger.clone_ref(), &camera); background.add_sublayer(&text); background.add_sublayer(&header); header.add_sublayer(&header_text); - parent_layer.add_sublayer(&background); - Self { background, header, text, header_text } } } + // ============= // === Model === // ============= @@ -478,13 +602,16 @@ impl Layers { /// The Model of the [`View`] component. #[derive(Clone, CloneRef, Debug)] pub struct Model { - display_object: display::object::Instance, - header: text::Area, + display_object: display::object::Instance, + entries: list_view::ListView, + header: text::Area, header_background: header_background::View, - header_text: Rc>, - header_overlay: header_overlay::View, - background: background::View, - entries: list_view::ListView, + header_text: Rc>, + header_overlay: header_overlay::View, + background: background::View, + selected_header: text::Area, + selection_header_background: selection_header_background::View, + selection_background: background::View, } impl display::Object for Model { @@ -503,8 +630,11 @@ impl component::Model for Model { let display_object = display::object::Instance::new(&logger); let header_overlay = header_overlay::View::new(&logger); let background = background::View::new(&logger); + let selection_background = background::View::new(&logger); let header_background = header_background::View::new(&logger); + let selection_header_background = selection_header_background::View::new(&logger); let header = text::Area::new(app); + let selected_header = text::Area::new(app); let entries = app.new_view::>(); entries.set_style_prefix(entry::STYLE_PATH); entries.set_background_color(HOVER_COLOR); @@ -512,8 +642,11 @@ impl component::Model for Model { entries.set_background_corners_radius(0.0); entries.hide_selection(); display_object.add_child(&background); + display_object.add_child(&selection_background); display_object.add_child(&header_background); + display_object.add_child(&selection_header_background); display_object.add_child(&header); + display_object.add_child(&selected_header); display_object.add_child(&header_overlay); display_object.add_child(&entries); @@ -521,24 +654,35 @@ impl component::Model for Model { display_object, header_overlay, header, + selected_header, header_text, background, + selection_background, header_background, + selection_header_background, entries, } } } impl Model { - /// Assign a set of layers to render the component group in. Must be called after constructing + /// Assign a set of layers to render the component group. Must be called after constructing /// the [`View`]. pub fn set_layers(&self, layers: &Layers) { - layers.background.add_exclusive(&self.background); - layers.header_text.add_exclusive(&self.header_overlay); - layers.background.add_exclusive(&self.entries); - self.entries.set_label_layer(&layers.text); - layers.header.add_exclusive(&self.header_background); - self.header.add_to_scene_layer(&layers.header_text); + // Set normal layers. + layers.normal.background.add_exclusive(&self.background); + layers.normal.header_text.add_exclusive(&self.header_overlay); + layers.normal.background.add_exclusive(&self.entries); + self.entries.set_label_layer(&layers.normal.text); + layers.normal.header.add_exclusive(&self.header_background); + self.header.add_to_scene_layer(&layers.normal.header_text); + // Set selected layers. + let mut params = self.entries.entry_params(); + params.selection_layer = Rc::new(Some(layers.selection.text.downgrade())); + self.entries.set_entry_params_and_recreate_entries(params); + layers.selection.background.add_exclusive(&self.selection_background); + layers.selection.header.add_exclusive(&self.selection_header_background); + self.selected_header.add_to_scene_layer(&layers.selection.header_text); } fn resize( @@ -551,6 +695,7 @@ impl Model { // === Background === self.background.size.set(size); + self.selection_background.size.set(size); // === Header Text === @@ -568,12 +713,14 @@ impl Model { let header_bottom_y = header_center_y - half_header_height; let header_text_y = header_bottom_y + header_text_height + header_padding_bottom; self.header.set_position_xy(Vector2(header_text_x, header_text_y)); + self.selected_header.set_position_xy(Vector2(header_text_x, header_text_y)); self.update_header_width(size, header_geometry); // === Header Background === self.header_background.height.set(header_height); + self.selection_header_background.height.set(header_height); let shadow_size = header_geometry.shadow_size; let distance_to_bottom = (-size.y / 2.0 - header_bottom_y).abs(); // We need to render both the header background and the shadow below it, so we add @@ -586,6 +733,8 @@ impl Model { let header_background_height = header_height + shadow_size * 2.0; self.header_background.size.set(Vector2(size.x, header_background_height)); self.header_background.set_position_y(header_center_y); + self.selection_header_background.size.set(Vector2(size.x, header_background_height)); + self.selection_header_background.set_position_y(header_center_y); // === Header Overlay === @@ -604,21 +753,33 @@ impl Model { let header_padding_left = header_geometry.padding_left; let header_padding_right = header_geometry.padding_right; let max_text_width = size.x - header_padding_left - header_padding_right; - self.header.set_content_truncated(self.header_text.borrow().clone(), max_text_width); + let header_text = self.header_text.borrow().clone(); + self.header.set_content_truncated(header_text.clone(), max_text_width); + self.selected_header.set_content_truncated(header_text, max_text_width); } fn selection_position( &self, is_header_selected: bool, + header_geometry: HeaderGeometry, size: Vector2, entries_selection_position: Vector2, + header_pos: f32, ) -> Vector2 { if is_header_selected { - Vector2(0.0, size.y / 2.0 - list_view::entry::HEIGHT / 2.0) + Vector2(0.0, size.y / 2.0 - header_geometry.height / 2.0 - header_pos) } else { - self.entries.position().xy() + entries_selection_position + let max_selection_pos_y = size.y / 2.0 - list_view::entry::HEIGHT - header_pos; + let selection_pos_y = entries_selection_position.y.min(max_selection_pos_y); + let selection_pos = Vector2(entries_selection_position.x, selection_pos_y); + self.entries.position().xy() + selection_pos } } + + fn selection_size(&self, header_height: f32, is_header_selected: bool) -> Vector2 { + let height = if is_header_selected { header_height } else { list_view::entry::HEIGHT }; + Vector2(self.entries.size.value().x, height) + } } diff --git a/app/gui/view/component-browser/component-group/src/set.rs b/app/gui/view/component-browser/component-group/src/set.rs index f6681bd23f1d..a7d3c670a72e 100644 --- a/app/gui/view/component-browser/component-group/src/set.rs +++ b/app/gui/view/component-browser/component-group/src/set.rs @@ -31,26 +31,37 @@ pub enum Group { } impl Group { - fn focus(&self) { + /// Focus group. + pub fn focus(&self) { match self { Group::OneColumn(group) => group.focus(), Group::Wide(group) => group.focus(), } } - fn defocus(&self) { + /// Defocus group. + pub fn defocus(&self) { match self { Group::OneColumn(group) => group.defocus(), Group::Wide(group) => group.defocus(), } } - fn is_mouse_over(&self) -> &frp::Sampler { + /// An FRP stream of `is_mouse_over` events. + pub fn is_mouse_over(&self) -> &frp::Sampler { match self { Group::OneColumn(group) => &group.is_mouse_over, Group::Wide(group) => &group.is_mouse_over, } } + + /// Position of the display object. + pub fn position(&self) -> Vector3 { + match self { + Group::OneColumn(group) => group.position(), + Group::Wide(group) => group.position(), + } + } } @@ -131,6 +142,7 @@ propagated_events! { is_header_selected: (GroupId, bool), header_accepted: GroupId, selection_position_target: (GroupId, Vector2), + selection_size: (GroupId, Vector2), focused: (GroupId, bool), } } @@ -249,6 +261,7 @@ impl Wrapper { (suggestion_accepted, move |e| (id, *e)), (expression_accepted, move |e| (id, *e)), (selection_position_target, move |p| (id, *p)), + (selection_size, move |p| (id, *p)), (is_header_selected, move |h| (id, *h)), (header_accepted, move |_| id) } diff --git a/app/gui/view/component-browser/component-group/src/wide.rs b/app/gui/view/component-browser/component-group/src/wide.rs index ade4fe8aa5ae..3578b6ca90ef 100644 --- a/app/gui/view/component-browser/component-group/src/wide.rs +++ b/app/gui/view/component-browser/component-group/src/wide.rs @@ -13,7 +13,9 @@ use crate::prelude::*; +use crate::background; use crate::entry; +use crate::theme; use crate::Colors; use enso_frp as frp; @@ -53,25 +55,6 @@ newtype_prim! { -// ======================== -// === Background Shape === -// ======================== - -/// The background of the Wide Component Group. -pub mod background { - use super::*; - - ensogl::define_shape_system! { - below = [list_view::background]; - (style:Style, color:Vector4) { - let color = Var::::from(color); - Plane().fill(color).into() - } - } -} - - - // ===================== // === ModelProvider === // ===================== @@ -139,6 +122,7 @@ ensogl::define_endpoints_2! { /// column if possible. If there are no more entries in this column, the selection will move to /// the next non-empty column to the left. selection_position_target(Vector2), + selection_size(Vector2), entry_count(usize), size(Vector2), } @@ -155,6 +139,7 @@ impl component::Frp> for Frp { let input = &api.input; let out = &api.output; let colors = Colors::from_main_color(network, style, &input.set_color, &input.set_dimmed); + let padding = style.get_number(theme::entry_list::padding); frp::extend! { network init <- source_(); entry_count <- input.set_entries.map(|p| p.entry_count()); @@ -173,6 +158,7 @@ impl component::Frp> for Frp { }); eval colors.background((c) model.background.color.set(c.into())); + eval colors.selected.background((c) model.selection_background.color.set(c.into())); eval input.set_no_items_label_text((text) model.set_no_items_label_text(text)); @@ -180,10 +166,12 @@ impl component::Frp> for Frp { background_height <- any(...); let background_width = input.set_width.clone_ref(); - size <- all_with(&background_width, &background_height, - |width, height| Vector2(*width, *height)); + size <- all_with3(&background_width, &background_height, &padding, + |width, height, padding| Vector2(*width, *height + *padding)); eval size((size) model.background.size.set(*size)); + eval size((size) model.selection_background.size.set(*size)); out.size <+ size; + out.selection_size <+ background_width.map(|&width| Vector2(width / 3.0,list_view::entry::HEIGHT)); // === "No items" label === @@ -248,13 +236,16 @@ impl component::Frp> for Frp { entries <- input.set_entries.map(move |p| ModelProvider::::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))); + _eval <- all_with3(&entries, &out.size, &padding, + f!((_, size, padding) column.resize_and_place(*size, *padding)) + ); } frp::extend! { network out.is_mouse_over <+ is_mouse_over; } - let params = entry::Params { colors: colors.clone_ref() }; + let selection_layer = default(); + let params = entry::Params { colors: colors.clone_ref(), selection_layer }; column.list_view.set_entry_params_and_recreate_entries(params); } } @@ -315,17 +306,17 @@ impl Column { } /// Resize the column and update its position. - fn resize_and_place(&self, size: Vector2) { + fn resize_and_place(&self, size: Vector2, padding: f32) { let width = size.x / COLUMNS as f32; let bg_height = size.y; let height = self.len() as f32 * ENTRY_HEIGHT; - self.list_view.resize(Vector2(width, height)); + self.list_view.resize(Vector2(width, height + padding)); let left_border = -(COLUMNS as f32 * width / 2.0) + width / 2.0; let pos_x = left_border + width * *self.id as f32; - let half_height = height / 2.0; + let half_height = bg_height / 2.0; let background_bottom = -bg_height / 2.0; - let pos_y = background_bottom + half_height; + let pos_y = background_bottom + half_height + padding / 2.0; self.list_view.set_position_x(pos_x); self.list_view.set_position_y(pos_y); } @@ -346,10 +337,11 @@ impl Column { /// The Model of the [`View`] component. Consists of `COLUMNS` columns. #[derive(Clone, CloneRef, Debug)] pub struct Model { - display_object: display::object::Instance, - background: background::View, - columns: Rc>>, - no_items_label: Label, + display_object: display::object::Instance, + background: background::View, + selection_background: background::View, + columns: Rc>>, + no_items_label: Label, } impl display::Object for Model { @@ -367,9 +359,12 @@ impl component::Model for Model { let display_object = display::object::Instance::new(&logger); let background = background::View::new(&logger); display_object.add_child(&background); + let selection_background = background::View::new(&logger); + display_object.add_child(&selection_background); let columns: Vec<_> = (0..COLUMNS).map(|i| Column::new(app, ColumnId::new(i))).collect(); let columns = Rc::new(columns); for column in columns.iter() { + column.set_style_prefix(entry::STYLE_PATH); column.hide_selection(); column.set_background_color(Rgba::transparent()); column.show_background_shadow(false); @@ -378,11 +373,26 @@ impl component::Model for Model { } let no_items_label = Label::new(app); - Model { no_items_label, display_object, background, columns } + Model { no_items_label, display_object, background, selection_background, columns } } } impl Model { + /// Assign a set of layers to render the component group. Must be called after constructing + /// the [`View`]. + pub fn set_layers(&self, layers: &crate::Layers) { + layers.normal.background.add_exclusive(&self.background); + layers.selection.background.add_exclusive(&self.selection_background); + let layer = &layers.selection.text; + for column in self.columns.iter() { + let mut params = column.list_view.entry_params(); + params.selection_layer = Rc::new(Some(layer.downgrade())); + column.list_view.set_entry_params_and_recreate_entries(params); + layers.normal.background.add_exclusive(&column.list_view); + column.list_view.set_label_layer(&layers.normal.text); + } + } + /// Set the text content of the "no items" label. fn set_no_items_label_text(&self, text: &str) { self.no_items_label.set_content(text); diff --git a/app/gui/view/debug_scene/component-group/src/lib.rs b/app/gui/view/debug_scene/component-group/src/lib.rs index 3b34739b6197..d765323e44ec 100644 --- a/app/gui/view/debug_scene/component-group/src/lib.rs +++ b/app/gui/view/debug_scene/component-group/src/lib.rs @@ -1,359 +1,433 @@ -// //! A debug scene which shows the Component Group visual component. -// -// // === Standard Linter Configuration === -// #![deny(non_ascii_idents)] -// #![warn(unsafe_code)] -// // === Non-Standard Linter Configuration === -// #![warn(missing_copy_implementations)] -// #![warn(missing_debug_implementations)] -// #![warn(missing_docs)] -// #![warn(trivial_casts)] -// #![warn(trivial_numeric_casts)] -// #![warn(unused_import_braces)] -// #![warn(unused_qualifications)] -// -// use ensogl_core::display::shape::*; -// use ensogl_core::prelude::*; -// use wasm_bindgen::prelude::*; -// -// use enso_text::Bytes; -// use ensogl_core::application::Application; -// use ensogl_core::data::color; -// use ensogl_core::display::object::ObjectOps; -// use ensogl_core::frp; -// use ensogl_hardcoded_theme as theme; -// use ensogl_list_view as list_view; -// use ensogl_list_view::entry::GlyphHighlightedLabelModel; -// use ensogl_scroll_area::ScrollArea; -// use ensogl_selector as selector; -// use ensogl_text_msdf_sys::run_once_initialized; -// use ide_view_component_group as component_group; -// use ide_view_component_group::entry; -// use ide_view_component_group::icon; -// use ide_view_component_group::Entry; -// 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 === -// // =================== -// -// /// An entry point. -// #[entry_point] -// pub fn main() { -// run_once_initialized(|| { -// let app = Application::new("root"); -// init(&app); -// mem::forget(app); -// }); -// } -// -// -// -// // ==================== -// // === Mock Entries === -// // ==================== -// -// const PREPARED_ITEMS: &[(&str, icon::Id)] = &[ -// ("long sample entry with text overflowing the width", icon::Id::Star), -// ("convert", icon::Id::Convert), -// ("table input", icon::Id::DataInput), -// ("text input", icon::Id::TextInput), -// ("number input", icon::Id::NumberInput), -// ("table output", icon::Id::TableEdit), -// ("dataframe clean", icon::Id::DataframeClean), -// ("data input", icon::Id::DataInput), -// ]; -// -// #[derive(Debug)] -// struct MockEntries { -// entries: Vec, -// count: Cell, -// } -// -// impl MockEntries { -// fn new(count: usize) -> Rc { -// const HIGHLIGHTED_ENTRY_NAME: &str = "convert"; -// const HIGHLIGHTED_RANGE: Range = Bytes(0)..Bytes(3); -// Rc::new(Self { -// entries: PREPARED_ITEMS -// .iter() -// .cycle() -// .take(count) -// .map(|&(label, icon)| entry::Model { -// icon, -// highlighted_text: GlyphHighlightedLabelModel { -// label: label.to_owned(), -// highlighted: if label == HIGHLIGHTED_ENTRY_NAME { -// vec![HIGHLIGHTED_RANGE.into()] -// } else { -// default() -// }, -// }, -// }) -// .collect(), -// count: Cell::new(count), -// }) -// } -// -// fn get_entry(&self, id: list_view::entry::Id) -> Option { -// self.entries.get(id).cloned() -// } -// } -// -// impl list_view::entry::ModelProvider for MockEntries { -// fn entry_count(&self) -> usize { -// self.count.get() -// } -// -// fn get(&self, id: list_view::entry::Id) -> Option { -// self.get_entry(id) -// } -// } -// -// -// -// // ================================== -// // === Component Group Controller === -// // ================================== -// -// /// An example abstraction to arrange component groups vertically inside the scroll area. -// /// -// /// It arranges `component_groups` on top of each other, with the first one being the top most. -// /// It also calculates the header positions for every component group to simulate the scrolling. -// /// While the [`ScrollArea`] moves every component group up we move headers of the partially -// /// visible component groups down. That makes the headers of the partially scrolled component -// /// groups visible at all times. -// #[derive(Debug, Clone, CloneRef)] -// struct ComponentGroupController { -// component_groups: Rc>, -// } -// -// impl ComponentGroupController { -// fn init( -// component_groups: &[component_group::View], -// network: &frp::Network, -// scroll_area: &ScrollArea, -// ) { -// Self { component_groups: Rc::new(component_groups.to_vec()) } -// .init_inner(network, scroll_area) -// } -// -// fn init_inner(&self, network: &frp::Network, scroll_area: &ScrollArea) { -// for (i, group) in self.component_groups.iter().enumerate() { -// let this = self.clone_ref(); -// frp::extend! { network -// eval group.size([group, this](size) -// group.set_position_y(-size.y / 2.0 - this.heights_sum(i)) -// ); -// is_scrolled <- scroll_area.scroll_position_y.map(f!([this] (s) *s > -// this.heights_sum(i))); change_hdr_pos <- -// scroll_area.scroll_position_y.gate(&is_scrolled); reset_hdr_pos <- -// is_scrolled.on_false(); eval change_hdr_pos([group, this](y) -// group.set_header_pos(*y - this.heights_sum(i))); eval_ -// reset_hdr_pos(group.set_header_pos(0.0)); } -// } -// } -// -// /// Return a sum of heights of the first `n` component groups, counting from the top. -// fn heights_sum(&self, n: usize) -> f32 { -// self.component_groups.iter().take(n).map(|g| g.size.value().y).sum() -// } -// } -// -// -// -// // ======================== -// // === Init Application === -// // ======================== -// -// -// // === Helpers ==== -// -// fn create_component_group( -// app: &Application, -// header: &str, -// layers: &component_group::Layers, -// ) -> component_group::View { -// let component_group = app.new_view::(); -// component_group.model().set_layers(layers); -// component_group.set_header(header.to_string()); -// component_group.set_width(150.0); -// component_group.set_position_x(75.0); -// component_group -// } -// -// fn create_wide_component_group(app: &Application) -> component_group::wide::View { -// let component_group = app.new_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::(); -// app.display.add_child(&slider); -// slider.frp.allow_click_selection(true); -// slider.frp.resize(Vector2(400.0, 30.0)); -// slider.frp.set_bounds.emit(selector::Bounds::new(0.0, 1.0)); -// slider.frp.set_caption(Some(caption.to_string())); -// slider -// } -// -// -// // === init === -// -// /// This is a workaround for the bug [#182193824](https://www.pivotaltracker.com/story/show/182193824). -// /// -// /// We add a tranparent shape to the [`ScrollArea`] content to make component groups visible. -// mod transparent_circle { -// use super::*; -// ensogl_core::define_shape_system! { -// (style:Style) { -// // As you can see even a zero-radius circle works as a workaround. -// let radius = 0.px(); -// Circle(radius).fill(color::Rgba::transparent()).into() -// } -// } -// } -// -// -// fn init(app: &Application) { -// theme::builtin::dark::register(&app); -// theme::builtin::light::register(&app); -// theme::builtin::light::enable(&app); -// -// let network = frp::Network::new("Component Group Debug Scene"); -// let scroll_area = ScrollArea::new(app); -// 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); -// app.display.add_child(&scroll_area); -// -// let camera = &scroll_area.content_layer().camera(); -// let parent_layer = scroll_area.content_layer(); -// let layers = component_group::Layers::new(&app.logger, camera, parent_layer); -// -// let group_name = "Long group name with text overflowing the width"; -// 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); -// 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`]. -// { -// let transparent_circle = transparent_circle::View::new(&app.logger); -// transparent_circle.size.set(Vector2(150.0, 150.0)); -// transparent_circle.set_position_xy(Vector2(200.0, -150.0)); -// scroll_area.content().add_child(&transparent_circle); -// std::mem::forget(transparent_circle); -// } -// -// // === Regular Component Group === -// -// ComponentGroupController::init( -// &[first_component_group.clone_ref(), second_component_group.clone_ref()], -// &network, -// &scroll_area, -// ); -// -// 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.clone_ref()); -// wide_component_group.set_entries(model_provider.clone_ref()); -// -// // === Color sliders === -// -// let red_slider = color_component_slider(app, "Red"); -// red_slider.set_position_y(350.0); -// red_slider.set_track_color(color::Rgba::new(1.0, 0.60, 0.60, 1.0)); -// let red_slider_frp = &red_slider.frp; -// -// let green_slider = color_component_slider(app, "Green"); -// green_slider.set_position_y(300.0); -// green_slider.set_track_color(color::Rgba::new(0.6, 1.0, 0.6, 1.0)); -// let green_slider_frp = &green_slider.frp; -// -// let blue_slider = color_component_slider(app, "Blue"); -// blue_slider.set_position_y(250.0); -// 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 = COMPONENT_GROUP_COLOR; -// frp::extend! { network -// init <- source_(); -// red_slider_frp.set_value <+ init.constant(default_color.red); -// green_slider_frp.set_value <+ init.constant(default_color.green); -// blue_slider_frp.set_value <+ init.constant(default_color.blue); -// let red_slider_value = &red_slider_frp.value; -// let green_slider_value = &green_slider_frp.value; -// let blue_slider_value = &blue_slider_frp.value; -// color <- all_with3(red_slider_value, green_slider_value, blue_slider_value, -// |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> = 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); -// std::mem::forget(green_slider); -// 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); -// } +//! A debug scene which shows the Component Group visual component. + +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +// === Non-Standard Linter Configuration === +#![warn(missing_copy_implementations)] +#![warn(missing_debug_implementations)] +#![warn(missing_docs)] +#![warn(trivial_casts)] +#![warn(trivial_numeric_casts)] +#![warn(unused_import_braces)] +#![warn(unused_qualifications)] + +use ensogl_core::display::shape::*; +use ensogl_core::prelude::*; +use wasm_bindgen::prelude::*; + +use enso_text::Bytes; +use ensogl_core::animation::physics::inertia; +use ensogl_core::application::Application; +use ensogl_core::data::color; +use ensogl_core::display::object::ObjectOps; +use ensogl_core::display::scene::layer::Layer; +use ensogl_core::frp; +use ensogl_core::Animation; +use ensogl_hardcoded_theme as theme; +use ensogl_list_view as list_view; +use ensogl_list_view::entry::GlyphHighlightedLabelModel; +use ensogl_scroll_area::ScrollArea; +use ensogl_selector as selector; +use ensogl_text_msdf_sys::run_once_initialized; +use ide_view_component_group as component_group; +use ide_view_component_group::entry; +use ide_view_component_group::icon; +use ide_view_component_group::Entry; +use list_view::entry::AnyModelProvider; + + + +// ================= +// === Constants === +// ================= + +const COMPONENT_GROUP_COLOR: color::Rgba = color::Rgba::new(0.527, 0.554, 0.18, 1.0); +/// The selection animation is faster than the default one because of the increased spring force. +const SELECTION_ANIMATION_SPRING_FORCE_MULTIPLIER: f32 = 1.5; +const COMPONENT_GROUP_WIDTH: f32 = 150.0; +const SCROLL_AREA_HEIGHT: f32 = list_view::entry::HEIGHT * 10.0; +const SCROLL_AREA_WIDTH: f32 = COMPONENT_GROUP_WIDTH * 4.0 + 20.0; + + + +// =================== +// === Entry Point === +// =================== + +/// An entry point. +#[entry_point] +pub fn main() { + run_once_initialized(|| { + let app = Application::new("root"); + init(&app); + mem::forget(app); + }); +} + + + +// ==================== +// === Mock Entries === +// ==================== + +const PREPARED_ITEMS: &[(&str, icon::Id)] = &[ + ("long sample entry with text overflowing the width", icon::Id::Star), + ("convert", icon::Id::Convert), + ("table input", icon::Id::DataInput), + ("text input", icon::Id::TextInput), + ("number input", icon::Id::NumberInput), + ("table output", icon::Id::TableEdit), + ("dataframe clean", icon::Id::DataframeClean), + ("data input", icon::Id::DataInput), +]; + +#[derive(Debug)] +struct MockEntries { + entries: Vec, + count: Cell, +} + +impl MockEntries { + fn new(count: usize) -> Rc { + const HIGHLIGHTED_ENTRY_NAME: &str = "convert"; + const HIGHLIGHTED_RANGE: Range = Bytes(0)..Bytes(3); + Rc::new(Self { + entries: PREPARED_ITEMS + .iter() + .cycle() + .take(count) + .map(|&(label, icon)| entry::Model { + icon, + highlighted_text: GlyphHighlightedLabelModel { + label: label.to_owned(), + highlighted: if label == HIGHLIGHTED_ENTRY_NAME { + vec![HIGHLIGHTED_RANGE.into()] + } else { + default() + }, + }, + }) + .collect(), + count: Cell::new(count), + }) + } + + fn get_entry(&self, id: list_view::entry::Id) -> Option { + self.entries.get(id).cloned() + } +} + +impl list_view::entry::ModelProvider for MockEntries { + fn entry_count(&self) -> usize { + self.count.get() + } + + fn get(&self, id: list_view::entry::Id) -> Option { + self.get_entry(id) + } +} + + + +// ================================== +// === Component Group Controller === +// ================================== + +/// An example abstraction to arrange component groups vertically inside the scroll area. +/// +/// It arranges `component_groups` on top of each other, with the first one being the top most. +/// It also calculates the header positions for every component group to simulate the scrolling. +/// While the [`ScrollArea`] moves every component group up we move headers of the partially +/// visible component groups down. That makes the headers of the partially scrolled component +/// groups visible at all times. +#[derive(Debug, Clone, CloneRef)] +struct ComponentGroupController { + component_groups: Rc>, +} + +impl ComponentGroupController { + fn init( + component_groups: &[component_group::View], + network: &frp::Network, + scroll_area: &ScrollArea, + ) { + Self { component_groups: Rc::new(component_groups.to_vec()) } + .init_inner(network, scroll_area) + } + + fn init_inner(&self, network: &frp::Network, scroll_area: &ScrollArea) { + for (i, group) in self.component_groups.iter().enumerate() { + let this = self.clone_ref(); + frp::extend! { network + eval group.size([group, this](size) + group.set_position_y(-size.y / 2.0 - this.heights_sum(i)) + ); + is_scrolled <- scroll_area.scroll_position_y.map(f!([this] (s) *s > this.heights_sum(i))); + change_hdr_pos <- scroll_area.scroll_position_y.gate(&is_scrolled); + reset_hdr_pos <- is_scrolled.on_false(); + eval change_hdr_pos([group, this](y) group.set_header_pos(*y - this.heights_sum(i))); + eval_ reset_hdr_pos(group.set_header_pos(0.0)); + } + } + } + + /// Return a sum of heights of the first `n` component groups, counting from the top. + fn heights_sum(&self, n: usize) -> f32 { + self.component_groups.iter().take(n).map(|g| g.size.value().y).sum() + } +} + + + +// ======================== +// === Init Application === +// ======================== + + +// === Helpers ==== + +fn create_component_group( + app: &Application, + header: &str, + layers: &component_group::Layers, +) -> component_group::View { + let component_group = app.new_view::(); + component_group.model().set_layers(layers); + component_group.set_header(header.to_string()); + component_group.set_width(COMPONENT_GROUP_WIDTH); + component_group.set_position_x(COMPONENT_GROUP_WIDTH * 3.5); + component_group +} + +fn create_wide_component_group( + app: &Application, + layers: &component_group::Layers, +) -> component_group::wide::View { + let component_group = app.new_view::(); + component_group.model().set_layers(layers); + component_group.set_width(COMPONENT_GROUP_WIDTH * 3.0); + let padding = 5.0; + component_group.set_position_xy(Vector2(COMPONENT_GROUP_WIDTH * 1.5 - padding, -150.0)); + component_group +} + +fn color_component_slider(app: &Application, caption: &str) -> selector::NumberPicker { + let slider = app.new_view::(); + app.display.add_child(&slider); + slider.frp.allow_click_selection(true); + slider.frp.resize(Vector2(400.0, 30.0)); + slider.frp.set_bounds.emit(selector::Bounds::new(0.0, 1.0)); + slider.frp.set_caption(Some(caption.to_string())); + slider +} + + +// === init === + +/// This is a workaround for the bug [#182193824](https://www.pivotaltracker.com/story/show/182193824). +/// +/// We add a tranparent shape to the [`ScrollArea`] content to make component groups visible. +mod transparent_circle { + use super::*; + ensogl_core::define_shape_system! { + (style:Style) { + // As you can see even a zero-radius circle works as a workaround. + let radius = 0.px(); + Circle(radius).fill(color::Rgba::transparent()).into() + } + } +} + +fn init(app: &Application) { + theme::builtin::dark::register(&app); + theme::builtin::light::register(&app); + theme::builtin::light::enable(&app); + + + // === Layers setup === + + let main_camera = app.display.default_scene.layers.main.camera(); + let selection_layer = Layer::new_with_cam(app.logger.sub("selection"), &main_camera); + let groups_layer = Layer::new_with_cam(app.logger.sub("component_groups"), &main_camera); + let selection_mask = Layer::new_with_cam(app.logger.sub("selection_mask"), &main_camera); + selection_layer.set_mask(&selection_mask); + app.display.default_scene.layers.main.add_sublayer(&groups_layer); + app.display.default_scene.layers.main.add_sublayer(&selection_layer); + + + // === Scroll area === + + let network = frp::Network::new("Component Group Debug Scene"); + let scroll_area = ScrollArea::new(app); + scroll_area.set_position_xy(Vector2(-COMPONENT_GROUP_WIDTH * 2.0, 100.0)); + scroll_area.resize(Vector2(SCROLL_AREA_WIDTH, SCROLL_AREA_HEIGHT)); + scroll_area.set_content_width(COMPONENT_GROUP_WIDTH * 4.0); + scroll_area.set_content_height(2000.0); + app.display.add_child(&scroll_area); + groups_layer.add_exclusive(&scroll_area); + + + // === Component groups === + + let normal_parent = &scroll_area.content_layer(); + let selected_parent = &selection_layer; + let layers = component_group::Layers::new(&app.logger, normal_parent, selected_parent); + let group_name = "Long group name with text overflowing the width"; + 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); + let wide_component_group = create_wide_component_group(app, &layers); + + scroll_area.content().add_child(&first_component_group); + scroll_area.content().add_child(&second_component_group); + scroll_area.content().add_child(&wide_component_group); + + // FIXME(#182193824): This is a workaround for a bug. See the docs of the + // [`transparent_circle`]. + { + let transparent_circle = transparent_circle::View::new(&app.logger); + transparent_circle.size.set(Vector2(150.0, 150.0)); + transparent_circle.set_position_xy(Vector2(200.0, -150.0)); + scroll_area.content().add_child(&transparent_circle); + std::mem::forget(transparent_circle); + } + + ComponentGroupController::init( + &[first_component_group.clone_ref(), second_component_group.clone_ref()], + &network, + &scroll_area, + ); + + 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.clone_ref()); + wide_component_group.set_entries(model_provider); + + // === Color sliders === + + let red_slider = color_component_slider(app, "Red"); + red_slider.set_position_y(350.0); + red_slider.set_track_color(color::Rgba::new(1.0, 0.60, 0.60, 1.0)); + let red_slider_frp = &red_slider.frp; + + let green_slider = color_component_slider(app, "Green"); + green_slider.set_position_y(300.0); + green_slider.set_track_color(color::Rgba::new(0.6, 1.0, 0.6, 1.0)); + let green_slider_frp = &green_slider.frp; + + let blue_slider = color_component_slider(app, "Blue"); + blue_slider.set_position_y(250.0); + 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 = COMPONENT_GROUP_COLOR; + frp::extend! { network + init <- source_(); + red_slider_frp.set_value <+ init.constant(default_color.red); + green_slider_frp.set_value <+ init.constant(default_color.green); + blue_slider_frp.set_value <+ init.constant(default_color.blue); + let red_slider_value = &red_slider_frp.value; + let green_slider_value = &green_slider_frp.value; + let blue_slider_value = &blue_slider_frp.value; + color <- all_with3(red_slider_value, green_slider_value, blue_slider_value, + |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> = 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), + } + }); + } + + + // === Selection box === + + let selection = component_group::selection_box::View::new(&app.logger); + selection_mask.add_exclusive(&selection); + app.display.add_child(&selection); + + let selection_animation = Animation::::new(&network); + let selection_size_animation = Animation::::new(&network); + let spring = inertia::Spring::default() * SELECTION_ANIMATION_SPRING_FORCE_MULTIPLIER; + selection_animation.set_spring.emit(spring); + /// This is an example code to position the selection box on the scene. + /// We transform the group-local position from [`multiview.selection_position_target`] to + /// global position. After that we restrict the Y-coordinate so that the selection box won't + /// go below the scroll area bottom border. + fn selection_position( + group_local_pos: Vector2, + group: &component_group::set::Group, + scroll_area: &ScrollArea, + ) -> Vector2 { + let scroll_area_pos = scroll_area.position() + scroll_area.content().position(); + let group_pos = scroll_area_pos + group.position(); + let mut pos = group_pos.xy() + group_local_pos; + let scroll_area_bottom = scroll_area.position().y - SCROLL_AREA_HEIGHT; + let lower_bound = scroll_area_bottom + list_view::entry::HEIGHT / 2.0; + pos.y = pos.y.max(lower_bound); + pos + } + frp::extend! { network + selection_size_animation.target <+ multiview.selection_size._1(); + selection_position <- multiview.selection_position_target.map( + f!([groups, scroll_area]((g, p)) { + let group = &groups[usize::from(g)]; + selection_position(*p, group, &scroll_area) + }) + ); + selection_animation.target <+ selection_position; + eval selection_animation.value ((pos) selection.set_position_xy(*pos)); + eval selection_size_animation.value ((pos) selection.size.set(*pos)); + } + selection_animation.target.emit(first_component_group.selection_position_target.value()); + selection_size_animation.target.emit(Vector2(150.0, list_view::entry::HEIGHT)); + selection_animation.skip.emit(()); + selection_size_animation.skip.emit(()); + + // === Forget === + + std::mem::forget(red_slider); + std::mem::forget(green_slider); + std::mem::forget(blue_slider); + std::mem::forget(scroll_area); + std::mem::forget(selection); + 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); + std::mem::forget(groups_layer); + std::mem::forget(selection_layer); + std::mem::forget(selection_mask); +} diff --git a/build-config.yaml b/build-config.yaml index e8abc3a00c3b..48d901b94e6f 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 4.5 MiB +wasm-size-limit: 4.86 MiB required-versions: node: =16.15.0 diff --git a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs index 009f4b5ad19f..64b06fa22053 100644 --- a/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs +++ b/lib/rust/ensogl/app/theme/hardcoded/src/lib.rs @@ -239,9 +239,16 @@ define_themes! { [light:0, dark:1] offset_y = shadow::offset_y , shadow::offset_y; } } + selection { + corners_radius = 10.0, 10.0; + horizontal_padding = 10.0, 10.0; + vertical_padding = 3.0, 3.0; + } background_color_intensity = 0.2, 0.2; + selection_color_intensity = 1.0, 1.0; dimmed_color_intensity = 0.5, 0.5; entry_list { + selected_color = Rgba::white(), Rgba::white(); background = Rgba::new(1.0, 0.0, 0.0, 0.5), Rgba::new(1.0, 0.0, 0.0, 0.5); highlight = Rgba::new(1.0, 0.0, 0.0, 0.5), Rgba::new(1.0, 0.0, 0.0, 0.5); text { diff --git a/lib/rust/ensogl/component/list-view/src/entry/list.rs b/lib/rust/ensogl/component/list-view/src/entry/list.rs index 56b9fc3b994d..0fb345a1386c 100644 --- a/lib/rust/ensogl/component/list-view/src/entry/list.rs +++ b/lib/rust/ensogl/component/list-view/src/entry/list.rs @@ -222,6 +222,11 @@ impl ListData { self.recreate_entries_with_style_prefix(style_prefix); } + /// Get previously set entry params. + pub fn entry_params(&self) -> E::Params { + self.entry_params.borrow().clone_ref() + } + /// Update displayed entries, giving new provider. New entries created by the function have /// their maximum width set to `max_width_px` and use the styles located at the `style_prefix` /// path. diff --git a/lib/rust/ensogl/component/list-view/src/lib.rs b/lib/rust/ensogl/component/list-view/src/lib.rs index 8c8079564cee..e074fa702d40 100644 --- a/lib/rust/ensogl/component/list-view/src/lib.rs +++ b/lib/rust/ensogl/component/list-view/src/lib.rs @@ -497,13 +497,16 @@ where E::Model: Default 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_moved <- mouse.distance.map(|dist| *dist > MOUSE_MOVE_THRESHOLD ).on_true(); + mouse_moved_in <- mouse_in.on_true(); + can_select <- any(&mouse_moved, &mouse_moved_in).gate(&mouse_in); mouse_y_in_scroll <- mouse.position.map(f!([model,scene](pos) { scene.screen_to_object_space(&model.scrolled_area,*pos).y })); mouse_pointed_entry <- mouse_y_in_scroll.map(f!([model](y) entry::List::::entry_at_y_position(*y,model.entries.entry_count()).entry() )); + mouse_selected_entry <- mouse_pointed_entry.sample(&can_select); // === Selected Entry === @@ -547,7 +550,6 @@ where E::Model: Default any(selected_entry_after_jump_down,selected_entry_after_moving_last); selected_entry_after_move <- any(&selected_entry_after_move_up,&selected_entry_after_move_down); - mouse_selected_entry <- mouse_pointed_entry.gate(&mouse_in).gate(&mouse_moved); frp.source.selected_entry <+ selected_entry_after_move; frp.source.selected_entry <+ mouse_selected_entry; @@ -573,8 +575,8 @@ where E::Model: Default // === Selection Size and Position === - selection_y.target <+ frp.selected_entry.map(|id| - id.map_or(0.0,entry::List::::position_y_of_entry) + selection_y.target <+ frp.selected_entry.filter_map(|id| + id.map(entry::List::::position_y_of_entry) ); selection_height.target <+ all_with(&frp.selected_entry, &style.selection_height, |id, h| if id.is_some() {*h} else {-SHAPE_MARGIN} @@ -682,6 +684,11 @@ where E::Model: Default let style_prefix = self.frp.style_prefix.value(); self.model.entries.set_entry_params_and_recreate_entries(params, style_prefix.into()); } + + /// Get previously set entry params. + pub fn entry_params(&self) -> E::Params { + self.model.entries.entry_params() + } } impl display::Object for ListView { diff --git a/lib/rust/ensogl/core/src/animation/frp/animation.rs b/lib/rust/ensogl/core/src/animation/frp/animation.rs index 61d5d44575bc..0485257a3091 100644 --- a/lib/rust/ensogl/core/src/animation/frp/animation.rs +++ b/lib/rust/ensogl/core/src/animation/frp/animation.rs @@ -40,11 +40,14 @@ pub const DEFAULT_PRECISION: f32 = 0.001; #[derivative(Clone(bound = ""))] #[allow(missing_docs)] pub struct Animation { - pub target: frp::Any, - pub precision: frp::Any, - pub skip: frp::Any, - pub value: frp::Stream, - pub on_end: frp::Stream<()>, + pub target: frp::Any, + pub precision: frp::Any, + pub skip: frp::Any, + pub set_spring: frp::Any, + pub set_mass: frp::Any, + pub set_drag: frp::Any, + pub value: frp::Stream, + pub on_end: frp::Stream<()>, } #[allow(missing_docs)] @@ -65,14 +68,20 @@ where mix::Repr: inertia::Value target <- any_mut::(); precision <- any_mut::(); skip <- any_mut::<()>(); + set_spring <- any_mut::(); + set_mass <- any_mut::(); + set_drag <- any_mut::(); eval target ((t) simulator.set_target_value(mix::into_space(t.clone()))); eval precision ((t) simulator.set_precision(*t)); eval_ skip (simulator.skip()); + eval set_spring ((s) simulator.set_spring(*s)); + eval set_mass ((m) simulator.set_mass(*m)); + eval set_drag ((d) simulator.set_drag(*d)); } let value = value_src.into(); let on_end = on_end_src.into(); network.store(&simulator); - Self { target, precision, skip, value, on_end } + Self { target, precision, skip, set_spring, set_mass, set_drag, value, on_end } } /// Constructor. The initial value is provided explicitly. diff --git a/lib/rust/ensogl/core/src/display/scene/layer.rs b/lib/rust/ensogl/core/src/display/scene/layer.rs index 167f23573aad..1b1c43f34974 100644 --- a/lib/rust/ensogl/core/src/display/scene/layer.rs +++ b/lib/rust/ensogl/core/src/display/scene/layer.rs @@ -198,6 +198,11 @@ impl Layer { WeakLayer { model } } + /// Add the display object to this layer without removing it from other layers. + pub fn add(&self, object: impl display::Object) { + object.display_object().add_to_display_layer(self); + } + /// Add the display object to this layer and remove it from any other layers. pub fn add_exclusive(&self, object: impl display::Object) { object.display_object().add_to_display_layer_exclusive(self); diff --git a/lib/rust/ensogl/core/src/system/gpu/shader/compiler.rs b/lib/rust/ensogl/core/src/system/gpu/shader/compiler.rs index d00f76bb10ba..335824a87c67 100644 --- a/lib/rust/ensogl/core/src/system/gpu/shader/compiler.rs +++ b/lib/rust/ensogl/core/src/system/gpu/shader/compiler.rs @@ -16,18 +16,18 @@ //! does not report compilation errors when the context is not available. //! //! # `Compiler` and `Controller` +//! +//! In order to handle WebGL context loss, we divide the responsibilities of compiler +//! management between two objects: a `Compiler`, and a `Controller`. +//! +//! The [`Compiler`] acts as an extension of the context; its state will be lost if the context +//! is lost. It is therefore responsible for keeping track of such information as the +//! currently-running jobs, which will no longer be relevant if context loss occurs. +//! +//! The [`Controller`] is not bound to a context; it holds state that is independent of any +//! particular context object, and uses this state to drive `Compiler` operation. 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`. -/// -/// The [`Compiler`] acts as an extension of the context; its state will be lost if the context -/// is lost. It is therefore responsible for keeping track of such information as the -/// currently-running jobs, which will no longer be relevant if context loss occurs. -/// -/// The [`Controller`] is not bound to a context; it holds state that is independent of any -/// particular context object, and uses this state to drive `Compiler` operation. use crate::prelude::*; use crate::system::gpu::context::native::traits::*; use crate::system::web::traits::*;