diff --git a/ide/CHANGELOG.md b/ide/CHANGELOG.md
index b881b6b2ede1..3ceba992cb2c 100644
--- a/ide/CHANGELOG.md
+++ b/ide/CHANGELOG.md
@@ -20,6 +20,9 @@
unnecessary library imports when selecting hints from node searcher. This
makes the generated textual code easier to read and reduces likelihood of
accidental name collision.
+- [Hovering over an output port shows a pop-up with the result type of a node]
+ [1312]. This allows discovering the result type of a node which can help with
+ debugging and development.
- [Visualizations can define context for preprocessor evaluation][1291]. Users
can now decide what module's context should be used for visualization
preprocessor. This allows providing visualization with standard library
@@ -195,6 +198,7 @@ you can find their release notes
https://www.youtube.com/watch?v=BYUAL4ksEgY&ab_channel=Enso
[podcast-future-of-enso]:
https://www.youtube.com/watch?v=rF8DuJPOfTs&t=1863s&ab_channel=Enso
+[1312]: https://github.com/enso-org/ide/pull/1312
diff --git a/ide/src/rust/ensogl/lib/components/src/label.rs b/ide/src/rust/ensogl/lib/components/src/label.rs
new file mode 100644
index 000000000000..9bd76e6c9d6f
--- /dev/null
+++ b/ide/src/rust/ensogl/lib/components/src/label.rs
@@ -0,0 +1,198 @@
+//! Label component. Appears as text with background.
+
+use crate::prelude::*;
+
+use enso_frp as frp;
+use enso_frp;
+use ensogl_core::application::Application;
+use ensogl_core::data::color;
+use ensogl_core::display::shape::*;
+use ensogl_core::display::traits::*;
+use ensogl_core::display;
+use ensogl_text as text;
+
+use ensogl_theme::component::label as theme;
+
+
+
+// ==========================
+// === Shapes Definitions ===
+// ==========================
+
+mod background {
+ use super::*;
+
+ ensogl_core::define_shape_system! {
+ (style:Style,bg_color:Vector4) {
+
+ let width = Var::::from("input_size.x");
+ let height = Var::::from("input_size.y");
+ let padding = style.get_number_or(theme::padding, 0.0);
+ let width = width - padding.px() * 2.0;
+ let height = height - padding.px() * 2.0;
+ let radius = &height / 2.0;
+ let shape = Rect((&width,&height)).corners_radius(&radius);
+ let shape = shape.fill(Var::::from(bg_color.clone()));
+
+
+ // === Shadow ===
+ let alpha = Var::::from(format!("({0}.w)",bg_color));
+ let border_size_f = 16.0;
+ let shaow_size = style.get_number_or(theme::shadow::size,0.0);
+ let shadow_size = shaow_size.px();
+ let shadow_width = &width + &shadow_size * 2.0;
+ let shadow_height = &height + &shadow_size * 2.0;
+ let shadow_radius = &shadow_height / 2.0;
+ let shadow = Rect((shadow_width,shadow_height)).corners_radius(shadow_radius);
+ let base_color = color::Rgba::from(style.get_color(theme::shadow));
+ let base_color = Var::::from(base_color);
+ let base_color = base_color.multiply_alpha(&alpha);
+ let fading_color = color::Rgba::from(style.get_color(theme::shadow::fading));
+ let fading_color = Var::::from(fading_color);
+ let fading_color = fading_color.multiply_alpha(&alpha);
+ let exponent = style.get_number_or(theme::shadow::exponent,2.0);
+ let shadow_color = color::LinearGradient::new()
+ .add(0.0,fading_color.into_linear())
+ .add(1.0,base_color.into_linear());
+ let shadow_color = color::SdfSampler::new(shadow_color)
+ .max_distance(border_size_f)
+ .slope(color::Slope::Exponent(exponent));
+ let shadow = shadow.fill(shadow_color);
+
+ (shadow+shape).into()
+ }
+ }
+}
+
+
+
+// ===========
+// === FRP ===
+// ===========
+
+ensogl_core::define_endpoints! {
+ Input {
+ set_content(String),
+ set_opacity(f32)
+ }
+ Output {
+ size (Vector2)
+ }
+}
+
+
+
+// =============
+// === Model ===
+// =============
+
+#[derive(Clone,Debug)]
+struct Model {
+ background : background::View,
+ label : text::Area,
+ display_object : display::object::Instance,
+ app : Application,
+ style : StyleWatch,
+}
+
+impl Model {
+ fn new(app: Application) -> Self {
+ let app = app.clone_ref();
+ let scene = app.display.scene();
+ let logger = Logger::new("TextLabel");
+ let display_object = display::object::Instance::new(&logger);
+ let label = app.new_view::();
+ let background = background::View::new(&logger);
+
+ // FIXME[MM/WD]: Depth sorting of labels to in front of everything else in the scene.
+ // Temporary solution. The depth management needs to allow defining relative position of
+ // the text and background and let the whole component to be set to am an arbitrary layer.
+ label.remove_from_scene_layer_DEPRECATED(&scene.layers.main);
+ label.add_to_scene_layer_DEPRECATED(&scene.layers.tooltip_text);
+ scene.layers.tooltip_background.add_exclusive(&background);
+
+ display_object.add_child(&background);
+ display_object.add_child(&label);
+
+ let style = StyleWatch::new(&app.display.scene().style_sheet);
+
+ Model { label, display_object, background, app, style }
+ }
+
+ pub fn height(&self) -> f32 {
+ self.style.get_number_or(theme::height, 0.0)
+ }
+
+ fn set_width(&self, width:f32) -> Vector2 {
+ let padding = self.style.get_number_or(theme::padding,0.0);
+ let text_size = self.style.get_number_or(theme::text::size,0.0);
+ let text_offset = self.style.get_number_or(theme::text::offset,0.0);
+ let height = self.height();
+ let size = Vector2(width * 1.25,height);
+ let padded_size = size + Vector2(padding,padding) * 2.0;
+ self.background.size.set(padded_size);
+ let text_origin = Vector2(padding / 2.0 + text_offset - size.x/2.0, text_size /2.0);
+ self.label.set_position_xy(text_origin);
+ padded_size
+ }
+
+ fn set_content(&self, t:&str) -> Vector2 {
+ self.label.set_content(t);
+ self.set_width(self.label.width.value())
+ }
+
+ fn set_opacity(&self, value:f32) {
+ let text_color_path = theme::text;
+ let text_color = self.style.get_color(text_color_path).multiply_alpha(value);
+ let text_color = color::Rgba::from(text_color);
+ self.label.frp.set_color_all.emit(text_color);
+ self.label.frp.set_default_color.emit(text_color);
+
+ let bg_color_path = theme::background;
+ let bg_color = self.style.get_color(bg_color_path).multiply_alpha(value);
+ let bg_color = color::Rgba::from(bg_color);
+ self.background.bg_color.set(bg_color.into())
+ }
+}
+
+
+
+// =======================
+// === Label Component ===
+// =======================
+
+#[allow(missing_docs)]
+#[derive(Clone,CloneRef,Debug)]
+pub struct Label {
+ model : Rc,
+ pub frp : Rc,
+}
+
+impl Label {
+ /// Constructor.
+ pub fn new(app:Application) -> Self {
+ let frp = Rc::new(Frp::new());
+ let model = Rc::new(Model::new(app.clone_ref()));
+ Label {frp,model}.init()
+ }
+
+ fn init(self) -> Self {
+ let frp = &self.frp;
+ let network = &frp.network;
+ let model = &self.model;
+
+ frp::extend! { network
+ frp.source.size <+ frp.set_content.map(f!((t)
+ model.set_content(t)
+ ));
+
+ eval frp.set_opacity((value) model.set_opacity(*value));
+ }
+
+ self
+ }
+}
+
+impl display::Object for Label {
+ fn display_object(&self) -> &display::object::Instance { &self.model.display_object }
+}
diff --git a/ide/src/rust/ensogl/lib/components/src/lib.rs b/ide/src/rust/ensogl/lib/components/src/lib.rs
index ddca4ec4d427..884be691141d 100644
--- a/ide/src/rust/ensogl/lib/components/src/lib.rs
+++ b/ide/src/rust/ensogl/lib/components/src/lib.rs
@@ -18,6 +18,7 @@
pub mod drop_down_menu;
pub mod list_view;
+pub mod label;
pub mod toggle_button;
/// Commonly used types and functions.
diff --git a/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs
index c6afec23931d..34177f5d95cd 100644
--- a/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs
+++ b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation.rs
@@ -1,5 +1,7 @@
//! FRP bindings to the animation engine.
+pub mod hysteretic;
+
use crate::prelude::*;
use crate::animation::physics::inertia;
diff --git a/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs
new file mode 100644
index 000000000000..cc88180e28af
--- /dev/null
+++ b/ide/src/rust/ensogl/lib/core/src/animation/frp/animation/hysteretic.rs
@@ -0,0 +1,113 @@
+//! Animation that has a delayed onset and offset.
+//!
+//! The basic idea behind this animation is, that it changes between two states: of (0.0) and on
+//! (1.0), but only after a delay (`st`/`et` start delay, end delay). If the animation gets
+//! canceled before the delay has passed, nothing happens, and the delay resets.
+//!
+//! This can be used to hide state changes that are only happening for a short duration. For example
+//! consider a UI element that is shown when hovering ofer an icon. When only moving the mouse
+//! cursor past the icon we want to avoid the pop-up appearing and disappearing right away. The
+//! `HystereticAnimation` allows this by setting a `start_delay_duration` long enough to avoid
+//! triggering during the time it takes to move past the icon. Thus, when the cursor moves past the
+//! icon nothing is visible to the user, but if the mosque cursor stays on the icon longer than
+//! `start_delay_duration`, the animation starts, and the pop-up becomes visible. In reverse, when
+//! moving the cursor between multiple icons, the pop-up should not permanently start and disappear
+//! and re-appear. Thus setting a `end_delay_duration` will avoid the pop-up from disappearing, if
+//! the time the cursor is between icons is less than the `end_delay_duration`. Instead, the hiding
+//! will only start iof the cursos has left any icon triggering the pop-up for longer than the
+//! `end_delay_duration`.
+//!
+use crate::prelude::*;
+
+use crate::Animation;
+use crate::Easing;
+
+use enso_frp as frp;
+
+
+
+// ===========
+// === Frp ===
+// ===========
+
+crate::define_endpoints! {
+ Input {
+ /// Trigger start of animation towards start state (0.0). Will be delayed by onset time.
+ to_start(),
+ /// Trigger start of animation towards end state (1.0). Will be delayed by onset time.
+ to_end(),
+ }
+ Output {
+ /// Represents the numeric state of the animation in the range 0..1.
+ value(f32),
+ /// Triggered when the state reaches 1.0.
+ on_end(),
+ /// Triggered when the state reaches 0.0.
+ on_start(),
+ }
+}
+
+
+
+// ===========================
+// === HystereticAnimation ===
+// ===========================
+
+/// Animation that has a delayed onset and offset.
+#[derive(CloneRef,Debug,Shrinkwrap)]
+pub struct HystereticAnimation {
+ #[allow(missing_docs)]
+ pub frp : FrpEndpoints,
+}
+
+impl HystereticAnimation {
+ #[allow(missing_docs)]
+ pub fn new(network:&frp::Network, start_delay_duration:f32, end_delay_duration:f32) -> Self {
+ let frp = Frp::extend(network);
+ let start_delay = Easing::new(network);
+ let end_delay = Easing::new(network);
+ start_delay.set_duration(start_delay_duration);
+ end_delay.set_duration(end_delay_duration);
+
+ let transition = Animation::::new(network);
+
+ frp::extend! { network
+
+ during_transition <- any_mut();
+
+ on_end <- frp.to_end.constant(());
+ on_end_while_active <- on_end.gate(&during_transition);
+ on_end_while_inactive <- on_end.gate_not(&during_transition);
+
+ on_start <- frp.to_start.constant(());
+ on_start_while_active <- on_start.gate(&during_transition);
+ on_start_while_inactive <- on_start.gate_not(&during_transition);
+
+ start_delay.target <+ on_start_while_inactive.constant(1.0);
+ end_delay.target <+ on_end_while_inactive.constant(1.0);
+
+ start_delay.stop_and_rewind <+ on_end.constant(0.0);
+ end_delay.stop_and_rewind <+ on_start.constant(0.0);
+
+ offset_start <- end_delay.on_end.map(|t| t.is_normal()).on_true();
+ onset_start <- start_delay.on_end.map(|t| t.is_normal()).on_true();
+
+ onset_end <- transition.value.map(|t| (t - 1.0).abs() < std::f32::EPSILON).on_true();
+ offset_end <- transition.value.map(|t| t.abs() < std::f32::EPSILON).on_true();
+
+ transition.target <+ onset_start.constant(1.0);
+ transition.target <+ offset_start.constant(0.0);
+ transition.target <+ on_end_while_active.constant(0.0);
+ transition.target <+ on_start_while_active.constant(1.0);
+
+ during_transition <+ bool(&onset_end,&onset_start);
+ during_transition <+ bool(&offset_end,&offset_start);
+
+ frp.source.value <+ transition.value;
+ frp.source.on_end <+ onset_end;
+ frp.source.on_start <+ offset_end;
+ }
+
+ HystereticAnimation{frp}
+ }
+}
diff --git a/ide/src/rust/ensogl/lib/core/src/data/color/data.rs b/ide/src/rust/ensogl/lib/core/src/data/color/data.rs
index 766f78688f6b..adb797703ffe 100644
--- a/ide/src/rust/ensogl/lib/core/src/data/color/data.rs
+++ b/ide/src/rust/ensogl/lib/core/src/data/color/data.rs
@@ -291,3 +291,16 @@ impl Default for Alpha {
Self {alpha,opaque}
}
}
+
+impl Alpha {
+ /// Return the color with a multiplied alpha channel.
+ pub fn multiply_alpha(self, alpha:f32) -> Color {
+ let alpha = self.alpha * alpha;
+ Color(Alpha{alpha, opaque: self.opaque })
+ }
+
+ /// Modify the color's alpha channel.
+ pub fn mod_alpha(&mut self, f:F) {
+ f(&mut self.alpha)
+ }
+}
diff --git a/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs b/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs
index 28237f0bc1c9..a688e76ec284 100644
--- a/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs
+++ b/ide/src/rust/ensogl/lib/core/src/data/color/gradient.rs
@@ -56,7 +56,7 @@ impl LinearGradient {
}
impls! { [Color] From<&LinearGradient> for Glsl
-where [Color:Copy + RefInto] {
+where [Color:RefInto] {
|t| {
let args = t.control_points.iter().map(|control_point| {
let offset = control_point.offset.glsl();
@@ -82,7 +82,7 @@ pub const DEFAULT_DISTANCE_GRADIENT_MAX_DISTANCE : f32 = 10.0;
/// A gradient which transforms a linear gradient to a gradient along the signed distance field.
/// The slope parameter modifies how fast the gradient values are changed, allowing for nice,
/// smooth transitions.
-#[derive(Copy,Clone,Debug)]
+#[derive(Clone,Debug)]
pub struct SdfSampler {
/// The distance from the shape border at which the gradient should start.
pub min_distance : f32,
diff --git a/ide/src/rust/ensogl/lib/core/src/display/scene.rs b/ide/src/rust/ensogl/lib/core/src/display/scene.rs
index 359ad3f9cae1..d91413a9e02b 100644
--- a/ide/src/rust/ensogl/lib/core/src/display/scene.rs
+++ b/ide/src/rust/ensogl/lib/core/src/display/scene.rs
@@ -603,11 +603,15 @@ impl Renderer {
/// should be abstracted away in the future.
#[derive(Clone,CloneRef,Debug)]
pub struct HardcodedLayers {
- pub viz : Layer,
- pub below_main : Layer,
+ pub viz : Layer,
+ pub below_main : Layer,
// main <- here is the 'main` layer inserted.
- pub cursor : Layer,
- pub label : Layer,
+ pub cursor : Layer,
+ pub label : Layer,
+
+ pub tooltip_background : Layer,
+ pub tooltip_text : Layer,
+
pub viz_fullscreen : Layer,
pub breadcrumbs : Layer,
layers : Layers,
@@ -622,23 +626,30 @@ impl Deref for HardcodedLayers {
impl HardcodedLayers {
pub fn new(logger:impl AnyLogger) -> Self {
- let layers = Layers::new(logger);
- let viz = layers.new_layer();
- let cursor = layers.new_layer();
- let label = layers.new_layer();
- let viz_fullscreen = layers.new_layer();
- let below_main = layers.new_layer();
- let breadcrumbs = layers.new_layer();
+ let layers = Layers::new(logger);
+ let viz = layers.new_layer();
+ let cursor = layers.new_layer();
+ let label = layers.new_layer();
+ let tooltip_background = layers.new_layer();
+ let tooltip_text = layers.new_layer();
+ let viz_fullscreen = layers.new_layer();
+ let below_main = layers.new_layer();
+ let breadcrumbs = layers.new_layer();
viz.set_camera(layers.main.camera());
label.set_camera(layers.main.camera());
+ tooltip_background.set_camera(layers.main.camera());
+ tooltip_text.set_camera(layers.main.camera());
below_main.set_camera(layers.main.camera());
layers.add_layers_order_dependency(&viz,&below_main);
layers.add_layers_order_dependency(&below_main,&layers.main);
layers.add_layers_order_dependency(&layers.main,&cursor);
layers.add_layers_order_dependency(&cursor,&label);
- layers.add_layers_order_dependency(&label,&viz_fullscreen);
+ layers.add_layers_order_dependency(&label,&tooltip_background);
+ layers.add_layers_order_dependency(&tooltip_background,&tooltip_text);
+ layers.add_layers_order_dependency(&tooltip_text,&viz_fullscreen);
layers.add_layers_order_dependency(&viz_fullscreen,&breadcrumbs);
- Self {layers,viz,cursor,label,viz_fullscreen,below_main,breadcrumbs}
+ Self {layers,viz,cursor,label,viz_fullscreen,below_main,breadcrumbs,tooltip_background,
+ tooltip_text}
}
}
diff --git a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs
index ac370a517932..237b31f0bff2 100644
--- a/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs
+++ b/ide/src/rust/ensogl/lib/core/src/display/shape/primitive/def/var.rs
@@ -640,3 +640,45 @@ impl From>> for Var {
}
}
}
+
+impl Var {
+ /// Return the color with the given alpha value.
+ pub fn with_alpha(self, alpha:&Var) -> Self {
+ match (self, alpha) {
+ (Var::Static (t), Var::Static(alpha)) => {
+ Var::Static(color::Rgba::new(t.data.red,t.data.green,t.data.blue,*alpha))
+ },
+ (t, alpha) => {
+ let t = t.glsl();
+ let alpha = alpha.glsl();
+ let var = format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{1})",t,alpha);
+ Var::Dynamic(var.into())
+ },
+ }
+ }
+
+ /// Return the color with the alpha value replaced by multiplying the colors' alpha value with
+ /// the given alpha value.
+ pub fn multiply_alpha(self, alpha:&Var) -> Self {
+ match (self, alpha) {
+ (Var::Static (t), Var::Static(alpha)) => {
+ let var = color::Rgba::new(t.data.red,t.data.green,t.data.blue,*alpha*t.data.alpha);
+ Var::Static(var)
+ },
+ (t, alpha) => {
+ let t = t.glsl();
+ let alpha = alpha.glsl();
+ let var = format!("srgba({0}.raw.x,{0}.raw.y,{0}.raw.z,{0}.raw.w*{1})",t,alpha);
+ Var::Dynamic(var.into())
+ },
+ }
+ }
+
+ /// Transform to LinearRgba.
+ pub fn into_linear(self) -> Var {
+ match self {
+ Var::Static(c) => Var::Static(c.into()),
+ Var::Dynamic(c) => Var::Dynamic(format!("rgba({0})",c.glsl()).into())
+ }
+ }
+}
diff --git a/ide/src/rust/ensogl/lib/core/src/gui.rs b/ide/src/rust/ensogl/lib/core/src/gui.rs
index 0e4a32581733..acc35f569da9 100644
--- a/ide/src/rust/ensogl/lib/core/src/gui.rs
+++ b/ide/src/rust/ensogl/lib/core/src/gui.rs
@@ -2,3 +2,4 @@
pub mod component;
pub mod cursor;
+pub mod style;
diff --git a/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs b/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs
index 76b0deb2a558..05eb5d0cfc2c 100644
--- a/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs
+++ b/ide/src/rust/ensogl/lib/core/src/gui/cursor.rs
@@ -6,10 +6,12 @@ use crate::Animation;
use crate::DEPRECATED_Animation;
use crate::DEPRECATED_Tween;
use crate::data::color;
+use crate::define_style;
use crate::display::scene::Scene;
use crate::display::shape::*;
use crate::display;
use crate::frp;
+use crate::gui::style::*;
@@ -30,105 +32,10 @@ fn DEFAULT_SIZE() -> Vector2 { Vector2(16.0,16.0) }
-// ==================
-// === StyleValue ===
-// ==================
-
-/// Defines a value of the cursor style.
-#[derive(Debug,Clone,Eq,PartialEq)]
-#[allow(missing_docs)]
-pub struct StyleValue {
- /// Defines the value of the style. In case it is set to `None`, the default value will be used.
- /// Please note that setting it to `None` has a different effect than not providing the value
- /// in the `Style` at all. If the value is provided it can override the existing values when
- /// used in a semigroup operation.
- pub value : Option,
-
- /// Defines if the state transition should be used. Sometimes disabling animation is required.
- /// A good example is the implementation of a selection box. When drawing selection box with the
- /// mouse, the user wants to see it in real-time, without it growing over time.
- pub animate : bool,
-}
-
-impl Default for StyleValue {
- fn default() -> Self {
- let value = default();
- let animate = true;
- Self {value,animate}
- }
-}
-
-impl StyleValue {
- /// Constructor.
- pub fn new(value:T) -> Self {
- let value = Some(value);
- let animate = true;
- Self {value,animate}
- }
-
- /// Constructor for a default value setter. Please note that this is not made a `Default` impl
- /// on purpose. This method creates a non-empty value setter which sets the target to its
- /// default value. Read `Style` docs to learn more.
- pub fn new_default() -> Self {
- let value = None;
- let animate = true;
- Self {value,animate}
- }
-
- /// Constructor with disabled animation.
- pub fn new_no_animation(value:T) -> Self {
- let value = Some(value);
- let animate = false;
- Self {value,animate}
- }
-}
-
-
-
// =============
// === Style ===
// =============
-macro_rules! define_style {( $( $(#$meta:tt)* $field:ident : $field_type:ty),* $(,)? ) => {
- /// Set of cursor style parameters. You can construct this object in FRP network, merge it using
- /// its `Semigroup` instance, and finally pass to the cursor to apply the style. Please note
- /// that cursor does not implement any complex style management (like pushing or popping a style
- /// from a style stack) on purpose, as it is stateful, while it is straightforward to implement
- /// it in FRP.
- #[derive(Debug,Clone,Default,PartialEq)]
- pub struct Style {
- $($(#$meta)? $field : Option>),*
- }
-
- impl Style {
- /// Create a new style with all fields set to default value. Please note that it is
- /// different than empty style, as this one overrides fields with default values when
- /// used in a semigroup operation.
- pub fn new_with_all_fields_default() -> Self {
- $(let $field = Some(StyleValue::new_default());)*
- Self {$($field),*}
- }
-
- /// Check whether the style is a default, empty one.
- pub fn is_default(&self) -> bool {
- $(self.$field.is_none())&&*
- }
- }
-
- impl PartialSemigroup<&Style> for Style {
- #[allow(clippy::clone_on_copy)]
- fn concat_mut(&mut self, other:&Self) {
- $(if self.$field . is_none() { self.$field = other.$field . clone() })*
- }
- }
-
- impl PartialSemigroup