From bc33eabc3c9a4a79feafdbc08eee9b9008e2a0c1 Mon Sep 17 00:00:00 2001 From: adamgerhant <116332429+adamgerhant@users.noreply.github.com> Date: Wed, 8 May 2024 14:36:15 -0700 Subject: [PATCH] Add grid color customization and choice to display as dots (#1743) * dot grid * fix warning: unreachable pattern * grid color select * add color for all grid types * Dot grid checkbox and remove prefixed Color functions * Display dot grid as grid aligned pixels * Dashed line comment * Code review and UI design widget placement updates * Isometric dotted grid * Early return when cos = 0 * Add spacing, x offset, and color to dot grids --------- Co-authored-by: Keavon Chambers --- Cargo.lock | 1 + editor/Cargo.toml | 1 + .../document/overlays/grid_overlays.rs | 206 ++++++++++++++++-- .../document/overlays/utility_functions.rs | 4 +- .../document/overlays/utility_types.rs | 49 ++++- .../portfolio/document/utility_types/misc.rs | 12 +- .../tool/tool_messages/gradient_tool.rs | 2 +- .../assets/icon-12px-solid/grid-dotted.svg | 11 + frontend/src/utility-functions/icons.ts | 2 + 9 files changed, 255 insertions(+), 33 deletions(-) create mode 100644 frontend/assets/icon-12px-solid/grid-dotted.svg diff --git a/Cargo.lock b/Cargo.lock index b977a231..552e6661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2376,6 +2376,7 @@ dependencies = [ "graphite-proc-macros", "image 0.24.9", "interpreted-executor", + "js-sys", "log", "num_enum 0.6.1", "once_cell", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index da743146..4f61db9c 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -26,6 +26,7 @@ quantization = [ wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"] [dependencies] +js-sys = "0.3.67" log = { workspace = true } bitflags = { workspace = true } thiserror = { workspace = true } diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index cc6789a2..30078084 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,13 +1,16 @@ -use crate::consts::COLOR_OVERLAY_GRAY; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType}; use crate::messages::prelude::*; -use glam::DVec2; + +use graphene_core::raster::color::Color; use graphene_core::renderer::Quad; +use glam::DVec2; + fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; + let grid_color: Color = document.snapping_state.grid.grid_color; let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { return; }; @@ -33,12 +36,60 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } else { DVec2::new(secondary_pos, primary_end) }; - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY)); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_string() + &grid_color.rgba_hex())), + None, + ); + } + } +} + +// In the best case, where the x distance/total dots is an integer, this will reduce draw requests from the current m(horizontal dots)*n(vertical dots) to m(horizontal lines) * 1(line changes). +// In the worst case, where the x distance/total dots is an integer+0.5, then each pixel will require a new line, and requests will be m(horizontal lines)*n(line changes = horizontal dots). +// The draw dashed line method will also be not grid aligned for tilted grids. +// TODO: Potentially create an image and render the image onto the canvas a single time. +// TODO: Implement this with a dashed line (`set_line_dash`), with integer spacing which is continuously adjusted to correct the accumulated error. +fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { + let origin = document.snapping_state.grid.origin; + let grid_color = document.snapping_state.grid.grid_color; + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { + return; + }; + let document_to_viewport = document.metadata().document_to_viewport; + let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); + + let min = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + let mut primary_start = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let mut primary_end = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + primary_start = (primary_start / spacing.x).floor() * spacing.x + origin.x % spacing.x; + primary_end = (primary_end / spacing.x).floor() * spacing.x + origin.x % spacing.x; + + // Round to avoid floating point errors + let total_dots = ((primary_end - primary_start) / spacing.x).round(); + + for line_index in 0..=((max - min) / spacing.y).ceil() as i32 { + let secondary_pos = (((min - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; + let start = DVec2::new(primary_start, secondary_pos); + let end = DVec2::new(primary_end, secondary_pos); + + let x_per_dot = (end.x - start.x) / total_dots; + for dot_index in 0..=total_dots as usize { + let exact_x = x_per_dot * dot_index as f64; + overlay_context.pixel( + document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), + Some(&("#".to_string() + &grid_color.rgba_hex())), + ) } } } fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { + let grid_color = document.snapping_state.grid.grid_color; let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let origin = document.snapping_state.grid.origin; let document_to_viewport = document.metadata().document_to_viewport; @@ -60,7 +111,12 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x; let start = DVec2::new(x_pos, min_y); let end = DVec2::new(x_pos, max_y); - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY)); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_string() + &grid_color.rgba_hex())), + None, + ); } for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] { @@ -74,15 +130,78 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y; let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos))); let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos))); - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY)); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_string() + &grid_color.rgba_hex())), + None, + ); } } } +fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { + let grid_color = document.snapping_state.grid.grid_color; + let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); + let origin = document.snapping_state.grid.origin; + let document_to_viewport = document.metadata().document_to_viewport; + let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); + let tan_a = angle_a.to_radians().tan(); + let tan_b = angle_b.to_radians().tan(); + let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing); + let Some(spacing_multiplier) = GridSnapping::compute_isometric_multiplier(y_axis_spacing, tan_a + tan_b, &document.navigation) else { + return; + }; + let isometric_spacing = spacing * spacing_multiplier; + + let min_x = bounds.0.iter().map(|&corner| corner.x).min_by(cmp).unwrap_or_default(); + let max_x = bounds.0.iter().map(|&corner| corner.x).max_by(cmp).unwrap_or_default(); + let spacing_x = isometric_spacing.x; + let tan = tan_a; + let multiply = -1.0; + let project = |corner: &DVec2| corner.y + multiply * tan * (corner.x - origin.x); + let inverse_project = |corner: &DVec2| corner.y - tan * multiply * (corner.x - origin.x); + let min_y = bounds.0.into_iter().min_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default(); + let max_y = bounds.0.into_iter().max_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default(); + let spacing_y = isometric_spacing.y; + let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing_y).ceil() as i32; + + let cos_a = angle_a.to_radians().cos(); + // If cos_a is 0 then there will be no intersections and thus no dots should be drawn + if cos_a.abs() <= 0.00001 { + return; + } + let x_offset = (((min_x - origin.x) / spacing_x).ceil()) * spacing_x + origin.x - min_x; + for line_index in 0..=lines { + let y_pos = (((inverse_project(&min_y) - origin.y) / spacing_y).ceil() + line_index as f64) * spacing_y + origin.y; + let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos))); + let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos))); + + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_string() + &grid_color.rgba_hex())), + Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length()), + ); + } +} + pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { - GridType::Rectangle { spacing } => grid_overlay_rectangular(document, overlay_context, spacing), - GridType::Isometric { y_axis_spacing, angle_a, angle_b } => grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b), + GridType::Rectangle { spacing } => { + if document.snapping_state.grid.dot_display { + grid_overlay_dot(document, overlay_context, spacing) + } else { + grid_overlay_rectangular(document, overlay_context, spacing) + } + } + GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { + if document.snapping_state.grid.dot_display { + grid_overlay_isometric_dot(document, overlay_context, y_axis_spacing, angle_a, angle_b) + } else { + grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b) + } + } } } @@ -105,9 +224,65 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } }) }; + let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| { + update_val::(grid, move |grid, color| { + if let Some(color) = color.value { + if let Some(update_color) = update(grid) { + *update_color = color; + } + } + }) + }; + let update_display = |grid, update: fn(&mut GridSnapping) -> Option<&mut bool>| { + update_val::(grid, move |grid, checkbox| { + if let Some(update) = update(grid) { + *update = checkbox.checked; + } + }) + }; + widgets.push(LayoutGroup::Row { widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()], }); + + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Type").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + RadioInput::new(vec![ + RadioEntryData::new("rectangular") + .label("Rectangular") + .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)), + RadioEntryData::new("isometric") + .label("Isometric") + .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)), + ]) + .min_width(200) + .selected_index(Some(match grid.grid_type { + GridType::Rectangle { .. } => 0, + GridType::Isometric { .. } => 1, + })) + .widget_holder(), + ], + }); + + let mut color_widgets = vec![TextLabel::new("Display").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; + color_widgets.extend([ + CheckboxInput::new(grid.dot_display) + .icon("GridDotted") + .tooltip("Display as dotted grid") + .on_update(update_display(grid, |grid| Some(&mut grid.dot_display))) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + ]); + color_widgets.push( + ColorButton::new(Some(grid.grid_color)) + .tooltip("Grid display color") + .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) + .widget_holder(), + ); + widgets.push(LayoutGroup::Row { widgets: color_widgets }); + widgets.push(LayoutGroup::Row { widgets: vec![ TextLabel::new("Origin").table_align(true).widget_holder(), @@ -127,23 +302,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .widget_holder(), ], }); - widgets.push(LayoutGroup::Row { - widgets: vec![ - TextLabel::new("Type").table_align(true).widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), - RadioInput::new(vec![ - RadioEntryData::new("rectangular") - .label("Rectangular") - .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)), - RadioEntryData::new("isometric") - .label("Isometric") - .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)), - ]) - .min_width(200) - .selected_index(Some(if matches!(grid.grid_type, GridType::Rectangle { .. }) { 0 } else { 1 })) - .widget_holder(), - ], - }); match grid.grid_type { GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row { diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 030e0725..5a35222c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -39,12 +39,12 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) { let handle_position = transform.transform_point2(in_handle); - overlay_context.line(handle_position, anchor_position, None); + overlay_context.line(handle_position, anchor_position, None, None); overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle))); } if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) { let handle_position = transform.transform_point2(out_handle); - overlay_context.line(handle_position, anchor_position, None); + overlay_context.line(handle_position, anchor_position, None, None); overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle))); } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index d8c2a98e..a52e8af1 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -4,8 +4,9 @@ use crate::messages::prelude::Message; use bezier_rs::Subpath; use graphene_core::renderer::Quad; +use wasm_bindgen::JsValue; -use core::f64::consts::PI; +use core::f64::consts::TAU; use glam::{DAffine2, DVec2}; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -38,10 +39,24 @@ impl OverlayContext { self.render_context.stroke(); } - pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) { + pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option) { let start = start.round() - DVec2::splat(0.5); let end = end.round() - DVec2::splat(0.5); - + if let Some(dash_width) = dash_width { + let array = js_sys::Array::new(); + array.push(&JsValue::from(1)); + array.push(&JsValue::from(dash_width - 1.)); + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::debug!("Error drawing dashed line: {:?}", error)) + .ok(); + } else { + let array = js_sys::Array::new(); + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::debug!("Error drawing dashed line: {:?}", error)) + .ok(); + } self.render_context.begin_path(); self.render_context.move_to(start.x, start.y); self.render_context.line_to(end.x, end.y); @@ -53,7 +68,7 @@ impl OverlayContext { let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.).expect("draw circle"); + self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU).expect("draw circle"); let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); @@ -84,13 +99,37 @@ impl OverlayContext { self.render_context.stroke(); } + pub fn pixel(&mut self, position: DVec2, color: Option<&str>) { + let size = 1.; + let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE); + + let position = position.round() - DVec2::splat(0.5); + let corner = position - DVec2::splat(size) / 2.; + + self.render_context.begin_path(); + self.render_context.rect(corner.x, corner.y, size, size); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.fill(); + } + + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let position = position.round(); + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("draw circle"); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); + self.render_context.fill(); + self.render_context.stroke(); + } pub fn pivot(&mut self, position: DVec2) { let (x, y) = (position.round() - DVec2::splat(0.5)).into(); // Circle self.render_context.begin_path(); - self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., PI * 2.).expect("draw circle"); + self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., TAU).expect("draw circle"); self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW)); self.render_context.fill(); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 1faf4d3d..cdd022ce 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,5 +1,8 @@ -use glam::DVec2; +use crate::consts::COLOR_OVERLAY_GRAY; +use graphene_core::raster::Color; + +use glam::DVec2; use std::fmt; #[repr(transparent)] @@ -86,6 +89,11 @@ impl Default for SnappingState { grid: GridSnapping { origin: DVec2::ZERO, grid_type: GridType::RECTANGLE, + grid_color: COLOR_OVERLAY_GRAY + .strip_prefix("#") + .and_then(|value| Color::from_rgb_str(value)) + .expect("Should create Color from prefixed hex string"), + dot_display: false, }, tolerance: 8., artboards: true, @@ -192,6 +200,8 @@ impl GridType { pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, + pub grid_color: Color, + pub dot_display: bool, } impl GridSnapping { // Double grid size until it takes up at least 10px. diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 0b7d8bc8..b5ff9e0b 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -255,7 +255,7 @@ impl Fsm for GradientToolFsmState { let Gradient { start, end, positions, .. } = gradient; let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); - overlay_context.line(start, end, None); + overlay_context.line(start, end, None, None); overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start)); overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End)); diff --git a/frontend/assets/icon-12px-solid/grid-dotted.svg b/frontend/assets/icon-12px-solid/grid-dotted.svg new file mode 100644 index 00000000..551300be --- /dev/null +++ b/frontend/assets/icon-12px-solid/grid-dotted.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index 37b3cf02..37204edd 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -18,6 +18,7 @@ import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg" import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg"; import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg"; import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg"; +import GridDotted from "@graphite-frontend/assets/icon-12px-solid/grid-dotted.svg"; import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg"; import Info from "@graphite-frontend/assets/icon-12px-solid/info.svg"; import KeyboardArrowDown from "@graphite-frontend/assets/icon-12px-solid/keyboard-arrow-down.svg"; @@ -58,6 +59,7 @@ const SOLID_12PX = { FullscreenEnter: { svg: FullscreenEnter, size: 12 }, FullscreenExit: { svg: FullscreenExit, size: 12 }, Grid: { svg: Grid, size: 12 }, + GridDotted: { svg: GridDotted, size: 12 }, Info: { svg: Info, size: 12 }, KeyboardArrowDown: { svg: KeyboardArrowDown, size: 12 }, KeyboardArrowLeft: { svg: KeyboardArrowLeft, size: 12 },