diff --git a/Cargo.lock b/Cargo.lock index ad4f1691426b..d4c23683c7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2751,6 +2751,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ensogl-example-built-in-shapes" +version = "0.1.0" +dependencies = [ + "ensogl-core", + "ensogl-hardcoded-theme", + "wasm-bindgen", +] + [[package]] name = "ensogl-example-cached-shape" version = "0.1.0" @@ -2976,6 +2985,7 @@ version = "0.1.0" dependencies = [ "ensogl-example-animation", "ensogl-example-auto-layout", + "ensogl-example-built-in-shapes", "ensogl-example-cached-shape", "ensogl-example-complex-shape-system", "ensogl-example-custom-shape-system", diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/def/class.rs b/lib/rust/ensogl/core/src/display/shape/primitive/def/class.rs index b2d22b2ec1e9..3998271c594f 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/def/class.rs +++ b/lib/rust/ensogl/core/src/display/shape/primitive/def/class.rs @@ -132,6 +132,13 @@ where for<'t> &'t Self: IntoOwned { Union(self, that) } + /// Unify two shapes, blending their colors based on the foreground shape's SDF value. This + /// means that even if these shapes overlap and the foreground is semi-transparent, it will + /// blend with the background only in the anti-aliased areas. + fn union_exclusive(&self, that: S) -> UnionExclusive> { + UnionExclusive(self, that) + } + /// Subtracts the argument from this shape. fn difference(&self, that: S) -> Difference> { Difference(self, that) diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/def/modifier.rs b/lib/rust/ensogl/core/src/display/shape/primitive/def/modifier.rs index bc9e48acfbe0..42e6ae5636c8 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/def/modifier.rs +++ b/lib/rust/ensogl/core/src/display/shape/primitive/def/modifier.rs @@ -115,17 +115,18 @@ macro_rules! _define_modifier { pub use immutable::*; define_modifiers! { - Translate translate (child) (v:Vector2) - Rotation rotation (child) (angle:Radians) - Scale scale (child) (value:f32) - FlipY flip_y (child) () - Union union (child1,child2) () - Difference difference (child1,child2) () - Intersection intersection (child1,child2) () - Fill fill (child) (color:Rgba) - Recolorize recolorize (child) (r: Rgba, g: Rgba, b: Rgba) - PixelSnap pixel_snap (child) () - Grow grow (child) (value:f32) - Shrink shrink (child) (value:f32) - Repeat repeat (child) (tile_size:Vector2) + Translate translate (child) (v:Vector2) + Rotation rotation (child) (angle:Radians) + Scale scale (child) (value:f32) + FlipY flip_y (child) () + Union union (child1,child2) () + UnionExclusive union_exclusive (child1,child2) () + Difference difference (child1,child2) () + Intersection intersection (child1,child2) () + Fill fill (child) (color:Rgba) + Recolorize recolorize (child) (r: Rgba, g: Rgba, b: Rgba) + PixelSnap pixel_snap (child) () + Grow grow (child) (value:f32) + Shrink shrink (child) (value:f32) + Repeat repeat (child) (tile_size:Vector2) } diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/glsl/shape.glsl b/lib/rust/ensogl/core/src/display/shape/primitive/glsl/shape.glsl index edafa5d48ed7..ca4870ebecf9 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/glsl/shape.glsl +++ b/lib/rust/ensogl/core/src/display/shape/primitive/glsl/shape.glsl @@ -49,11 +49,15 @@ Color unpremultiply(PremultipliedColor c) { return color(rgb, alpha); } +PremultipliedColor blend_with_ratio(PremultipliedColor bg, PremultipliedColor fg, float ratio) { + vec4 raw = fg.repr.raw + (1.0 - ratio) * bg.repr.raw; + return PremultipliedColor(rgba(raw)); +} + /// Implements glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA) /// in the [`Color`]'s color space. See docs of [`Color`] to learn more. PremultipliedColor blend(PremultipliedColor bg, PremultipliedColor fg) { - vec4 raw = fg.repr.raw + (1.0 - fg.repr.raw.a) * bg.repr.raw; - return PremultipliedColor(rgba(raw)); + return blend_with_ratio(bg, fg, fg.repr.raw.a); } Srgba srgba(PremultipliedColor color) { @@ -370,7 +374,7 @@ Shape inverse (Shape s1) { return shape(s1.id, inverse(s1.sdf), s1.color); } -Shape unify (Shape s1, Shape s2) { +Shape unify (Shape bg, Shape fg) { if (input_display_mode == DISPLAY_MODE_CACHED_SHAPES_TEXTURE) { // In DISPLAY_MODE_CACHED_SHAPES_TEXTURE the color has not [`alpha`] field applied (See // [`color`] documentation for explanation). That means, that even outside the @@ -383,11 +387,19 @@ Shape unify (Shape s1, Shape s2) { // results ([`alpha`] field). // * We want to keep the color consistent near border of the both shapes. // The code below meets the both conditions. - if (s2.sdf.distance > s1.sdf.distance) { - s2.color.repr.raw *= s2.alpha; + if (fg.sdf.distance > bg.sdf.distance) { + fg.color.repr.raw *= fg.alpha; } } - return shape(s1.id, unify(s1.sdf, s2.sdf), blend(s1.color, s2.color)); + return shape(bg.id, unify(bg.sdf, fg.sdf), blend(bg.color, fg.color)); +} + +// Unify two shapes, blending their colors based on the foreground shape's SDF value. This means +// that even if these shapes overlap and the foreground is semi-transparent, it will blend with +// the background only in the anti-aliased areas. +Shape unify_exclusive (Shape bg, Shape fg) { + float ratio = render(fg.sdf); + return shape(bg.id, unify(bg.sdf, fg.sdf), blend_with_ratio(bg.color, fg.color, ratio)); } Shape difference (Shape s1, Shape s2) { diff --git a/lib/rust/ensogl/core/src/display/shape/primitive/shader/canvas.rs b/lib/rust/ensogl/core/src/display/shape/primitive/shader/canvas.rs index 3cb30d1dcd0a..8e5d20435553 100644 --- a/lib/rust/ensogl/core/src/display/shape/primitive/shader/canvas.rs +++ b/lib/rust/ensogl/core/src/display/shape/primitive/shader/canvas.rs @@ -176,6 +176,14 @@ impl Canvas { }) } + /// Create an exclusive union shape from the provided shape components. + pub fn union_exclusive(&mut self, num: usize, s1: Shape, s2: Shape) -> Shape { + self.if_not_defined(num, |this| { + let expr = format!("return unify_exclusive({},{});", s1.getter(), s2.getter()); + this.new_shape_from_expr(&expr) + }) + } + /// Create a difference shape from the provided shape components. pub fn difference(&mut self, num: usize, s1: Shape, s2: Shape) -> Shape { self.if_not_defined(num, |this| { diff --git a/lib/rust/ensogl/examples/Cargo.toml b/lib/rust/ensogl/examples/Cargo.toml index ec9c46fabf49..5a56c933f08e 100644 --- a/lib/rust/ensogl/examples/Cargo.toml +++ b/lib/rust/ensogl/examples/Cargo.toml @@ -10,16 +10,17 @@ crate-type = ["cdylib", "rlib"] [dependencies] ensogl-example-animation = { path = "animation" } ensogl-example-auto-layout = { path = "auto-layout" } +ensogl-example-built-in-shapes = { path = "built-in-shapes" } ensogl-example-cached-shape = { path = "cached-shape" } ensogl-example-complex-shape-system = { path = "complex-shape-system" } ensogl-example-custom-shape-system = { path = "custom-shape-system" } ensogl-example-dom-symbols = { path = "dom-symbols" } ensogl-example-drop-down = { path = "drop-down" } ensogl-example-drop-manager = { path = "drop-manager" } -ensogl-example-focus-management = { path = "focus-management" } ensogl-example-easing-animator = { path = "easing-animator" } -ensogl-example-list-view = { path = "list-view" } +ensogl-example-focus-management = { path = "focus-management" } ensogl-example-grid-view = { path = "grid-view" } +ensogl-example-list-view = { path = "list-view" } ensogl-example-mouse-events = { path = "mouse-events" } ensogl-example-profiling-run-graph = { path = "profiling-run-graph" } ensogl-example-render-profile-flamegraph = { path = "render-profile-flamegraph" } diff --git a/lib/rust/ensogl/examples/built-in-shapes/Cargo.toml b/lib/rust/ensogl/examples/built-in-shapes/Cargo.toml new file mode 100644 index 000000000000..21ee6bdbf3e5 --- /dev/null +++ b/lib/rust/ensogl/examples/built-in-shapes/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ensogl-example-built-in-shapes" +version = "0.1.0" +authors = ["Enso Team "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +ensogl-core = { path = "../../core" } +wasm-bindgen = { workspace = true } +ensogl-hardcoded-theme = { path = "../../../ensogl/app/theme/hardcoded" } + +# Stop wasm-pack from running wasm-opt, because we run it from our build scripts in order to customize options. +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/lib/rust/ensogl/examples/built-in-shapes/src/lib.rs b/lib/rust/ensogl/examples/built-in-shapes/src/lib.rs new file mode 100644 index 000000000000..1260e51f4f88 --- /dev/null +++ b/lib/rust/ensogl/examples/built-in-shapes/src/lib.rs @@ -0,0 +1,331 @@ +//! Example scene showing the usage of built-in high-level shapes. + +// === Standard Linter Configuration === +#![deny(non_ascii_idents)] +#![warn(unsafe_code)] +#![allow(clippy::bool_to_int_with_if)] +#![allow(clippy::let_and_return)] + +use ensogl_core::display::shape::*; +use ensogl_core::display::world::*; +use ensogl_core::prelude::*; + +use ensogl_core::data::color; +use ensogl_core::display; +use ensogl_core::display::navigation::navigator::Navigator; +use ensogl_core::display::object::ObjectOps; + + + +// ============== +// === Shapes === +// ============== + +mod rectangle { + use super::*; + ensogl_core::shape! {( + style: Style, + color: Vector4, + corner_radius: f32, + inset: f32, + border: f32, + border_color: Vector4, + clip: Vector2, + ) { + // === Canvas === + let canvas_width = Var::::from("input_size.x"); + let canvas_height = Var::::from("input_size.y"); + + // === Clip === + // Clipping scales the shape in such a way, that the visible part will occupy whole + // canvas area. Thus, we need to recompute the new canvas size for the scaled shape. + let canvas_clip_height_diff = &canvas_height * (clip.y() * 2.0); + let canvas_clip_width_diff = &canvas_width * (clip.x() * 2.0); + let canvas_height = canvas_height + &canvas_clip_height_diff; + let canvas_width = canvas_width + &canvas_clip_width_diff; + + // === Body === + let inset2 = (&inset * 2.0).px(); + let width = &canvas_width - &inset2; + let height = &canvas_height - &inset2; + let color = Var::::from(color); + let body = Rect((&width, &height)).corners_radius(corner_radius.px()); + let body = body.fill(color); + + // === Border === + let border = body.grow(border.px()); + let border_color = Var::::from(border_color); + let border = border.fill(border_color); + + // === Shape === + let shape = border.union_exclusive(&body); + + // === Clip Adjustment === + let shape = shape.translate((-canvas_clip_width_diff/2.0, -canvas_clip_height_diff/2.0)); + shape.into() + } + } +} + + +/// A rectangle shape with the following configurable properties: +/// - The body color of the shape. +/// - The corner radius of the shape. +/// - The inset, padding between edge of the frame and shape itself. +/// - The border width and color. +/// - The clipping of the shape (e.g. clipping bottom half of the shape). +/// +/// # Performance +/// This shape has been specifically designed to be utilized across various sections of the GUI. Its +/// numerous parameters enable a highly adaptable approach to drawing a diverse range of shapes, +/// such as circles, rings, or ring segments. The advantage of having a singular shape for these +/// cases is that a single draw call can be used to render multiple GUI elements, which ultimately +/// enhances performance. +#[derive(Clone, CloneRef, Debug, Deref, Default)] +#[allow(missing_docs)] +pub struct Rectangle { + pub view: rectangle::View, +} + +impl Rectangle { + fn modify_view(&self, f: impl FnOnce(&rectangle::View)) -> &Self { + f(&self.view); + self + } + + /// Constructor. + pub fn new() -> Self { + Self::default() + } + + /// Builder-style modifier, allowing setting shape properties without creating a temporary + /// variable after its construction. + pub fn build(self, f: impl FnOnce(&Self)) -> Self { + f(&self); + self + } + + /// Set the color of the body of the shape. + pub fn set_color(&self, color: color::Rgba) -> &Self { + self.modify_view(|view| view.color.set(color.into())) + } + + /// Set the corner radius. If the corner radius will be larger than possible (e.g. larger than + /// the shape dimension), it will be clamped to the highest possible value. + pub fn set_corner_radius(&self, radius: f32) -> &Self { + self.modify_view(|view| view.corner_radius.set(radius)) + } + + /// Set the corner radius to maximum. If the width and height of the shape are equal, it will + /// result in a circle. + pub fn set_corner_radius_max(&self) -> &Self { + // We are using here a value bigger than anything we will ever need. We are not using + // biggest possible GLSL float value in order not to get rendering artifacts. + let max_radius = 1000000.0; + self.set_corner_radius(max_radius) + } + + /// Set the padding between edge of the frame and shape itself. If you want to use border, you + /// should always set the inset at least of the size of the border. If you do not want the + /// border to be animated, you can use [`Self::set_inset_border`] instead. + pub fn set_inset(&self, inset: f32) -> &Self { + self.modify_view(|view| view.inset.set(inset)) + } + + /// Set the border size of the shape. If you want to use border, you should always set the inset + /// at least of the size of the border. If you do not want the border to be animated, you can + /// use [`Self::set_inset_border`] instead. + pub fn set_border(&self, border: f32) -> &Self { + self.modify_view(|view| view.border.set(border)) + } + + /// Set both the inset and border at once. See documentation of [`Self::set_border`] and + /// [`Self::set_inset`] to learn more. + pub fn set_inset_border(&self, border: f32) -> &Self { + self.set_inset(border).set_border(border) + } + + /// Set the border color. + pub fn set_border_color(&self, color: color::Rgba) -> &Self { + self.modify_view(|view| view.border_color.set(color.into())) + } + + /// Set clipping of the shape. The clipping is normalized, which means, that the value of 0.5 + /// means that we are clipping 50% of the shape. The clipping is performed always on the left + /// and on the bottom of the shape. If you want to clip other sides of the shape, you can rotate + /// it after clipping or use one of the predefined helper functions, such as + /// [`Self::keep_bottom_half`]. + pub fn set_clip(&self, clip: Vector2) -> &Self { + self.modify_view(|view| view.clip.set(clip)) + } + + /// Keep only the top half of the shape. + pub fn keep_top_half(&self) -> &Self { + self.set_clip(Vector2(0.0, 0.5)) + } + + /// Keep only the bottom half of the shape. + pub fn keep_bottom_half(&self) -> &Self { + self.keep_top_half().flip() + } + + /// Keep only the right half of the shape. + pub fn keep_right_half(&self) -> &Self { + self.set_clip(Vector2(0.5, 0.0)) + } + + /// Keep only the left half of the shape. + pub fn keep_left_half(&self) -> &Self { + self.keep_right_half().flip() + } + + /// Keep only the top right quarter of the shape. + pub fn keep_top_right_quarter(&self) -> &Self { + self.set_clip(Vector2(0.5, 0.5)) + } + + /// Keep only the bottom right quarter of the shape. + pub fn keep_bottom_right_quarter(&self) -> &Self { + self.keep_top_right_quarter().rotate_90() + } + + /// Keep only the bottom left quarter of the shape. + pub fn keep_bottom_left_quarter(&self) -> &Self { + self.keep_bottom_right_quarter().rotate_180() + } + + /// Keep only the top left quarter of the shape. + pub fn keep_top_left_quarter(&self) -> &Self { + self.keep_bottom_left_quarter().rotate_270() + } + + /// Flip the shape via its center. This is equivalent to rotating the shape by 180 degrees. + pub fn flip(&self) -> &Self { + self.rotate_180() + } + + /// Rotate the shape by 90 degrees. + pub fn rotate_90(&self) -> &Self { + self.modify_view(|view| view.set_rotation_z(-std::f32::consts::PI / 2.0)) + } + + /// Counter rotate the shape by 90 degrees. + pub fn counter_rotate_90(&self) -> &Self { + self.modify_view(|view| view.set_rotation_z(std::f32::consts::PI / 2.0)) + } + + /// Rotate the shape by 180 degrees. + pub fn rotate_180(&self) -> &Self { + self.modify_view(|view| view.set_rotation_z(-std::f32::consts::PI)) + } + + /// Counter rotate the shape by 180 degrees. + pub fn rotate_270(&self) -> &Self { + self.modify_view(|view| view.set_rotation_z(-3.0 / 2.0 * std::f32::consts::PI)) + } + + /// Counter rotate the shape by 270 degrees. + pub fn counter_rotate_270(&self) -> &Self { + self.modify_view(|view| view.set_rotation_z(3.0 / 2.0 * std::f32::consts::PI)) + } +} + +impl display::Object for Rectangle { + fn display_object(&self) -> &display::object::Instance { + self.view.display_object() + } +} + +/// Rectangle constructor. +#[allow(non_snake_case)] +fn Rectangle() -> Rectangle { + Rectangle::default() +} + +/// Rounded rectangle constructor. It is a wrapper around [`Rectangle`] with a corner radius set. +#[allow(non_snake_case)] +fn RoundedRectangle(radius: f32) -> Rectangle { + let shape = Rectangle(); + shape.set_corner_radius(radius); + shape +} + +/// Circle constructor. It is a wrapper around [`Rectangle`] with a corner radius set to maximum. +#[allow(non_snake_case)] +fn Circle() -> Rectangle { + let shape = Rectangle(); + shape.set_corner_radius_max(); + shape +} + + +// =================== +// === Entry Point === +// =================== + +/// The example entry point. +#[entry_point] +#[allow(dead_code)] +pub fn main() { + let world = World::new().displayed_in("root"); + let scene = &world.default_scene; + let camera = scene.camera().clone_ref(); + let navigator = Navigator::new(scene, &camera); + + let shapes = [ + Circle().build(|t| { + t.set_size(Vector2::new(100.0, 100.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) + .keep_bottom_left_quarter(); + }), + RoundedRectangle(10.0).build(|t| { + t.set_size(Vector2::new(100.0, 100.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)); + }), + RoundedRectangle(10.0).build(|t| { + t.set_size(Vector2::new(100.0, 50.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) + .keep_top_half(); + }), + RoundedRectangle(10.0).build(|t| { + t.set_size(Vector2::new(100.0, 50.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) + .keep_bottom_half(); + }), + RoundedRectangle(10.0).build(|t| { + t.set_size(Vector2::new(50.0, 100.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) + .keep_right_half(); + }), + RoundedRectangle(10.0).build(|t| { + t.set_size(Vector2::new(50.0, 100.0)) + .set_color(color::Rgba::new(0.5, 0.0, 0.0, 0.3)) + .set_inset_border(5.0) + .set_border_color(color::Rgba::new(0.0, 0.0, 1.0, 1.0)) + .keep_left_half(); + }), + ]; + + let root = display::object::Instance::new(); + root.set_size(Vector2::new(300.0, 100.0)); + root.use_auto_layout().set_column_count(3).set_gap((10.0, 10.0)); + for shape in &shapes { + root.add_child(shape); + } + world.add_child(&root); + + world.keep_alive_forever(); + mem::forget(navigator); + mem::forget(root); + mem::forget(shapes); +} diff --git a/lib/rust/ensogl/examples/src/lib.rs b/lib/rust/ensogl/examples/src/lib.rs index ac5b9c6ce578..b4ce15cfde9a 100644 --- a/lib/rust/ensogl/examples/src/lib.rs +++ b/lib/rust/ensogl/examples/src/lib.rs @@ -31,6 +31,7 @@ // ============== pub use ensogl_example_animation as animation; +pub use ensogl_example_built_in_shapes as built_in_shapes; pub use ensogl_example_cached_shape as cached_shape; pub use ensogl_example_complex_shape_system as complex_shape_system; pub use ensogl_example_dom_symbols as dom_symbols;