🎉 Feat masks

This commit is contained in:
AzazelN28 2025-01-22 16:41:25 +01:00 committed by Aitor Moreno
parent 3ea52a0198
commit f8d58cb74e
9 changed files with 249 additions and 102 deletions

View file

@ -108,7 +108,7 @@
(h/call internal-module "_set_shape_clip_content" clip-content)) (h/call internal-module "_set_shape_clip_content" clip-content))
(defn set-shape-type (defn set-shape-type
[type] [type {:keys [masked]}]
(cond (cond
(= type :circle) (= type :circle)
(h/call internal-module "_set_shape_kind_circle") (h/call internal-module "_set_shape_kind_circle")
@ -119,6 +119,9 @@
(= type :bool) (= type :bool)
(h/call internal-module "_set_shape_kind_bool") (h/call internal-module "_set_shape_kind_bool")
(= type :group)
(h/call internal-module "_set_shape_kind_group" masked)
:else :else
(h/call internal-module "_set_shape_kind_rect"))) (h/call internal-module "_set_shape_kind_rect")))
@ -537,6 +540,7 @@
(let [shape (nth shapes index) (let [shape (nth shapes index)
id (dm/get-prop shape :id) id (dm/get-prop shape :id)
type (dm/get-prop shape :type) type (dm/get-prop shape :type)
masked (dm/get-prop shape :masked-group)
selrect (dm/get-prop shape :selrect) selrect (dm/get-prop shape :selrect)
clip-content (if (= type :frame) clip-content (if (= type :frame)
(not (dm/get-prop shape :show-content)) (not (dm/get-prop shape :show-content))
@ -563,7 +567,7 @@
shadows (dm/get-prop shape :shadow)] shadows (dm/get-prop shape :shadow)]
(use-shape id) (use-shape id)
(set-shape-type type) (set-shape-type type {:masked masked})
(set-shape-clip-content clip-content) (set-shape-clip-content clip-content)
(set-shape-selrect selrect) (set-shape-selrect selrect)
(set-shape-rotation rotation) (set-shape-rotation rotation)

View file

@ -110,34 +110,35 @@
[self k v] [self k v]
(when ^boolean shape/*wasm-sync* (when ^boolean shape/*wasm-sync*
(api/use-shape (:id self)) (api/use-shape (:id self))
(case k (let [masked (:masked-group self)]
:type (api/set-shape-type v) (case k
:bool-type (api/set-shape-bool-type v) :type (api/set-shape-type v {:masked masked})
:bool-content (api/set-shape-bool-content v) :bool-type (api/set-shape-bool-type v)
:selrect (api/set-shape-selrect v) :bool-content (api/set-shape-bool-content v)
:show-content (if (= (:type self) :frame) :selrect (api/set-shape-selrect v)
(api/set-shape-clip-content (not v)) :show-content (if (= (:type self) :frame)
(api/set-shape-clip-content false)) (api/set-shape-clip-content (not v))
:rotation (api/set-shape-rotation v) (api/set-shape-clip-content false))
:transform (api/set-shape-transform v) :rotation (api/set-shape-rotation v)
:fills (api/set-shape-fills v) :transform (api/set-shape-transform v)
:strokes (api/set-shape-strokes v) :fills (api/set-shape-fills v)
:blend-mode (api/set-shape-blend-mode v) :strokes (api/set-shape-strokes v)
:opacity (api/set-shape-opacity v) :blend-mode (api/set-shape-blend-mode v)
:hidden (api/set-shape-hidden v) :opacity (api/set-shape-opacity v)
:shapes (api/set-shape-children v) :hidden (api/set-shape-hidden v)
:blur (api/set-shape-blur v) :shapes (api/set-shape-children v)
:svg-attrs (when (= (:type self) :path) :blur (api/set-shape-blur v)
(api/set-shape-path-attrs v)) :svg-attrs (when (= (:type self) :path)
:constraints-h (api/set-constraints-h v) (api/set-shape-path-attrs v))
:constraints-v (api/set-constraints-v v) :constraints-h (api/set-constraints-h v)
:content (cond :constraints-v (api/set-constraints-v v)
(= (:type self) :path) :content (cond
(api/set-shape-path-content v) (= (:type self) :path)
(api/set-shape-path-content v)
(= (:type self) :svg-raw) (= (:type self) :svg-raw)
(api/set-shape-svg-raw-content (api/get-static-markup self))) (api/set-shape-svg-raw-content (api/get-static-markup self)))
nil) nil))
;; when something synced with wasm ;; when something synced with wasm
;; is modified, we need to request ;; is modified, we need to request
;; a new render. ;; a new render.

View file

@ -10,7 +10,8 @@ mod utils;
mod view; mod view;
use crate::mem::SerializableResult; use crate::mem::SerializableResult;
use crate::shapes::{BoolType, ConstraintH, ConstraintV, Kind, Path, TransformEntry}; use crate::shapes::{BoolType, ConstraintH, ConstraintV, Group, Kind, Path, TransformEntry};
use crate::state::State; use crate::state::State;
use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_from_u32_quartet;
@ -63,19 +64,19 @@ pub extern "C" fn set_canvas_background(raw_color: u32) {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn render(timestamp: i32) { pub extern "C" fn render(timestamp: i32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
state.start_render_loop(timestamp).expect("Error rendering"); state.start_render_loop(timestamp).expect("Error rendering");
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn render_from_cache() { pub extern "C" fn render_from_cache() {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
state.render_from_cache(); state.render_from_cache();
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn process_animation_frame(timestamp: i32) { pub extern "C" fn process_animation_frame(timestamp: i32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
state state
.process_animation_frame(timestamp) .process_animation_frame(timestamp)
@ -120,7 +121,15 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_circle() { pub extern "C" fn set_shape_kind_group(masked: bool) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() {
shape.set_kind(Kind::Group(Group::new(masked)));
}
}
#[no_mangle]
pub extern "C" fn set_shape_kind_circle() {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
@ -129,7 +138,7 @@ pub unsafe extern "C" fn set_shape_kind_circle() {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_rect() { pub extern "C" fn set_shape_kind_rect() {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
@ -141,7 +150,7 @@ pub unsafe extern "C" fn set_shape_kind_rect() {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_path() { pub extern "C" fn set_shape_kind_path() {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.set_kind(Kind::Path(Path::default())); shape.set_kind(Kind::Path(Path::default()));
@ -149,7 +158,7 @@ pub unsafe extern "C" fn set_shape_kind_path() {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_kind_bool() { pub extern "C" fn set_shape_kind_bool() {
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
match shape.kind() { match shape.kind() {
@ -160,7 +169,7 @@ pub unsafe extern "C" fn set_shape_kind_bool() {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_bool_type(raw_bool_type: u8) { pub extern "C" fn set_shape_bool_type(raw_bool_type: u8) {
let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.set_bool_type(BoolType::from(raw_bool_type)) shape.set_bool_type(BoolType::from(raw_bool_type))
@ -176,7 +185,7 @@ pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) { pub extern "C" fn set_shape_clip_content(clip_content: bool) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.set_clip(clip_content); shape.set_clip(clip_content);
@ -184,7 +193,7 @@ pub unsafe extern "C" fn set_shape_clip_content(clip_content: bool) {
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { pub extern "C" fn set_shape_rotation(rotation: f32) {
let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer");
if let Some(shape) = state.current_shape() { if let Some(shape) = state.current_shape() {
shape.set_rotation(rotation); shape.set_rotation(rotation);

View file

@ -40,6 +40,18 @@ fn get_time() -> i32 {
unsafe { emscripten_run_script_int(script.as_ptr()) } unsafe { emscripten_run_script_int(script.as_ptr()) }
} }
pub struct NodeRenderState {
pub id: Uuid,
// We use this bool to keep that we've traversed all the children inside this node.
pub visited_children: bool,
// This is used to clip the content of frames.
pub clip_bounds: Option<math::Rect>,
// This is a flag to indicate that we've already drawn the mask of a masked group.
pub visited_mask: bool,
// This bool indicates that we're drawing the mask shape.
pub mask: bool,
}
pub(crate) struct RenderState { pub(crate) struct RenderState {
gpu_state: GpuState, gpu_state: GpuState,
pub options: RenderOptions, pub options: RenderOptions,
@ -57,8 +69,8 @@ pub(crate) struct RenderState {
pub render_request_id: Option<i32>, pub render_request_id: Option<i32>,
// Indicates whether the rendering process has pending frames. // Indicates whether the rendering process has pending frames.
pub render_in_progress: bool, pub render_in_progress: bool,
// Stack of nodes pending to be rendered. The boolean flag indicates if the node has already been visited. The rect the optional bounds to clip. // Stack of nodes pending to be rendered.
pub pending_nodes: Vec<(Uuid, bool, Option<math::Rect>)>, pub pending_nodes: Vec<NodeRenderState>,
} }
impl RenderState { impl RenderState {
@ -78,7 +90,6 @@ impl RenderState {
let debug_surface = final_surface let debug_surface = final_surface
.new_surface_with_dimensions((width, height)) .new_surface_with_dimensions((width, height))
.unwrap(); .unwrap();
let mut font_provider = skia::textlayout::TypefaceFontProvider::new(); let mut font_provider = skia::textlayout::TypefaceFontProvider::new();
let default_font = skia::FontMgr::default() let default_font = skia::FontMgr::default()
.new_from_data(DEFAULT_FONT_BYTES, None) .new_from_data(DEFAULT_FONT_BYTES, None)
@ -172,14 +183,6 @@ impl RenderState {
.flush_and_submit_surface(&mut self.final_surface, None); .flush_and_submit_surface(&mut self.final_surface, None);
} }
pub fn translate(&mut self, dx: f32, dy: f32) {
self.drawing_surface.canvas().translate((dx, dy));
}
pub fn scale(&mut self, sx: f32, sy: f32) {
self.drawing_surface.canvas().scale((sx, sy));
}
pub fn reset_canvas(&mut self) { pub fn reset_canvas(&mut self) {
self.drawing_surface.canvas().restore_to_count(1); self.drawing_surface.canvas().restore_to_count(1);
self.render_surface.canvas().restore_to_count(1); self.render_surface.canvas().restore_to_count(1);
@ -306,12 +309,20 @@ impl RenderState {
} }
} }
self.reset_canvas(); self.reset_canvas();
self.scale( self.drawing_surface.canvas().scale((
self.viewbox.zoom * self.options.dpr(), self.viewbox.zoom * self.options.dpr(),
self.viewbox.zoom * self.options.dpr(), self.viewbox.zoom * self.options.dpr(),
); ));
self.translate(self.viewbox.pan_x, self.viewbox.pan_y); self.drawing_surface
self.pending_nodes = vec![(Uuid::nil(), false, None)]; .canvas()
.translate((self.viewbox.pan_x, self.viewbox.pan_y));
self.pending_nodes = vec![NodeRenderState {
id: Uuid::nil(),
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: false,
}];
self.render_in_progress = true; self.render_in_progress = true;
self.process_animation_frame(tree, modifiers, timestamp)?; self.process_animation_frame(tree, modifiers, timestamp)?;
Ok(()) Ok(())
@ -413,60 +424,154 @@ impl RenderState {
} }
let mut i = 0; let mut i = 0;
while let Some((node_id, visited_children, clip_bounds)) = self.pending_nodes.pop() { while let Some(node_render_state) = self.pending_nodes.pop() {
let element = tree.get_mut(&node_id).ok_or( let NodeRenderState {
"Error: Element with root_id {node_id} not found in the tree.".to_string(), id: node_id,
visited_children,
clip_bounds,
visited_mask,
mask,
} = node_render_state;
let element = tree.get(&node_render_state.id).ok_or(
"Error: Element with root_id {node_render_state.id} not found in the tree."
.to_string(),
)?; )?;
if !visited_children { if visited_children {
if !node_id.is_nil() { if !visited_mask {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() { match element.kind {
debug::render_debug_element(self, &element, false); Kind::Group(group) => {
continue; // When we're dealing with masked groups we need to
} else { // do a separate extra step to draw the mask (the last
debug::render_debug_element(self, &element, true); // element of a masked group) and blend (using
// the blend mode 'destination-in') the content
// of the group and the mask.
if group.masked {
self.pending_nodes.push(NodeRenderState {
id: node_id,
visited_children: true,
clip_bounds: None,
visited_mask: true,
mask: false,
});
if let Some(&mask_id) = element.mask_id() {
self.pending_nodes.push(NodeRenderState {
id: mask_id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: true,
});
}
}
}
_ => {}
} }
}
let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
if let Some(image_filter) =
element.image_filter(self.viewbox.zoom * self.options.dpr())
{
paint.set_image_filter(image_filter);
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.render_surface.canvas().save_layer(&layer_rec);
self.drawing_surface.canvas().save();
if !node_id.is_nil() {
self.render_shape(element, modifiers.get(&element.id), clip_bounds);
} else { } else {
self.apply_drawing_to_render_canvas(); // Because masked groups needs two rendering passes (first drawing
} // the content and then drawing the mask), we need to do an
self.drawing_surface.canvas().restore(); // extra restore.
match element.kind {
// Set the node as visited before processing children Kind::Group(group) => {
self.pending_nodes.push((node_id, true, None)); if group.masked {
if element.is_recursive() { self.render_surface.canvas().restore();
let children_clip_bounds = }
(!node_id.is_nil() & element.clip()).then(|| element.bounds()); }
_ => {}
for child_id in element.children_ids().iter().rev() {
self.pending_nodes
.push((*child_id, false, children_clip_bounds));
} }
} }
} else {
self.render_surface.canvas().restore(); self.render_surface.canvas().restore();
continue;
} }
// If we didn't visited_children this shape, then we need to do
if !node_render_state.id.is_nil() {
if !element.bounds().intersects(self.viewbox.area) || element.hidden() {
debug::render_debug_shape(self, element, false);
continue;
} else {
debug::render_debug_shape(self, element, true);
}
}
// Masked groups needs two rendering passes, the first one rendering
// the content and the second one rendering the mask so we need to do
// an extra save_layer to keep all the masked group separate from other
// already drawn elements.
match element.kind {
Kind::Group(group) => {
if group.masked {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.render_surface.canvas().save_layer(&layer_rec);
}
}
_ => {}
}
let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
// When we're rendering the mask shape we need to set a special blend mode
// called 'destination-in' that keeps the drawn content within the mask.
// @see https://skia.org/docs/user/api/skblendmode_overview/
if mask {
let mut mask_paint = skia::Paint::default();
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
self.render_surface.canvas().save_layer(&mask_rec);
}
if let Some(image_filter) = element.image_filter(self.viewbox.zoom * self.options.dpr())
{
paint.set_image_filter(image_filter);
}
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.render_surface.canvas().save_layer(&layer_rec);
self.drawing_surface.canvas().save();
if !node_render_state.id.is_nil() {
self.render_shape(
&mut element.clone(),
modifiers.get(&element.id),
clip_bounds,
);
} else {
self.apply_drawing_to_render_canvas();
}
self.drawing_surface.canvas().restore();
// Set the node as visited_children before processing children
self.pending_nodes.push(NodeRenderState {
id: node_id,
visited_children: true,
clip_bounds: None,
visited_mask: false,
mask: mask,
});
if element.is_recursive() {
let children_clip_bounds =
(!node_render_state.id.is_nil() & element.clip()).then(|| element.bounds());
for child_id in element.children_ids().iter().rev() {
self.pending_nodes.push(NodeRenderState {
id: *child_id,
visited_children: false,
clip_bounds: children_clip_bounds,
visited_mask: false,
mask: false,
});
}
}
// We try to avoid doing too many calls to get_time // We try to avoid doing too many calls to get_time
if i % NODE_BATCH_THRESHOLD == 0 && get_time() - timestamp > MAX_BLOCKING_TIME_MS { if i % NODE_BATCH_THRESHOLD == 0 && get_time() - timestamp > MAX_BLOCKING_TIME_MS {
return Ok(()); return Ok(());
} }
i += 1; i += 1;
} }

View file

@ -39,7 +39,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
canvas.draw_str("WASM RENDERER", p, &font, &paint); canvas.draw_str("WASM RENDERER", p, &font, &paint);
} }
pub fn render_debug_element(render_state: &mut RenderState, element: &Shape, intersected: bool) { pub fn render_debug_shape(render_state: &mut RenderState, element: &Shape, intersected: bool) {
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
paint.set_color(if intersected { paint.set_color(if intersected {

View file

@ -78,6 +78,7 @@ fn draw_image_fill_in_container(
Kind::SVGRaw(_) => { Kind::SVGRaw(_) => {
canvas.clip_rect(container, skia::ClipOp::Intersect, true); canvas.clip_rect(container, skia::ClipOp::Intersect, true);
} }
Kind::Group(_) => unreachable!("A group should not have fills"),
} }
// Draw the image with the calculated destination rectangle // Draw the image with the calculated destination rectangle
@ -123,6 +124,6 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill) {
canvas.draw_path(&skia_path, &fill.to_paint(&selrect)); canvas.draw_path(&skia_path, &fill.to_paint(&selrect));
} }
} }
(_, _) => todo!(), (_, _) => unreachable!("This shape should not have fills"),
} }
} }

View file

@ -364,7 +364,7 @@ fn draw_image_stroke_in_container(
Kind::Circle(rect) => { Kind::Circle(rect) => {
draw_stroke_on_circle(canvas, stroke, rect, &outer_rect, svg_attrs, dpr_scale) draw_stroke_on_circle(canvas, stroke, rect, &outer_rect, svg_attrs, dpr_scale)
} }
Kind::SVGRaw(_) => todo!(), Kind::SVGRaw(_) | Kind::Group(_) => unreachable!("This shape should not have strokes"),
Kind::Path(p) | Kind::Bool(_, p) => { Kind::Path(p) | Kind::Bool(_, p) => {
canvas.save(); canvas.save();
let mut path = p.to_skia_path(); let mut path = p.to_skia_path();
@ -457,7 +457,7 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, stroke: &Stroke) {
dpr_scale, dpr_scale,
); );
} }
Kind::SVGRaw(_) => todo!(), Kind::SVGRaw(_) | Kind::Group(_) => unreachable!("This shape should not have strokes"),
} }
} }
} }

View file

@ -9,6 +9,7 @@ use skia::Matrix;
mod blurs; mod blurs;
mod bools; mod bools;
mod fills; mod fills;
mod groups;
mod modifiers; mod modifiers;
mod paths; mod paths;
mod shadows; mod shadows;
@ -19,6 +20,7 @@ mod transform;
pub use blurs::*; pub use blurs::*;
pub use bools::*; pub use bools::*;
pub use fills::*; pub use fills::*;
pub use groups::*;
pub use modifiers::*; pub use modifiers::*;
pub use paths::*; pub use paths::*;
pub use shadows::*; pub use shadows::*;
@ -36,6 +38,7 @@ pub enum Kind {
Path(Path), Path(Path),
Bool(BoolType, Path), Bool(BoolType, Path),
SVGRaw(SVGRaw), SVGRaw(SVGRaw),
Group(Group),
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -269,7 +272,11 @@ impl Shape {
Kind::Path(_) => { Kind::Path(_) => {
self.set_svg_attr(name, value); self.set_svg_attr(name, value);
} }
Kind::Rect(_, _) | Kind::Circle(_) | Kind::SVGRaw(_) | Kind::Bool(_, _) => todo!(), Kind::Rect(_, _)
| Kind::Circle(_)
| Kind::SVGRaw(_)
| Kind::Bool(_, _)
| Kind::Group(_) => unreachable!("This shape should have path attrs"),
}; };
} }
@ -340,9 +347,19 @@ impl Shape {
self.clip_content self.clip_content
} }
pub fn mask_id(&self) -> Option<&Uuid> {
self.children.first()
}
pub fn children_ids(&self) -> Vec<Uuid> { pub fn children_ids(&self) -> Vec<Uuid> {
if let Kind::Bool(_, _) = self.kind { if let Kind::Bool(_, _) = self.kind {
vec![] vec![]
} else if let Kind::Group(group) = self.kind {
if group.masked {
self.children[1..self.children.len()].to_vec()
} else {
self.children.clone()
}
} else { } else {
self.children.clone() self.children.clone()
} }

View file

@ -0,0 +1,10 @@
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Group {
pub masked: bool,
}
impl Group {
pub fn new(masked: bool) -> Self {
Group { masked }
}
}