Component Group Entry with icons and text highlighting. (#3459)

This PR extends the Component Group Entry with icon and option to highlight the text. Here the convert has highlighted "con".

https://user-images.githubusercontent.com/3919101/169046537-4f8b823c-322e-40dc-8abb-24d1d7092341.mp4


### Important Notes

Although this PR includes effort for adjusting Component Group style to better reflect the design, it is not entirely finished: the selection still works badly and will be fixed in another PR.
This commit is contained in:
Adam Obuchowicz 2022-05-24 09:48:19 +02:00 committed by GitHub
parent 297ef4251c
commit eef0738f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1729 additions and 1582 deletions

15
Cargo.lock generated
View File

@ -1467,6 +1467,7 @@ name = "debug-scene-component-group"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"enso-frp", "enso-frp",
"enso-text",
"ensogl-core", "ensogl-core",
"ensogl-hardcoded-theme", "ensogl-hardcoded-theme",
"ensogl-list-view", "ensogl-list-view",
@ -1477,6 +1478,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "debug-scene-icons"
version = "0.1.0"
dependencies = [
"ensogl",
"ensogl-hardcoded-theme",
"ide-view-component-group",
"wasm-bindgen",
]
[[package]] [[package]]
name = "debug-scene-interface" name = "debug-scene-interface"
version = "0.1.0" version = "0.1.0"
@ -1811,6 +1822,7 @@ name = "enso-debug-scene"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"debug-scene-component-group", "debug-scene-component-group",
"debug-scene-icons",
"debug-scene-interface", "debug-scene-interface",
"debug-scene-visualization", "debug-scene-visualization",
] ]
@ -3620,13 +3632,14 @@ name = "ide-view-component-group"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"enso-frp", "enso-frp",
"ensogl-core", "ensogl",
"ensogl-gui-component", "ensogl-gui-component",
"ensogl-hardcoded-theme", "ensogl-hardcoded-theme",
"ensogl-label", "ensogl-label",
"ensogl-list-view", "ensogl-list-view",
"ensogl-shadow", "ensogl-shadow",
"ensogl-text", "ensogl-text",
"failure",
] ]
[[package]] [[package]]

View File

@ -9,10 +9,11 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
enso-frp = { version = "0.1.0", path = "../../../../../lib/rust/frp" } enso-frp = { version = "0.1.0", path = "../../../../../lib/rust/frp" }
ensogl-core = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/core" } ensogl = { version = "0.1.0", path = "../../../../../lib/rust/ensogl" }
ensogl-gui-component = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/gui" } 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-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-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-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" } ensogl-text = { version = "0.1.0", path = "../../../../../lib/rust/ensogl/component/text" }
ensogl-label = { path = "../../../../../lib/rust/ensogl/component/label/" } ensogl-label = { path = "../../../../../lib/rust/ensogl/component/label/" }
failure = { version = "0.1.6" }

View File

@ -2,18 +2,22 @@
//! //!
//! The entry data is represented by the [`Model`] and visualized by the [`View`]. //! The entry data is represented by the [`Model`] and visualized by the [`View`].
use ensogl_core::prelude::*; use crate::prelude::*;
use crate::icon;
use crate::Colors;
use enso_frp as frp; use enso_frp as frp;
use ensogl_core::application::Application; use ensogl::application::Application;
use ensogl_core::data::color::Rgba; use ensogl::data::color;
use ensogl_core::display; use ensogl::display;
use ensogl_core::display::scene::Layer; use ensogl::display::scene::Layer;
use ensogl_core::display::style; use ensogl::display::style;
use ensogl_core::display::Scene; use ensogl::display::Scene;
use ensogl_hardcoded_theme::application::component_browser::component_group::entries as theme; use ensogl_hardcoded_theme::application::component_browser::component_group::entry_list as theme;
use ensogl_list_view as list_view; use ensogl_list_view as list_view;
use ensogl_list_view::entry::Label; use ensogl_list_view::entry::GlyphHighlightedLabel;
use ensogl_list_view::entry::GlyphHighlightedLabelModel;
@ -41,9 +45,19 @@ pub const STYLE_PATH: &str = theme::HERE.str;
/// Data underlying an entry in a component group view. /// Data underlying an entry in a component group view.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, Debug, Default, From)] #[derive(Clone, Debug, Default)]
pub struct Model { pub struct Model {
pub label: String, pub icon: icon::Id,
pub highlighted_text: GlyphHighlightedLabelModel,
}
impl From<String> for Model {
fn from(label: String) -> Self {
Model {
icon: icon::Id::Star,
highlighted_text: GlyphHighlightedLabelModel { label, highlighted: default() },
}
}
} }
impl From<&str> for Model { impl From<&str> for Model {
@ -63,17 +77,22 @@ impl From<&str> for Model {
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)] #[derive(Clone, CloneRef, Debug)]
pub struct Params { pub struct Params {
pub color: frp::Sampler<Rgba>, pub colors: Colors,
} }
impl Default for Params { impl Default for Params {
fn default() -> Self { fn default() -> Self {
let network = frp::Network::new("component_group::Params::default"); let network = frp::Network::new("component_browser::entry::Params::default");
frp::extend! { network frp::extend! { network
color_source <- source::<Rgba>(); icon_strong <- source::<color::Rgba>().sampler();
color <- color_source.sampler(); icon_weak <- source::<color::Rgba>().sampler();
header_text <- source::<color::Rgba>().sampler();
entry_text <- source::<color::Rgba>().sampler();
background <- source::<color::Rgba>().sampler();
} }
Self { color }
let colors = Colors { icon_strong, icon_weak, header_text, entry_text, background };
Self { colors }
} }
} }
@ -83,43 +102,97 @@ impl Default for Params {
// === View === // === View ===
// ============ // ============
// === CurrentIcon ===
/// The structure keeping a currently displayed incon in Component Group Entry [`View`]. Remembering
/// id allows us to skip icon generation when not changed.
#[derive(Debug, Default)]
struct CurrentIcon {
shape: Option<icon::Any>,
id: Option<icon::Id>,
}
// === View ===
/// A visual representation of a [`Model`]. /// A visual representation of a [`Model`].
#[derive(Clone, CloneRef, Debug)] #[derive(Clone, CloneRef, Debug)]
pub struct View { pub struct View {
logger: Logger, logger: Logger,
display_object: display::object::Instance, display_object: display::object::Instance,
label: Label, icon: Rc<RefCell<CurrentIcon>>,
max_width_px: frp::Source<f32>,
icon_strong_color: frp::Sampler<color::Rgba>,
icon_weak_color: frp::Sampler<color::Rgba>,
label: GlyphHighlightedLabel,
} }
impl list_view::Entry for View { impl list_view::Entry for View {
type Model = Model; type Model = Model;
type Params = Params; type Params = Params;
fn new(app: &Application, style_prefix: &style::Path, params: &Self::Params) -> Self { fn new(app: &Application, style_prefix: &style::Path, Params { colors }: &Params) -> Self {
let logger = Logger::new("component-group::Entry"); let logger = Logger::new("component-group::Entry");
let display_object = display::object::Instance::new(&logger); let display_object = display::object::Instance::new(&logger);
let label = Label::new(app, style_prefix); let icon: Rc<RefCell<CurrentIcon>> = default();
let label = GlyphHighlightedLabel::new(app, style_prefix, &());
display_object.add_child(&label); display_object.add_child(&label);
let network = &label.network; let network = &label.inner.network;
let color = params.color.clone(); let style = &label.inner.style_watch;
let label_frp = &label.label.frp; let icon_text_gap = style.get_number(theme::icon_text_gap);
frp::extend! { network frp::extend! { network
init <- source_(); init <- source_();
color <- all(&color, &init)._0(); max_width_px <- source::<f32>();
label_frp.set_default_color <+ color; icon_text_gap <- all(&icon_text_gap, &init)._0();
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));
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();
} }
init.emit(()); init.emit(());
Self {
Self { logger, display_object, label } logger,
display_object,
icon,
max_width_px,
icon_strong_color,
icon_weak_color,
label,
}
} }
fn update(&self, model: &Self::Model) { fn update(&self, model: &Self::Model) {
self.label.update(&model.label); 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);
}
} }
fn set_max_width(&self, max_width_px: f32) { fn set_max_width(&self, max_width_px: f32) {
self.label.set_max_width(max_width_px); self.max_width_px.emit(max_width_px);
} }
fn set_label_layer(&self, label_layer: &Layer) { fn set_label_layer(&self, label_layer: &Layer) {

View File

@ -0,0 +1,921 @@
//! All icons that are used in the Component Browser.
mod common_part;
mod define_macro;
use crate::icon::common_part::*;
use crate::prelude::*;
use crate::display;
use crate::display::Scene;
use ensogl::display::object::ObjectOps;
use ensogl::display::shape::compound::path::path;
use ensogl::display::Attribute;
use ensogl_hardcoded_theme::application::searcher::icons as theme;
use std::f32::consts::PI;
// =================
// === Constants ===
// =================
/// The width and height of all icon.
pub const SIZE: f32 = 16.0;
/// This constant exists for development purposes only, and is published for debug scene.
/// Due to a rendering error, shapes appear too big when the camera is zoomed in very closely.
/// (Documented here: https://github.com/enso-org/ide/issues/1698)
/// In the user interface, this is not a big problem, since icon are usually shown at lower zoom
/// levels. But it is a problem during development of icon when it becomes necessary to inspect
/// them closely. In those situations, one can apply `.shrink(0.35)` to shapes to compensate for the
/// bug and make them appear at the correct size while the camera is zoomed in. But that work-around
/// will make them appear too thin on the default zoom level.
///
/// To make it easy to turn this shrinking on and off before and after working on icon, we define
/// the constant `SHRINK_AMOUNT` and apply `.shrink(SHRINK_AMOUNT.px())` to all icon. In every
/// commit, `SHRINK_AMOUNT` should be set to 0.0 to make icon look best in the user interface. But
/// during work on the icon, it can temporarily be set to 0.35.
pub const SHRINK_AMOUNT: f32 = 0.0;
// ==============
// === Errors ===
// ==============
/// Error occuring when we try parse string being an invalid icon name to icon Id.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Unknown icon '{}'.", name)]
pub struct UnknownIcon {
/// The copied icon name from parsed string.
pub name: String,
}
// ===============
// === AnyIcon ===
// ===============
/// One of the icon generated from the [`define_icons`] macro. Returned from `create_shape` method.
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Any {
/// The underlying icon shape.
#[derivative(Debug = "ignore")]
pub view: Box<dyn display::Object>,
/// Strong (darker, or more contrasting) color parameter.
pub strong_color: DynamicParam<Attribute<Vector4>>,
/// Weak (lighter, or less contrasting) color parameter.
pub weak_color: DynamicParam<Attribute<Vector4>>,
}
impl display::Object for Any {
fn display_object(&self) -> &display::object::Instance<Scene> {
self.view.display_object()
}
}
// =============
// === Icons ===
// =============
crate::define_icons! {
/// A five-pointed star.
pub mod star(Star) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let shape = FiveStar(7.0.px(),0.447);
let shape = shape.fill(style.get_color(theme::favorites));
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A rounded rectangle with an arrow pointing in from the left.
pub mod data_input(DataInput) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Border ===
let border =
Rect((10.0.px(),13.0.px())).corners_radius(1.5.px()).translate_x(1.5.px());
// Taking just an outline.
let border = &border - border.shrink(1.0.px());
// Creating a gap for the arrow to pass through.
let gap = Rect((2.0.px(),4.0.px())).translate_x((-3.0).px());
let border = border - gap;
// === Arrow ===
let arrow =
arrow(11.0,1.0,4.0,5.0).rotate((PI/2.0).radians()).translate_x(4.0.px());
// === Shape ===
let shape = border + arrow;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A rounded rectangle with an arrow pointing out to the right.
pub mod data_output(DataOutput) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Border ===
let border = Rect((9.0.px(),13.0.px())).corners_radius(1.5.px());
let border = border.translate_x((-2.5).px());
// Taking just an outline.
let border = &border - border.shrink(1.0.px());
// Creating a gap for the arrow to pass through.
let gap = Rect((2.0.px(),4.0.px())).translate_x((1.5).px());
let border = border - gap;
// === Arrow ===
let arrow =
arrow(11.0,1.0,4.0,5.0).rotate((PI/2.0).radians()).translate_x(8.0.px());
// === Shape ===
let shape = border + arrow;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A rounded rectangle with the letter "A" and a text cursor.
pub mod text_input(TextInput) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Border ===
let border = Rect((16.0.px(),11.0.px())).corners_radius(1.5.px());
// Using just the outline.
let border = &border - border.shrink(1.0.px());
// Creating a gap for the cursor.
let gap = Rect((3.0.px(),13.0.px())).translate_x((3.5).px());
let border = border - gap;
// === Cursor ===
let cursor = cursor().translate_x(3.5.px());
// === Letter ===
// We construct the letter "A", consisting of a diagonal stroke on the left,
// a diagonal stroke on the right and a horizontal bar in the middle.
let left_stroke = Segment((0.0.px(),2.5.px()),((-2.5).px(),(-2.5).px()),1.0.px());
let right_stroke = Segment((0.0.px(),2.5.px()),(2.5.px(),(-2.5).px()),1.0.px());
let bar = Rect((4.0.px(),1.0.px())).translate_y((-1.0).px());
let letter = left_stroke + right_stroke + bar;
let letter = letter.translate_x((-2.5).px());
// === Shape ===
let shape = border + cursor + letter;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A rounded rectangle with the number "5" and a text cursor.
pub mod number_input(NumberInput) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Border ===
let border = Rect((16.0.px(),11.0.px())).corners_radius(5.5.px());
// Using just the outline.
let border = &border - border.shrink(1.0.px());
// Creating a gap for the cursor.
let gap = Rect((3.0.px(),13.0.px())).translate_x((3.5).px());
let border = border - gap;
// === Cursor ===
let cursor = cursor().translate_x(3.5.px());
// === Number 5 ===
// The number "5" consists of a short horizontal bar at the top, a vertical bar
// connected to it on the left and a big arc below, connected to the vertical bar.
let top = Rect((3.0.px(),1.0.px()));
let left =
Rect((1.0.px(),3.0.px())).translate_x((-1.0).px()).translate_y((-1.0).px());
// == Number 5 Arc ==
let arc_center = Vector2(-0.25_f32,-3.5_f32);
// The point where the inner side of the arc connects with the vertical bar.
let arc_connection = Vector2(-0.5_f32,-2.5_f32);
// Offset from the arc center to the connection.
let connection_offset = arc_connection - arc_center;
let stroke_width = 1.0;
// The outer radius of the arc.
let radius: f32 = connection_offset.norm() + stroke_width;
let connection_direction = connection_offset.x.atan2(connection_offset.y);
let arc = arc(radius,stroke_width,connection_direction,228_f32.to_radians());
let arc = arc.translate((arc_center.x.px(),arc_center.y.px()));
let number = (top + left + arc).translate_x((-2.0).px()).translate_y(2.5.px());
// === Shape ===
let shape = border + cursor + number;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A table with 4x2 cells and a cursor shape in front of it.
pub mod table_edit(TableEdit) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// We need to create the table in two parts, left and right of the cursor to achieve
// the right cell arangement.
let left_table = table(2,2).translate(((-8.0).px(),(-4.5).px()));
let right_table = table(2,2).translate(((-1.0).px(),(-4.5).px()));
let gap = Rect((3.0.px(),13.0.px()));
let cursor = cursor();
let shape = left_table + right_table - gap + cursor;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// An arrow to the left on top and an arrow to the right below.
pub mod convert(Convert) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let upper_arrow = arrow(10.0,1.0,4.5,6.0).rotate((-PI/2.0).radians());
let upper_arrow = upper_arrow.translate(((-8.0).px(),1.0.px()));
let lower_arrow = arrow(10.0,1.0,4.5,6.0).rotate((PI/2.0).radians());
let lower_arrow = lower_arrow.translate((8.0.px(),(-1.5).px()));
let shape = upper_arrow + lower_arrow;
let shape = shape.fill(strong_color);
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
/// A table with an eraser in front.
pub mod dataframe_clean(DataframeClean) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let table_color = weak_color;
let table =
table(2,3).translate(((-8.0).px(),(-6.5).px())).fill(table_color.clone());
let bottom_line =
Rect((13.0.px(),1.0.px())).corners_radius(1.0.px()).fill(table_color);
let bottom_line = bottom_line.translate_y((-6.0).px());
let eraser = Rect((9.0.px(),5.0.px())).corners_radius(1.0.px());
let eraser_bg = eraser.grow(1.5.px());
let eraser_bg = eraser_bg.rotate((-0.25 * std::f32::consts::PI).radians());
let eraser_bg = eraser_bg.translate((3.5.px(),(-1.5).px()));
let eraser_inner = Rect((7.0.px(),3.0.px()));
let eraser_bar = Rect((1.0.px(),4.0.px())).translate_x((-1.0).px());
let eraser = eraser - eraser_inner + eraser_bar;
let eraser = eraser.fill(strong_color);
let eraser = eraser.rotate((-0.25 * std::f32::consts::PI).radians());
let eraser = eraser.translate((3.5.px(),(-1.5).px()));
let shape = table - eraser_bg + eraser + bottom_line;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A light column on the left, a dark column in the middle and a plus on the right.
pub mod add_column(AddColumn) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let old_color = weak_color;
let new_color = strong_color;
let old_column = table(1,3).translate(((-8.0).px(),(-6.5).px())).fill(old_color);
let new_column =
table(1,3).translate(((-4.0).px(),(-6.5).px())).fill(new_color.clone());
let plus = plus(5.0,1.0).fill(new_color).translate_x(5.0.px());
let shape = old_column + new_column + plus;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A light row at the top, a dark row in the middle and a plus at the bottom.
pub mod add_row(AddRow) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let old_color = weak_color;
let new_color = strong_color;
let old_row = table(3,1).translate(((-6.5).px(),3.0.px())).fill(old_color);
let new_row =
table(3,1).translate(((-6.5).px(),(-1.0).px())).fill(new_color.clone());
let plus = plus(5.0,1.0).fill(new_color).translate_y((-5.0).px());
let shape = old_row + new_row + plus;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two light columns on the left and one dark column detached on the right.
pub mod select_column(SelectColumn) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let unselected = table(2,3).translate(((-8.0).px(),(-6.5).px()));
let unselected = unselected.fill(weak_color);
let selected = table(1,3).translate((3.0.px(),(-6.5).px()));
let selected = selected.fill(strong_color);
let shape = unselected + selected;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two light rows at the top and one dark row detached at the bottom.
pub mod select_row(SelectRow) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let unselected = table(3,2).translate(((-6.5).px(),(-1.0).px()));
let unselected = unselected.fill(weak_color);
let selected = table(3,1).translate(((-6.5).px(),(-8.0).px()));
let selected = selected.fill(strong_color);
let shape = unselected + selected;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A light column, a dark column and a lightning bolt on the right.
pub mod dataframe_map_column(DataframeMapColumn) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let weak_color = weak_color;
let strong_color = strong_color;
let weak_column = table(1,3).translate(((-8.0).px(),(-6.5).px())).fill(weak_color);
let strong_column =
table(1,3).translate(((-4.0).px(),(-6.5).px())).fill(strong_color.clone());
let lightning = lightning_bolt().translate_x(5.25.px()).fill(strong_color);
let shape = weak_column + strong_column + lightning;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A light row, a dark row and a lightning bolt below.
pub mod dataframe_map_row(DataframeMapRow) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let weak_color = weak_color;
let strong_color = strong_color;
let weak_row = table(3,1).translate(((-6.5).px(),3.0.px())).fill(weak_color);
let strong_row =
table(3,1).translate(((-6.5).px(),(-1.0).px())).fill(strong_color.clone());
let lightning = lightning_bolt().rotate((PI/2.0).radians());
let lightning = lightning.translate_y((-5.25).px()).fill(strong_color);
let shape = weak_row + strong_row + lightning;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two columns with a plus in-between.
pub mod dataframes_join(DataframesJoin) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let column_color = weak_color;
let plus_color = strong_color;
let left_column =
table(1,3).translate(((-8.0).px(),(-6.5).px())).fill(column_color.clone());
let right_column = table(1,3).translate((3.0.px(),(-6.5).px())).fill(column_color);
let plus = plus(5.0,1.0).fill(plus_color);
let shape = left_column + right_column + plus;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two rows with a plus in-between.
pub mod dataframes_union(DataframesUnion) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let row_color = weak_color;
let plus_color = strong_color;
let top_row = table(3,1).translate(((-6.5).px(),3.0.px())).fill(row_color.clone());
let bottom_row = table(3,1).translate(((-6.5).px(),(-8.0).px())).fill(row_color);
let plus = plus(5.0,1.0).fill(plus_color);
let shape = top_row + bottom_row + plus;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A capital "Σ".
pub mod sigma(Sigma) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let shape = path(2.0,&[
( 4.0 , 4.0),
( 4.0 , 5.5),
(-5.0 , 5.5),
( 0.5 , 0.0),
(-5.0 , -5.5),
( 4.0 , -5.5),
( 5.0 , -3.5),
]);
let shape = shape.fill(style.get_color(theme::transform));
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The shape of a sheet of paper that has been ripped apart with a vertical crack through the
/// middle. Both pieces contain two thin rectangles as a simple representation of lines of text.
pub mod split_text(SplitText) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Page border ===
let page = Rect((16.0.px(),14.0.px())).corners_radius(2.0.px());
let page = &page - page.shrink(1.0.px());
let gap = Rect((3.0.px(),15.0.px())).translate_x(0.5.px());
let page = page - gap;
// === Lines ===
let line1 = Rect((3.0.px(),1.0.px())).translate_x((-4.5).px());
let line2 = Rect((2.0.px(),1.0.px())).translate(((-5.0).px(),(-3.0).px()));
let line3 = Rect((2.0.px(),1.0.px())).translate_x(5.0.px());
let line4 = Rect((3.0.px(),1.0.px())).translate((4.5.px(),(-3.0).px()));
let page = page + line1 + line2 + line3 + line4;
let page = page.fill(weak_color);
// === Crack ===
let crack = path(1.0,&[
( 0.0 , 6.5),
(-1.25 , 3.25),
( 0.0 , 0.0),
(-1.25 , -3.25),
( 0.0 , -6.5),
]);
let crack = crack.fill(strong_color);
let crack_left = crack.translate_x((-1.0).px());
let crack_right = crack.translate_x(2.0.px());
// === Shape ===
let shape = page + crack_left + crack_right;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Some rectangles and circles in different colors.
pub mod data_science(DataScience) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let blue = style.get_color(theme::data_science::blue);
let rect1 = Rect((4.0.px(),4.0.px())).translate(((-5.5).px(),3.0.px())).fill(blue);
let rect2 = Rect((4.0.px(),4.0.px())).translate_y((-5.5).px()).fill(blue);
let gray = style.get_color(theme::data_science::gray);
let circle1 = Circle(2.0.px()).translate_y(5.5.px()).fill(gray);
let circle2 = Circle(2.0.px()).translate(((-5.5).px(),(-3.0).px())).fill(gray);
let circle3 = Circle(2.0.px()).translate((5.5.px(),(-3.0).px())).fill(gray);
let red = style.get_color(theme::data_science::red);
let circle4 = Circle(2.0.px()).fill(red);
let shape = rect1 + rect2 + circle1 + circle2 + circle3 + circle4;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A WiFi symbol, consisting of a small circle and three arcs of increasing size above it.
pub mod network(Network) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let circle = Circle(1.0.px())
.fill(style.get_color(theme::network::_0));
let arc1 = RoundedArc((10.5/3.0*1.0).px(),(PI/2.0).radians(),1.5.px())
.fill(style.get_color(theme::network::_1));
let arc2 = RoundedArc((10.5/3.0*2.0).px(),(PI/2.0).radians(),1.5.px())
.fill(style.get_color(theme::network::_2));
let arc3 = RoundedArc((10.5/3.0*3.0).px(),(PI/2.0).radians(),1.5.px())
.fill(style.get_color(theme::network::_3));
let shape = circle + arc1 + arc2 + arc3;
let shape = shape.translate_y((-5.5).px());
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A dark rectangle containing the simple terminal prompt ">_".
pub mod system(System) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let background = Rect((14.0.px(),14.0.px())).corners_radius(2.0.px());
let background = background.translate_y((-0.5).px());
let background = background.fill(style.get_color(theme::system::background));
let greater = path(1.5,&[
(-3.75 , 2.25),
(-1.25 , -0.25),
(-3.75 , -2.25),
]);
let bar = Rect((4.0.px(),1.5.px())).translate((2.5.px(),(-2.75).px()));
let content = greater + bar;
let content = content.fill(style.get_color(theme::system::content));
let shape = background + content;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Four rounded rectangles in different colors aranged in a grid.
pub mod libraries(Libraries) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let rect0 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect0 = rect0.fill(style.get_color(theme::libraries::_0));
let rect0 = rect0.translate(((-3.75).px(),3.75.px()));
let rect1 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect1 = rect1.fill(style.get_color(theme::libraries::_1));
let rect1 = rect1.translate(((-3.75).px(),(-3.75).px()));
let rect2 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect2 = rect2.fill(style.get_color(theme::libraries::_2));
let rect2 = rect2.translate((3.75.px(),(-3.75).px()));
let rect3 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect3 = rect3.fill(style.get_color(theme::libraries::_3));
let rect3 = rect3.translate((3.75.px(),3.75.px()));
let shape = rect0 + rect1 + rect2 + rect3;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A plus and three rounded rectangles in different colors aranged in a grid.
pub mod marketplace(Marketplace) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let plus = plus(6.5,1.5);
let plus = plus.fill(style.get_color(theme::libraries::_0));
let plus = plus.translate(((-3.75).px(),3.75.px()));
let rect1 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect1 = rect1.fill(style.get_color(theme::libraries::_1));
let rect1 = rect1.translate(((-3.75).px(),(-3.75).px()));
let rect2 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect2 = rect2.fill(style.get_color(theme::libraries::_2));
let rect2 = rect2.translate((3.75.px(),(-3.75).px()));
let rect3 = Rect((6.5.px(),6.5.px())).corners_radius(1.0.px());
let rect3 = rect3.fill(style.get_color(theme::libraries::_3));
let rect3 = rect3.translate((3.75.px(),3.75.px()));
let shape = plus + rect1 + rect2 + rect3;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two half arrow, one on top and pointing to the right, one at the bottom and pointing to the
/// left. The shape has an outline in a darker color.
pub mod io(IO) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let half_arrow = arrow(14.0,5.0,7.0,11.0).rotate((PI/2.0).radians()) - HalfPlane();
let upper = half_arrow.translate((7.0.px(),0.5.px()));
let lower = half_arrow.rotate(PI.radians()).translate(((-7.0).px(),(-1.0).px()));
let base = upper + lower;
let outer = base.fill(strong_color);
let inner = base.shrink(0.5.px()).fill(weak_color);
let shape = outer + inner;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The shape of a funnel, consisting of a big upside-down triangle at the top connected with
/// a thin rectangular tube shape below with a triangular end piece. The whole shape has an
/// outline.
pub mod preparation(Preparation) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
// === Outline ===
let outline = path(1.0,&[
(-6.5 , 6.0),
( 6.0 , 6.0),
( 6.0 , 5.5),
( 1.0 , 0.5),
( 1.0 , -7.0),
(-1.5 , -4.5),
(-1.5 , 0.5),
(-6.5 , 5.5),
(-6.5 , 6.0),
]);
let outline = outline.fill(strong_color);
// === Fill ===
let big_triangle = Triangle(13.5.px(),6.75.px()).rotate(PI.radians());
let big_triangle = big_triangle.translate(((-0.25).px(),2.625.px()));
let pipe = Rect((2.5.px(),6.0.px())).translate(((-0.25).px(),(-1.5).px()));
let small_triangle = Triangle(5.0.px(),2.5.px()).rotate((-PI/2.0).radians());
let small_triangle = small_triangle.translate(((-0.25).px(),(-4.5).px()));
let fill = big_triangle + pipe + small_triangle;
let fill = fill.fill(weak_color);
// === Shape ===
let shape = fill.shrink(SHRINK_AMOUNT.px()) + outline.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// Two intersecting circles. The circles, their outlines and the intersection are displayed in
/// different colors.
pub mod join(Join) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let left_circle = Circle(5.0.px()).translate_x((-3.0).px());
let right_circle = Circle(5.0.px()).translate_x(3.0.px());
let left_outline = &left_circle - left_circle.shrink(0.5.px());
let right_outline = &right_circle - right_circle.shrink(0.5.px());
let intersection = &left_circle * &right_circle;
let left_circle = left_circle.fill(weak_color.clone());
let right_circle = right_circle.fill(weak_color);
let intersection = intersection.fill(style.get_color(theme::join::medium));
let left_outline = left_outline.fill(strong_color.clone());
let right_outline = right_outline.fill(strong_color);
let shape =
left_circle + right_circle + intersection + left_outline + right_outline;
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A page with three lines representing text. The upper line is part of an arrow pointing out
/// to the right.
pub mod text(Text) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let page = Rect((10.0.px(),14.0.px())).corners_radius(2.0.px());
let page = page.translate_x((-2.0).px());
let page = &page - page.shrink(1.0.px());
let arrow = arrow(13.0,1.0,3.0,6.0)
.rotate((PI/2.0).radians())
.translate((8.0.px(),3.0.px()));
let line1 = Rect((6.0.px(),1.0.px())).translate_x((-2.0).px());
let line2 = line1.translate_y((-3.0).px());
let shape = page + arrow + line1 + line2;
let shape = shape.fill(strong_color);
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// A clock shape.
pub mod date_and_time(DateAndTime) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let circle = Circle(7.75.px());
let circle = &circle - circle.shrink(1.0.px());
let big_hand = Segment((0.0.px(),0.0.px()),(3.0.px(),(-2.0).px()),1.5.px());
let small_hand = Segment((0.0.px(),0.0.px()),(0.0.px(),2.5.px()),1.5.px());
let shape = circle + big_hand + small_hand;
let shape = shape.translate((0.25.px(),0.25.px()));
let shape = shape.fill(style.get_color(theme::date_and_time));
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The shape location marker. A thick circle outline going over into a triangle that poins
/// down. Around the tip there is an ellipse outline.
pub mod spatial(Spatial) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let circle = Circle(4.5.px()).translate_y(3.5.px());
let circle = &circle - circle.shrink(2.0.px());
let triangle = Triangle(7.0,5.75).rotate(PI.radians()).translate_y((-2.125).px());
let marker = circle + &triangle;
let ellipse = Ellipse(6.5.px(),2.5.px()).translate_y((-5.0).px());
let ellipse = &ellipse - ellipse.shrink(1.0.px());
// If we used just the triangle for the gap then it would also cut into the lower
// part of the ellipse.
let ellipse_gap = triangle.grow(1.5.px()) - HalfPlane().translate_y((-5.0).px());
let ellipse = ellipse - ellipse_gap;
let shape = marker + ellipse;
let shape = shape.fill(style.get_color(theme::spatial));
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The shape of a christal ball with a bas below.
pub mod predictive(Predictive) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let circle = Circle(5.5.px());
let sphere = &circle - circle.shrink(1.0.px());
let reflection1 = arc(3.5,1.0,-114.0_f32.to_radians(),-95.0_f32.to_radians());
let reflection2 = arc(3.5,1.0,276.0_f32.to_radians(),13.0_f32.to_radians());
let sphere = sphere + reflection1 + reflection2;
let sphere = sphere.translate_y(1.5.px());
let base = Triangle(21.0,8.0).translate_y((-4.0).px());
let base = base * Rect((13.0.px(),5.0.px())).translate_y((-5.0).px());
let base = base - circle.translate_y(1.5.px()).grow(2.0.px());
let shape = sphere + base;
let shape = shape.fill(style.get_color(theme::predictive));
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The shape of an android.
pub mod machine_learning(MachineLearning) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let body = Rect((10.0.px(),15.0.px()))
.corners_radiuses(5.0.px(),5.0.px(),2.0.px(),2.0.px())
.translate_y((-0.5).px());
let body = &body - body.shrink(1.0.px());
let collar = Rect((9.0.px(),1.0.px()));
let left_eye = Rect((1.5.px(),1.5.px())).translate(((-1.75).px(),2.75.px()));
let right_eye = Rect((1.5.px(),1.5.px())).translate((1.75.px(),2.75.px()));
let antenna = Rect((1.0.px(),1.5.px())).translate_y(7.25.px());
let left_arm = Rect((1.0.px(),4.5.px())).translate(((-6.5).px(),(-2.75).px()));
let right_arm = Rect((1.0.px(),4.5.px())).translate((6.5.px(),(-2.75).px()));
let shape = body + collar + left_eye + right_eye + antenna + left_arm + right_arm;
let shape = shape.fill(style.get_color(theme::machine_learning));
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
/// The simplified shape of a camera. It consists of a small red circle in a bigger circle
/// outline, representing the lens and a base above that the camera is mounted on.
pub mod computer_vision(ComputerVision) {
ensogl::define_shape_system! {
above = [crate::background, ensogl_list_view::background, ensogl_list_view::selection];
(style: Style, strong_color: Vector4, weak_color: Vector4) {
let lens =
Circle(2.0.px()).fill(style.get_color(theme::computer_vision::highlight));
let outline = Circle(4.5.px()) - Circle(3.5.px());
let outline = outline.fill(strong_color);
let base =
Circle(7.0.px()).translate_y(6.0.px()) * HalfPlane().translate_y(7.0.px());
let base = base + Rect((14.0.px(),2.0.px())).translate_y(7.0.px());
let base = base - Circle(5.5.px());
let base = base.fill(weak_color);
let shape = lens + outline + base;
let shape = shape.translate_y((-2.0).px());
let shape = shape.shrink(SHRINK_AMOUNT.px());
shape.into()
}
}
}
}
impl Default for Id {
fn default() -> Self {
Self::Star
}
}

View File

@ -0,0 +1,88 @@
use crate::prelude::*;
use std::f32::consts::PI;
// =====================
// === Common Shapes ===
// =====================
/// An arrow shape consisting of a straight line and a triangular head. The arrow points upwards and
/// the tip is positioned at the origin.
pub fn arrow(length: f32, width: f32, head_length: f32, head_width: f32) -> AnyShape {
// We overlap the line with the head by this amount to make sure that the renderer does not
// display a gap between them.
const OVERLAP: f32 = 1.0;
let line_length = length - head_length + OVERLAP;
let line = Rect((width.px(), line_length.px()));
let line = line.translate_y((-line_length / 2.0 - head_length + OVERLAP).px());
let head = Triangle(head_width, head_length).translate_y((-head_length / 2.0).px());
(line + head).into()
}
/// An infinite grid of horizontal and vertical lines. The width of each line is given by
/// `stroke_width`, the distance between horizontal lines and the distance between vertical lines
/// are both given by `cell_size`. The origin is at the intersection of a horizontal and a vertical
/// line, touching the horizontal line from the top and the vertical line from the left.
pub fn grid(stroke_width: f32, cell_size: f32) -> AnyShape {
let horizontal = HalfPlane();
let horizontal = horizontal.translate_y(stroke_width.px()) - horizontal;
let vertical = HalfPlane().rotate((PI / 2.0).radians());
let vertical = vertical.translate_x(stroke_width.px()) - vertical;
(horizontal + vertical).repeat((cell_size.px(), cell_size.px())).into()
}
/// A cursor shape, looking roughly like a capital "I".
pub fn cursor() -> AnyShape {
let middle = Rect((1.0.px(), 15.0.px()));
let top = Rect((5.0.px(), 1.0.px())).translate_y(7.5.px());
let bottom = Rect((5.0.px(), 1.0.px())).translate_y((-7.5).px());
(middle + top + bottom).into()
}
/// An arc around the origin. `outer_radius` determines the distance from the origin to the outer
/// edge of the arc, `stroke_width` the width of the arc. The arc starts at `start_angle`, relative
/// to the origin and ends at `end_angle`. The ends are flat not rounded, as in [`RoundedArc`].
pub fn arc(outer_radius: f32, stroke_width: f32, start_angle: f32, end_angle: f32) -> AnyShape {
let circle = Circle(outer_radius.px()) - Circle((outer_radius - stroke_width).px());
let inner_angle = (end_angle - start_angle).rem_euclid(2.0 * PI);
let mid_angle = start_angle + inner_angle / 2.0;
let angle = PlaneAngleFast(inner_angle.radians()).rotate(mid_angle.radians());
// The implementation of `PlaneAngleFast` adds 0.5 px to the actual distance to avoid artifacts
// in corner cases. We apply `grow` to compensate for that and get the shape that we really
// want.
let angle = angle.grow(0.5.px());
(circle * angle).into()
}
/// The shape of a table, given by a grid with size `columns` x `rows`. The stroke width is 1.0 and
/// the cell size 4.0. The origin is at the lower left corner.
pub fn table(columns: i32, rows: i32) -> AnyShape {
const STROKE_WIDTH: f32 = 1.0;
const CELL_SIZE: f32 = 4.0;
let width = columns as f32 * CELL_SIZE + STROKE_WIDTH;
let height = rows as f32 * CELL_SIZE + STROKE_WIDTH;
let bounds =
Rect((width.px(), height.px())).translate(((width / 2.0).px(), (height / 2.0).px()));
let grid = grid(STROKE_WIDTH, CELL_SIZE);
(grid * bounds).into()
}
/// A plus, consisting of two strokes of length `size` and width `stroke_width`, intersecting at the
/// origin.
pub fn plus(size: f32, stroke_width: f32) -> AnyShape {
let horizontal = Rect((size.px(), stroke_width.px()));
let vertical = Rect((stroke_width.px(), size.px()));
(horizontal + vertical).into()
}
/// A shape resembling a lightning bolt, centered at the origin.
pub fn lightning_bolt() -> AnyShape {
let top = Triangle(3.0.px(), 6.0.px()).translate(((-1.0).px(), 1.9.px()));
let bottom = Triangle(3.0.px(), 6.0.px()).rotate(PI.radians());
let bottom = bottom.translate((1.0.px(), (-1.9).px()));
let lightning = (top + bottom).rotate((PI / 6.0).radians());
lightning.into()
}

View File

@ -0,0 +1,113 @@
// ==========================
// === Define Icons Macro ===
// ==========================
// Lint gives an erroneous warning: the main is actually needed, because we generate modules here.
#[allow(clippy::needless_doctest_main)]
/// Macro for defining icon set.
///
/// The macro takes many modules with attached "variant name". Inside the modules, there should
/// be icon defined with `ensogl::define_shape_system!` macro. The macro will also generate an
/// enum called `Id` gathering all icon' "variant names". The enum will allow for dynamically
/// creating given icon shape view (returned as [`crate::icon::AnyIcon`]).
///
/// # Example
///
/// ```
/// use ensogl::prelude::*;
/// use ensogl::display::shape::*;
/// use ide_view_component_group::icon;
/// use ide_view_component_group::define_icons;
///
/// define_icons! {
/// /// The example of icon.
/// pub mod icon1(Icon1) {
/// // This is a normal module and you may define whatever you want. It must however
/// // define shape system with the macro below; otherwise the generated code wont compile.
/// //
/// // `use super::*` import is added silently.
/// ensogl::define_shape_system! {
/// (style:Style, strong_color: Vector4, weak_color: Vector4) {
/// Plane().into()
/// }
/// }
/// }
///
/// pub mod icon2(Icon2) {
/// ensogl::define_shape_system! {
/// (style:Style, strong_color: Vector4, weak_color: Vector4) {
/// Plane().fill(strong_color).into()
/// }
/// }
/// }
/// }
///
/// fn main () {
/// let app = ensogl::application::Application::new("root");
/// let logger = Logger::new("icon");
/// let icon1 = Id::Icon1.create_shape(&logger, Vector2(10.0, 10.0));
/// let icon2_id: Id = "Icon2".parse().unwrap();
/// assert_eq!(icon2_id, Id::Icon2);
/// let icon2 = icon2_id.create_shape(&logger, Vector2(11.0, 11.0));
/// app.display.default_scene.add_child(&icon1);
/// app.display.default_scene.add_child(&icon2);
///
/// // Invalid icon
/// let icon3 = "Icon3".parse::<Id>();
/// assert!(icon3.is_err());
/// }
#[macro_export]
macro_rules! define_icons {
($(
$(#$meta:tt)*
pub mod $name:ident($variant:ident) {
$($content:tt)*
}
)*) => {
$(
$(#$meta)*
pub mod $name {
use super::*;
$($content)*
}
)*
/// An identifier of one of the icons generated by the same `define_icons` macro invocation.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Id {
$($variant),*
}
impl Id {
/// Create icon's shape with given size.
pub fn create_shape(&self, logger: impl AnyLogger, size: Vector2) -> $crate::icon::Any {
match self {$(
Self::$variant => {
let view = $name::View::new(logger);
view.size.set(size);
let strong_color = view.strong_color.clone_ref();
let weak_color = view.weak_color.clone_ref();
let view = Box::new(view);
$crate::icon::Any {view, strong_color, weak_color}
}
)*}
}
/// Call `f` for each possible icon id.
pub fn for_each<F: FnMut(Self)>(mut f: F) {
$(f(Self::$variant);)*
}
}
impl FromStr for Id {
type Err = $crate::icon::UnknownIcon;
fn from_str(s: &str) -> Result<Id, Self::Err> {
match s {
$(stringify!($variant) => Ok(Self::$variant),)*
name => Err(Self::Err {name: name.to_owned() }),
}
}
}
}
}

View File

@ -17,6 +17,8 @@
//! component group boundaries. See `Header Backgound` section in the [`Model::resize`] method. //! component group boundaries. See `Header Backgound` section in the [`Model::resize`] method.
#![recursion_limit = "512"] #![recursion_limit = "512"]
// === Features ===
#![feature(option_result_contains)]
// === Standard Linter Configuration === // === Standard Linter Configuration ===
#![deny(non_ascii_idents)] #![deny(non_ascii_idents)]
#![warn(unsafe_code)] #![warn(unsafe_code)]
@ -29,22 +31,22 @@
#![warn(unused_import_braces)] #![warn(unused_import_braces)]
#![warn(unused_qualifications)] #![warn(unused_qualifications)]
use ensogl_core::application::traits::*; use crate::prelude::*;
use ensogl_core::display::shape::*; use ensogl::application::traits::*;
use ensogl_core::prelude::*;
use crate::display::scene::layer;
use enso_frp as frp; use enso_frp as frp;
use ensogl_core::application::shortcut::Shortcut; use ensogl::application::shortcut::Shortcut;
use ensogl_core::application::Application; use ensogl::application::Application;
use ensogl_core::data::color; use ensogl::data::color;
use ensogl_core::display; use ensogl::data::text;
use ensogl_core::display::camera::Camera2d; use ensogl::display;
use ensogl_core::display::scene::layer; use ensogl::display::camera::Camera2d;
use ensogl_gui_component::component; use ensogl_gui_component::component;
use ensogl_hardcoded_theme::application::component_browser::component_group as theme; use ensogl_hardcoded_theme::application::component_browser::component_group as theme;
use ensogl_list_view as list_view; use ensogl_list_view as list_view;
use ensogl_shadow as shadow; use ensogl_shadow as shadow;
use ensogl_text as text;
// ============== // ==============
@ -52,12 +54,20 @@ use ensogl_text as text;
// ============== // ==============
pub mod entry; pub mod entry;
pub mod icon;
pub mod wide; pub mod wide;
pub use entry::View as Entry; pub use entry::View as Entry;
/// A module containing common imports.
pub mod prelude {
pub use ensogl::application::traits::*;
pub use ensogl::display::shape::*;
pub use ensogl::prelude::*;
}
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
@ -86,7 +96,7 @@ const HEADER_SHADOW_PEAK: f32 = list_view::entry::HEIGHT / 2.0;
pub mod background { pub mod background {
use super::*; use super::*;
ensogl_core::define_shape_system! { ensogl::define_shape_system! {
below = [list_view::background]; below = [list_view::background];
(style:Style, color:Vector4) { (style:Style, color:Vector4) {
let color = Var::<color::Rgba>::from(color); let color = Var::<color::Rgba>::from(color);
@ -103,7 +113,7 @@ pub mod background {
pub mod header_background { pub mod header_background {
use super::*; use super::*;
ensogl_core::define_shape_system! { ensogl::define_shape_system! {
above = [background, list_view::background]; above = [background, list_view::background];
(style:Style, color:Vector4, height: f32, shadow_height_multiplier: f32) { (style:Style, color:Vector4, height: f32, shadow_height_multiplier: f32) {
let color = Var::<color::Rgba>::from(color); let color = Var::<color::Rgba>::from(color);
@ -133,9 +143,9 @@ pub mod header_background {
pub mod header_overlay { pub mod header_overlay {
use super::*; use super::*;
use ensogl_core::display::shape::constants::HOVER_COLOR; use ensogl::display::shape::constants::HOVER_COLOR;
ensogl_core::define_shape_system! { ensogl::define_shape_system! {
above = [background]; above = [background];
() { () {
let bg_color = HOVER_COLOR; let bg_color = HOVER_COLOR;
@ -182,11 +192,75 @@ impl HeaderGeometry {
// ==============
// === Colors ===
// ==============
/// Colors used in the Component Group View.
///
/// This structure, used in both [`ide_component_group::View`] and
/// [`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`].
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)]
pub struct Colors {
// Note: The FRP nodes below must be samplers, otherwise their values set during initialization
// (by emitting `init` event in `Colors::from_main_color) will be lost - the FRP system does
// not keep value of nodes if they are not connected to anything, and those nodes won't be
// before returning from `from_main_color`.
pub icon_strong: frp::Sampler<color::Rgba>,
pub icon_weak: frp::Sampler<color::Rgba>,
pub header_text: frp::Sampler<color::Rgba>,
pub entry_text: frp::Sampler<color::Rgba>,
pub background: frp::Sampler<color::Rgba>,
}
impl Colors {
/// Constructs [`Colors`] structure, where each variant is based on the "main" `color`
/// parameter.
pub fn from_main_color(
network: &frp::Network,
style: &StyleWatchFrp,
color: &frp::Stream<color::Rgba>,
is_dimmed: &frp::Stream<bool>,
) -> Self {
fn mix((c1, c2): &(color::Rgba, color::Rgba), coefficient: &f32) -> color::Rgba {
color::mix(*c1, *c2, *coefficient)
}
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 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);
frp::extend! { network
init <- source_();
one <- init.constant(1.0);
let is_dimmed = is_dimmed.clone_ref();
intensity <- is_dimmed.switch(&one, &dimmed_intensity);
app_bg <- all(&app_bg, &init)._0();
app_bg_and_input <- all(&app_bg, color);
main <- app_bg_and_input.all_with(&intensity, mix);
app_bg_and_main <- all(&app_bg, &main);
header_text <- app_bg_and_main.all_with(&header_intensity, mix).sampler();
bg <- app_bg_and_main.all_with(&bg_intensity, mix).sampler();
app_bg_and_entry_text <- all(&app_bg, &entry_text_);
entry_text <- app_bg_and_entry_text.all_with(&intensity, mix).sampler();
icon_weak <- app_bg_and_main.all_with(&icon_weak_intensity, mix).sampler();
icon_strong <- main.sampler();
}
init.emit(());
Self { icon_weak, icon_strong, header_text, entry_text, background: bg }
}
}
// =========== // ===========
// === FRP === // === FRP ===
// =========== // ===========
ensogl_core::define_endpoints_2! { ensogl::define_endpoints_2! {
Input { Input {
/// Accept the currently selected suggestion. Should be bound to "Suggestion Acceptance Key" /// Accept the currently selected suggestion. Should be bound to "Suggestion Acceptance Key"
/// described in /// described in
@ -225,19 +299,24 @@ impl component::Frp<Model> for Frp {
let out = &api.output; let out = &api.output;
let header_text_font = style.get_text(theme::header::text::font); let header_text_font = style.get_text(theme::header::text::font);
let header_text_size = style.get_number(theme::header::text::size); let header_text_size = style.get_number(theme::header::text::size);
let entry_list_padding = style.get_number(theme::entry_list::padding);
// === Geometry === // === Geometry ===
frp::extend! { network frp::extend! { network
let header_geometry = HeaderGeometry::from_style(style, network); let header_geometry = HeaderGeometry::from_style(style, network);
height <- all_with(&input.set_entries, &header_geometry, |entries, header_geom| { height <- all_with3(&input.set_entries, &header_geometry, &entry_list_padding,
entries.entry_count() as f32 * list_view::entry::HEIGHT + header_geom.height |entries, header_geom, padding| {
}); let entries_height = entries.entry_count() as f32 * list_view::entry::HEIGHT;
entries_height + header_geom.height + padding
}
);
out.size <+ all_with(&input.set_width, &height, |w, h| Vector2(*w, *h)); out.size <+ all_with(&input.set_width, &height, |w, h| Vector2(*w, *h));
size_and_header_geometry <- all3(&out.size, &header_geometry, &input.set_header_pos); size_and_header_geometry <- all3(&out.size, &header_geometry, &input.set_header_pos);
eval size_and_header_geometry(((size, hdr_geom, hdr_pos)) size_and_header_geom_and_padding <- all(&size_and_header_geometry, &entry_list_padding);
model.resize(*size, *hdr_geom, *hdr_pos) eval size_and_header_geom_and_padding((((size, hdr_geom, hdr_pos), padding))
model.resize(*size, *hdr_geom, *hdr_pos, *padding)
); );
@ -249,36 +328,15 @@ impl component::Frp<Model> for Frp {
// === Colors === // === Colors ===
let colors = Colors::from_main_color(network, style, &input.set_color, &input.set_dimmed);
fn mix(colors: &(color::Rgba, color::Rgba), coefficient: &f32) -> color::Rgba { let params = entry::Params { colors: colors.clone_ref() };
color::mix(colors.0, colors.1, *coefficient)
}
let app_bg_color = 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 dimmed_intensity = style.get_number(theme::dimmed_color_intensity);
let entry_text_color = style.get_color(theme::entries::text::color);
frp::extend! { network
init <- source_();
one <- init.constant(1.0);
intensity <- input.set_dimmed.switch(&one, &dimmed_intensity);
app_bg_color <- all(&app_bg_color, &init)._0();
app_bg_and_input_color <- all(&app_bg_color, &input.set_color);
main_color <- app_bg_and_input_color.all_with(&intensity, mix);
app_bg_and_main_color <- all(&app_bg_color, &main_color);
header_color <- app_bg_and_main_color.all_with(&header_intensity, mix);
bg_color <- app_bg_and_main_color.all_with(&bg_intensity, mix);
app_bg_and_entry_text_color <- all(&app_bg_color, &entry_text_color);
entry_color_with_intensity <- app_bg_and_entry_text_color.all_with(&intensity, mix);
entry_color_sampler <- entry_color_with_intensity.sampler();
}
let params = entry::Params { color: entry_color_sampler };
model.entries.set_entry_params_and_recreate_entries(params); model.entries.set_entry_params_and_recreate_entries(params);
// === Header === // === Header ===
frp::extend! { network frp::extend! { network
init <- source_();
header_text_font <- all(&header_text_font, &init)._0(); header_text_font <- all(&header_text_font, &init)._0();
model.header.set_font <+ header_text_font; model.header.set_font <+ header_text_font;
header_text_size <- all(&header_text_size, &init)._0(); header_text_size <- all(&header_text_size, &init)._0();
@ -289,9 +347,9 @@ impl component::Frp<Model> for Frp {
model.update_header_width(*size, *hdr_geom); model.update_header_width(*size, *hdr_geom);
}) })
); );
model.header.set_default_color <+ header_color; model.header.set_default_color <+ colors.header_text;
eval bg_color((c) model.background.color.set(c.into())); eval colors.background((c) model.background.color.set(c.into()));
eval bg_color((c) model.header_background.color.set(c.into())); eval colors.background((c) model.header_background.color.set(c.into()));
} }
@ -330,12 +388,11 @@ impl component::Frp<Model> for Frp {
out.is_header_selected <+ bool(&deselect_header, &select_header); out.is_header_selected <+ bool(&deselect_header, &select_header);
model.entries.select_entry <+ select_header.constant(None); model.entries.select_entry <+ select_header.constant(None);
out.selection_position_target <+ all_with4( out.selection_position_target <+ all_with3(
&out.is_header_selected, &out.is_header_selected,
&out.size, &out.size,
&header_geometry,
&model.entries.selection_position_target, &model.entries.selection_position_target,
f!((h_sel, size, h, esp) model.selection_position(*h_sel, *size, *h, *esp)) f!((h_sel, size, esp) model.selection_position(*h_sel, *size, *esp))
); );
} }
@ -343,7 +400,7 @@ impl component::Frp<Model> for Frp {
} }
fn default_shortcuts() -> Vec<Shortcut> { fn default_shortcuts() -> Vec<Shortcut> {
use ensogl_core::application::shortcut::ActionType::*; use ensogl::application::shortcut::ActionType::*;
(&[(Press, "tab", "accept_suggestion")]) (&[(Press, "tab", "accept_suggestion")])
.iter() .iter()
.map(|(a, b, c)| View::self_shortcut(*a, *b, *c)) .map(|(a, b, c)| View::self_shortcut(*a, *b, *c))
@ -462,7 +519,13 @@ impl Model {
self.header.add_to_scene_layer(&layers.header_text); self.header.add_to_scene_layer(&layers.header_text);
} }
fn resize(&self, size: Vector2, header_geometry: HeaderGeometry, header_pos: f32) { fn resize(
&self,
size: Vector2,
header_geometry: HeaderGeometry,
header_pos: f32,
entry_list_padding: f32,
) {
// === Background === // === Background ===
self.background.size.set(size); self.background.size.set(size);
@ -511,8 +574,8 @@ impl Model {
// === Entries === // === Entries ===
self.entries.resize(size - Vector2(0.0, header_height)); self.entries.resize(size - Vector2(0.0, header_height - entry_list_padding));
self.entries.set_position_y(-header_height / 2.0); self.entries.set_position_y(-header_height / 2.0 + entry_list_padding / 2.0);
} }
fn update_header_width(&self, size: Vector2, header_geometry: HeaderGeometry) { fn update_header_width(&self, size: Vector2, header_geometry: HeaderGeometry) {
@ -526,11 +589,10 @@ impl Model {
&self, &self,
is_header_selected: bool, is_header_selected: bool,
size: Vector2, size: Vector2,
header_geometry: HeaderGeometry,
entries_selection_position: Vector2, entries_selection_position: Vector2,
) -> Vector2 { ) -> Vector2 {
if is_header_selected { if is_header_selected {
Vector2(0.0, size.y / 2.0 - header_geometry.height / 2.0) Vector2(0.0, size.y / 2.0 - list_view::entry::HEIGHT / 2.0)
} else { } else {
self.entries.position().xy() + entries_selection_position self.entries.position().xy() + entries_selection_position
} }
@ -564,7 +626,7 @@ pub type View = component::ComponentView<Model, Frp>;
mod tests { mod tests {
use super::*; use super::*;
use enso_frp::future::EventOutputExt; use enso_frp::future::EventOutputExt;
use ensogl_core::control::io::mouse; use ensogl::control::io::mouse;
use ensogl_list_view::entry::AnyModelProvider; use ensogl_list_view::entry::AnyModelProvider;
macro_rules! expect_entry_selected { macro_rules! expect_entry_selected {

View File

@ -11,19 +11,17 @@
//! //!
//! [Component Group]: crate::component_group::View //! [Component Group]: crate::component_group::View
use ensogl_core::application::traits::*; use crate::prelude::*;
use ensogl_core::display::shape::*;
use ensogl_core::prelude::*;
use crate::entry; use crate::entry;
use crate::Colors;
use enso_frp as frp; use enso_frp as frp;
use ensogl_core::application::shortcut::Shortcut; use ensogl::application::shortcut::Shortcut;
use ensogl_core::application::Application; use ensogl::application::Application;
use ensogl_core::data::color::Rgba; use ensogl::data::color::Rgba;
use ensogl_core::display; use ensogl::display;
use ensogl_gui_component::component; use ensogl_gui_component::component;
use ensogl_hardcoded_theme::application::component_browser::component_group as theme;
use ensogl_label::Label; use ensogl_label::Label;
use ensogl_list_view as list_view; use ensogl_list_view as list_view;
use list_view::entry::AnyModelProvider; use list_view::entry::AnyModelProvider;
@ -63,7 +61,7 @@ newtype_prim! {
pub mod background { pub mod background {
use super::*; use super::*;
ensogl_core::define_shape_system! { ensogl::define_shape_system! {
below = [list_view::background]; below = [list_view::background];
(style:Style, color:Vector4) { (style:Style, color:Vector4) {
let color = Var::<Rgba>::from(color); let color = Var::<Rgba>::from(color);
@ -118,7 +116,7 @@ impl<const COLUMNS: usize> list_view::entry::ModelProvider<Entry> for ModelProvi
// === FRP === // === FRP ===
// =========== // ===========
ensogl_core::define_endpoints_2! { ensogl::define_endpoints_2! {
Input { Input {
/// Accept the currently selected suggestion. Should be bound to "Suggestion Acceptance Key" /// Accept the currently selected suggestion. Should be bound to "Suggestion Acceptance Key"
/// described in /// described in
@ -126,7 +124,8 @@ ensogl_core::define_endpoints_2! {
accept_suggestion(), accept_suggestion(),
select_entry(ColumnId, entry::Id), select_entry(ColumnId, entry::Id),
set_entries(AnyModelProvider<Entry>), set_entries(AnyModelProvider<Entry>),
set_background_color(Rgba), set_color(Rgba),
set_dimmed(bool),
set_width(f32), set_width(f32),
set_no_items_label_text(String), set_no_items_label_text(String),
} }
@ -154,13 +153,11 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
let network = &api.network; let network = &api.network;
let input = &api.input; let input = &api.input;
let out = &api.output; let out = &api.output;
let entry_text_color = style.get_color(theme::entries::text::color); let colors = Colors::from_main_color(network, style, &input.set_color, &input.set_dimmed);
frp::extend! { network frp::extend! { network
init <- source_(); init <- source_();
entry_count <- input.set_entries.map(|p| p.entry_count()); entry_count <- input.set_entries.map(|p| p.entry_count());
out.entry_count <+ entry_count; out.entry_count <+ entry_count;
entry_text_color <- all(&entry_text_color, &init)._0();
entry_color_sampler <- entry_text_color.sampler();
selected_column_and_entry <- any(...); selected_column_and_entry <- any(...);
update_selected_entry <- selected_column_and_entry.sample(&out.size); update_selected_entry <- selected_column_and_entry.sample(&out.size);
@ -173,7 +170,7 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
} }
}); });
eval input.set_background_color((c) model.background.color.set(c.into())); eval colors.background((c) model.background.color.set(c.into()));
eval input.set_no_items_label_text((text) model.set_no_items_label_text(text)); eval input.set_no_items_label_text((text) model.set_no_items_label_text(text));
@ -231,13 +228,13 @@ impl<const COLUMNS: usize> component::Frp<Model<COLUMNS>> for Frp {
eval entries((e) column.set_entries(e)); eval entries((e) column.set_entries(e));
_eval <- all_with(&entries, &out.size, f!((_, size) column.resize_and_place(*size))); _eval <- all_with(&entries, &out.size, f!((_, size) column.resize_and_place(*size)));
} }
let params = entry::Params { color: entry_color_sampler.clone() }; let params = entry::Params { colors: colors.clone_ref() };
column.list_view.set_entry_params_and_recreate_entries(params); column.list_view.set_entry_params_and_recreate_entries(params);
} }
} }
fn default_shortcuts() -> Vec<Shortcut> { fn default_shortcuts() -> Vec<Shortcut> {
use ensogl_core::application::shortcut::ActionType::*; use ensogl::application::shortcut::ActionType::*;
(&[(Press, "tab", "accept_suggestion")]) (&[(Press, "tab", "accept_suggestion")])
.iter() .iter()
.map(|(a, b, c)| View::<COLUMNS>::self_shortcut(*a, *b, *c)) .map(|(a, b, c)| View::<COLUMNS>::self_shortcut(*a, *b, *c))

View File

@ -9,5 +9,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
debug-scene-component-group = { path = "component-group" } debug-scene-component-group = { path = "component-group" }
debug-scene-icons = { path = "icons" }
debug-scene-interface = { path = "interface" } debug-scene-interface = { path = "interface" }
debug-scene-visualization = { path = "visualization" } debug-scene-visualization = { path = "visualization" }

View File

@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
enso-frp = { path = "../../../../../lib/rust/frp" } enso-frp = { path = "../../../../../lib/rust/frp" }
enso-text = { path = "../../../../../lib/rust/text" }
ensogl-core = { path = "../../../../../lib/rust/ensogl/core" } ensogl-core = { path = "../../../../../lib/rust/ensogl/core" }
ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" } ensogl-selector = { path = "../../../../../lib/rust/ensogl/component/selector" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" } ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }

View File

@ -16,16 +16,21 @@ use ensogl_core::display::shape::*;
use ensogl_core::prelude::*; use ensogl_core::prelude::*;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use enso_text::Bytes;
use ensogl_core::application::Application; use ensogl_core::application::Application;
use ensogl_core::data::color; use ensogl_core::data::color;
use ensogl_core::display::object::ObjectOps; use ensogl_core::display::object::ObjectOps;
use ensogl_core::frp; use ensogl_core::frp;
use ensogl_hardcoded_theme as theme; use ensogl_hardcoded_theme as theme;
use ensogl_list_view as list_view; use ensogl_list_view as list_view;
use ensogl_list_view::entry::GlyphHighlightedLabelModel;
use ensogl_scroll_area::ScrollArea; use ensogl_scroll_area::ScrollArea;
use ensogl_selector as selector; use ensogl_selector as selector;
use ensogl_text_msdf_sys::run_once_initialized; use ensogl_text_msdf_sys::run_once_initialized;
use ide_view_component_group as component_group; 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; use list_view::entry::AnyModelProvider;
@ -50,15 +55,15 @@ pub fn main() {
// === Mock Entries === // === Mock Entries ===
// ==================== // ====================
const PREPARED_ITEMS: &[&str; 8] = &[ const PREPARED_ITEMS: &[(&str, icon::Id)] = &[
"long sample entry with text overflowing the width", ("long sample entry with text overflowing the width", icon::Id::Star),
"convert", ("convert", icon::Id::Convert),
"table input", ("table input", icon::Id::DataInput),
"text input", ("text input", icon::Id::TextInput),
"number input", ("number input", icon::Id::NumberInput),
"table output", ("table output", icon::Id::TableEdit),
"data output", ("dataframe clean", icon::Id::DataframeClean),
"data input", ("data input", icon::Id::DataInput),
]; ];
#[derive(Debug)] #[derive(Debug)]
@ -69,23 +74,40 @@ struct MockEntries {
impl MockEntries { impl MockEntries {
fn new(count: usize) -> Rc<Self> { fn new(count: usize) -> Rc<Self> {
const HIGHLIGHTED_ENTRY_NAME: &str = "convert";
const HIGHLIGHTED_RANGE: Range<Bytes> = Bytes(0)..Bytes(3);
Rc::new(Self { Rc::new(Self {
entries: PREPARED_ITEMS.iter().cycle().take(count).map(|&label| label.into()).collect(), 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), count: Cell::new(count),
}) })
} }
fn get_entry(&self, id: list_view::entry::Id) -> Option<component_group::entry::Model> { fn get_entry(&self, id: list_view::entry::Id) -> Option<entry::Model> {
self.entries.get(id).cloned() self.entries.get(id).cloned()
} }
} }
impl list_view::entry::ModelProvider<component_group::Entry> for MockEntries { impl list_view::entry::ModelProvider<Entry> for MockEntries {
fn entry_count(&self) -> usize { fn entry_count(&self) -> usize {
self.count.get() self.count.get()
} }
fn get(&self, id: list_view::entry::Id) -> Option<component_group::entry::Model> { fn get(&self, id: list_view::entry::Id) -> Option<entry::Model> {
self.get_entry(id) self.get_entry(id)
} }
} }
@ -162,6 +184,7 @@ fn create_component_group(
component_group component_group
} }
fn color_component_slider(app: &Application, caption: &str) -> selector::NumberPicker { fn color_component_slider(app: &Application, caption: &str) -> selector::NumberPicker {
let slider = app.new_view::<selector::NumberPicker>(); let slider = app.new_view::<selector::NumberPicker>();
app.display.add_child(&slider); app.display.add_child(&slider);
@ -196,7 +219,6 @@ fn init(app: &Application) {
theme::builtin::light::enable(&app); theme::builtin::light::enable(&app);
let network = frp::Network::new("Component Group Debug Scene"); let network = frp::Network::new("Component Group Debug Scene");
let scroll_area = ScrollArea::new(app); let scroll_area = ScrollArea::new(app);
scroll_area.set_position_xy(Vector2(0.0, 100.0)); scroll_area.set_position_xy(Vector2(0.0, 100.0));
scroll_area.resize(Vector2(170.0, 400.0)); scroll_area.resize(Vector2(170.0, 400.0));

View File

@ -0,0 +1,14 @@
[package]
name = "debug-scene-icons"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
ensogl = { path = "../../../../../lib/rust/ensogl" }
ensogl-hardcoded-theme = { path = "../../../../../lib/rust/ensogl/app/theme/hardcoded" }
ide-view-component-group = { path = "../../component-browser/component-group" }
wasm-bindgen = { version = "=0.2.78" }

View File

@ -0,0 +1,90 @@
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use ensogl::system::web::traits::*;
use ide_view_component_group::prelude::*;
use wasm_bindgen::prelude::*;
use ensogl::application::Application;
use ensogl::data::color;
use ensogl::display::navigation::navigator::Navigator;
use ensogl::display::DomSymbol;
use ensogl::system::web;
use ide_view_component_group::icon;
use ide_view_component_group::icon::SHRINK_AMOUNT;
// =============
// === Frame ===
// =============
/// A rectangular frame to mark the edges of icon. It can be displayed under them to see if they
/// are centered properly.
mod frame {
use super::*;
ensogl::define_shape_system! {
(style:Style) {
let inner = Rect((icon::SIZE.px(), icon::SIZE.px()));
let outer = inner.grow(0.2.px());
let shape = (outer - inner).fill(color::Rgba::black());
shape.shrink(SHRINK_AMOUNT.px()).into()
}
}
}
// ===================
// === Entry Point ===
// ===================
/// An entry point that displays all icon on a grid.
#[wasm_bindgen]
#[allow(dead_code)]
pub fn entry_point_searcher_icons() {
let logger = Logger::new("Icons example");
let app = Application::new("root");
ensogl_hardcoded_theme::builtin::dark::register(&app);
ensogl_hardcoded_theme::builtin::light::register(&app);
ensogl_hardcoded_theme::builtin::light::enable(&app);
let world = app.display.clone();
mem::forget(app);
let scene = &world.default_scene;
mem::forget(Navigator::new(scene, &scene.camera()));
// === Grid ===
let grid_div = web::document.create_div_or_panic();
grid_div.set_style_or_warn("width", "1000px");
grid_div.set_style_or_warn("height", "16px");
grid_div.set_style_or_warn("background-size", "1.0px 1.0px");
grid_div.set_style_or_warn(
"background-image",
"linear-gradient(to right, grey 0.05px, transparent 0.05px),
linear-gradient(to bottom, grey 0.05px, transparent 0.05px)",
);
let grid = DomSymbol::new(&grid_div);
scene.dom.layers.back.manage(&grid);
world.add_child(&grid);
grid.set_size(Vector2(1000.0, icon::SIZE));
mem::forget(grid);
// === Icons ===
let mut x = 0.0;
icon::Id::for_each(|id| {
let shape = id.create_shape(&logger, Vector2(icon::SIZE, icon::SIZE));
shape.weak_color.set(color::Rgba(0.475, 0.494, 0.145, 1.0).into());
shape.strong_color.set(color::Rgba(0.612, 0.627, 0.388, 1.0).into());
shape.set_position_x(x);
x += 20.0;
world.add_child(&shape);
mem::forget(shape);
});
}

View File

@ -21,5 +21,6 @@
// ============== // ==============
pub use debug_scene_component_group as component_group; pub use debug_scene_component_group as component_group;
pub use debug_scene_icons as icons;
pub use debug_scene_interface as interface; pub use debug_scene_interface as interface;
pub use debug_scene_visualization as visualization; pub use debug_scene_visualization as visualization;

View File

@ -22,8 +22,6 @@ use ensogl_component::list_view::ListView;
// === Export === // === Export ===
// ============== // ==============
pub mod icons;
pub use ensogl_component::list_view::entry; pub use ensogl_component::list_view::entry;
@ -38,12 +36,12 @@ pub const SEARCHER_WIDTH: f32 = 480.0;
/// ///
/// Because we don't implement clipping yet, the best UX is when searcher height is almost multiple /// Because we don't implement clipping yet, the best UX is when searcher height is almost multiple
/// of entry height + padding. /// of entry height + padding.
pub const SEARCHER_HEIGHT: f32 = 184.5; pub const SEARCHER_HEIGHT: f32 = 183.5;
const ACTION_LIST_GAP: f32 = 180.0; const ACTION_LIST_WIDTH: f32 = 180.0;
const LIST_DOC_GAP: f32 = 15.0; const LIST_DOC_GAP: f32 = 15.0;
const DOCUMENTATION_WIDTH: f32 = SEARCHER_WIDTH - ACTION_LIST_GAP - LIST_DOC_GAP; const DOCUMENTATION_WIDTH: f32 = SEARCHER_WIDTH - ACTION_LIST_WIDTH - LIST_DOC_GAP;
const ACTION_LIST_X: f32 = (ACTION_LIST_GAP - SEARCHER_WIDTH) / 2.0; const ACTION_LIST_X: f32 = (ACTION_LIST_WIDTH - SEARCHER_WIDTH) / 2.0;
const DOCUMENTATION_X: f32 = (SEARCHER_WIDTH - DOCUMENTATION_WIDTH) / 2.0; const DOCUMENTATION_X: f32 = (SEARCHER_WIDTH - DOCUMENTATION_WIDTH) / 2.0;
@ -150,7 +148,7 @@ impl Model {
} }
fn set_height(&self, h: f32) { fn set_height(&self, h: f32) {
self.list.resize(Vector2(ACTION_LIST_GAP, h)); self.list.resize(Vector2(ACTION_LIST_WIDTH, h));
self.documentation.visualization_frp.inputs.set_size.emit(Vector2(DOCUMENTATION_WIDTH, h)); self.documentation.visualization_frp.inputs.set_size.emit(Vector2(DOCUMENTATION_WIDTH, h));
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,11 @@
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
use enso_build::prelude::*; use enso_build::prelude::*;
fn main() -> Result { fn main() -> Result {
enso_build::cli::main::main() enso_build::cli::main::main()
} }

View File

@ -189,7 +189,7 @@ define_themes! { [light:0, dark:1]
} }
height = 27.0, 27.0; height = 27.0, 27.0;
padding { padding {
left = 16.5, 16.5; left = 11.0, 11.0;
right = 2.5, 2.5; right = 2.5, 2.5;
bottom = 5.0, 5.0; bottom = 5.0, 5.0;
} }
@ -203,26 +203,33 @@ define_themes! { [light:0, dark:1]
offset_y = shadow::offset_y , shadow::offset_y; offset_y = shadow::offset_y , shadow::offset_y;
} }
} }
entries { background_color_intensity = 0.2, 0.2;
dimmed_color_intensity = 0.5, 0.5;
entry_list {
text { text {
font = "DejaVuSans", "DejaVuSans"; font = "DejaVuSans", "DejaVuSans";
size = 12.0, 12.0; size = 12.0, 12.0;
color = Rgba(0.4,0.4,0.4,1.0), Rgba(0.4,0.4,0.4,1.0); color = Rgba(0.4,0.4,0.4,1.0), Rgba(0.4,0.4,0.4,1.0);
highlight_bold = 0.02, 0.02;
}
icon_text_gap = 5.0, 5.0;
icon {
size = 16.0, 16.0;
weak_color_intensity = 0.5, 0.5;
}
padding = 4.0, 4.0;
entry {
padding = 7.0, 7.0;
}
highlight {
height = 30.0, 30.0
} }
} }
background_color_intensity = 0.2, 0.2;
dimmed_color_intensity = 0.5, 0.5;
} }
} }
searcher { searcher {
action_list_gap = 10.0, 10.0; action_list_gap = 10.0, 10.0;
padding = 5.0, 5.0; padding = 5.0, 5.0;
selection {
padding {
horizontal = 2.0, 2.0;
vertical = 2.0, 2.0
}
}
icons { icons {
favorites = Rgba(0.98,0.584,0.122,1.0) , Rgba(0.98,0.584,0.122,1.0); favorites = Rgba(0.98,0.584,0.122,1.0) , Rgba(0.98,0.584,0.122,1.0);
io { io {
@ -557,14 +564,19 @@ define_themes! { [light:0, dark:1]
highlight = Rgba(0.906,0.914,0.922,1.0) , Lcha(1.0,0.0,0.0,0.15); // rgb(231,233,235) highlight = Rgba(0.906,0.914,0.922,1.0) , Lcha(1.0,0.0,0.0,0.15); // rgb(231,233,235)
text = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7); text = Lcha(0.0,0.0,0.0,0.7) , Lcha(1.0,0.0,0.0,0.7);
text { text {
highlight = selection, Rgba(0.275,0.549,0.839,1.0); // ... , rgb(70 140 214)
selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7); selection = Lcha(0.7,0.0,0.125,0.7) , Lcha(0.7,0.0,0.125,0.7);
font = "DejaVuSansMono", "DejaVuSansMono"; font = "DejaVuSansMono", "DejaVuSansMono";
size = 12.0, 12.0; size = 12.0, 12.0;
highlight_bold = 0.02, 0.02;
}
entry {
padding = 10.0, 10.0;
} }
highlight { highlight {
height = 24.0, 24.0;
corner_radius = 12.0, 12.0; corner_radius = 12.0, 12.0;
} }
padding = 5.0, 5.0;
} }
} }
colors { colors {

View File

@ -22,8 +22,6 @@ pub mod list;
// === Constants === // === Constants ===
// ================= // =================
/// Padding inside entry in pixels.
pub const PADDING: f32 = 14.0;
/// The overall entry's height (including padding). /// The overall entry's height (including padding).
pub const HEIGHT: f32 = 30.0; pub const HEIGHT: f32 = 30.0;
@ -48,12 +46,12 @@ pub use list::List;
/// ///
/// The entries should not assume any padding - it will be granted by ListView itself. The Display /// The entries should not assume any padding - it will be granted by ListView itself. The Display
/// Object position of this component is docked to the middle of left entry's boundary. It differs /// Object position of this component is docked to the middle of left entry's boundary. It differs
/// from usual behaviour of EnsoGl components, but makes the entries alignment much simpler. /// from usual behaviour of EnsoGl components, but makes the entries' alignment much simpler.
/// ///
/// This trait abstracts over model and its updating in order to support re-using shapes and gui /// This trait abstracts over model and its updating in order to support re-using shapes and gui
/// components, so they are not deleted and created again. The ListView component does not create /// components, so they are not deleted and created again. The ListView component does not create
/// Entry object for each entry provided, and during scrolling, the instantiated objects will be /// Entry object for each entry provided, and during scrolling, the instantiated objects will be
/// reused: they position will be changed and they will be updated using `update` method. /// reused: they position will be changed, and they will be updated using `update` method.
pub trait Entry: CloneRef + Debug + display::Object + 'static { pub trait Entry: CloneRef + Debug + display::Object + 'static {
/// The model of this entry. The entry should be a representation of data from the Model. /// The model of this entry. The entry should be a representation of data from the Model.
/// For example, the entry being just a caption can have [`String`] as its model - the text to /// For example, the entry being just a caption can have [`String`] as its model - the text to
@ -92,14 +90,14 @@ pub trait Entry: CloneRef + Debug + display::Object + 'static {
pub struct Label { pub struct Label {
display_object: display::object::Instance, display_object: display::object::Instance,
pub label: text::Area, pub label: text::Area,
text: Rc<RefCell<String>>, text: frp::Source<String>,
max_width_px: Rc<Cell<f32>>, max_width_px: frp::Source<f32>,
/// The `network` is public to allow extending it in components based on a [`Label`]. This /// The `network` is public to allow extending it in components based on a [`Label`]. This
/// should only be done for components that are small extensions of a Label, where creating a /// should only be done for components that are small extensions of a Label, where creating a
/// separate network for them would be an unnecessary overhead. /// separate network for them would be an unnecessary overhead.
/// Note: Networks extending this field will not outlive [`Label`]. /// Note: Networks extending this field will not outlive [`Label`].
pub network: enso_frp::Network, pub network: enso_frp::Network,
style_watch: StyleWatchFrp, pub style_watch: StyleWatchFrp,
} }
impl Label { impl Label {
@ -108,8 +106,6 @@ impl Label {
let logger = Logger::new("list_view::entry::Label"); let logger = Logger::new("list_view::entry::Label");
let display_object = display::object::Instance::new(logger); let display_object = display::object::Instance::new(logger);
let label = app.new_view::<ensogl_text::Area>(); let label = app.new_view::<ensogl_text::Area>();
let text = default();
let max_width_px = default();
let network = frp::Network::new("list_view::entry::Label"); let network = frp::Network::new("list_view::entry::Label");
let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet); let style_watch = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let text_style = style_prefix.sub("text"); let text_style = style_prefix.sub("text");
@ -120,6 +116,8 @@ impl Label {
display_object.add_child(&label); display_object.add_child(&label);
frp::extend! { network frp::extend! { network
init <- source::<()>(); init <- source::<()>();
text <- source::<String>();
max_width_px <- source::<f32>();
color <- all(&color,&init)._0(); color <- all(&color,&init)._0();
font <- all(&font,&init)._0(); font <- all(&font,&init)._0();
size <- all(&size,&init)._0(); size <- all(&size,&init)._0();
@ -128,15 +126,12 @@ impl Label {
label.set_font <+ font; label.set_font <+ font;
label.set_default_text_size <+ size.map(|v| text::Size(*v)); label.set_default_text_size <+ size.map(|v| text::Size(*v));
eval size ((size) label.set_position_y(size/2.0)); eval size ((size) label.set_position_y(size/2.0));
label.set_content_truncated <+ all(&text, &max_width_px);
} }
init.emit(()); init.emit(());
Self { display_object, label, text, max_width_px, network, style_watch } Self { display_object, label, text, max_width_px, network, style_watch }
} }
fn update_label_content(&self) {
let text = self.text.borrow().clone();
self.label.set_content_truncated(text, self.max_width_px.get());
}
} }
impl Entry for Label { impl Entry for Label {
@ -148,15 +143,11 @@ impl Entry for Label {
} }
fn update(&self, model: &Self::Model) { fn update(&self, model: &Self::Model) {
self.text.replace(model.clone()); self.text.emit(model.clone());
self.update_label_content();
} }
fn set_max_width(&self, max_width_px: f32) { fn set_max_width(&self, max_width_px: f32) {
if self.max_width_px.get() != max_width_px { self.max_width_px.emit(max_width_px);
self.max_width_px.set(max_width_px);
self.update_label_content();
}
} }
fn set_label_layer(&self, label_layer: &display::scene::Layer) { fn set_label_layer(&self, label_layer: &display::scene::Layer) {
@ -184,9 +175,10 @@ pub struct GlyphHighlightedLabelModel {
} }
/// The [`Entry`] similar to the [`Label`], but allows highlighting some parts of text. /// The [`Entry`] similar to the [`Label`], but allows highlighting some parts of text.
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)] #[derive(Clone, CloneRef, Debug)]
pub struct GlyphHighlightedLabel { pub struct GlyphHighlightedLabel {
inner: Label, pub inner: Label,
highlight: frp::Source<Vec<text::Range<text::Bytes>>>, highlight: frp::Source<Vec<text::Range<text::Bytes>>>,
} }
@ -194,19 +186,20 @@ impl Entry for GlyphHighlightedLabel {
type Model = GlyphHighlightedLabelModel; type Model = GlyphHighlightedLabelModel;
type Params = (); type Params = ();
fn new(app: &Application, style_prefix: &Path, _params: &Self::Params) -> Self { fn new(app: &Application, style_prefix: &Path, (): &Self::Params) -> Self {
let inner = Label::new(app, style_prefix); let inner = Label::new(app, style_prefix);
let network = &inner.network; let network = &inner.network;
let text_style = style_prefix.sub("text"); let text_style = style_prefix.sub("text");
let highlight_color = inner.style_watch.get_color(text_style.sub("highlight")); let highlight_bold = inner.style_watch.get_number(text_style.sub("highlight_bold"));
let label = &inner.label; let label = &inner.label;
frp::extend! { network frp::extend! { network
highlight <- source::<Vec<text::Range<text::Bytes>>>(); highlight <- source::<Vec<text::Range<text::Bytes>>>();
highlight_changed <- all(highlight,highlight_color); content_changed <- label.content.constant(());
eval highlight_changed ([label]((highlight,color)) { set_highlight <- all(highlight, highlight_bold, content_changed);
eval set_highlight ([label]((highlight, bold, ())) {
for range in highlight { for range in highlight {
label.set_color_bytes(range,color); label.set_sdf_bold(range, text::style::SdfBold::new(*bold));
} }
}); });
} }

View File

@ -20,8 +20,8 @@ use ensogl_core::display::style;
/// A displayed entry in select component. /// A displayed entry in select component.
/// ///
/// The Display Object position of this component is docked to the middle of left entry's boundary. /// The Display Object position of this component is docked to the middle of left entry's boundary.
/// It differs from usual behaviour of EnsoGL components, but makes the entries alignment much /// It differs from usual behaviour of EnsoGL components, but makes the entries' alignment much
/// simpler: In vast majority of cases we want to align list elements to the left. /// simpler: In the vast majority of cases we want to align list elements to the left.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug)] #[derive(Clone, CloneRef, Debug)]
#[clone_ref(bound = "E:CloneRef")] #[clone_ref(bound = "E:CloneRef")]
@ -165,7 +165,7 @@ impl<E: Entry> ListData<E, E::Params> {
&self, &self,
mut range: Range<entry::Id>, mut range: Range<entry::Id>,
max_width_px: f32, max_width_px: f32,
style_prefix: style::Path, style_prefix: &style::Path,
) { ) {
range.end = range.end.min(self.provider.get().entry_count()); range.end = range.end.min(self.provider.get().entry_count());
if range != self.entries_range.get() { if range != self.entries_range.get() {
@ -173,7 +173,7 @@ impl<E: Entry> ListData<E, E::Params> {
let provider = self.provider.get(); let provider = self.provider.get();
let current_entries: HashSet<entry::Id> = let current_entries: HashSet<entry::Id> =
with(self.entries.borrow_mut(), |mut entries| { with(self.entries.borrow_mut(), |mut entries| {
entries.resize_with(range.len(), || self.create_new_entry(&style_prefix)); entries.resize_with(range.len(), || self.create_new_entry(style_prefix));
entries.iter().filter_map(|entry| entry.id.get()).collect() entries.iter().filter_map(|entry| entry.id.get()).collect()
}); });
let missing = range.clone().filter(|id| !current_entries.contains(id)); let missing = range.clone().filter(|id| !current_entries.contains(id));
@ -271,7 +271,6 @@ impl<E: Entry> ListData<E, E::Params> {
let entry = E::new(&self.app, style_prefix, &self.entry_params.borrow()); let entry = E::new(&self.app, style_prefix, &self.entry_params.borrow());
let entry = DisplayedEntry { id: default(), entry }; let entry = DisplayedEntry { id: default(), entry };
entry.entry.set_label_layer(&layer); entry.entry.set_label_layer(&layer);
entry.entry.set_position_x(entry::PADDING);
self.add_child(&entry.entry); self.add_child(&entry.entry);
entry entry
} }

View File

@ -43,6 +43,7 @@ use ensogl_core::data::color;
use ensogl_core::display; use ensogl_core::display;
use ensogl_core::display::scene::layer::Layer; use ensogl_core::display::scene::layer::Layer;
use ensogl_core::display::shape::*; use ensogl_core::display::shape::*;
use ensogl_core::display::style;
use ensogl_core::Animation; use ensogl_core::Animation;
use ensogl_hardcoded_theme as theme; use ensogl_hardcoded_theme as theme;
use ensogl_shadow as shadow; use ensogl_shadow as shadow;
@ -67,7 +68,8 @@ const DEFAULT_STYLE_PATH: &str = theme::widget::list_view::HERE.str;
/// The size of shadow under element. It is not counted in the component width and height. /// The size of shadow under element. It is not counted in the component width and height.
pub const SHADOW_PX: f32 = 10.0; pub const SHADOW_PX: f32 = 10.0;
const SHAPE_PADDING: f32 = 5.0; /// The additional padding inside list view background and selection, added for better antialiasing
pub const SHAPE_MARGIN: f32 = 5.0;
// === Selection === // === Selection ===
@ -83,10 +85,8 @@ pub mod selection {
(style: Style, color: Vector4, corner_radius: f32) { (style: Style, color: Vector4, corner_radius: f32) {
let sprite_width : Var<Pixels> = "input_size.x".into(); let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into(); let sprite_height : Var<Pixels> = "input_size.y".into();
let padding_inner_x = style.get_number(theme::application::searcher::selection::padding::horizontal); let width = sprite_width - 2.0.px() * SHAPE_MARGIN;
let padding_inner_y = style.get_number(theme::application::searcher::selection::padding::vertical); let height = sprite_height - 2.0.px() * SHAPE_MARGIN;
let width = sprite_width - 2.0.px() * SHAPE_PADDING + 2.0.px() * padding_inner_x;
let height = sprite_height - 2.0.px() * SHAPE_PADDING + 2.0.px() * padding_inner_y;
let color = Var::<color::Rgba>::from(color); let color = Var::<color::Rgba>::from(color);
let rect = Rect((&width,&height)).corners_radius(corner_radius); let rect = Rect((&width,&height)).corners_radius(corner_radius);
let shape = rect.fill(color); let shape = rect.fill(color);
@ -110,8 +110,8 @@ pub mod background {
(style: Style, shadow_alpha: f32, corners_radius_px: f32, color: Vector4) { (style: Style, shadow_alpha: f32, corners_radius_px: f32, color: Vector4) {
let sprite_width : Var<Pixels> = "input_size.x".into(); let sprite_width : Var<Pixels> = "input_size.x".into();
let sprite_height : Var<Pixels> = "input_size.y".into(); let sprite_height : Var<Pixels> = "input_size.y".into();
let width = sprite_width - SHADOW_PX.px() * 2.0 - SHAPE_PADDING.px() * 2.0; let width = sprite_width - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
let height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_PADDING.px() * 2.0; let height = sprite_height - SHADOW_PX.px() * 2.0 - SHAPE_MARGIN.px() * 2.0;
let color = Var::<color::Rgba>::from(color); let color = Var::<color::Rgba>::from(color);
let rect = Rect((&width,&height)).corners_radius(corners_radius_px); let rect = Rect((&width,&height)).corners_radius(corners_radius_px);
let shape = rect.fill(color); let shape = rect.fill(color);
@ -184,27 +184,22 @@ impl<E: Entry> Model<E> {
self.background.shadow_alpha.set(alpha); self.background.shadow_alpha.set(alpha);
} }
fn padding(&self) -> f32 {
// FIXME : StyleWatch is unsuitable here, as it was designed as an internal tool for shape
// system (#795)
let styles = StyleWatch::new(&self.app.display.default_scene.style_sheet);
styles.get_number(theme::application::searcher::padding)
}
fn doubled_padding_with_shape_padding(&self) -> f32 {
2.0 * self.padding() + SHAPE_PADDING
}
/// Update the displayed entries list when _view_ has changed - the list was scrolled or /// Update the displayed entries list when _view_ has changed - the list was scrolled or
/// resized. /// resized.
fn update_after_view_change(&self, view: &View, style_prefix: display::style::Path) { fn update_after_view_change(
&self,
view: &View,
padding: f32,
entry_padding: f32,
style_prefix: &display::style::Path,
) {
let visible_entries = Self::visible_entries(view, self.entries.entry_count()); let visible_entries = Self::visible_entries(view, self.entries.entry_count());
let padding = self.doubled_padding_with_shape_padding(); let padding = Vector2(2.0 * padding, 2.0 * padding);
let padding = Vector2(padding, padding); let margin = Vector2(2.0 * SHAPE_MARGIN, 2.0 * SHAPE_MARGIN);
let entry_width = view.size.x - padding.x;
let shadow = Vector2(2.0 * SHADOW_PX, 2.0 * SHADOW_PX); let shadow = Vector2(2.0 * SHADOW_PX, 2.0 * SHADOW_PX);
self.entries.set_position_x(-view.size.x / 2.0); let entry_width = view.size.x - 2.0 * entry_padding;
self.background.size.set(view.size + padding + shadow); self.entries.set_position_x(-view.size.x / 2.0 + entry_padding);
self.background.size.set(view.size + padding + shadow + margin);
self.scrolled_area.set_position_y(view.size.y / 2.0 - view.position_y); self.scrolled_area.set_position_y(view.size.y / 2.0 - view.position_y);
self.entries.update_entries(visible_entries, entry_width, style_prefix); self.entries.update_entries(visible_entries, entry_width, style_prefix);
} }
@ -216,8 +211,7 @@ impl<E: Entry> Model<E> {
style_prefix: display::style::Path, style_prefix: display::style::Path,
) { ) {
let visible_entries = Self::visible_entries(view, provider.entry_count()); let visible_entries = Self::visible_entries(view, provider.entry_count());
let padding = self.doubled_padding_with_shape_padding(); let entry_width = view.size.x;
let entry_width = view.size.x - padding;
let entries = &self.entries; let entries = &self.entries;
entries.update_entries_new_provider(provider, visible_entries, entry_width, style_prefix); entries.update_entries_new_provider(provider, visible_entries, entry_width, style_prefix);
} }
@ -338,6 +332,71 @@ ensogl_core::define_endpoints! {
} }
} }
/// A structure containing FRP nodes connected to appropriate style values.
///
/// [`ListView`] is a general-use component, and it different places it could be styled differently.
/// Therefore the [`ListView`] users (developers) may set the style prefix from where the
/// style values will be read. This structure keeps a network connecting a style values from a
/// particular prefix with its fields. It allows also reconnecting to another prefix without losing
/// the fields (so the connections from them will remain intact).
#[derive(Clone, CloneRef, Debug)]
struct StyleFrp {
style_connection_network: Rc<CloneRefCell<Option<frp::Network>>>,
background_color: frp::Any<color::Rgba>,
selection_color: frp::Any<color::Rgba>,
selection_corner_radius: frp::Any<f32>,
selection_height: frp::Any<f32>,
padding: frp::Any<f32>,
entry_padding: frp::Any<f32>,
}
impl StyleFrp {
fn new(network: &frp::Network) -> Self {
let style_connection_network = default();
frp::extend! { network
background_color <- any(...);
selection_color <- any(...);
selection_corner_radius <- any(...);
selection_height <- any(...);
padding <- any(...);
entry_padding <- any(...);
}
Self {
style_connection_network,
background_color,
selection_color,
selection_corner_radius,
selection_height,
padding,
entry_padding,
}
}
/// Connect the structure's fields with new style prefix. The bindings with the previous
/// prefix will be removed.
fn connect_with_prefix(&self, style: &StyleWatchFrp, prefix: &style::Path) {
let style_connection_network = frp::Network::new("list_view::StyleFrp");
let background_color = style.get_color(prefix.sub("background"));
let selection_color = style.get_color(prefix.sub("highlight"));
let selection_corner_radius =
style.get_number(prefix.sub("highlight").sub("corner_radius"));
let selection_height = style.get_number(prefix.sub("highlight").sub("height"));
let padding = style.get_number(prefix.sub("padding"));
let entry_padding = style.get_number(prefix.sub("entry").sub("padding"));
frp::extend! { style_connection_network
init <- source_();
self.background_color <+ all(&background_color, &init)._0();
self.selection_color <+ all(&selection_color, &init)._0();
self.selection_corner_radius <+ all(&selection_corner_radius, &init)._0();
self.selection_height <+ all(&selection_height, &init)._0();
self.padding <+ all(&padding, &init)._0();
self.entry_padding <+ all(&entry_padding, &init)._0();
}
// At this point the old network is dropped, and old connections are removed.
self.style_connection_network.set(Some(style_connection_network));
init.emit(());
}
}
// ========================== // ==========================
@ -353,6 +412,7 @@ ensogl_core::define_endpoints! {
pub struct ListView<E: Entry> { pub struct ListView<E: Entry> {
model: Model<E>, model: Model<E>,
pub frp: Frp<E>, pub frp: Frp<E>,
style_frp: StyleFrp,
} }
impl<E: Entry> Deref for ListView<E> { impl<E: Entry> Deref for ListView<E> {
@ -369,11 +429,11 @@ where E::Model: Default
pub fn new(app: &Application) -> Self { pub fn new(app: &Application) -> Self {
let frp = Frp::new(); let frp = Frp::new();
let model = Model::new(app); let model = Model::new(app);
ListView { model, frp }.init(app) let style_frp = StyleFrp::new(&frp.network);
ListView { model, frp, style_frp }.init(app)
} }
fn init(self, app: &Application) -> Self { fn init(self, app: &Application) -> Self {
const MAX_SCROLL: f32 = entry::HEIGHT / 2.0;
const MOUSE_MOVE_THRESHOLD: f32 = std::f32::EPSILON; const MOUSE_MOVE_THRESHOLD: f32 = std::f32::EPSILON;
let frp = &self.frp; let frp = &self.frp;
@ -384,11 +444,8 @@ where E::Model: Default
let view_y = Animation::<f32>::new(network); let view_y = Animation::<f32>::new(network);
let selection_y = Animation::<f32>::new(network); let selection_y = Animation::<f32>::new(network);
let selection_height = Animation::<f32>::new(network); let selection_height = Animation::<f32>::new(network);
let style = StyleWatchFrp::new(&scene.style_sheet); let style_watch = StyleWatchFrp::new(&scene.style_sheet);
use theme::widget::list_view as list_view_style; let style = &self.style_frp;
let default_background_color = style.get_color(list_view_style::background);
let selection_color = style.get_color(list_view_style::highlight);
let selection_corner_radius = style.get_number(list_view_style::highlight::corner_radius);
frp::extend! { network frp::extend! { network
@ -403,8 +460,7 @@ where E::Model: Default
background_corners_radius <- any( background_corners_radius <- any(
&default_background_corners_radius,&frp.set_background_corners_radius); &default_background_corners_radius,&frp.set_background_corners_radius);
eval background_corners_radius ((px) model.background.corners_radius_px.set(*px)); eval background_corners_radius ((px) model.background.corners_radius_px.set(*px));
default_background_color <- all(&default_background_color,&init)._0(); background_color <- any(&style.background_color, &frp.set_background_color);
background_color <- any(&default_background_color,&frp.set_background_color);
eval background_color ((color) model.background.color.set(color.into())); eval background_color ((color) model.background.color.set(color.into()));
@ -490,39 +546,46 @@ where E::Model: Default
selection_y.target <+ frp.selected_entry.map(|id| selection_y.target <+ frp.selected_entry.map(|id|
id.map_or(0.0,entry::List::<E>::position_y_of_entry) id.map_or(0.0,entry::List::<E>::position_y_of_entry)
); );
selection_height.target <+ frp.selected_entry.map(f!([](id) selection_height.target <+ all_with(&frp.selected_entry, &style.selection_height, |id, h|
if id.is_some() {entry::HEIGHT} else {0.0} if id.is_some() {*h} else {-SHAPE_MARGIN}
)); );
selection_y.skip <+ frp.set_entries.constant(()); selection_y.skip <+ frp.set_entries.constant(());
selection_height.skip <+ frp.set_entries.constant(()); selection_height.skip <+ frp.set_entries.constant(());
selection_sprite_y <- all_with(&selection_y.value, &selection_height.value, selection_sprite_y <- all_with3(&selection_y.value, &selection_height.value, &style.selection_height,
|y, h| y + (entry::HEIGHT - h) / 2.0 |y, h, max_h| y + (max_h - h) / 2.0
); );
eval selection_sprite_y ((y) model.selection.set_position_y(*y)); eval selection_sprite_y ((y) model.selection.set_position_y(*y));
frp.source.selection_size <+ all_with(&frp.size,&selection_height.value,f!([](size,height) { frp.source.selection_size <+ all_with3(&frp.size, &style.padding, &selection_height.value, f!([](size, padding, height) {
let width = size.x; let width = size.x - 2.0 * padding;
Vector2(width,*height) Vector2(width,*height)
})); }));
eval frp.selection_size ((size) model.selection.size.set(*size)); eval frp.selection_size ([model](size) {
let margin = Vector2(SHAPE_MARGIN, SHAPE_MARGIN);
model.selection.size.set(*size + 2.0 * margin)
});
eval_ frp.hide_selection (model.selection.unset_parent()); eval_ frp.hide_selection (model.selection.unset_parent());
// === Scrolling === // === Scrolling ===
selection_top_after_move_up <- selected_entry_after_move_up.map(|id| max_scroll <- style.selection_height.map(|h| *h / 2.0).sampler();
id.map(|id| entry::List::<E>::y_range_of_entry(id).end) selection_top_after_move_up <- selected_entry_after_move_up.map2(&style.selection_height, |id, h|
id.map(|id| entry::List::<E>::position_y_of_entry(id) + *h / 2.0)
); );
min_scroll_after_move_up <- selection_top_after_move_up.map(|top| min_scroll_after_move_up <- selection_top_after_move_up.map2(&max_scroll, |top, max_scroll|
top.unwrap_or(MAX_SCROLL) top.unwrap_or(*max_scroll)
); );
scroll_after_move_up <- min_scroll_after_move_up.map2(&frp.scroll_position,|min,current| scroll_after_move_up <- min_scroll_after_move_up.map2(&frp.scroll_position,|min,current|
current.max(*min) current.max(*min)
); );
selection_bottom_after_move_down <- selected_entry_after_move_down.map(|id| selection_bottom_after_move_down <- selected_entry_after_move_down.map2(&style.selection_height, |id, h|
id.map(|id| entry::List::<E>::y_range_of_entry(id).start) id.map(|id| entry::List::<E>::position_y_of_entry(id) - *h / 2.0)
); );
max_scroll_after_move_down <- selection_bottom_after_move_down.map2(&frp.size, max_scroll_after_move_down <- selection_bottom_after_move_down.map4(
|y,size| y.map_or(MAX_SCROLL, |y| y + size.y) &frp.size,
&style.padding,
&max_scroll,
|y, size, padding, max_scroll| y.map_or(*max_scroll, |y| y + size.y - 2.0 * padding)
); );
scroll_after_move_down <- max_scroll_after_move_down.map2(&frp.scroll_position, scroll_after_move_down <- max_scroll_after_move_down.map2(&frp.scroll_position,
|max_scroll,current| current.min(*max_scroll) |max_scroll,current| current.min(*max_scroll)
@ -530,52 +593,51 @@ where E::Model: Default
frp.source.scroll_position <+ scroll_after_move_up; frp.source.scroll_position <+ scroll_after_move_up;
frp.source.scroll_position <+ scroll_after_move_down; frp.source.scroll_position <+ scroll_after_move_down;
frp.source.scroll_position <+ frp.scroll_jump; frp.source.scroll_position <+ frp.scroll_jump;
frp.source.scroll_position <+ frp.set_entries.constant(MAX_SCROLL); frp.source.scroll_position <+ max_scroll.sample(&frp.set_entries);
view_y.target <+ frp.scroll_position; view_y.target <+ frp.scroll_position;
view_y.target <+ frp.set_entries.constant(MAX_SCROLL); view_y.target <+ max_scroll.sample(&frp.set_entries);
view_y.skip <+ frp.set_entries.constant(()); view_y.skip <+ frp.set_entries.constant(());
view_y.target <+ init.constant(MAX_SCROLL); view_y.target <+ max_scroll.sample(&init);
view_y.skip <+ init; view_y.skip <+ init;
// === Resize === // === Resize ===
frp.source.size <+ frp.resize.map(f!([model](size) frp.source.size <+ frp.resize;
size - Vector2(model.padding(),model.padding()))
);
// === Update Entries === // === Update Entries ===
view_info <- all_with(&view_y.value,&frp.size, |y,size| view_info <- all_with3(&view_y.value, &frp.size, &style.padding, |&y, &size, &padding| {
View{position_y:*y,size:*size} let padding = Vector2(2.0 * padding, 2.0 * padding);
); View { position_y: y, size: size - padding }
});
default_style_prefix <- init.constant(DEFAULT_STYLE_PATH.to_string()); default_style_prefix <- init.constant(DEFAULT_STYLE_PATH.to_string());
style_prefix <- any(&default_style_prefix,&frp.set_style_prefix); style_prefix <- any(&default_style_prefix,&frp.set_style_prefix);
frp.source.style_prefix <+ style_prefix; eval style_prefix ([model, style, style_watch](path) {
eval style_prefix ((path) style.connect_with_prefix(&style_watch, &path.into());
model.entries.recreate_entries_with_style_prefix(path.into())); model.entries.recreate_entries_with_style_prefix(path.into());
view_and_style <- all(&view_info,&style_prefix); });
view_and_style <- all(view_info, style.padding, style.entry_padding, style_prefix);
// This should go before handling mouse events to have proper checking of // This should go before handling mouse events to have proper checking of
eval view_and_style (((view,style_prefix)) eval view_and_style (((view, padding, entry_padding, style))
model.update_after_view_change(view,style_prefix.into())); model.update_after_view_change(view, *padding, *entry_padding, &style.into()));
_new_entries <- frp.set_entries.map2(&view_and_style, f!((entries,(view,style_prefix)) _new_entries <- frp.set_entries.map2(&view_and_style, f!((entries, (view, _, _, style))
model.set_entries(entries.clone_ref(),view,style_prefix.into())) model.set_entries(entries.clone_ref(), view, style.into())
); ));
frp.source.selection_position_target <+ all_with3( frp.source.selection_position_target <+ all_with4(
&selection_y.target, &selection_y.target,
&view_y.target, &view_y.target,
&frp.size, &frp.size,
|selection_y, view_y, size| Vector2(0.0, (size.y / 2.0) - view_y + selection_y) &style.padding,
|sel_y, view_y, size, padding| Vector2(0.0, (size.y / 2.0 - padding) - view_y + sel_y)
); );
selection_color <- all(&selection_color, &init)._0(); eval style.selection_color ((color) model.selection.color.set(color.into()));
selection_corner_radius <- all(&selection_corner_radius, &init)._0(); eval style.selection_corner_radius ((radius) model.selection.corner_radius.set(*radius));
eval selection_color ((color) model.selection.color.set(color.into()));
eval selection_corner_radius ((radius) model.selection.corner_radius.set(*radius));
} }
init.emit(()); init.emit(());
frp.scroll_jump(MAX_SCROLL); frp.scroll_jump(max_scroll.value());
self self
} }
@ -645,6 +707,7 @@ mod tests {
use approx::assert_relative_eq; use approx::assert_relative_eq;
use enso_frp::future::EventOutputExt; use enso_frp::future::EventOutputExt;
use ensogl_core::display::style::data::DataMatch;
#[test] #[test]
fn navigating_list_view_with_keyboard() { fn navigating_list_view_with_keyboard() {
@ -695,11 +758,17 @@ mod tests {
#[test] #[test]
fn selection_position() { fn selection_position() {
use ensogl_hardcoded_theme::widget::list_view as theme;
let app = Application::new("root"); let app = Application::new("root");
ensogl_hardcoded_theme::builtin::light::register(&app);
ensogl_hardcoded_theme::builtin::light::enable(&app);
let style_sheet = &app.display.default_scene.style_sheet;
style_sheet.set(theme::highlight::height, entry::HEIGHT);
let padding = style_sheet.value(theme::padding).unwrap().number().unwrap();
let list_view = ListView::<entry::Label>::new(&app); let list_view = ListView::<entry::Label>::new(&app);
let provider = let provider =
AnyModelProvider::<entry::Label>::new(vec!["Entry 1", "Entry 2", "Entry 3", "Entry 4"]); AnyModelProvider::<entry::Label>::new(vec!["Entry 1", "Entry 2", "Entry 3", "Entry 4"]);
list_view.resize(Vector2(100.0, entry::HEIGHT * 3.0)); list_view.resize(Vector2(100.0, entry::HEIGHT * 3.0 + padding * 2.0));
list_view.set_entries(provider); list_view.set_entries(provider);
list_view.select_entry(Some(0)); list_view.select_entry(Some(0));
assert_relative_eq!(list_view.selection_position_target.value().x, 0.0); assert_relative_eq!(list_view.selection_position_target.value().x, 0.0);

View File

@ -58,7 +58,7 @@ impl component::Frp<Model> for Frp {
fn init(api: &Self::Private, app: &Application, model: &Model, _style: &StyleWatchFrp) { fn init(api: &Self::Private, app: &Application, model: &Model, _style: &StyleWatchFrp) {
let network = &api.network; let network = &api.network;
let line = &model.line.events; let line = &model.line.events;
frp::extend! { TRACE_ALL network frp::extend! { network
eval api.input.set_size((size) model.set_size(*size)); eval api.input.set_size((size) model.set_size(*size));
eval api.input.set_color((color) model.set_color(*color)); eval api.input.set_color((color) model.set_color(*color));
eval api.input.set_cap((direction) model.set_cap(*direction)); eval api.input.set_cap((direction) model.set_cap(*direction));

View File

@ -872,7 +872,7 @@ impl<Host, T: Object<Host>> Object<Host> for &T {
// === ObjectOps === // === ObjectOps ===
// ================= // =================
impl<Host, T: Object<Host>> ObjectOps<Host> for T {} impl<Host, T: Object<Host> + ?Sized> ObjectOps<Host> for T {}
/// Implementation of operations available for every struct which implements `display::Object`. /// Implementation of operations available for every struct which implements `display::Object`.
/// To learn more about the design, please refer to the documentation of [`Instance`]. /// To learn more about the design, please refer to the documentation of [`Instance`].
@ -894,7 +894,7 @@ pub trait ObjectOps<Host = Scene>: Object<Host> {
/// Add another display object as a child to this display object. Children will inherit all /// Add another display object as a child to this display object. Children will inherit all
/// transformations of their parents. /// transformations of their parents.
fn add_child<T: Object<Host>>(&self, child: &T) { fn add_child<T: Object<Host> + ?Sized>(&self, child: &T) {
self.display_object()._add_child(child.display_object()); self.display_object()._add_child(child.display_object());
} }

View File

@ -373,7 +373,7 @@ macro_rules! newtype_struct_impls {
/// Smart constructor. /// Smart constructor.
$(#$meta)* $(#$meta)*
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn $name($field:$field_type) -> $name { $name {$field} } pub const fn $name($field:$field_type) -> $name { $name {$field} }
impl From<&$name> for $name { fn from(t:&$name) -> Self { *t } } impl From<&$name> for $name { fn from(t:&$name) -> Self { *t } }
impl From<&&$name> for $name { fn from(t:&&$name) -> Self { **t } } impl From<&&$name> for $name { fn from(t:&&$name) -> Self { **t } }