Fully visible group name in partially scrolled Component Group View (#3447)

[ci no changelog needed]

[Task link](https://www.pivotaltracker.com/story/show/181725003)

This PR implements a fully visible component group header while scrolling the group (using the ScrollArea).
The header moves in sync with scrolling movements (using new `set_header_pos` FRP input), so it looks like the component group is scrolled. ScrollArea masks the "scrolled" entries above the header. This design allows a fully visible header even though our renderer doesn't support nested layers masking yet.

The screencast:

https://user-images.githubusercontent.com/6566674/168320360-2c2017b2-0ef5-42ce-9c79-82b9641c1d73.mp4


The most recent one, with the updated demo scene from develop:


https://user-images.githubusercontent.com/6566674/168555268-8552c4b0-f887-4388-89a1-e65ddf668be6.mp4

# Important Notes
- I fixed the API of the list view so now it supports non-hardcoded scene layers (previously it did not). I also believe it was implemented incorrectly.
- I've found a [pretty weird bug](https://www.pivotaltracker.com/story/show/182193824): the component group inside the ScrollArea is invisible unless I add some arbitrary shape to the scroll area content. I use a `transparent_circle` for this purpose in the demo scene. The bug is probably related to masking the sublayers, though I wasn't able to reproduce it properly on a simpler example.
- The selection box is removed from the demo scene as agreed with @farmaazon . The correct implementation has proven to be much harder than I expected, and we will implement another approach in a separate PR.
- I also modified the `shadow::Parameters` so that it uses `Var`s instead of plain values.
This commit is contained in:
Ilya Bogdanov 2022-05-17 16:52:08 +03:00 committed by GitHub
parent 6b6b1430bc
commit 4ebf637fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 375 additions and 184 deletions

2
Cargo.lock generated
View File

@ -757,6 +757,7 @@ dependencies = [
"ensogl-core",
"ensogl-hardcoded-theme",
"ensogl-list-view",
"ensogl-scroll-area",
"ensogl-selector",
"ensogl-text-msdf-sys",
"ide-view-component-group",
@ -2355,6 +2356,7 @@ dependencies = [
"ensogl-hardcoded-theme",
"ensogl-label",
"ensogl-list-view",
"ensogl-shadow",
"ensogl-text",
]

View File

@ -11,6 +11,7 @@ crate-type = ["cdylib", "rlib"]
enso-frp = { version = "0.1.0", path = "../../../../../lib/rust/frp" }
ensogl-core = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/core" }
ensogl-gui-component = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/gui" }
ensogl-shadow = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/shadow" }
ensogl-hardcoded-theme = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-list-view = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/list-view" }
ensogl-text = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/text" }

View File

@ -5,6 +5,16 @@
//!
//! To learn more about component groups, see the [Component Browser Design
//! Document](https://github.com/enso-org/design/blob/e6cffec2dd6d16688164f04a4ef0d9dff998c3e7/epics/component-browser/design.md).
//!
//! # Header and its shadow
//!
//! 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
//! 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.
#![recursion_limit = "512"]
// === Standard Linter Configuration ===
@ -28,9 +38,12 @@ use ensogl_core::application::shortcut::Shortcut;
use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::camera::Camera2d;
use ensogl_core::display::scene::layer;
use ensogl_gui_component::component;
use ensogl_hardcoded_theme::application::component_browser::component_group as theme;
use ensogl_list_view as list_view;
use ensogl_shadow as shadow;
use ensogl_text as text;
@ -45,6 +58,23 @@ pub use entry::View as Entry;
// =================
// === Constants ===
// =================
/// The distance (in pixels) from the top border of the component group at which
/// the header shadow reaches its maximum intensity.
///
/// When the header is on top of the component group (default position) the shadow is invisible.
/// Once the header moves down (see [`Frp::set_header_pos`]) we start to linearly increase the
/// intensity of the shadow until it reaches its maximum at [`HEADER_SHADOW_PEAK`] distance from
/// the top. The intensity does not change all the way down from there, but the shadow is clipped at
/// the bottom of the component group so that it will never escape the borders of the group and
/// will not cover any neighboring elements of the scene. (see [`Model::resize`] method)
const HEADER_SHADOW_PEAK: f32 = list_view::entry::HEIGHT / 2.0;
// ==========================
// === Shapes Definitions ===
// ==========================
@ -66,6 +96,37 @@ pub mod background {
}
// === Header Background ===
/// The background of the header. It consists of a rectangle that matches the [`background`] in
/// color and a shadow underneath it.
pub mod header_background {
use super::*;
ensogl_core::define_shape_system! {
above = [background, list_view::background];
(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();
let height: Var<Pixels> = height.into();
let bg = Rect((width.clone(), height.clone())).fill(color);
// We use wider and shorter rect for the shadow because of the visual artifacts that
// will appear otherwise:
// 1. Rounded corners of the shadow are visible if the rect is too narrow. By widening
// it we keep the shadow sharp and flat for the whole width of the header.
// 2. Visual glitching similar to z-fighting occurs on the border of the elements
// when the shadow rect has the exact same size as the background. We shrink the
// height by 1 pixel to avoid it.
let shadow_rect = Rect((width * 2.0, height - 1.0.px()));
let mut shadow_parameters = shadow::parameters_from_style_path(style, theme::header::shadow);
shadow_parameters.size = shadow_parameters.size * shadow_height_multiplier;
let shadow = shadow::from_shape_with_parameters(shadow_rect.into(), shadow_parameters);
(shadow + bg).into()
}
}
}
// === Header Overlay ===
/// The transparent overlay over Component Group View Header, used for capturing mouse events.
@ -95,6 +156,7 @@ struct HeaderGeometry {
padding_left: f32,
padding_right: f32,
padding_bottom: f32,
shadow_size: f32,
}
impl HeaderGeometry {
@ -103,12 +165,13 @@ impl HeaderGeometry {
let padding_left = style.get_number(theme::header::padding::left);
let padding_right = style.get_number(theme::header::padding::right);
let padding_bottom = style.get_number(theme::header::padding::bottom);
let shadow_size = style.get_number(theme::header::shadow::size);
frp::extend! { network
init <- source_();
theme <- all_with5(&init,&height,&padding_left,&padding_right,&padding_bottom,
|_,&height,&padding_left,&padding_right,&padding_bottom|
Self{height,padding_left,padding_right,padding_bottom}
theme <- all_with6(&init,&height,&padding_left,&padding_right,&padding_bottom,&shadow_size,
|_,&height,&padding_left,&padding_right,&padding_bottom,&shadow_size|
Self{height,padding_left,padding_right,padding_bottom,shadow_size}
);
theme_sampler <- theme.sampler();
}
@ -134,6 +197,13 @@ ensogl_core::define_endpoints_2! {
set_color(color::Rgba),
set_dimmed(bool),
set_width(f32),
/// Sets the y-position of the header from the top of the component group.
///
/// It can't move past the top and bottom borders of the component group.
///
/// We use it to simulate entries scrolling with a fixed-position header. Though in fact we
/// move the header down while moving the whole component group up.
set_header_pos(f32),
}
Output {
selected_entry(Option<entry::Id>),
@ -165,8 +235,16 @@ impl component::Frp<Model> for Frp {
entries.entry_count() as f32 * list_view::entry::HEIGHT + header_geom.height
});
out.size <+ all_with(&input.set_width, &height, |w, h| Vector2(*w, *h));
size_and_header_geometry <- all(&out.size, &header_geometry);
eval size_and_header_geometry(((size, hdr_geom)) model.resize(*size, *hdr_geom));
size_and_header_geometry <- all3(&out.size, &header_geometry, &input.set_header_pos);
eval size_and_header_geometry(((size, hdr_geom, hdr_pos))
model.resize(*size, *hdr_geom, *hdr_pos)
);
// === Show/hide shadow ===
shadow <- input.set_header_pos.map(|p| (*p / HEADER_SHADOW_PEAK).min(1.0));
eval shadow((v) model.header_background.shadow_height_multiplier.set(*v));
}
@ -206,13 +284,14 @@ impl component::Frp<Model> for Frp {
header_text_size <- all(&header_text_size, &init)._0();
model.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)) {
(text, (size, hdr_geom, _)) {
model.header_text.replace(text.clone());
model.update_header_width(*size, *hdr_geom);
})
);
model.header.set_default_color <+ header_color;
eval bg_color((c) model.background.color.set(c.into()));
eval bg_color((c) model.header_background.color.set(c.into()));
}
@ -274,6 +353,45 @@ impl component::Frp<Model> for Frp {
// ==============
// === Layers ===
// ==============
/// 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.
#[derive(Debug, Clone, CloneRef)]
pub struct Layers {
background: layer::Layer,
text: layer::Layer,
header: layer::Layer,
header_text: layer::Layer,
}
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);
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 ===
// =============
@ -281,12 +399,13 @@ impl component::Frp<Model> for Frp {
/// The Model of the [`View`] component.
#[derive(Clone, CloneRef, Debug)]
pub struct Model {
display_object: display::object::Instance,
header: text::Area,
header_text: Rc<RefCell<String>>,
header_overlay: header_overlay::View,
background: background::View,
entries: list_view::ListView<Entry>,
display_object: display::object::Instance,
header: text::Area,
header_background: header_background::View,
header_text: Rc<RefCell<String>>,
header_overlay: header_overlay::View,
background: background::View,
entries: list_view::ListView<Entry>,
}
impl display::Object for Model {
@ -303,30 +422,47 @@ impl component::Model for Model {
fn new(app: &Application, logger: &Logger) -> Self {
let header_text = default();
let display_object = display::object::Instance::new(&logger);
let background = background::View::new(&logger);
let header = text::Area::new(app);
let header_overlay = header_overlay::View::new(&logger);
let background = background::View::new(&logger);
let header_background = header_background::View::new(&logger);
let header = text::Area::new(app);
let entries = app.new_view::<list_view::ListView<Entry>>();
entries.set_style_prefix(entry::STYLE_PATH);
entries.set_background_color(HOVER_COLOR);
entries.show_background_shadow(false);
entries.set_background_corners_radius(0.0);
entries.hide_selection();
display_object.add_child(&background);
display_object.add_child(&header_background);
display_object.add_child(&header);
display_object.add_child(&header_overlay);
display_object.add_child(&entries);
let label_layer = &app.display.default_scene.layers.label;
header.add_to_scene_layer(label_layer);
entries.hide_selection();
Model { display_object, header, header_text, header_overlay, background, entries }
Model {
display_object,
header_overlay,
header,
header_text,
background,
header_background,
entries,
}
}
}
impl Model {
fn resize(&self, size: Vector2, header_geometry: HeaderGeometry) {
/// Assign a set of layers to render the component group in. 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);
}
fn resize(&self, size: Vector2, header_geometry: HeaderGeometry, header_pos: f32) {
// === Background ===
self.background.size.set(size);
@ -339,13 +475,34 @@ impl Model {
let header_text_height = self.header.height.value();
let header_padding_bottom = header_geometry.padding_bottom;
let header_height = header_geometry.height;
let header_center_y = size.y / 2.0 - header_height / 2.0;
let header_bottom_y = header_center_y - header_height / 2.0;
let half_header_height = header_height / 2.0;
let header_center_y = size.y / 2.0 - half_header_height;
let header_center_y = header_center_y - header_pos;
let header_center_y = header_center_y.max(-size.y / 2.0 + half_header_height);
let header_center_y = header_center_y.min(size.y / 2.0 - half_header_height);
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.update_header_width(size, header_geometry);
// === Header Background ===
self.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
// `shadow_size` and `header_height` to calculate the final `size` of the
// `header_background` shape. We use `shadow_size * 2.0`, because the shadow extends by
// `shadow_size` in each direction around the base shape, not only down. We cap the
// `shadow_size` by the distance to the bottom of the component group so that the shadow is
// not visible outside the component group background.
let shadow_size = shadow_size.min(distance_to_bottom);
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);
// === Header Overlay ===
self.header_overlay.set_position_y(header_center_y);

View File

@ -12,6 +12,7 @@ enso-frp = { path = "../../../../../lib/rust/frp" }
ensogl-core = { path = "../../../../../lib/rust/ensogl/core" }
ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ensogl-scroll-area = { path = "../../../../../lib/rust/ensogl/component/scroll-area" }
ensogl-list-view = { path = "../../../../../lib/rust/ensogl/component/list-view" }
ensogl-text-msdf-sys = { path = "../../../../../lib/rust/ensogl/component/text/msdf-sys" }
ide-view-component-group = { path = "../../component-browser/component-group" }

View File

@ -12,6 +12,7 @@
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
use ensogl_core::display::shape::*;
use ensogl_core::prelude::*;
use wasm_bindgen::prelude::*;
@ -19,11 +20,10 @@ use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display::object::ObjectOps;
use ensogl_core::frp;
use ensogl_core::Animation;
use ensogl_hardcoded_theme as theme;
use ensogl_list_view as list_view;
use ensogl_scroll_area::ScrollArea;
use ensogl_selector as selector;
use ensogl_selector::Bounds;
use ensogl_text_msdf_sys::run_once_initialized;
use ide_view_component_group as component_group;
use list_view::entry::AnyModelProvider;
@ -75,12 +75,6 @@ impl MockEntries {
})
}
fn set_count(&self, count: usize) {
if self.entries.len() >= count {
self.count.set(count);
}
}
fn get_entry(&self, id: list_view::entry::Id) -> Option<component_group::entry::Model> {
self.entries.get(id).cloned()
}
@ -98,6 +92,56 @@ impl list_view::entry::ModelProvider<component_group::Entry> for MockEntries {
// ==================================
// === 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<Vec<component_group::View>>,
}
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 ===
// ========================
@ -105,41 +149,19 @@ impl list_view::entry::ModelProvider<component_group::Entry> for MockEntries {
// === Helpers ====
fn create_selection() -> list_view::selection::View {
let selection = list_view::selection::View::new(Logger::new("Selection"));
selection.size.set(Vector2(150.0, list_view::entry::HEIGHT));
selection.corner_radius.set(5.0);
selection
}
fn create_component_group(app: &Application) -> component_group::View {
fn create_component_group(
app: &Application,
header: &str,
layers: &component_group::Layers,
) -> component_group::View {
let component_group = app.new_view::<component_group::View>();
app.display.add_child(&component_group);
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 wide_component_group(app: &Application) -> component_group::wide::View {
let wide_component_group = app.new_view::<component_group::wide::View>();
app.display.add_child(&wide_component_group);
wide_component_group.set_position_x(250.0);
wide_component_group.set_width(450.0);
wide_component_group.set_background_color(color::Rgba(0.927, 0.937, 0.913, 1.0));
wide_component_group.set_no_items_label_text("No local variables.");
wide_component_group
}
fn items_slider(app: &Application) -> selector::NumberPicker {
let slider = app.new_view::<selector::NumberPicker>();
app.display.add_child(&slider);
slider.frp.resize(Vector2(400.0, 50.0));
slider.frp.allow_click_selection(true);
slider.frp.set_bounds(Bounds::new(0.0, 15.0));
slider.set_position_y(-250.0);
slider.frp.set_caption(Some("Items count:".to_string()));
slider
}
fn color_component_slider(app: &Application, caption: &str) -> selector::NumberPicker {
let slider = app.new_view::<selector::NumberPicker>();
app.display.add_child(&slider);
@ -153,83 +175,76 @@ fn color_component_slider(app: &Application, caption: &str) -> selector::NumberP
// === 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 items_slider = items_slider(app);
let network = frp::Network::new("Component Group Debug Scene");
let selection = create_selection();
let selection_animation = Animation::<Vector2>::new(&network);
let wide_selection = create_selection();
let wide_selection_animation = Animation::<Vector2>::new(&network);
let component_group = create_component_group(app);
component_group.add_child(&selection);
let scroll_area = ScrollArea::new(app);
scroll_area.set_position_xy(Vector2(0.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";
component_group.set_header(group_name.to_string());
component_group.set_position_x(-150.0);
let dimmed_component_group = create_component_group(app);
dimmed_component_group.set_dimmed(true);
dimmed_component_group.set_header("Input / Output".to_string());
dimmed_component_group.set_position_x(-350.0);
let wide_component_group = wide_component_group(app);
wide_component_group.add_child(&wide_selection);
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);
scroll_area.content().add_child(&first_component_group);
scroll_area.content().add_child(&second_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 ===
frp::extend! { network
selection_animation.target <+ component_group.selection_position_target;
eval selection_animation.value ((pos) selection.set_position_xy(*pos));
eval component_group.suggestion_accepted ([](id) DEBUG!("Accepted Suggestion {id}"));
eval component_group.expression_accepted ([](id) DEBUG!("Accepted Expression {id}"));
eval_ component_group.header_accepted ([] DEBUG!("Accepted Header"));
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"));
}
selection_animation.target.emit(component_group.selection_position_target.value());
selection_animation.skip.emit(());
ComponentGroupController::init(
&[first_component_group.clone_ref(), second_component_group.clone_ref()],
&network,
&scroll_area,
);
// === Wide Component Group ===
wide_selection.color.set(color::Rgba(0.527, 0.554, 0.18, 1.0).into());
frp::extend! { network
wide_selection_animation.target <+ wide_component_group.selection_position_target;
eval wide_selection_animation.value ((pos) wide_selection.set_position_xy(*pos));
eval wide_component_group.suggestion_accepted ([](id) DEBUG!("[Wide] Accepted Suggestion {id}"));
eval wide_component_group.expression_accepted ([](id) DEBUG!("[Wide] Accepted Expression {id}"));
no_entries <- wide_component_group.entry_count.map(|count| *count == 0);
hide_selection <- no_entries.on_true();
show_selection <- no_entries.on_false();
eval_ hide_selection (wide_selection.color.set(color::Rgba::transparent().into()));
eval_ show_selection (wide_selection.color.set(color::Rgba(0.527, 0.554, 0.18, 1.0).into()));
}
wide_selection_animation.target.emit(wide_component_group.selection_position_target.value());
wide_selection_animation.skip.emit(());
// === Setup slider to change entry count ===
let mock_entries = MockEntries::new(25);
let mock_entries = MockEntries::new(15);
let model_provider = AnyModelProvider::from(mock_entries.clone_ref());
frp::extend! { network
int_value <- items_slider.frp.output.value.map(|v| *v as usize);
eval int_value([component_group, dimmed_component_group, wide_component_group](i) {
mock_entries.set_count(*i);
component_group.set_entries(model_provider.clone_ref());
dimmed_component_group.set_entries(model_provider.clone_ref());
wide_component_group.set_entries(model_provider.clone_ref());
});
}
items_slider.frp.set_value(10.0);
// Select the bottom left entry at the start.
let first_column = component_group::wide::ColumnId::new(0);
wide_component_group.select_entry(first_column, 0);
first_component_group.set_entries(model_provider.clone_ref());
second_component_group.set_entries(model_provider);
// === Color sliders ===
@ -259,9 +274,8 @@ fn init(app: &Application) {
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));
component_group.set_color <+ color;
dimmed_component_group.set_color <+ color;
eval color((c) selection.color.set(c.into()));
first_component_group.set_color <+ color;
second_component_group.set_color <+ color;
}
init.emit(());
@ -271,11 +285,9 @@ fn init(app: &Application) {
std::mem::forget(red_slider);
std::mem::forget(green_slider);
std::mem::forget(blue_slider);
std::mem::forget(items_slider);
std::mem::forget(scroll_area);
std::mem::forget(network);
std::mem::forget(selection);
std::mem::forget(component_group);
std::mem::forget(dimmed_component_group);
std::mem::forget(wide_component_group);
std::mem::forget(wide_selection);
std::mem::forget(first_component_group);
std::mem::forget(second_component_group);
std::mem::forget(layers);
}

View File

@ -27,6 +27,7 @@ use ensogl_component::text;
use ensogl_hardcoded_theme as theme;
// =================
// === Constants ===
// =================

View File

@ -97,7 +97,7 @@ impl ProjectList {
app.display.default_scene.layers.panel.add_exclusive(&display_object);
caption.set_content("Open Project");
caption.add_to_scene_layer(&app.display.default_scene.layers.panel_text);
list.set_label_layer(app.display.default_scene.layers.panel_text.id());
list.set_label_layer(&app.display.default_scene.layers.panel_text);
ensogl::shapes_order_dependencies! {
app.display.default_scene => {

View File

@ -135,7 +135,7 @@ impl Model {
let style = StyleWatch::new(&app.display.default_scene.style_sheet);
let action_list_gap_path = ensogl_hardcoded_theme::application::searcher::action_list_gap;
let action_list_gap = style.get_number_or(action_list_gap_path, 0.0);
list.set_label_layer(scene.layers.node_searcher_text.id());
list.set_label_layer(&scene.layers.node_searcher_text);
list.set_position_y(-action_list_gap);
list.set_position_x(ACTION_LIST_X);
documentation.set_position_x(DOCUMENTATION_X);

View File

@ -193,6 +193,15 @@ define_themes! { [light:0, dark:1]
right = 2.5, 2.5;
bottom = 5.0, 5.0;
}
shadow = shadow , shadow;
shadow {
size = 27.0 , 27.0;
spread = shadow::spread , shadow::spread;
fading = shadow::fading , shadow::fading;
exponent = shadow::exponent , shadow::exponent;
offset_x = shadow::offset_x , shadow::offset_x;
offset_y = shadow::offset_y , shadow::offset_y;
}
}
entries {
text {

View File

@ -7,7 +7,8 @@ use crate::entry::Entry;
use ensogl_core::application::Application;
use ensogl_core::display;
use ensogl_core::display::scene::layer::LayerId;
use ensogl_core::display::scene::layer::Layer;
use ensogl_core::display::scene::layer::WeakLayer;
use ensogl_core::display::style;
@ -72,7 +73,7 @@ pub struct ListData<E, P> {
entries_range: Rc<CloneCell<Range<entry::Id>>>,
entry_params: Rc<RefCell<P>>,
provider: Rc<CloneRefCell<entry::AnyModelProvider<E>>>,
label_layer: Rc<Cell<LayerId>>,
label_layer: Rc<RefCell<WeakLayer>>,
}
impl<E, P: Default> ListData<E, P> {
@ -85,7 +86,7 @@ impl<E, P: Default> ListData<E, P> {
let entry_params = default();
let display_object = display::object::Instance::new(&logger);
let provider = default();
let label_layer = Rc::new(Cell::new(app.display.default_scene.layers.label.id()));
let label_layer = Rc::new(RefCell::new(app.display.default_scene.layers.label.downgrade()));
Self {
logger,
app,
@ -148,20 +149,11 @@ impl<E, P> ListData<E, P> {
impl<E: Entry, P> ListData<E, P> {
/// Sets the scene layer where the labels will be placed.
pub fn set_label_layer(&self, label_layer: LayerId) {
let layers = &self.app.display.default_scene.layers;
if let Some(layer) = layers.get_sublayer(self.label_layer.get()) {
for entry in &*self.entries.borrow() {
entry.entry.set_label_layer(&layer);
}
} else {
error!(
self.logger,
"Cannot set layer {label_layer:?} for labels: the layer does not \
exist in the scene"
);
pub fn set_label_layer(&self, label_layer: &Layer) {
for entry in &*self.entries.borrow() {
entry.entry.set_label_layer(label_layer);
}
self.label_layer.set(label_layer);
self.label_layer.replace(label_layer.downgrade());
}
}
@ -268,7 +260,7 @@ impl<E: Entry> ListData<E, E::Params> {
fn create_new_entry(&self, style_prefix: &style::Path) -> DisplayedEntry<E> {
let layers = &self.app.display.default_scene.layers;
let layer = layers.get_sublayer(self.label_layer.get()).unwrap_or_else(|| {
let layer = self.label_layer.borrow().upgrade().unwrap_or_else(|| {
error!(
self.logger,
"Cannot set layer {self.label_layer:?} for labels: the layer does \

View File

@ -41,7 +41,7 @@ use ensogl_core::application::shortcut;
use ensogl_core::application::Application;
use ensogl_core::data::color;
use ensogl_core::display;
use ensogl_core::display::scene::layer::LayerId;
use ensogl_core::display::scene::layer::Layer;
use ensogl_core::display::shape::*;
use ensogl_core::Animation;
use ensogl_hardcoded_theme as theme;
@ -581,7 +581,7 @@ where E::Model: Default
}
/// Sets the scene layer where the labels will be placed.
pub fn set_label_layer(&self, layer: LayerId) {
pub fn set_label_layer(&self, layer: &Layer) {
self.model.entries.set_label_layer(layer);
}

View File

@ -229,4 +229,9 @@ impl ScrollArea {
pub fn content(&self) -> &display::object::Instance {
&self.model.content
}
/// A scene layer containing the content of the ScrollArea.
pub fn content_layer(&self) -> &layer::Layer {
&self.model.display_object.layer.masked_layer
}
}

View File

@ -30,15 +30,16 @@ use ensogl_hardcoded_theme as theme;
/// Defines the appearance of a shadow
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub struct Parameters {
base_color: color::Rgba,
fading: color::Rgba,
size: f32,
spread: f32,
exponent: f32,
offset_x: f32,
offset_y: f32,
pub base_color: Var<color::Rgba>,
pub fading: Var<color::Rgba>,
pub size: Var<f32>,
pub spread: Var<f32>,
pub exponent: Var<f32>,
pub offset_x: Var<f32>,
pub offset_y: Var<f32>,
}
/// Loads shadow parameters from the given style, at the given path. The structure of the style
@ -46,18 +47,18 @@ pub struct Parameters {
pub fn parameters_from_style_path(style: &StyleWatch, path: impl Into<style::Path>) -> Parameters {
let path: style::Path = path.into();
Parameters {
base_color: style.get_color(&path),
fading: style.get_color(&path.sub("fading")),
size: style.get_number(&path.sub("size")),
spread: style.get_number(&path.sub("spread")),
exponent: style.get_number(&path.sub("exponent")),
offset_x: style.get_number(&path.sub("offset_x")),
offset_y: style.get_number(&path.sub("offset_y")),
base_color: style.get_color(&path).into(),
fading: style.get_color(&path.sub("fading")).into(),
size: style.get_number(&path.sub("size")).into(),
spread: style.get_number(&path.sub("spread")).into(),
exponent: style.get_number(&path.sub("exponent")).into(),
offset_x: style.get_number(&path.sub("offset_x")).into(),
offset_y: style.get_number(&path.sub("offset_y")).into(),
}
}
/// Utility method to retrieve the size of the shadow. can be used to determine shape padding etc.
pub fn size(style: &StyleWatch) -> f32 {
pub fn size(style: &StyleWatch) -> Var<f32> {
let parameters = parameters_from_style_path(style, theme::shadow);
parameters.size
}
@ -93,15 +94,13 @@ pub fn from_shape_with_parameters_and_alpha(
parameters: Parameters,
alpha: &Var<f32>,
) -> AnyShape {
let grow = Var::<f32>::from(parameters.size);
let grow = parameters.size.clone();
let shadow = base_shape.grow(grow);
let shadow = shadow.translate((parameters.offset_x.px(), parameters.offset_y.px()));
let base_color = Var::<color::Rgba>::from(parameters.base_color);
let base_color = base_color.multiply_alpha(alpha);
let base_color = parameters.base_color.multiply_alpha(alpha);
let fading_color = Var::<color::Rgba>::from(parameters.fading);
let fading_color = fading_color.multiply_alpha(alpha);
let fading_color = parameters.fading.multiply_alpha(alpha);
let shadow_color = color::gradient::Linear::<Var<color::LinearRgba>>::new(
fading_color.into_linear(),

View File

@ -3,6 +3,7 @@
use crate::prelude::*;
use crate::system::gpu::shader::glsl::traits::*;
use crate::display::shape::Var;
use crate::system::gpu::shader::glsl::Glsl;
@ -96,9 +97,9 @@ pub const DEFAULT_DISTANCE_GRADIENT_SIZE: f32 = 10.0;
#[derive(Clone, Debug)]
pub struct SdfSampler<Gradient> {
/// The distance from the shape border at which the gradient should start.
pub spread: f32,
pub spread: Var<f32>,
/// The size of the gradient in the SDF space.
pub size: f32,
pub size: Var<f32>,
/// The gradient slope modifier. Defines how fast the gradient values change.
pub slope: Slope,
/// The underlying gradient.
@ -109,21 +110,21 @@ impl<Gradient> SdfSampler<Gradient> {
/// Constructs a new gradient with `spread` and `size` set to
/// `DEFAULT_DISTANCE_GRADIENT_SPREAD` and `DEFAULT_DISTANCE_GRADIENT_SIZE` respectively.
pub fn new(gradient: Gradient) -> Self {
let spread = DEFAULT_DISTANCE_GRADIENT_SPREAD;
let size = DEFAULT_DISTANCE_GRADIENT_SIZE;
let spread = DEFAULT_DISTANCE_GRADIENT_SPREAD.into();
let size = DEFAULT_DISTANCE_GRADIENT_SIZE.into();
let slope = Slope::Smooth;
Self { spread, size, slope, gradient }
}
/// Constructor setter for the `spread` field.
pub fn spread(mut self, t: f32) -> Self {
self.spread = t;
pub fn spread(mut self, t: impl Into<Var<f32>>) -> Self {
self.spread = t.into();
self
}
/// Constructor setter for the `size` field.
pub fn size(mut self, t: f32) -> Self {
self.size = t;
pub fn size(mut self, t: impl Into<Var<f32>>) -> Self {
self.size = t.into();
self
}
@ -153,7 +154,7 @@ impls! {[G:RefInto<Glsl>] From<&SdfSampler<G>> for Glsl {
let size = iformat!("{g.size.glsl()}");
let offset = iformat!("-shape.sdf.distance + {g.spread.glsl()}");
let norm = iformat!("clamp(({offset}) / ({size}))");
let t = match g.slope {
let t = match &g.slope {
Slope::Linear => norm,
Slope::Smooth => iformat!("smoothstep(0.0,1.0,{norm})"),
Slope::Exponent(exp) => iformat!("pow({norm},{exp.glsl()})"),
@ -167,7 +168,7 @@ impls! {[G:RefInto<Glsl>] From<&SdfSampler<G>> for Glsl {
macro_rules! define_slope {
($($(#$docs:tt)* $name:ident $(($($arg:ident : $arg_type:ty),*))? $fn_name:ident),* $(,)?) => {
/// Defines how fast gradient values change.
#[derive(Copy,Clone,Debug)]
#[derive(Clone,Debug)]
pub enum Slope {
$($(#$docs)* $name $(($($arg_type),*))? ),*
}
@ -191,8 +192,8 @@ define_slope! {
Smooth smooth,
/// Raises the normalized gradient offset to the given power and uses it as the interpolation
/// step.
Exponent(exp:f32) exponent,
Exponent(exp:Var<f32>) exponent,
/// Raises the normalized gradient offset to the given power and uses it as the interpolation
/// step.
InvExponent(exp:f32) inv_exponent,
InvExponent(exp:Var<f32>) inv_exponent,
}

View File

@ -40,6 +40,16 @@ impl PixelDistance for f32 {
}
}
impl PixelDistance for Var<f32> {
type Output = Var<Pixels>;
fn px(&self) -> Self::Output {
match self {
Var::Static(v) => v.px(),
Var::Dynamic(v) => Var::Dynamic(v.clone()),
}
}
}
// ===============

View File

@ -33,6 +33,7 @@ use enso_profiler_data as data;
use std::collections;
// ============
// === main ===
// ============