Update UI widget designs to improve color consistency and add a narrow height mode

This commit is contained in:
Keavon Chambers
2025-09-08 21:48:22 -07:00
parent 485152bf8d
commit 22aa8c1264
41 changed files with 364 additions and 197 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -79,18 +79,17 @@ impl LayoutMessageHandler {
LayoutGroup::Section { layout, .. } => { LayoutGroup::Section { layout, .. } => {
stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val))); stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val)));
} }
LayoutGroup::Table { rows } => { LayoutGroup::Table { rows } => {
for (row_index, cell) in rows.iter().enumerate() { for (row_index, row) in rows.iter().enumerate() {
for (cell_index, entry) in cell.iter().enumerate() { for (cell_index, cell) in row.iter().enumerate() {
// Return if this is the correct ID // Return if this is the correct ID
if entry.widget_id == widget_id { if cell.widget_id == widget_id {
widget_path.push(row_index); widget_path.push(row_index);
widget_path.push(cell_index); widget_path.push(cell_index);
return Some((entry, widget_path)); return Some((cell, widget_path));
} }
if let Widget::PopoverButton(popover) = &entry.widget { if let Widget::PopoverButton(popover) = &cell.widget {
stack.extend( stack.extend(
popover popover
.popover_layout .popover_layout
@@ -118,6 +118,8 @@ pub struct TextButton {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
pub tooltip: String, pub tooltip: String,
#[serde(skip)] #[serde(skip)]
@@ -179,6 +181,8 @@ pub struct ColorInput {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
#[serde(rename = "menuDirection")] #[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>, pub menu_direction: Option<MenuDirection>,
@@ -87,6 +87,8 @@ pub struct DropdownInput {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
pub tooltip: String, pub tooltip: String,
#[serde(skip)] #[serde(skip)]
@@ -195,6 +197,8 @@ pub struct NumberInput {
// Disabled // Disabled
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
// Value // Value
#[widget_builder(constructor)] #[widget_builder(constructor)]
pub value: Option<f64>, pub value: Option<f64>,
@@ -333,6 +337,8 @@ pub struct RadioInput {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI) // This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")] #[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>, pub selected_index: Option<u32>,
@@ -409,6 +415,8 @@ pub struct TextInput {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
pub tooltip: String, pub tooltip: String,
pub centered: bool, pub centered: bool,
@@ -40,6 +40,8 @@ pub enum SeparatorType {
pub struct TextLabel { pub struct TextLabel {
pub disabled: bool, pub disabled: bool,
pub narrow: bool,
pub bold: bool, pub bold: bool,
pub italic: bool, pub italic: bool,
@@ -195,6 +195,7 @@ trait TableRowLayout {
fn element_widget(&self, index: usize) -> WidgetHolder { fn element_widget(&self, index: usize) -> WidgetHolder {
TextButton::new(self.identifier()) TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into()) .on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.narrow(true)
.widget_holder() .widget_holder()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
@@ -227,11 +228,13 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
.enumerate() .enumerate()
.map(|(index, row)| { .map(|(index, row)| {
vec![ vec![
TextLabel::new(format!("{index}")).widget_holder(), TextLabel::new(format!("{index}")).narrow(true).widget_holder(),
row.element.element_widget(index), row.element.element_widget(index),
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(), TextLabel::new(format_transform_matrix(row.transform)).narrow(true).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(), TextLabel::new(format!("{}", row.alpha_blending)).narrow(true).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(), TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0)))
.narrow(true)
.widget_holder(),
] ]
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -315,104 +318,130 @@ impl TableRowLayout for Vector {
match self.style.fill.clone() { match self.style.fill.clone() {
Fill::None => table_rows.push(vec![ Fill::None => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(), TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_holder(),
]), ]),
Fill::Solid(color) => table_rows.push(vec![ Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(), TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), ColorInput::new(FillChoice::Solid(color))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder(),
]), ]),
Fill::Gradient(gradient) => { Fill::Gradient(gradient) => {
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Fill").widget_holder(), TextLabel::new("Fill").narrow(true).widget_holder(),
ColorInput::new(FillChoice::Gradient(gradient.stops)) ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true) .disabled(true)
.menu_direction(Some(MenuDirection::Top)) .menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder(), .widget_holder(),
]); ]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(), TextLabel::new("Fill Gradient Type").narrow(true).widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(), TextLabel::new(gradient.gradient_type.to_string()).narrow(true).widget_holder(),
]); ]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(), TextLabel::new("Fill Gradient Start").narrow(true).widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(), TextLabel::new(format_dvec2(gradient.start)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient End").narrow(true).widget_holder(),
TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_holder(),
]); ]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
} }
} }
if let Some(stroke) = self.style.stroke.clone() { if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None }; let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke").widget_holder(), TextLabel::new("Stroke").narrow(true).widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(), ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_holder(),
]); ]);
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").widget_holder(), TextLabel::new("Stroke Weight").narrow(true).widget_holder(),
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_holder(),
TextLabel::new(if stroke.dash_lengths.is_empty() { TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string() "-".to_string()
} else { } else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", ")) format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
}) })
.narrow(true)
.widget_holder(), .widget_holder(),
]); ]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").widget_holder(), TextLabel::new("Stroke Dash Offset").narrow(true).widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(), TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
]); ]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(), TextLabel::new("Stroke Cap").narrow(true).widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(), TextLabel::new(stroke.cap.to_string()).narrow(true).widget_holder(),
]); ]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(), TextLabel::new("Stroke Join").narrow(true).widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(), TextLabel::new(stroke.join.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Align").narrow(true).widget_holder(),
TextLabel::new(stroke.align.to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").narrow(true).widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").narrow(true).widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).narrow(true).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").narrow(true).widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_holder(),
]); ]);
} }
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", "); let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear }; let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]); table_rows.push(vec![
TextLabel::new("Colinear Handle IDs").narrow(true).widget_holder(),
TextLabel::new(colinear).narrow(true).widget_holder(),
]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(), TextLabel::new("Upstream Nested Layers").narrow(true).widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() { TextLabel::new(if self.upstream_nested_layers.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)" "Yes (this preserves references to its upstream nested layers for editing by tools)"
} else { } else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)" "No (this doesn't preserve references to its upstream nested layers for editing by tools)"
}) })
.narrow(true)
.widget_holder(), .widget_holder(),
]); ]);
} }
VectorTableTab::Points => { VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"])); table_rows.push(column_headings(&["", "position"]));
table_rows.extend( table_rows.extend(self.point_domain.iter().map(|(id, position)| {
self.point_domain vec![
.iter() TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]), TextLabel::new(format!("{position}")).narrow(true).widget_holder(),
); ]
}));
} }
VectorTableTab::Segments => { VectorTableTab::Segments => {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"])); table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![ vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
TextLabel::new(format!("{start}")).widget_holder(), TextLabel::new(format!("{start}")).narrow(true).widget_holder(),
TextLabel::new(format!("{end}")).widget_holder(), TextLabel::new(format!("{end}")).narrow(true).widget_holder(),
TextLabel::new(format!("{handles:?}")).widget_holder(), TextLabel::new(format!("{handles:?}")).narrow(true).widget_holder(),
] ]
})); }));
} }
@@ -420,9 +449,9 @@ impl TableRowLayout for Vector {
table_rows.push(column_headings(&["", "segment_range", "fill"])); table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| { table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![ vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", id.inner())).narrow(true).widget_holder(),
TextLabel::new(format!("{segment_range:?}")).widget_holder(), TextLabel::new(format!("{segment_range:?}")).narrow(true).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(), TextLabel::new(format!("{}", fill.inner())).narrow(true).widget_holder(),
] ]
})); }));
} }
@@ -477,7 +506,11 @@ impl TableRowLayout for Color {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb()) format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
} }
fn element_widget(&self, _index: usize) -> WidgetHolder { fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder() ColorInput::new(FillChoice::Solid(*self))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_holder()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)]; let widgets = vec![self.element_widget(0)];
@@ -494,8 +527,9 @@ impl TableRowLayout for GradientStops {
} }
fn element_widget(&self, _index: usize) -> WidgetHolder { fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Gradient(self.clone())) ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top)) .menu_direction(Some(MenuDirection::Top))
.disabled(true)
.narrow(true)
.widget_holder() .widget_holder()
} }
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> { fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
@@ -16,7 +16,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget; use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode; use graphene_std::vector::style::RenderMode;
#[impl_message(Message, PortfolioMessage, Document)] #[impl_message(Message, PortfolioMessage, Document)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)] #[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
@@ -176,8 +176,8 @@ pub enum DocumentMessage {
node_id: NodeId, node_id: NodeId,
is_layer: bool, is_layer: bool,
}, },
SetViewMode { SetRenderMode {
view_mode: ViewMode, render_mode: RenderMode,
}, },
AddTransaction, AddTransaction,
StartTransaction, StartTransaction,
@@ -39,7 +39,7 @@ use graphene_std::table::Table;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::style::ViewMode; use graphene_std::vector::style::RenderMode;
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez}; use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
@@ -92,9 +92,10 @@ pub struct DocumentMessageHandler {
pub document_ptz: PTZ, pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools. /// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode, pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport. /// The current mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently. /// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode, #[serde(alias = "view_mode")]
pub render_mode: RenderMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork. /// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more. /// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visibility_settings: OverlaysVisibilitySettings, pub overlays_visibility_settings: OverlaysVisibilitySettings,
@@ -163,7 +164,7 @@ impl Default for DocumentMessageHandler {
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(), commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
document_ptz: PTZ::default(), document_ptz: PTZ::default(),
document_mode: DocumentMode::DesignMode, document_mode: DocumentMode::DesignMode,
view_mode: ViewMode::default(), render_mode: RenderMode::default(),
overlays_visibility_settings: OverlaysVisibilitySettings::default(), overlays_visibility_settings: OverlaysVisibilitySettings::default(),
rulers_visible: true, rulers_visible: true,
graph_view_overlay_open: false, graph_view_overlay_open: false,
@@ -1266,8 +1267,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer }); responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
responses.add(DocumentMessage::EndTransaction); responses.add(DocumentMessage::EndTransaction);
} }
DocumentMessage::SetViewMode { view_mode } => { DocumentMessage::SetRenderMode { render_mode } => {
self.view_mode = view_mode; self.render_mode = render_mode;
responses.add_front(NodeGraphMessage::RunDocumentGraph); responses.add_front(NodeGraphMessage::RunDocumentGraph);
} }
DocumentMessage::AddTransaction => { DocumentMessage::AddTransaction => {
@@ -1821,9 +1822,9 @@ impl DocumentMessageHandler {
pub document_ptz: PTZ, pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools. /// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode, pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport. /// The current mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently. /// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode, pub view_mode: RenderMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork. /// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more. /// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visibility_settings: OverlaysVisibilitySettings, pub overlays_visibility_settings: OverlaysVisibilitySettings,
@@ -1841,7 +1842,7 @@ impl DocumentMessageHandler {
commit_hash: old_message_handler.commit_hash, commit_hash: old_message_handler.commit_hash,
document_ptz: old_message_handler.document_ptz, document_ptz: old_message_handler.document_ptz,
document_mode: old_message_handler.document_mode, document_mode: old_message_handler.document_mode,
view_mode: old_message_handler.view_mode, render_mode: old_message_handler.view_mode,
overlays_visibility_settings: old_message_handler.overlays_visibility_settings, overlays_visibility_settings: old_message_handler.overlays_visibility_settings,
rulers_visible: old_message_handler.rulers_visible, rulers_visible: old_message_handler.rulers_visible,
graph_view_overlay_open: old_message_handler.graph_view_overlay_open, graph_view_overlay_open: old_message_handler.graph_view_overlay_open,
@@ -2532,28 +2533,30 @@ impl DocumentMessageHandler {
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
RadioInput::new(vec![ RadioInput::new(vec![
RadioEntryData::new("normal") RadioEntryData::new("Normal")
.icon("ViewModeNormal") .icon("RenderModeNormal")
.tooltip("View Mode: Normal") .tooltip("Render Mode: Normal")
.on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()), .on_update(|_| DocumentMessage::SetRenderMode { render_mode: RenderMode::Normal }.into()),
RadioEntryData::new("outline") RadioEntryData::new("Outline")
.icon("ViewModeOutline") .icon("RenderModeOutline")
.tooltip("View Mode: Outline") .tooltip("Render Mode: Outline")
.on_update(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()), .on_update(|_| DocumentMessage::SetRenderMode { render_mode: RenderMode::Outline }.into()),
RadioEntryData::new("pixels") // RadioEntryData::new("PixelPreview")
.icon("ViewModePixels") // .icon("RenderModePixels")
.tooltip("View Mode: Pixels") // .tooltip("Render Mode: Pixel Preview")
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()), // .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(320) }.into()),
// RadioEntryData::new("SvgPreview")
// .icon("RenderModeSvg")
// .tooltip("Render Mode: SVG Preview")
// .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1845) }.into()),
]) ])
.selected_index(match self.view_mode { .selected_index(Some(self.render_mode as u32))
ViewMode::Normal => Some(0), .narrow(true)
_ => Some(1),
})
.widget_holder(), .widget_holder(),
// PopoverButton::new() // PopoverButton::new()
// .popover_layout(vec![ // .popover_layout(vec![
// LayoutGroup::Row { // LayoutGroup::Row {
// widgets: vec![TextLabel::new("View Mode").bold(true).widget_holder()], // widgets: vec![TextLabel::new("Render Mode").bold(true).widget_holder()],
// }, // },
// LayoutGroup::Row { // LayoutGroup::Row {
// widgets: vec![TextLabel::new("Coming soon").widget_holder()], // widgets: vec![TextLabel::new("Coming soon").widget_holder()],
+2 -2
View File
@@ -149,7 +149,7 @@ impl NodeGraphExecutor {
export_format: graphene_std::application_io::ExportFormat::Canvas, export_format: graphene_std::application_io::ExportFormat::Canvas,
#[cfg(not(any(feature = "resvg", feature = "vello")))] #[cfg(not(any(feature = "resvg", feature = "vello")))]
export_format: graphene_std::application_io::ExportFormat::Svg, export_format: graphene_std::application_io::ExportFormat::Svg,
view_mode: document.view_mode, render_mode: document.render_mode,
hide_artboards: false, hide_artboards: false,
for_export: false, for_export: false,
}; };
@@ -198,7 +198,7 @@ impl NodeGraphExecutor {
}, },
time: Default::default(), time: Default::default(),
export_format: graphene_std::application_io::ExportFormat::Svg, export_format: graphene_std::application_io::ExportFormat::Svg,
view_mode: document.view_mode, render_mode: document.render_mode,
hide_artboards: export_config.transparent_background, hide_artboards: export_config.transparent_background,
for_export: true, for_export: true,
}; };
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<circle cx="5.75" cy="5.75" r="5.75" />
</svg>

After

Width:  |  Height:  |  Size: 109 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M5.75,1A4.75,4.75,0,1,0,10.5,5.75,4.755,4.755,0,0,0,5.75,1m0-1A5.75,5.75,0,1,1,0,5.75,5.75,5.75,0,0,1,5.75,0z" />
</svg>

After

Width:  |  Height:  |  Size: 192 B

@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<rect x="5" y="2" width="1" height="1" />
<rect x="2" y="3" width="1" height="1" />
<rect x="0" y="3" width="1" height="1" />
<rect x="5" y="6" width="1" height="1" />
<rect x="2" y="7" width="1" height="1" />
<rect x="0" y="7" width="1" height="1" />
<rect x="2" y="9" width="1" height="1" />
<rect x="6" y="3" width="1" height="1" />
<rect x="8" y="3" width="1" height="1" />
<rect x="10" y="3" width="1" height="1" />
<rect x="3" y="4" width="1" height="1" />
<rect x="1" y="4" width="1" height="1" />
<rect x="6" y="7" width="1" height="1" />
<rect x="8" y="7" width="1" height="1" />
<rect x="10" y="7" width="1" height="1" />
<rect x="6" y="9" width="1" height="1" />
<rect x="8" y="9" width="1" height="1" />
<rect x="7" y="8" width="1" height="1" />
<rect x="9" y="8" width="1" height="1" />
<rect x="7" y="10" width="1" height="1" />
<rect x="3" y="8" width="1" height="1" />
<rect x="1" y="8" width="1" height="1" />
<rect x="3" y="10" width="1" height="1" />
<rect x="5" y="8" width="1" height="1" />
<rect x="5" y="10" width="1" height="1" />
<rect x="7" y="2" width="1" height="1" />
<rect x="9" y="2" width="1" height="1" />
<rect x="8" y="1" width="1" height="1" />
<rect x="4" y="3" width="1" height="1" />
<rect x="7" y="6" width="1" height="1" />
<rect x="9" y="6" width="1" height="1" />
<rect x="4" y="7" width="1" height="1" />
<rect x="4" y="9" width="1" height="1" />
<rect x="6" y="1" width="1" height="1" />
<rect x="3" y="2" width="1" height="1" />
<rect x="1" y="2" width="1" height="1" />
<rect x="3" y="0" width="1" height="1" />
<rect x="6" y="5" width="1" height="1" />
<rect x="8" y="5" width="1" height="1" />
<rect x="10" y="5" width="1" height="1" />
<rect x="3" y="6" width="1" height="1" />
<rect x="1" y="6" width="1" height="1" />
<rect x="5" y="0" width="1" height="1" />
<rect x="2" y="1" width="1" height="1" />
<rect x="5" y="4" width="1" height="1" />
<rect x="2" y="5" width="1" height="1" />
<rect x="0" y="5" width="1" height="1" />
<rect x="7" y="0" width="1" height="1" />
<rect x="4" y="1" width="1" height="1" />
<rect x="7" y="4" width="1" height="1" />
<rect x="9" y="4" width="1" height="1" />
<rect x="4" y="5" width="1" height="1" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M0,11V8.5H1V10H2.5v1z" />
<path d="M0,2.5V0H2.5V1H1V2.5z" />
<path d="M9.5,11V10H11V8.5h1V11z" />
<path d="M11,2.5V1H9.5V0H12V2.5z" />
<path d="M5.75,0.75C8.51,0.75 10.75,2.878 10.75,5.5C10.75,8.122 8.51,10.25 5.75,10.25C2.99,10.25 0.75,8.122 0.75,5.5C0.75,2.878 2.99,0.75 5.75,0.75zM2,6L3.333,6L3.5,6.5L3.333,7L2,7L2,8L4,8L4.5,6.5L5,8L6,8L6.571,6.286L7,8L9.5,8L10,6L10,5L8.5,5L8.5,6L9,6L8.75,7L7.75,7L7.5,6L7.833,4L9,4L9,3L6,3L6,5L5.5,6.5L5,5L5,3L2,3L1.5,4.5L2,6zM2.667,4L4,4L4,5L2.667,5L2.5,4.5L2.667,4z" />
</svg>

After

Width:  |  Height:  |  Size: 592 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="8" />
</svg>

Before

Width:  |  Height:  |  Size: 100 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8,1c3.9,0,7,3.1,7,7s-3.1,7-7,7s-7-3.1-7-7S4.1,1,8,1 M8,0C3.6,0,0,3.6,0,8s3.6,8,8,8s8-3.6,8-8S12.4,0,8,0L8,0z" />
</svg>

Before

Width:  |  Height:  |  Size: 192 B

@@ -1,23 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<rect x="7" y="1" width="2" height="2" />
<rect x="13" y="4" width="2" height="2" />
<rect x="13" y="7" width="2" height="2" />
<rect x="13" y="10" width="2" height="2" />
<rect x="7" y="13" width="2" height="2" />
<rect x="10" y="13" width="2" height="2" />
<rect x="4" y="13" width="2" height="2" />
<rect x="1" y="10" width="2" height="2" />
<rect x="1" y="7" width="2" height="2" />
<rect x="1" y="4" width="2" height="2" />
<rect x="4" y="1" width="2" height="2" />
<rect x="10" y="10" width="2" height="2" />
<rect x="4" y="10" width="2" height="2" />
<rect x="7" y="10" width="2" height="2" />
<rect x="10" y="7" width="2" height="2" />
<rect x="4" y="7" width="2" height="2" />
<rect x="7" y="7" width="2" height="2" />
<rect x="10" y="4" width="2" height="2" />
<rect x="4" y="4" width="2" height="2" />
<rect x="7" y="4" width="2" height="2" />
<rect x="10" y="1" width="2" height="2" />
</svg>

Before

Width:  |  Height:  |  Size: 987 B

@@ -40,6 +40,8 @@
export { className as class }; export { className as class };
export let classes: Record<string, boolean> = {}; export let classes: Record<string, boolean> = {};
export let narrow = false;
$: extraClasses = Object.entries(classes) $: extraClasses = Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" ");
@@ -82,7 +84,7 @@
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components --> <!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:row={direction === "row"} class:column={direction === "column"}> <div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
{#each widgets as component, index} {#each widgets as component, index}
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")} {@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
{#if checkboxInput} {#if checkboxInput}
@@ -202,11 +204,17 @@
.widget-span.row { .widget-span.row {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
min-height: 32px; --row-height: 32px;
min-height: var(--row-height);
&.narrow {
--row-height: 24px;
}
> * { > * {
--widget-height: 24px; --widget-height: 24px;
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0; // Vertically center the widget within the row
margin: calc((var(--row-height) - var(--widget-height)) / 2) 0;
min-height: var(--widget-height); min-height: var(--widget-height);
&:not(.multiline) { &:not(.multiline) {
@@ -14,7 +14,7 @@
<tr> <tr>
{#each row as cell} {#each row as cell}
<td> <td>
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} /> <WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} narrow={true} />
</td> </td>
{/each} {/each}
</tr> </tr>
@@ -18,6 +18,7 @@
export let flush = false; export let flush = false;
export let minWidth = 0; export let minWidth = 0;
export let disabled = false; export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined; export let tooltip: string | undefined = undefined;
export let menuListChildren: MenuListEntry[][] | undefined = undefined; export let menuListChildren: MenuListEntry[][] | undefined = undefined;
@@ -54,6 +55,7 @@
class:hover-icon={hoverIcon && !disabled} class:hover-icon={hoverIcon && !disabled}
class:emphasized class:emphasized
class:disabled class:disabled
class:narrow
class:flush class:flush
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined} style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
title={tooltip} title={tooltip}
@@ -99,7 +101,7 @@
align-items: center; align-items: center;
flex: 0 0 auto; flex: 0 0 auto;
white-space: nowrap; white-space: nowrap;
height: 24px; height: var(--widget-height);
margin: 0; margin: 0;
padding: 0 8px; padding: 0 8px;
box-sizing: border-box; box-sizing: border-box;
@@ -107,8 +109,13 @@
border-radius: 2px; border-radius: 2px;
background: var(--button-background-color); background: var(--button-background-color);
color: var(--button-text-color); color: var(--button-text-color);
--button-background-color: var(--color-5-dullgray); --button-background-color: var(--color-4-dimgray);
--button-text-color: var(--color-e-nearwhite); --button-text-color: var(--color-e-nearwhite);
--widget-height: 24px;
&.narrow.narrow {
--widget-height: 20px;
}
&:hover, &:hover,
&.open { &.open {
@@ -13,6 +13,7 @@
export let value: FillChoice; export let value: FillChoice;
export let disabled = false; export let disabled = false;
export let narrow = false;
export let allowNone = false; export let allowNone = false;
export let menuDirection: MenuDirection = "Bottom"; export let menuDirection: MenuDirection = "Bottom";
// export let allowTransparency = false; // TODO: Implement // export let allowTransparency = false; // TODO: Implement
@@ -25,7 +26,7 @@
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1; $: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
</script> </script>
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}> <LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltip}>
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner> <button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
<!-- {#if disabled && value instanceof Color && !value.none} <!-- {#if disabled && value instanceof Color && !value.none}
<TextLabel>sRGB</TextLabel> <TextLabel>sRGB</TextLabel>
@@ -55,6 +56,10 @@
position: relative; position: relative;
min-width: 80px; min-width: 80px;
&.narrow.narrow {
--widget-height: 20px;
}
> button { > button {
border: none; border: none;
border-radius: 2px; border-radius: 2px;
@@ -20,6 +20,7 @@
export let drawIcon = false; export let drawIcon = false;
export let interactive = true; export let interactive = true;
export let disabled = false; export let disabled = false;
export let narrow = false;
export let tooltip: string | undefined = undefined; export let tooltip: string | undefined = undefined;
export let minWidth = 0; export let minWidth = 0;
export let maxWidth = 0; export let maxWidth = 0;
@@ -86,7 +87,11 @@
<LayoutRow <LayoutRow
class="dropdown-input" class="dropdown-input"
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}) }} classes={{ narrow }}
styles={{
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
}}
bind:this={self} bind:this={self}
data-dropdown-input data-dropdown-input
> >
@@ -107,12 +112,12 @@
</LayoutRow> </LayoutRow>
<MenuList <MenuList
on:naturalWidth={({ detail }) => (minWidth = detail)} on:naturalWidth={({ detail }) => (minWidth = detail)}
{activeEntry}
on:activeEntry={({ detail }) => (activeEntry = detail)} on:activeEntry={({ detail }) => (activeEntry = detail)}
on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)} on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)}
on:hoverOutEntry={() => dispatchHoverOutEntry()} on:hoverOutEntry={() => dispatchHoverOutEntry()}
{open}
on:open={({ detail }) => (open = detail)} on:open={({ detail }) => (open = detail)}
{open}
{activeEntry}
{entries} {entries}
{drawIcon} {drawIcon}
{interactive} {interactive}
@@ -125,13 +130,18 @@
<style lang="scss" global> <style lang="scss" global>
.dropdown-input { .dropdown-input {
position: relative; position: relative;
--widget-height: 24px;
&.narrow.narrow {
--widget-height: 20px;
}
.dropdown-box { .dropdown-box {
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
background: var(--color-1-nearblack);
height: 24px;
border-radius: 2px; border-radius: 2px;
background: var(--color-1-nearblack);
height: var(--widget-height);
.dropdown-label { .dropdown-label {
margin: 0; margin: 0;
@@ -23,6 +23,7 @@
export let label: string | undefined = undefined; export let label: string | undefined = undefined;
export let spellcheck = false; export let spellcheck = false;
export let disabled = false; export let disabled = false;
export let narrow = false;
export let textarea = false; export let textarea = false;
export let tooltip: string | undefined = undefined; export let tooltip: string | undefined = undefined;
export let placeholder: string | undefined = undefined; export let placeholder: string | undefined = undefined;
@@ -73,7 +74,7 @@
</script> </script>
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. --> <!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
<LayoutRow class={`field-input ${className}`} classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}> <LayoutRow class={`field-input ${className}`} classes={{ disabled, narrow, ...classes }} style={styleName} {styles} {tooltip}>
{#if !textarea} {#if !textarea}
<input <input
type="text" type="text"
@@ -127,9 +128,13 @@
background: var(--color-1-nearblack); background: var(--color-1-nearblack);
flex-direction: row-reverse; flex-direction: row-reverse;
&.narrow.narrow {
--widget-height: 20px;
}
label { label {
flex: 0 0 auto; flex: 0 0 auto;
line-height: 18px; line-height: calc(var(--widget-height) - 6px);
padding: 3px 0; padding: 3px 0;
padding-right: 4px; padding-right: 4px;
margin-left: 8px; margin-left: 8px;
@@ -147,8 +152,8 @@
flex: 1 1 100%; flex: 1 1 100%;
width: 0; width: 0;
min-width: 30px; min-width: 30px;
height: 18px; height: calc(var(--widget-height) - 6px);
line-height: 18px; line-height: calc(var(--widget-height) - 6px);
margin: 0 8px; margin: 0 8px;
padding: 3px 0; padding: 3px 0;
outline: none; // Ok for input/textarea element outline: none; // Ok for input/textarea element
@@ -189,7 +194,7 @@
} }
textarea { textarea {
min-height: calc(18px * 3); min-height: calc((var(--widget-height) - 6px) * 3);
margin: 3px; margin: 3px;
padding: 0 5px; padding: 0 5px;
box-sizing: border-box; box-sizing: border-box;
@@ -22,6 +22,9 @@
// Disabled // Disabled
export let disabled = false; export let disabled = false;
// Narrow
export let narrow = false;
// Value // Value
// When `value` is not provided (i.e. it's `undefined`), a dash is displayed. // When `value` is not provided (i.e. it's `undefined`), a dash is displayed.
export let value: number | undefined = undefined; // NOTE: Do not update this directly, do so by calling `updateValue()` instead. export let value: number | undefined = undefined; // NOTE: Do not update this directly, do so by calling `updateValue()` instead.
@@ -642,6 +645,7 @@
<FieldInput <FieldInput
class="number-input" class="number-input"
classes={{ classes={{
narrow,
increment: mode === "Increment", increment: mode === "Increment",
range: mode === "Range", range: mode === "Range",
}} }}
@@ -653,6 +657,7 @@
on:pointerdown={onDragPointerDown} on:pointerdown={onDragPointerDown}
{label} {label}
{disabled} {disabled}
{narrow}
{tooltip} {tooltip}
{styles} {styles}
hideContextMenu={true} hideContextMenu={true}
@@ -709,7 +714,13 @@
text-align: center; text-align: center;
} }
&.narrow {
--widget-height: 20px;
}
&.increment { &.increment {
--arrow-radius: 3px;
// Widen the label and input margins from the edges by an extra 8px to make room for the increment arrows // Widen the label and input margins from the edges by an extra 8px to make room for the increment arrows
label { label {
margin-left: 8px; margin-left: 8px;
@@ -738,7 +749,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
margin: 0; margin: 0;
padding: 9px 0; padding: calc(var(--widget-height) / 2 - var(--arrow-radius)) 0;
border: none; border: none;
border-radius: 2px; border-radius: 2px;
background: rgba(var(--color-1-nearblack-rgb), 0.5); background: rgba(var(--color-1-nearblack-rgb), 0.5);
@@ -759,7 +770,7 @@
width: 0; width: 0;
height: 0; height: 0;
border-style: solid; border-style: solid;
border-width: 3px 0 3px 3px; border-width: var(--arrow-radius) 0 var(--arrow-radius) var(--arrow-radius);
border-color: transparent transparent transparent var(--color-e-nearwhite); border-color: transparent transparent transparent var(--color-e-nearwhite);
} }
} }
@@ -775,7 +786,7 @@
width: 0; width: 0;
height: 0; height: 0;
border-style: solid; border-style: solid;
border-width: 3px 3px 3px 0; border-width: var(--arrow-radius) var(--arrow-radius) var(--arrow-radius) 0;
border-color: transparent var(--color-e-nearwhite) transparent transparent; border-color: transparent var(--color-e-nearwhite) transparent transparent;
} }
} }
@@ -845,7 +856,7 @@
appearance: none; appearance: none;
border-radius: 2px; border-radius: 2px;
width: 4px; width: 4px;
height: 22px; height: calc(var(--widget-height) - 2px);
background: #494949; // Becomes var(--color-5-dullgray) with screen blend mode over var(--color-1-nearblack) background background: #494949; // Becomes var(--color-5-dullgray) with screen blend mode over var(--color-1-nearblack) background
} }
@@ -862,7 +873,7 @@
border: none; border: none;
border-radius: 2px; border-radius: 2px;
width: 4px; width: 4px;
height: 22px; height: calc(var(--widget-height) - 2px);
background: #494949; // Becomes var(--color-5-dullgray) with screen blend mode over var(--color-1-nearblack) background background: #494949; // Becomes var(--color-5-dullgray) with screen blend mode over var(--color-1-nearblack) background
} }
@@ -900,7 +911,7 @@
border-radius: 2px; border-radius: 2px;
margin-left: -2px; margin-left: -2px;
width: 4px; width: 4px;
height: 22px; height: calc(var(--widget-height) - 2px);
top: 1px; top: 1px;
left: calc(var(--progress-factor) * 100%); left: calc(var(--progress-factor) * 100%);
background: #5b5b5b; // Becomes var(--color-6-lowergray) with screen blend mode over var(--color-1-nearblack) background background: #5b5b5b; // Becomes var(--color-6-lowergray) with screen blend mode over var(--color-1-nearblack) background
@@ -13,6 +13,7 @@
export let selectedIndex: number | undefined = undefined; export let selectedIndex: number | undefined = undefined;
export let disabled = false; export let disabled = false;
export let minWidth = 0; export let minWidth = 0;
export let narrow = false;
$: mixed = selectedIndex === undefined && !disabled; $: mixed = selectedIndex === undefined && !disabled;
@@ -24,7 +25,7 @@
} }
</script> </script>
<LayoutRow class="radio-input" classes={{ disabled, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}> <LayoutRow class="radio-input" classes={{ disabled, narrow, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
{#each entries as entry, index} {#each entries as entry, index}
<button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}> <button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
{#if entry.icon} {#if entry.icon}
@@ -39,12 +40,13 @@
<style lang="scss" global> <style lang="scss" global>
.radio-input { .radio-input {
background: var(--color-5-dullgray); background: var(--color-4-dimgray);
border-radius: 2px; border-radius: 2px;
height: 24px; --widget-height: 24px;
height: var(--widget-height);
button { button {
background: var(--color-5-dullgray); background: var(--color-4-dimgray);
fill: var(--color-e-nearwhite); fill: var(--color-e-nearwhite);
border-radius: 2px; border-radius: 2px;
height: 20px; height: 20px;
@@ -100,33 +102,35 @@
} }
} }
&.mixed { &.narrow.narrow {
background: var(--color-4-dimgray); --widget-height: 20px;
height: var(--widget-height);
button {
height: 16px;
}
}
&.mixed {
button:not(:hover), button:not(:hover),
&.disabled button:hover { &.disabled button:hover {
background: var(--color-5-dullgray); background: var(--color-5-dullgray);
} }
} }
&.disabled { &.disabled button {
background: var(--color-4-dimgray); color: var(--color-8-uppergray);
button { svg {
background: var(--color-4-dimgray); fill: var(--color-8-uppergray);
color: var(--color-8-uppergray); }
&.active {
background: var(--color-8-uppergray);
color: var(--color-2-mildblack);
svg { svg {
fill: var(--color-8-uppergray); fill: var(--color-2-mildblack);
}
&.active {
background: var(--color-8-uppergray);
color: var(--color-2-mildblack);
svg {
fill: var(--color-2-mildblack);
}
} }
} }
} }
@@ -11,6 +11,8 @@
export let placeholder: string | undefined = undefined; export let placeholder: string | undefined = undefined;
// Disabled // Disabled
export let disabled = false; export let disabled = false;
// Narrow
export let narrow = false;
// Value // Value
export let value: string; export let value: string;
// Styling // Styling
@@ -76,6 +78,7 @@
spellcheck={true} spellcheck={true}
{label} {label}
{disabled} {disabled}
{narrow}
{tooltip} {tooltip}
{placeholder} {placeholder}
bind:this={self} bind:this={self}
@@ -6,6 +6,7 @@
export { styleName as style }; export { styleName as style };
export let styles: Record<string, string | number | undefined> = {}; export let styles: Record<string, string | number | undefined> = {};
export let disabled = false; export let disabled = false;
export let narrow = false;
export let bold = false; export let bold = false;
export let italic = false; export let italic = false;
export let monospace = false; export let monospace = false;
@@ -27,6 +28,7 @@
<label <label
class={`text-label ${className} ${extraClasses}`.trim()} class={`text-label ${className} ${extraClasses}`.trim()}
class:disabled class:disabled
class:narrow
class:bold class:bold
class:italic class:italic
class:monospace class:monospace
@@ -48,6 +50,10 @@
// Force Safari to not draw a text cursor, even though this element has `user-select: none` // Force Safari to not draw a text cursor, even though this element has `user-select: none`
cursor: default; cursor: default;
&.narrow.narrow {
--widget-height: 20px;
}
&.disabled { &.disabled {
color: var(--color-8-uppergray); color: var(--color-8-uppergray);
} }
+15
View File
@@ -942,6 +942,8 @@ export class ColorInput extends WidgetProps {
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
menuDirection!: MenuDirection | undefined; menuDirection!: MenuDirection | undefined;
// allowTransparency!: boolean; // TODO: Implement // allowTransparency!: boolean; // TODO: Implement
@@ -1039,6 +1041,8 @@ export class DropdownInput extends WidgetProps {
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
@Transform(({ value }: { value: string }) => value || undefined) @Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined; tooltip!: string | undefined;
@@ -1127,6 +1131,9 @@ export class NumberInput extends WidgetProps {
disabled!: boolean; disabled!: boolean;
// Narrow
narrow!: boolean;
// Value // Value
value!: number | undefined; value!: number | undefined;
@@ -1204,6 +1211,8 @@ export class RadioInput extends WidgetProps {
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
selectedIndex!: number | undefined; selectedIndex!: number | undefined;
minWidth!: number; minWidth!: number;
@@ -1261,6 +1270,8 @@ export class TextButton extends WidgetProps {
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
@Transform(({ value }: { value: string }) => value || undefined) @Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined; tooltip!: string | undefined;
@@ -1302,6 +1313,8 @@ export class TextInput extends WidgetProps {
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
minWidth!: number; minWidth!: number;
maxWidth!: number; maxWidth!: number;
@@ -1317,6 +1330,8 @@ export class TextLabel extends WidgetProps {
// Props // Props
disabled!: boolean; disabled!: boolean;
narrow!: boolean;
bold!: boolean; bold!: boolean;
italic!: boolean; italic!: boolean;
+8 -6
View File
@@ -37,6 +37,10 @@ import License12px from "@graphite-frontend/assets/icon-12px-solid/license-12px.
import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg"; import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg"; import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg"; import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
import RenderModeNormal from "@graphite-frontend/assets/icon-12px-solid/render-mode-normal.svg";
import RenderModeOutline from "@graphite-frontend/assets/icon-12px-solid/render-mode-outline.svg";
import RenderModePixels from "@graphite-frontend/assets/icon-12px-solid/render-mode-pixels.svg";
import RenderModeSvg from "@graphite-frontend/assets/icon-12px-solid/render-mode-svg.svg";
import Snapping from "@graphite-frontend/assets/icon-12px-solid/snapping.svg"; import Snapping from "@graphite-frontend/assets/icon-12px-solid/snapping.svg";
import SwapHorizontal from "@graphite-frontend/assets/icon-12px-solid/swap-horizontal.svg"; import SwapHorizontal from "@graphite-frontend/assets/icon-12px-solid/swap-horizontal.svg";
import SwapVertical from "@graphite-frontend/assets/icon-12px-solid/swap-vertical.svg"; import SwapVertical from "@graphite-frontend/assets/icon-12px-solid/swap-vertical.svg";
@@ -80,6 +84,10 @@ const SOLID_12PX = {
Link: { svg: Link, size: 12 }, Link: { svg: Link, size: 12 },
Overlays: { svg: Overlays, size: 12 }, Overlays: { svg: Overlays, size: 12 },
Remove: { svg: Remove, size: 12 }, Remove: { svg: Remove, size: 12 },
RenderModeNormal: { svg: RenderModeNormal, size: 12 },
RenderModeOutline: { svg: RenderModeOutline, size: 12 },
RenderModePixels: { svg: RenderModePixels, size: 12 },
RenderModeSvg: { svg: RenderModeSvg, size: 12 },
Snapping: { svg: Snapping, size: 12 }, Snapping: { svg: Snapping, size: 12 },
SwapHorizontal: { svg: SwapHorizontal, size: 12 }, SwapHorizontal: { svg: SwapHorizontal, size: 12 },
SwapVertical: { svg: SwapVertical, size: 12 }, SwapVertical: { svg: SwapVertical, size: 12 },
@@ -197,9 +205,6 @@ import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
import TurnNegative90 from "@graphite-frontend/assets/icon-16px-solid/turn-negative-90.svg"; import TurnNegative90 from "@graphite-frontend/assets/icon-16px-solid/turn-negative-90.svg";
import TurnPositive90 from "@graphite-frontend/assets/icon-16px-solid/turn-positive-90.svg"; import TurnPositive90 from "@graphite-frontend/assets/icon-16px-solid/turn-positive-90.svg";
import UserManual from "@graphite-frontend/assets/icon-16px-solid/user-manual.svg"; import UserManual from "@graphite-frontend/assets/icon-16px-solid/user-manual.svg";
import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg";
import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg";
import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-pixels.svg";
import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg"; import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg";
import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg"; import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg";
import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg"; import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg";
@@ -318,9 +323,6 @@ const SOLID_16PX = {
TurnNegative90: { svg: TurnNegative90, size: 16 }, TurnNegative90: { svg: TurnNegative90, size: 16 },
TurnPositive90: { svg: TurnPositive90, size: 16 }, TurnPositive90: { svg: TurnPositive90, size: 16 },
UserManual: { svg: UserManual, size: 16 }, UserManual: { svg: UserManual, size: 16 },
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
ViewModePixels: { svg: ViewModePixels, size: 16 },
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 }, ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 }, ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 }, ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
+3 -2
View File
@@ -2,7 +2,7 @@ use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, UVec2}; use glam::{DAffine2, UVec2};
use graphene_core::text::FontCache; use graphene_core::text::FontCache;
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
use graphene_core::vector::style::ViewMode; use graphene_core::vector::style::RenderMode;
use std::fmt::Debug; use std::fmt::Debug;
use std::future::Future; use std::future::Future;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@@ -240,7 +240,8 @@ pub struct RenderConfig {
pub viewport: Footprint, pub viewport: Footprint,
pub export_format: ExportFormat, pub export_format: ExportFormat,
pub time: TimingInformation, pub time: TimingInformation,
pub view_mode: ViewMode, #[serde(alias = "view_mode")]
pub render_mode: RenderMode,
pub hide_artboards: bool, pub hide_artboards: bool,
pub for_export: bool, pub for_export: bool,
} }
+7 -5
View File
@@ -650,14 +650,16 @@ impl PathStyle {
} }
} }
/// Represents different ways of rendering an object /// Ways the user can choose to view the artwork in the viewport.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
pub enum ViewMode { pub enum RenderMode {
/// Render with normal coloration at the current viewport resolution /// Render with normal coloration at the current viewport resolution
#[default] #[default]
Normal, Normal = 0,
/// Render only the outlines of shapes at the current viewport resolution /// Render only the outlines of shapes at the current viewport resolution
Outline, Outline,
/// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher // /// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher
Pixels, // PixelPreview,
// /// Render a preview of how the object would be exported as an SVG.
// SvgPreview,
} }
+1 -1
View File
@@ -307,7 +307,7 @@ async fn render<'a: 'n, T: 'n + Render + WasmNotSend>(
ctx.footprint(); ctx.footprint();
let render_params = RenderParams { let render_params = RenderParams {
view_mode: render_config.view_mode, render_mode: render_config.render_mode,
hide_artboards: render_config.hide_artboards, hide_artboards: render_config.hide_artboards,
for_export: render_config.for_export, for_export: render_config.for_export,
footprint, footprint,
+4 -4
View File
@@ -3,7 +3,7 @@ use glam::DAffine2;
use graphene_core::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; use graphene_core::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use graphene_core::gradient::{Gradient, GradientType}; use graphene_core::gradient::{Gradient, GradientType};
use graphene_core::uuid::generate_uuid; use graphene_core::uuid::generate_uuid;
use graphene_core::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin, ViewMode}; use graphene_core::vector::style::{Fill, PaintOrder, PathStyle, RenderMode, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use std::fmt::Write; use std::fmt::Write;
pub trait RenderExt { pub trait RenderExt {
@@ -163,9 +163,9 @@ impl RenderExt for PathStyle {
/// Renders the shape's fill and stroke attributes as a string with them concatenated together. /// Renders the shape's fill and stroke attributes as a string with them concatenated together.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> String { fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> String {
let view_mode = render_params.view_mode; let render_mode = render_params.render_mode;
match view_mode { match render_mode {
ViewMode::Outline => { RenderMode::Outline => {
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT); let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
// Outline strokes should be non-scaling by default // Outline strokes should be non-scaling by default
+9 -9
View File
@@ -19,7 +19,7 @@ use graphene_core::transform::{Footprint, Transform};
use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::uuid::{NodeId, generate_uuid};
use graphene_core::vector::Vector; use graphene_core::vector::Vector;
use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::click_target::{ClickTarget, FreePoint};
use graphene_core::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign, ViewMode}; use graphene_core::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign};
use graphene_core::{Artboard, Graphic}; use graphene_core::{Artboard, Graphic};
use kurbo::Affine; use kurbo::Affine;
use num_traits::Zero; use num_traits::Zero;
@@ -159,7 +159,7 @@ pub struct RenderContext {
/// Static state used whilst rendering /// Static state used whilst rendering
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct RenderParams { pub struct RenderParams {
pub view_mode: ViewMode, pub render_mode: RenderMode,
pub footprint: Footprint, pub footprint: Footprint,
pub thumbnail: bool, pub thumbnail: bool,
/// Don't render the rectangle for an artboard to allow exporting with a transparent background. /// Don't render the rectangle for an artboard to allow exporting with a transparent background.
@@ -555,14 +555,14 @@ impl Render for Table<Graphic> {
let mut layer = false; let mut layer = false;
let blend_mode = match render_params.view_mode { let blend_mode = match render_params.render_mode {
ViewMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(), _ => alpha_blending.blend_mode.to_peniko(),
}; };
let mut bounds = RenderBoundingBox::None; let mut bounds = RenderBoundingBox::None;
let opacity = row.alpha_blending.opacity(render_params.for_mask); let opacity = row.alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
bounds = row.element.bounding_box(transform, true); bounds = row.element.bounding_box(transform, true);
if let RenderBoundingBox::Rectangle(bounds) = bounds { if let RenderBoundingBox::Rectangle(bounds) = bounds {
@@ -852,8 +852,8 @@ impl Render for Table<Vector> {
} }
// If we're using opacity or a blend mode, we need to push a layer // If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.view_mode { let blend_mode = match render_params.render_mode {
ViewMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => row.alpha_blending.blend_mode.to_peniko(), _ => row.alpha_blending.blend_mode.to_peniko(),
}; };
let mut layer = false; let mut layer = false;
@@ -969,8 +969,8 @@ impl Render for Table<Vector> {
}; };
// Render the path // Render the path
match render_params.view_mode { match render_params.render_mode {
ViewMode::Outline => { RenderMode::Outline => {
let outline_stroke = kurbo::Stroke { let outline_stroke = kurbo::Stroke {
width: LAYER_OUTLINE_STROKE_WEIGHT, width: LAYER_OUTLINE_STROKE_WEIGHT,
miter_limit: 4., miter_limit: 4.,
@@ -57,7 +57,7 @@ The right side of the control bar has controls related to the active document an
| Overlays | <p>When checked (default), overlays are shown. When unchecked, they are hidden. Overlays are the temporary contextual visualizations (like bounding boxes and vector manipulators) that are usually blue and appear atop the viewport when using tools.</p> | | Overlays | <p>When checked (default), overlays are shown. When unchecked, they are hidden. Overlays are the temporary contextual visualizations (like bounding boxes and vector manipulators) that are usually blue and appear atop the viewport when using tools.</p> |
| Snapping | <p>When checked (default), drawing and dragging shapes and vector points means they will snap to other areas of geometric interest like corners or anchor points. When unchecked, the selection moves freely.<br /><br />Fine-grained options are available by clicking the overflow button to access its options popover menu. Each option has a tooltip explaining what it does by hovering the cursor over it.</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/snapping-popover__5.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>Snapping options relating to **Bounding Boxes**:</p><p><ul><li>**Align with Edges**: Snaps to horizontal/vertical alignment with the edges of any layer's bounding box.</li><li>**Corner Points**: Snaps to the four corners of any layer's bounding box.</li><li>**Center Points**: Snaps to the center point of any layer's bounding box.</li><li>**Edge Midpoints**: Snaps to any of the four points at the middle of the edges of any layer's bounding box.</li><li>**Distribute Evenly**: Snaps to a consistent distance offset established by the bounding boxes of nearby layers (due to a bug, **Corner Points** and **Center Points** must be enabled).</li></ul></p><p>Snapping options relating to **Paths**:</p><p><ul><li>**Align with Anchor Points**: Snaps to horizontal/vertical alignment with the anchor points of any vector path.</li><li>**Anchor Points**: Snaps to the anchor point of any vector path.</li><li>**Line Midpoints**: Snaps to the point at the middle of any straight line segment of a vector path.</li><li>**Path Intersection Points**: Snaps to any points where vector paths intersect.</li><li>**Along Paths**: Snaps along the length of any vector path.</li><li>**Normal to Paths**: Snaps a line to a point perpendicular to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li><li>**Tangent to Paths**: Snaps a line to a point tangent to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li></ul></p> | | Snapping | <p>When checked (default), drawing and dragging shapes and vector points means they will snap to other areas of geometric interest like corners or anchor points. When unchecked, the selection moves freely.<br /><br />Fine-grained options are available by clicking the overflow button to access its options popover menu. Each option has a tooltip explaining what it does by hovering the cursor over it.</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/snapping-popover__5.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>Snapping options relating to **Bounding Boxes**:</p><p><ul><li>**Align with Edges**: Snaps to horizontal/vertical alignment with the edges of any layer's bounding box.</li><li>**Corner Points**: Snaps to the four corners of any layer's bounding box.</li><li>**Center Points**: Snaps to the center point of any layer's bounding box.</li><li>**Edge Midpoints**: Snaps to any of the four points at the middle of the edges of any layer's bounding box.</li><li>**Distribute Evenly**: Snaps to a consistent distance offset established by the bounding boxes of nearby layers (due to a bug, **Corner Points** and **Center Points** must be enabled).</li></ul></p><p>Snapping options relating to **Paths**:</p><p><ul><li>**Align with Anchor Points**: Snaps to horizontal/vertical alignment with the anchor points of any vector path.</li><li>**Anchor Points**: Snaps to the anchor point of any vector path.</li><li>**Line Midpoints**: Snaps to the point at the middle of any straight line segment of a vector path.</li><li>**Path Intersection Points**: Snaps to any points where vector paths intersect.</li><li>**Along Paths**: Snaps along the length of any vector path.</li><li>**Normal to Paths**: Snaps a line to a point perpendicular to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li><li>**Tangent to Paths**: Snaps a line to a point tangent to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li></ul></p> |
| Grid | <p>When checked (off by default), grid lines are shown and snapping to them becomes active. The initial grid scale is 1 document unit, helping you draw pixel-perfect artwork.</p><ul><li><p>**Type** sets whether the grid pattern is made of squares or triangles.</p><p>**Rectangular** is a pattern of horizontal and vertical lines:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-rectangular-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has one option unique to this mode:</p><ul><li>**Spacing** is the width and height of the rectangle grid cells.</li></ul><p>**Isometric** is a pattern of triangles:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-isometric-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has two options unique to this mode:</p><ul><li>**Y Spacing** is the height between vertical repetitions of the grid.</li><li>**Angles** is the slant of the upward and downward sloped grid lines.</li></ul></li><li>**Display** gives control over the appearance of the grid. The **Display as dotted grid** checkbox (off by default) replaces the solid lines with dots at their intersection points.</li><li>**Origin** is the position in the canvas where the repeating grid pattern begins from. If you need an offset for the grid where an intersection occurs at a specific location, set those coordinates.</li></ul> | | Grid | <p>When checked (off by default), grid lines are shown and snapping to them becomes active. The initial grid scale is 1 document unit, helping you draw pixel-perfect artwork.</p><ul><li><p>**Type** sets whether the grid pattern is made of squares or triangles.</p><p>**Rectangular** is a pattern of horizontal and vertical lines:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-rectangular-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has one option unique to this mode:</p><ul><li>**Spacing** is the width and height of the rectangle grid cells.</li></ul><p>**Isometric** is a pattern of triangles:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-isometric-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has two options unique to this mode:</p><ul><li>**Y Spacing** is the height between vertical repetitions of the grid.</li><li>**Angles** is the slant of the upward and downward sloped grid lines.</li></ul></li><li>**Display** gives control over the appearance of the grid. The **Display as dotted grid** checkbox (off by default) replaces the solid lines with dots at their intersection points.</li><li>**Origin** is the position in the canvas where the repeating grid pattern begins from. If you need an offset for the grid where an intersection occurs at a specific location, set those coordinates.</li></ul> |
| View Mode | <p>**Normal** (default): The artwork is rendered normally.</p><p>**Outline**: The artwork is rendered as a wireframe.</p><p>**Pixels**: **Not implemented yet.** The artwork is rendered as it would appear when exported as a bitmap image at 100% scale regardless of the viewport zoom level.</p> | | Render Mode | <p>**Normal** (default): The artwork is rendered normally.</p><p>**Outline**: The artwork is rendered as a wireframe.</p><p>**Pixel Preview**: **Not implemented yet.** The artwork is rendered as it would appear when exported as a bitmap image at 100% scale regardless of the viewport zoom level.</p><p>**SVG Preview**: **Not implemented yet.** The artwork is rendered as it would appear when exported as an SVG image.</p> |
| Zoom In | <p>Zooms the viewport in to the next whole increment.</p> | | Zoom In | <p>Zooms the viewport in to the next whole increment.</p> |
| Zoom Out | <p>Zooms the viewport out to the next whole increment.</p> | | Zoom Out | <p>Zooms the viewport out to the next whole increment.</p> |
| Reset Tilt and Zoom to 100% | <p>Resets the viewport tilt to 0°. Resets the viewport zoom to 100% which matches the canvas and viewport pixel scale 1:1.</p> | | Reset Tilt and Zoom to 100% | <p>Resets the viewport tilt to 0°. Resets the viewport zoom to 100% which matches the canvas and viewport pixel scale 1:1.</p> |